文章目录

  • 传送门
  • 前言
  • 背景知识
    • 并行与并发
    • 线程与进程
    • 内存模型
      • 1. 计算机内存模型
      • `2. Java内存模型`
        • 2.1 内存交互
          • 2.1.1 交互操作
          • 2.1.2 交互规则
        • `2.2 并发编程特征`
          • 原子性(Atomicity)
          • 可见性(Visibility)
            • `先行发生原则(Happens-Before)`
          • 有序性(Ordering)
  • Java线程
    • 线程状态
    • 线程创建
    • 线程控制
      • `join[加入/等待]`
      • `sleep休眠`
      • yield让步
      • suspend挂起
      • interrupt中断
      • priority优先级
      • daemon[后台/守护]线程
    • `线程安全`
      • `1.[阻塞同步/互斥同步](重量级、悲观锁)`
        • `1.1 Synchronized锁`
          • `Synchronized锁原理`
          • `Deadlock死锁`
          • `可重入性`
          • 注意事项
          • Java对象头
          • [锁机制/锁优化]
            • 自旋锁
            • 锁消除
            • 锁粗化
            • `偏向锁`
            • `轻量级锁`
        • 1.2 `ReentrantLock锁`
          • ReentrantReadWriteLock
          • ReentrantLock源码
      • 2.非阻塞同步(轻量级、乐观锁)
        • 2.1 原子类
          • `原子类原理`
        • `2.2 Volatile`
          • `Volatile原理`
      • 3.无同步
        • `ThreadLocal`
          • `ThreadLocal原理`
        • 无共享数据
    • 线程通信
      • 等待通知
      • Condition
    • 其他
      • 线程组ThreadGroup
      • 一些代码
  • 并发包
    • 基础概念
      • `CAS`
        • ABA问题
      • `AQS`
    • 原子类
    • 并发锁
      • ReentrantLock
      • ReentrantReadWriteLock
      • StampedLock
    • 并发集合
      • Map
        • ConcurrentHashMap
        • ConcurrentSkipListMap
      • List
        • CopyOnWriteArrayList
          • CopyOnWriteArrayList原理
          • CopyOnWriteArrayList源码
      • Set
        • CopyOnWriteArraySet
        • ConcurrentSkipListSet
      • Queue
        • BlockingQueue
        • 非阻塞队列
          • ConncurrentLinkedDeque
          • ConcurrentLinkedQueue
    • 并发工具
      • CountDownLatch
        • CountDownLatch原理
      • CyclicBarrier
      • `Semaphore`
      • Exchanger
      • Phaser
    • `线程池`
      • Executors
        • ThreadPoolExecutor
          • ThreadPoolExecutor源码
        • ScheduledThreadPoolExecutor
      • ExecutorCompletionService
      • ForkJoinPool
      • Future
  • 源码
  • 练习
  • 总结

传送门

  1. 明翰Java教学系列之认识Java篇

  2. 明翰Java教学系列之基础语法篇

  3. 明翰Java教学系列之初级面向对象篇

  4. 明翰Java教学系列之数组篇

  5. 明翰Java教学系列之进阶面向对象篇

  6. 明翰Java教学系列之异常篇

  7. 明翰Java教学系列之集合篇

  8. 明翰Java教学系列之多线程篇


前言

单线程往往是一条逻辑执行流,从上到下的的执行,
如果在执行过程中遇到了阻塞,那么程序会停止在原地,
我们使用debug模式时可以看到这种情况,但单线程的能力有限。
试想一下,如果Tomcat是单线程的话,
那么高并发就无从谈起了对吗。

多线程就是多个逻辑执行流在执行,多个执行流之间可以保持独立。

可以理解成:
单线程就是餐厅里只有一个服务员,多线程就是餐厅里有多个服务员。
在顾客比较多的时候一个服务员肯定是忙不过来的,
那么多线程就派上用场了。

需要注意的是多线程也是需要消耗资源的(例如CPU、内存),
是空间换时间的一种玩法,
如果服务端的内存和CPU的使用率都很低,
那么使用多线程会是一种提升效率的好方法,
是否需要使用多线程、需要多少线程需要自行判断。

随着多核处理器越来越便宜与系统资源的低利用率,
我们需要利用多线程来大幅提升程序的性能。
多线程虽然好用,但里面的坑也不少,
并且很多多线程的问题和bug并不是每次都能复现,
很有可能成为所谓的灵异事件,
因此,掌握多线程与Java并发的知识就显得尤为重要。

其实,多线程无处不在,即使我们没有显式的编写多线程的代码,
多线程也隐式的存在于我们的代码中,
例如:各种开源框架、Servlet、Tomcat、JVM的GC回收等等。


背景知识

并行与并发

一些同学容易混淆这两个概念,
并发(concurrency)与并行(parallel)是有区别的。
并行是指在同一时刻有多条指令在[多个/多核]CPU上同时执行。

并发是指在同一时刻只能有一条指令在CPU上执行,
但多条指令被CPU快速[切换/调度]执行(时间分片),
感觉上好像是多个指令在同时执行,但同时执行的指令只有一个,
线程的切换往往伴随着一些计算资源的开销,
CPU保存当前线程状态切换下一个线程,以便下次再加载回来。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1oltMJMj-1680311422314)(evernotecid://BCE3D193-8584-4CB1-94B3-46FF37A1AC6C/appyinxiangcom/12192613/ENResource/p334)]

线程与进程

我们可以简单的理解操作系统(Windows、L
inux、MacOS)里的一个程序在开始运行(入驻内存)后,
就成为了一个进程。每个运行中的程序就是一个进程,
例如:微信、QQ、迅雷影音、浏览器等等。

当一个程序运行时,程序内部可能包含了多个逻辑执行流,
每个执行流就是一个线程。

线程与进程是包含的关系。一个进程至少包含一个线程,
至多可以包含n个线程,一个线程必须从属于一个进程。

  • 进程(Process):
    进程是操作系统中独立存在的实体,
    拥有独立的系统资源(内存、文件句柄、安全证书等),
    进程之间不共享内存,进程之间的通信较困难。
    进程是程序的一种动态形式,是cpu,内存等资源占用的基本单位,
    而线程是不能独立的占有这些资源的。

  • 线程(Thread):
    线程是进程中某个单一顺序的控制流,
    线程是进程的基本执行单位,一个进程内可以包含多个线程,
    当进程被初始化后,主线程就被创建了。线程可以拥有自己的栈、程序计数器(Program Counter)、局部变量等,同一进程下的多个线程共享该进程所拥有的全部资源,线程切换相对而言开销较小,可视为轻量级进程。线程的执行是抢占式的,相同进程下的多个线程可以并发执行并相互通信。

多线程的优势:
有效提升资源(CPU)利用率以及程序的性能、吞吐量、响应速度,
利用多核CPU彰显巨大威力。

多线程的劣势:
线程之间的切换带来额外的性能开销,
多线程基础知识掌握不好的话,
会编写出灵异事件的bug。
例如:
多线程的安全性、死锁等问题,
并且由于多线程的执行不确定性和随机性,
导致分析问题难度增加。

多线程的应用场景:
有很多同学会吐槽说,多线程学完了发现并没有什么用武之地嘛,
那可就大错特错了。

我们每天都在接触多线程,只不过是自己不知道而已,
例如Web服务器的请求就是多线程的。
这里又罗列出一些多线程的场景,以供大家参考。

  1. 异步处理&非阻塞,可以把占据长时间的程序中的任务放到新线程去处理,缩短响应时间。在I/O阻塞时,程序可以用另一个线程去做别的事情而并非一直傻傻的在等待I/O返回;
  2. 定时向大量(例如100万以上)的用户发送邮件&消息&信息;
  3. 统计分析的业务场景,让每个线程去统计一个部门的某类信息;
  4. 后台进程,例如GC线程;
  5. 多线程操作文件,提高程序执行时间;

下面这段引用自网络:
假设有一个请求需要执行3个很缓慢的io操作(比如数据库查询或文件查询),
那么正常的数据可能是:

a.读取文件1(10ms)
b.处理1的数据(1ms)
c.读取文件2(10ms)
d.处理2的数据(1ms)
e.读取文件3(10ms)
f.处理3的数据(1ms)
g.整合1,2,3的数据结果(1ms)
单线程总共需要34ms,但如果你把ab,cd,ef分别分给3个线程去做,就只需要12ms了。

再假设
a.读取文件1(1ms)
b.处理1的数据(1ms)
c.读取文件2(1ms)
d.处理2的数据(1ms)
e.读取文件3(28ms)
f.处理3的数据(1ms)
g.整合1,2,3的数据结果(1ms)

单线程总共需要34ms,如果还是按照上面的划分方案,
类似于木桶原理,
速度取决于最慢的那个线程。
在这个例子里,第三个线程执行了29ms,
那么最后这个请求的耗时是30ms,比起不用单线程,就节省了4ms,
但有可能线程调度切换也要花个1-2ms,
因此这个方案显示的优势就不明显了,
还带来了程序复杂性的提升,不值得。

所以我们要优化文件3的读取速度,
可以采用缓存,减少一些重复读取,
假设所有用户都请求这个请求,
相当于所有的用户都需要读取文件3,那你想想,
100个人进行了这个请求,
相当于你花在读取这个文件上的时间久是28 * 100 = 2800ms,
如果你把这个文件缓存起来,那只要第一个用户的请求读取了,
第二个用户不需要读取了,
从内存读取是很快的,可能1ms都不到。


内存模型

在继续往下了解多线程之前,
我们很有必要先要了解一下内存模型相关的概念,
了解这些知识有助于让我们更好的理解多线程里的一些特性和底层原理,
让我们的步伐更扎实。

至于“内存模型”这四个字,可以理解成对内存操作过程的一种抽象。
说白了:Java内存是怎么"玩"的?
这个"玩"的过程用一些文字和图片表示出来。

1. 计算机内存模型

在了解Java内存模型之前,我们需要先了解一下计算机内存模型,
循序渐进,更容易理解后面的知识。

在我之前的一篇文章:明翰计算机基础知识
中有介绍过一些计算机理论知识,
其中里面有讲到过CPU、寄存器、CPU高速缓存、内存等知识点。

在这里简短的赘述一下,细节可以看一眼那篇文章。
CPU在运行时需要与内存进行读写操作,
但内存对于CPU来说实在是太慢了,
于是引入了CPU高速缓存(简称缓存)来加快运行速度,
缓存是进行高速数据交换的存储器。

当CPU要读取数据时,首先在缓存中查找,
找到就立即读取并送给CPU处理,
如果没有找到,则从内存复制到缓存里,
后续操作中,CPU直接去[读/写]缓存中的数据,
当CPU处理结束后,再把数据从缓存同步到内存中。

多核CPU中每个核都对应一个自己的缓存,
而多个缓存又会共享同一个主内存(简称主存)。

那在多核CPU并发操作同一块主存数据时,
就可能会产生[缓存不一致/线程不安全]问题,
就好像每个缓存里对于同一个变量所存储的数据都不一样,
那么以哪个缓存里的数据为基准呢?

例如:
两个线程a、b,分别读取x=0,并把x读取到2个缓存中。
线程a读取缓存中的x值并+1,此时x=1,之后写回到主存中。
线程a读取缓存中的x值并+1,此时x=1,之后写回到主存中。
最后,x的值应该是2,但x=1。

那么每个核在对缓存进行操作时都需要遵守一些协议,
来保证不会出现缓存一致性问题。
具体是什么协议不再展开。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FYiMoiPz-1680311422320)(evernotecid://BCE3D193-8584-4CB1-94B3-46FF37A1AC6C/appyinxiangcom/12192613/ENResource/p336)]

(不同架构的计算机会有不一样的内存模型,
此外这种缓存一致性问题在分布式缓存中也会有所体现,正所谓知识是相通的。)

2. Java内存模型

不止计算机有内存模型,Java虚拟机规范也定义了Java自己的内存模型,
简称:JMM(Java Memory Model)。

JMM描述了Java程序中各种变量(线程共享变量)的访问规则,
以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。

JMM可以屏蔽掉各种硬件与操作系统之间的内存访问差异,
使得Java程序在内存访问层面可以跨平台。

JMM主要负责定义程序中线程公有变量的访问规则,
其中包括实例变量、[类变量/静态变量],不包括局部变量和形参,
因为后者是线程私有,不存在[共享/竞争]问题,
在JVM中变量与内存之间的存取细节:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KymPVUnL-1680311422322)(evernotecid://BCE3D193-8584-4CB1-94B3-46FF37A1AC6C/appyinxiangcom/12192613/ENResource/p337)]

JMM规定线程所有的共享变量都必须存储在主内存(Main Memory)中,
每个线程都有自己的工作内存(Working Memory),
工作内存会从主内存中复制一些自己要用到的变量,
形成一个变量副本存储在自己的工作内存中。

线程对变量的读写都必须在工作内存中完成,不能直接去读写主内存的变量,
每个线程之间也无法直接去读写对方的工作内存,必须通过主内存来完成。

可见性问题:
在A线程上修改的变量内容,在B线程上是没办法立即看到的,
因为A,B线程都在各自的工作内存中玩耍。

2.1 内存交互

JMM定义了8种操作来完成主内存与工作内存之间的交互,
每一种操作都是原子的。
我们可以把这8种操作来分成4类,有助于结构化记忆。

2.1.1 交互操作

锁:
锁定(lock):将主内存变量标识为某个线程独享的状态。
解锁(unlock):将处于锁定状态的主内存变量释放,释放后的变量才能被其他线程锁定。

借(从主内存中借):
读取(read):将主内存变量的值从主内存传递到工作内存中,供load操作使用。
载入(load):将read操作从主内存传递过来的值复制到工作内存变量中。

用:
使用(use):将工作内存变量中的值传递给执行引擎,在使用该变量时触发。
赋值(assign):将工作内存变量赋值。

还(从工作内存中还):
存储(store):将工作内存变量的值传递到主内存中,供write操作使用。
写入(write):将store操作从工作内存传递过来的值覆盖到主内存变量中。

2.1.2 交互规则
  1. 如果要把变量从主内存中复制到工作内存中,必须先执行read再执行load。
  2. 如果要把变量从工作内存中复制到主内存中,必须先执行store再执行write。
  3. read与load、store与write必须成对出现,不允许单独使用。
  4. 工作内存变量改变后必须立刻同步回主内存,用于保证其他线程可以看到自己的修改。
  5. 如果没有发生任何操作,不允许工作内存变量同步回主内存。
  6. 只能在主内存中创建新对象,不允许在工作内存自己创建对象。
  7. 在同一时间只允许一个线程对变量进行lock操作,其他线程必须等待,但允许重复执行lock。
  8. lock后会清空工作内存变量的值,需要重新执行load和assign。
  9. 不允许对没有lock的变量使用unlock,也不允许unlock其他线程lock住的变量。
  10. 执行unlock之前必须先把工作内存变量同步回主内存中。

每次使用工作内存变量前都必须先从主内存中获取最新的值,
用于保证可以看到其他线程对变量所修改的值。

2.2 并发编程特征

一些CPU相关术语:

中文 英文 描述
内存屏障 memory [barriers/fences] 一组处理器指令,用于实现对内存操作的顺序限制
原子操作 atomic operations 不可中断的一个或一系列操作
缓冲行 cache line CPU高速缓存中可以分配的最小存储单位。处理器填写缓存行时会加载整个缓存行,现代CPU需要执行几百次CPU指令。
缓冲行填充 cache line fill 当处理器识别到从内存中读取操作数是可缓存的,处理器读取整个高速缓存行到适当的CPU缓存(L1,L2,L3或所有)
缓存命中 cache hit 如果进行高速缓存行填充操作的内存位置仍然是下次处理器访问的地址时,处理器从缓存中读取操作数,而不是从内存中读取。
写命中 write hit 当处理器将操作数写回到一个内存缓存的区域时,它首先会检查这个缓存的内存地址是否在缓存行中,如果存在一个有效的缓存行,则处理器将这个操作数写回到缓存,而不是写回到内存,这个操作被称为写命中。
写缺失 write misses the cache 一个有效的缓存行被写入到不存在的内存区域。
原子性(Atomicity)

所谓原子性:要么全部成功,要么全部失败。
JMM中的那些操作都具有原子性,但如果需要一个更大的原子性范围,
则需要使用lock和unlock,但这两个操作并没有直接开放给我们,
但却提供了2个字节码指令monitorenter和monitorexit来隐式使用上面的操作,这就是synchronized锁的底层。

可见性(Visibility)

可见性是指当一个线程修改了共享变量的值,
其他线程是否能立即看到这个修改。

在JMM中,变量修改后,将新值同步回主内存,
在变量读取前,从主内存刷新变量值,
通过依赖主内存作为传递媒介的方式来实现可见性。

先行发生原则(Happens-Before)

先行发生原则通过一些规则来判断数据是否存在竞争、冲突、线程是否安全,
可以理解为A happens before B。

什么是先行发生呢?
在JMM中定义两项操作的执行顺序关系,如果操作A先于操作B发生,
在操作B之前可以观察到操作A对共享变量的改动。

默认先行发生规则:

  • 程序次序规则(Program Order Rule):在一个线程内,按照程序的控制流顺序,谁写在前面谁先发生;
  • 管理锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面(时间层面上的后面)对同一个锁的lock操作;
  • volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面(时间层面上的后面)对这个变量的读操作;
  • 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作;
  • 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行;
  • 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生与被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生;
  • 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始;
  • 传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那么就可以得出操作A先行发生于操作C的结论;
有序性(Ordering)

指令重排,计算机CPU为了优化效率可能会对底层指令进行乱序执行,
CPU运算结束后会把结果进行重组以保证顺序性。

CPU不能保证代码的编写顺序与执行顺序是一致的。
类似的情况,Java的JVM中也会有这种指令重排。

指令重排:

  • 编译器优化的重排序(编译器级),编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
  • 指令级并行的重排序(处理器级):现代处理器采用了指令级并行技术将多条指令重叠执行,如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;
  • 内存系统的重排序(处理器级):由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的;

编译器会禁止特定类型的编译器重排序(不是所有编译器重排序都要禁止),
处理器重排序规则会要求Java编译器在生成指令序列时,
插入特定类型的内存屏障指令,
通过内存屏障来禁止特定类型的处理器重排序。

as-if-serial:
不管怎么重排序(编译器和处理器为了提高并行度),
单线程程序的执行结果不能被改变,
编译器、runtime和处理器都必须遵守as-if-serial语义。


Java线程

线程状态

首先Java在JDK1.5之后的Thread类有6种状态(看了JDK源码),
网上很多文章写的是5种,
区别在于其中RUNNABLE包含了原来的RUNNABLE和RUNNING,
原来的BLOCKED分解成:BLOCKED、WAITING、TIMED_WAITING。
每个线程在同一时间只能有一种状态,
这6种状态是JAVA的线程状态而非操作系统的线程状态。

线程被创建并启动后,不会一直霸占着CPU独自运行,
CPU需要在多个线程中快速切换,
线程的执行策略是抢占式的(也依赖于线程优先级),
线程状态也会在运行与阻塞中不断切换。

名称 描述
NEW ([新建/初始]) 使用new()创建一个线程对象后,该线程属于[新建/初始]状态,此时初始化成员变量,分配线程所需要的资源,但不会执行线程体,此时还没有调用start()。
RUNNABLE(可运行) 该状态包含了操作系统中的RUNNING(运行中)与READY(就绪),调用start()启动线程后,该线程属于READY,线程的运行需要依赖于CPU的调度,具有随机性,获得CPU调度后线程状态变成了RUNNING,开始执行线程体。
BLOCKED(阻塞) 当前线程在等待其他线程synchronized锁释放时,会进入阻塞状态。等到其他线程释放synchronized锁时,当前线程进入可运行状态。多线程情况下为了保证线程同步会使用synchronized锁机制。
WAITING(等待) 当前线程等待其他线程执行操作(通知或中断),此时CPU不会分配执行时间,需要显式的被其他线程唤醒。
TIMED_WAITING(计时等待) 当前线程在一定时间范围内等待其他线程执行操作,超时后自动唤醒。
TERMINATED([终止/死亡]) 线程终止状态,线程终止之后不可再调用start(),否则将抛异常。

根据上面的线程状态,就可以推出线程的生命周期,就是一个线程从出生到死亡的过程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oAlWn1CR-1680311422323)(evernotecid://BCE3D193-8584-4CB1-94B3-46FF37A1AC6C/appyinxiangcom/12192613/ENResource/p335)]
图片参考《Java并发编程的艺术》

线程创建

有多种方式创建与启动线程,分别为:

  • 实现Runnable接口,重写run();
  • 实现Callable接口,重写call(),结合FutureTask;
  • 继承Thread类,重写run();
  • 通过线程池创建,主要使用ExecutorService、Executors等(详见本文线程池章节);

注意:在执行main()时候,
其实main()本身是要启动一个main线程的,也叫主线程。
主线程与我们自己新建的线程是截然不同的线程,请不要混淆。

实现Runnable接口:
实现了Runnable接口的类在使用多线程时,
可以更方便的访问共享实例变量,
如果需要访问当前线程对象,
则需要使用Thread.currentThread()。

public class RunnableDemo1 {public static void main(String[] args) {RunnableDemo r1 = new RunnableDemo();Thread t1 = new Thread(r1);Thread t2 = new Thread(r1);t1.start();t2.start();}
}class RunnableDemo implements Runnable {int i = 0;/*** 线程要执行的代码*/public void run() {for (; i < 10; i++) {// Thread.currentThread().getName()可以获取当前线程名称System.out.println(Thread.currentThread().getName() + "==>" + i);}}
}

实现Callable接口:
JDK1.5才出现的接口,可以视为Runnable的升级版,
主要用于方便获取线程的返回值与异常,
可以修改返回值的类型。

public class CallableDemo1 {public static void main(String[] args) {//注意Callable需要泛型支持FutureTask<Boolean> ft1 = new FutureTask<>(new CallableDemo(1));FutureTask<Boolean> ft2 = new FutureTask<>(new CallableDemo(0));FutureTask<Boolean> ft3 = new FutureTask<>(new CallableDemo(2));Thread t1 = new Thread(ft1);Thread t2 = new Thread(ft2);Thread t3 = new Thread(ft3);t1.start();t2.start();t3.start();// 获取线程返回值try {//get方法获取返回值时候会导致主线程阻塞,直到call()结束并返回为止System.out.println(ft1.get());System.out.println(ft2.get());System.out.println(ft3.get());} catch (Exception e) {//可以catch住线程体里的异常e.printStackTrace();}}
}class CallableDemo implements Callable<Boolean> {int flag;int i = 0;public CallableDemo(int flag) {this.flag = flag;}/*** 线程要执行的代码*/public Boolean call() throws Exception {for (; i < 10; i++) {// Thread.currentThread().getName()可以获取当前线程名称System.out.println(Thread.currentThread().getName() + "==>" + i);}if (flag == 1) {return true;} else if(flag == 0){return false;} else {throw new Exception("哇咔咔咔");}}
}

继承Thread类:
由于Java是单继承机制,导致继承了Thread类就不能继承其他类,
没有扩展性,而Callable、Runnable接口就没有这种问题。

继承Thread相对于实现接口而言,不能共享实例变量,
但使用线程的方法更加简单与方便,
例如:获取线程的id,线程名,线程状态等。

public class ThreadDemo1 {public static void main(String[] args) {//创建三个线程对象ThreadDemo t1 = new ThreadDemo();ThreadDemo t2 = new ThreadDemo();ThreadDemo t3 = new ThreadDemo();//启动三个线程对象t1.start();t2.start();t3.start();}
}class ThreadDemo extends Thread{/*** 线程要执行的代码*/public void run() {for(int i = 0 ; i < 10 ; i ++) {//this.getName()可以获取当前线程名称System.out.println(this.getName()+"==>"+i);}}
}

run()与start()的区别:

  • run(),仅仅是封装被线程执行的代码,直接调用是普通方法;
  • start(),启动线程,再由JVM去调用该线程的run()方法;

线程控制

JDK通过提供一些方法和策略来控制线程的执行。
countDownLatch等待?

join[加入/等待]

让线程A等线程B完成之后再执行,
我们可以在线程A中使用线程B的join(),
此时线程A将阻塞,直到线程B执行完再恢复执行。

我们可以将大问题划分成许多个小问题,
再为每个小问题分配一个线程,
当所有的小问题都完成后,再调用主线程来接着往下走。

public class JoinDemo1 {public static void main(String[] args) throws Exception {System.out.println("start");JoinThread r = new JoinThread();Thread t1 = new Thread(r);Thread t2 = new Thread(r);t1.start();t2.start();t1.join();t2.join();// 主线程等待t1,t2两个线程都执行完毕再继续往下走。// 此处t1,t2会并发执行,并不会因为t1.join()先调用就执行完t1再执行t2,// join()方法也可以加入超时时间,如果超过时间则不再等待System.out.println("done");}
}class JoinThread implements Runnable {public void run() {for (int i = 0; i < 100; i++) {System.out.println(Thread.currentThread().getName()+" "+i);}}
}

在这个例子中,如果没有加join(),
则在t1,t2两个线程还没有执行完就会打印出done,
因为线程是争抢执行的,主线程开完2个子线程后就直接走到done了,
2个子线程的执行相当于是异步,
主线程不会等待t1,t2两个线程执行完毕再执行。

需要注意,在本例中,t1,t2会是并发执行,而非t1走完再走t2。
至于为什么t1,t2是并发执行而不是串行,我想应该作者就是想这么设计的,
因为如果把join设计成串行执行,那效率会大打折扣,
相当于没有用到多线程并发。

sleep休眠

Thread.sleep()可以让正在执行的线程暂停若干毫秒,
并进入等待状态,时间到了之后自动恢复。

在休眠时间范围内即使当前没有任何可执行的线程,
休眠中的线程也不会被执行。
如果当前线程持有锁,在休眠期间不释放对象锁,
如果当前线程持有synchronized锁并sleep(),则其他线程仍不能访问,
休眠结束后,当前线程进入就绪状态等待被CPU调用。

public class DaemonDemo1 {public static void main(String[] args) {for (int i = 0; i < 10; i++) {if (i == 5) {try {//暂停5秒Thread.sleep(5 * 1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName() + " " + i);}}
}

yield让步

Thread.yield()使当前线程放弃CPU调度,
但是不使当前线程进入[阻塞/等待],即线程仍处于[可运行]状态,
由[运行中]变成[就绪],随时可能再次获取CPU调度。

相当于认为当前线程已执行了足够的时间从而转到另一个线程,
也有可能出现刚调用完Thread.yield()放弃CPU调度,
当前线程立刻又获得CPU调度。

public class YieldDemo1 {public static void main(String[] args) {YieldThread r = new YieldThread();new Thread(r).start();new Thread(r).start();}
}class YieldThread implements Runnable {public void run() {for (int i = 0; i < 100; i++) {if(i==50) {Thread.yield();}System.out.println(Thread.currentThread().getName()+" "+i);}}
}

suspend挂起

suspend()和resume()配套使用,代表暂停与恢复功能,
suspend()使得线程进入阻塞状态,并且不会自动恢复,
必须其对应的resume()被调用,才能使得线程重新进入可执行状态。
因为不建议使用,所以本文不再讲解。

此外,stop()代表终止线程,也不建议使用。

interrupt中断

interrupt()设置标志位,请求终止线程,
interrupt不会真正停止一个线程,
它只是发了一个信号,设置了一个标志位,告诉线程应该结束了。

未完待续

priority优先级

所谓优先级就是谁先执行谁后执行的问题,
每个线程在运行时都具有一定的优先级,
优先级高的线程具有较多的执行机会,
优先级低的线程具有较少的执行机会。
每个线程的默认优先级与创建它的父线程的优先级相同。
优先级范围只能是1-10之间。

setPriority()可以传入一个正整数作为参数,
但一般建议使用Thread的常量来设置优先级:
Thread.MAX_PRIORITY=10
Thread.NORM_PRIORITY=5(默认)
Thread.MIN_PRIORITY=1

public class PriorityDemo {public static void main(String[] args) {PriorityThread t1 = new PriorityThread();PriorityThread t2 = new PriorityThread();t1.setPriority(Thread.MAX_PRIORITY);t2.setPriority(Thread.MIN_PRIORITY);t1.start();t2.start();System.out.println("得到线程优先级t1="+t1.getPriority());System.out.println("得到线程优先级t2="+t2.getPriority());}
}
class PriorityThread extends Thread{public void run(){for(int i = 0 ; i < 100 ; i ++){System.out.println(" "+getName()+":"+i);}}
}

daemon[后台/守护]线程

后台线程是指在后台运行的线程,为其他线程提供服务,
JVM的GC就是后台线程。

如果前台线程全部死亡,后台线程会自动死亡。
前台线程创建的子线程默认是前台线程,
后台线程创建的子线程默认是后台线程。
把某线程设置为后台线程的操作必须在线程启动之前,否则会抛异常。

public class DaemonDemo1 {public static void main(String[] args) {Thread t1 = new Thread(new DaemonThread());//设置为后台线程t1.setDaemon(true);t1.start();for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName()+" "+i);}//后台线程还没有完全运行完就会死掉,因为主线程先死了。//查看是否为后台线程System.out.println(t1.isDaemon());System.out.println(Thread.currentThread().isDaemon());}
}
class DaemonThread implements Runnable{public void run() {for (int i = 0; i < 100; i++) {System.out.println(Thread.currentThread().getName()+" "+i);}}
}

线程安全

线程的安全应该是多线程基础概念中最最最重要的知识点了,必须要掌握。
线程安全体现在多个线程之间存在共享数据访问,
如果没有共享数据,那么线程安全不安全就无从谈起。

众所周知,
多线程是由CPU调度来获取执行且具有随机性与争抢性(还有指令重排),
而多个线程又共享进程的内存,可以同时读写共享内存中堆上的对象数据,
很有可能某一线程读写了其他线程正在读写的[对象/数据]。

当多个线程访问一个对象时,
如果不用考虑这些线程在运行时环境下的调度和交替执行,
也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,
调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。

使用多线程访问一个共享[对象/数据]时,如果没有相应的机制来协同访问,
可能造成运算结果[错误/异常],数据状态不稳定,即线程安全问题。

举个栗子:
某商城系统中某件商品的库存是1,代码中判断如果库存=0就不能再购买了,
如果库存>0则能购买,购买成功后,库存减1,
在单线程时代这个逻辑是没有问题的。

但如果此时并发来了若干个线程,大家都要去买这个商品,
这时运算的结果可能出乎我们的意料。
有可能若干个线程同时进入判断库存>0,都是判断通过,
进而大家都购买成功,造成库存为负数。

库存出现负数表示线程不安全,
那我们需要使用一种安全的机制,那么是什么机制呢?
有很多人会把[线程安全]和[线程同步]这两个概念弄混淆,
甚至认为这2个概念是相同的,其实不然。
[线程同步]是[线程安全]的一种实现方式而已,[线程安全]不止一种实现方式,
保障[线程安全]的实现机制有:
[互斥同步/阻塞同步]、非阻塞同步、无同步。

按照线程的安全程度来排序,
我们可以将被共享的数据分为:
不可变,绝对线程安全,相对线程安全,线程兼容,线程对立。

未完待续

线程安全的实现方法如下文所示
(参考《深入理解Java虚拟机》,周志明)

1.[阻塞同步/互斥同步](重量级、悲观锁)

线程同步是指多个线程同时访问某资源时(线程共享数据时),
采用一系列的机制(转换和控制线程的执行),
以保证同一时间内只有一个线程访问该资源。

此外,锁这个概念经常出现在计算机科学领域中,
例如:MySQL的锁、Redis的锁、多线程的锁等等。

其实核心概念非常简单,就是线程A在执行某段代码前,
先获取一个锁(互斥锁,谁拿到谁执行,其他线程必须排队等待),
在线程A没有执行完之前,线程B也想获取这个锁从而执行这段代码,
但因为线程A还没有释放锁,因此线程B只能等待(进入阻塞状态),
等到线程A执行完代码释放了锁之后,
线程B就可以获得锁从而执行这段代码,在线程B执行期间,
其他线程依然只能等待(进入阻塞状态),
相当于用锁来保护某段代码同一时间只能有一个线程可以执行。
这样就是线程安全,线程安全需要使用多线程的同步机制。

可以说是让线程进入阻塞状态进行等待,
当获得相应的信号(唤醒,时间)时,
才可以进入线程的准备就绪状态,
准备就绪状态的所有线程,通过竞争,进入运行状态。

1.1 Synchronized锁

多线程并发情况下,
synchronized关键字能够保证在同一时间只有一个线程执行某段代码,
而其他线程需要等待正在执行的线程执行完毕之后才有机会去执行。

而对于非synchronized关键字修饰的方法和代码块,
其他线程均可畅通无阻的执行,在并发场景下,可能导致线程安全问题。

synchronized关键字可以加在方法上与代码块上,
也可叫做synchronized方法(同步方法)与synchronized块(同步块),
synchronized块相比较而言可以更加精准的控制要加锁的范围,灵活性较高。

例子1:

public class SynchronizedDemo0 implements Runnable {public void run() {synchronized(this) {//this代表SynchronizedDemo0对象for(int i = 0; i<3; i++) {//模拟执行动作System.out.println(Thread.currentThread().getName()+" "+i);}}}public static void main(String[] args) {//只new了一个SynchronizedDemo0对象,让三个线程来共享,从而造成同步执行。SynchronizedDemo0 s = new SynchronizedDemo0();//新建三个线程Thread t1 = new Thread(s);Thread t2 = new Thread(s);Thread t3 = new Thread(s);t1.start();t2.start();t3.start();}
}

例子2:

public class SynchronizedDemo1 {public static void main(String[] args) throws Exception {Item item1 = new Item();item1.count = 1;item1.name = "Java编程思想"; SynchronizedThread r1 = new SynchronizedThread(item1);// 开启10个线程来购买商品for (int i = 0; i < 10; i++) {new Thread(r1).start();}Thread.sleep(1 * 1000);System.out.println(item1.count);}
}/*** 模拟商品*/
class Item {// 商品名称String name;// 商品库存Integer count;
}
/*** 模拟购买线程*/
class SynchronizedThread implements Runnable {private Item item;public SynchronizedThread(Item item) {this.item = item;}public void run() {//每个线程都要先获取item对象的监视器的锁,才能进入。synchronized (item) {if (item.count > 0) {System.out.println("购买成功");item.count--;} else {System.out.println("购买失败,库存不足");}}}
}

例子3:

public class SynchronizedDemo2 {static int j ;public static void main(String[] args) {Thread t1 = new XX();Thread t2 = new XX2();Thread t3 = new Thread(new XX3());Thread t4 = new Thread(new XX4());t1.start();t2.start();t3.start();t4.start();    }public static synchronized void add(){j++;System.out.println("加1后,j="+j);}public static synchronized void delete(){j--;System.out.println("减1,j="+j);}
}
class XX extends Thread{public void run(){SynchronizedDemo2.add();}
}
class XX2 extends Thread{public void run(){SynchronizedDemo2.add();}
}
class XX3 implements Runnable{public void run() {SynchronizedDemo2.delete();}
}
class XX4 implements Runnable{public void run() {SynchronizedDemo2.delete();}
}

例子4:

public class SynchronizedDemo3 {public static void main(String[] args) {SynchronizedDemo3Father f = new SynchronizedDemo3Son();f.eat();}
}class SynchronizedDemo3Father {public synchronized void eat() {System.out.println("我是爸爸");}
}class SynchronizedDemo3Son extends SynchronizedDemo3Father {public synchronized void eat() {System.out.println("我是儿子");super.eat();}
}
Synchronized锁原理

synchronized关键字经过编译之后,
会在同步块的开始处插入monitorenter指令,
在同步块[结束处/异常处]插入monitorexit指令,
这两个字节码指令都需要指定一个要[锁定/解锁]的对象。
(我们可以通过javap -v Xxx.class命令来反编译看到里面的具体细节。)

这个要[锁定/解锁]的对象可以理解成锁,
那么这个要[锁定/解锁]的对象是谁呢?

  • 对于[普通/实例]Synchronized方法,锁是[this对象引用/调用该方法的对象引用];
  • 对于静态Synchronized方法,锁是当前类对象(Class对象),该类的所有对象引用共享一把锁;
  • 对于Synchronized块,锁是是传入参数的对象引用,Java中的任意一个对象都可以当做Synchronized的锁;

监视器对象:
Java中有一个概念叫监视器对象(monitor),
Java中的任何对象引用的内部都有一个监视器对象,
用来检测并发时的重入问题,在非多线程情况下,
监视器不起作用,在synchronized情况下监视器才起作用。

当一个线程试图访问同步代码时,会占用对象引用的监视器对象,
又称[监视器锁/monitor锁/内置锁/内部锁],
或者也可以说每个Java对象引用都可以用来做一个实现同步的监视器锁。
线程在执行完同步代码之后或抛异常后则自动释放监视器锁。

Synchronized的语义底层是通过monitor对象来完成,
monitor依赖操作系统的MutexLock(互斥锁)来实现,
当monitor被占用时就会处于锁定状态,
线程执行monitorenter指令时尝试获取monitor的所有权,
当前的这个线程就是这个monitor的owner。

在synchronized情况下,任何时刻只能有一个线程可以获取监视器锁,
而其他未获得锁的线程只能阻塞,等到那个线程放弃监视器的锁,
这个线程才能获取,进而执行。

被synchronized包含的区域被也被称为临界区(Critical Section),
同一时间内只有一个线程处于临界区内,保证了线程的安全。

执行monitorenter指令时:

  • 首先尝试获取(即将要加锁的)对象的锁,如果这个对象没有被其他线程锁定或当前线程已经拥有了这个对象的锁,则把锁的计数器进入数加1,当前线程拥有这个对象的锁;
  • 如果这个对象已经被其他线程锁定,当前线程获取对象的锁失败,则当前线程进入阻塞状态,直到其他线程释放这个锁为止;

执行monitorexit指令时:

  • 把锁的计数器进入数减1,当减到0时,锁被释放,其他线程有机会获取到锁;

什么时候当前线程会释放监视器的锁?

  • 当synchronized[块/方法]执行完毕;
  • 当synchronized[块/方法]执行中使用break&return跳出来时;
  • 当synchronized[块/方法]执行中遇到exception或error跳出来时;
  • 当synchronized[块/方法]执行中使用了监视器所属对象的wait()时;

什么时候当前线程不会释放监视器的锁?

  • Thread.sleep();
  • Thread.yield();
  • Thread.suspend();

monitor监视器源码是C++写的,在虚拟机的ObjectMonitor.hpp文件中:

ObjectMonitor() {_header       = NULL;_count        = 0;_waiters      = 0,_recursions   = 0;  // 线程重入次数_object       = NULL;  // 存储Monitor对象_owner        = NULL;  // 持有当前线程的owner_WaitSet      = NULL;  // wait状态的线程列表_WaitSetLock  = 0 ;_Responsible  = NULL ;_succ         = NULL ;_cxq          = NULL ;  // 单向列表FreeNext      = NULL ;_EntryList    = NULL ;  // 处于等待锁状态block状态的线程列表_SpinFreq     = 0 ;_SpinClock    = 0 ;OwnerIsThread = 0 ;_previous_owner_tid = 0;}

synchronized与内存模型:
当我们使用synchronized进行同步的时候,
真正被同步的是在工作线程中的数据,
简单的说就是在[同步块/同步方法]执行完后,
对被锁定的对象做的任何修改要在释放锁之前写回到主内存中。

在进入同步块得到锁之后,被锁定对象的数据是从主内存中读出来的,
持有锁的线程的工作内存中的数据副本一定和主内存中的数据视图是同步的。

Deadlock死锁

从自己整理的面试攻略上同步

可重入性

synchronized锁具有可重入性,
synchronized同步块对同一条线程来说是可重入的,
并不会出现自己把自己锁死的问题。

一般来讲,当线程A持锁,线程B想获锁时会发生[等待/阻塞],
但如果是线程A自己再次调用获锁时,
就会调用成功,允许线程再次获得自己已经持有的锁。

重入性的实现为:
监视器为每个锁关联一个“获取计数值”和“所有者线程”。
当计数值为0时,则认为当前锁没有被任何线程所持有。
当某线程获取一个未被持有的锁时,监视器记录锁的持有者以及让计数值+1,
如果当前线程再次获取这个锁时,计数值再+1,
当线程退出synchronized块后,计数值-1,
当计数值为0时,表示当前锁被释放。

如果没有可重入性,则下面会造成线程死锁。

public class SynchronizedDemo3 {public static void main(String[] args) {SynchronizedDemo3Father f = new SynchronizedDemo3Son();f.eat();}
}class SynchronizedDemo3Father {public synchronized void eat() {System.out.println("我是爸爸");}
}class SynchronizedDemo3Son extends SynchronizedDemo3Father {public synchronized void eat() {System.out.println("我是儿子");super.eat();}
}

对于重入概念的了解,为下面的ReentrantLock(可重入锁)预热。

注意事项

很多同学只知道加上synchronized代表同步,
程序员想不出现库存为负数的情况,
于是就在service层的方法上加synchronized,
殊不知这样会带来大问题。

我们一般会把controller、service、dao这三层类设置为单例(Spring默认),
这就相当于把锁的粒度放在了service层,
会导致所有的商品在购买时全部阻塞,造成性能瓶颈。
正确的做法是我们只需要把锁的粒度放在商品对象上即可,
即监视器为商品对象,
这样只有并发线程对同一个商品对象操作的时候才会上锁,
而不是两个不同的商品来的并发也阻塞。

这样就保证了并发与线程安全,
记得要重写商品对象的equals()与hashcode()。

虽然Java允许使用任何对象的监视器来获得锁,
但我们应该使用可能被并发访问的共享对象。

持有锁的线程执行完同步块代码,锁就释放了,
释放出来的锁会被其他线程争抢,一旦被某线程抢到锁后,
没抢到锁的线程只能被阻塞,等待锁释放。

Java对象头

理解Java对象头,对后面的多线程锁机制打基础,
尤其是理解偏向锁、轻量级锁。

在HotSpot虚拟机中,Java对象的内存分布:

  • 对象头,存储运行时数据;
  • 实例数据,存储类,父类信息;
  • 填充数据,由于虚拟机要求对象起始地址必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐;

其中,对象头包含若干字段:

  • Mark Word,存储对象自身的运行时数据,例如:哈希码HashCode、GC分代年龄、[锁标记位/锁标志位]等,这部分数据的长度在32位的虚拟机中为32位,在64位虚拟机中为64位;
  • Class Metadata Address,存储指向方法区对象类型数据的指针;
  • Array Length,存储数组的长度(当对象为数组时才有);

其中,Mark Word存储内容包含:

锁状态 主要存储内容 是否为偏向锁(1位) 锁标志位(2位)
无锁 哈希码,GC分代年龄 0 01
偏向锁 线程ID,[时间戳/Epoch],GC分代年龄 1 01
GC标记 空,不需要记录信息 11
轻量级锁 指向栈桢中的锁记录(Lock Record)的指针 00
重量级锁 指向[互斥量/重量级锁]的指针 10
[锁机制/锁优化]

在JDK1.6中,Java对Synchronized锁(重量级锁)进行了优化,
引入了偏向锁、轻量级锁以减少[获取/释放]锁时带来的性能问题,
使得在某些情况下,Synchronized锁不再那么重了。

一个锁有4种状态,级别从低到高依次是:
无锁、偏向锁、轻量级锁、重量级锁。

他们会随着竞争情况而逐渐升级(可以升级但是不可以降级),
不能降级策略是为了提高释放锁和释放锁的效率。

自旋锁

Java线程映射了操作系统的原生线程,
我们需要操作系统来[阻塞/唤醒]([挂起/恢复])一个Java线程,
这些操作需要涉及到从操作系统中的[用户态]与[核心态/内核态]频繁互换,
从而导致更多的CPU资源浪费,对性能有影响,
所以我们可以说synchronized锁是重量级的操作。

官方研发团队发现在许多应用场景中,
多线程共享数据的锁定状态只会持续很短的时间,
例如:多核CPU可以并行执行、同步块内代码[简单/少]、硬件配置高等。
只为了短时间的锁定状态而频繁的去[阻塞/唤醒]线程就显得很不划算。

因此,虚拟机会对此进行一些优化,
我们可以让等待锁释放的线程在不放弃CPU执行权的情况下稍等一下,
如果锁释放的速度很快,
那当前线程就可以避免[阻塞/唤醒]操作的前提下快速获得锁。

为了让线程稍等片刻,
我们可以在通知操作系统阻塞线程之前加入一段自旋等待的过程,
让当前线程循环若干次轮询锁的状态,锁可用时退出循环,
如果做了多次循环发现锁还是不可用,再进行阻塞操作。

自旋锁不能完全代替线程阻塞,
自旋锁虽然避开了线程切换的开销,但没有放弃CPU执行权,
如果锁被占用的时间很长,用这种方式不合适,浪费资源。

JDK1.6引入了自适应的自旋锁,自旋时间不再固定,
由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来动态决定。

如果在同一个锁对象上,自旋等待刚刚成功获得过锁,
那么虚拟机就会认为这次自旋也很有可能再次成功,
进而它将允许自旋等待持续相对更长的时间。

如果对于某个锁,自旋很少成功,
那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费资源。

锁消除

虚拟机即时编译器在运行时,监测到不可能存在共享数据竞争的锁时,
会对这些锁进行[消除/删除]。

许多同步措施并不是程序员自己主观上加的,
而是在调用底层代码中被动加的。

锁消除的主要判断依据是来源于逃逸分析的数据支持,
如果判断在一段代码中,
堆上的所有数据都不会逃逸出去从而被其他线程访问到,
那就可以把它们当作栈上数据对待,并认为它们是线程私有的,
既然是线程私有,就没有必要使用同步加锁操作。

锁粗化

如果虚拟机检测到有一串连续操作频繁的对同一个对象进行加锁解锁,
例如在循环中的互斥同步操作(造成不必要的性能损耗),
则虚拟机会将加锁同步的范围粗化到整个操作序列的外部,
这样就会导致多次循环[加锁/解锁]操作,只需要一次即可。

偏向锁

大部分情况下,都会是同一个线程进入同一块同步代码块的,
这是偏向锁出现的原因。
适用于只有一个线程访问同步块的场景。

如果程序中的很多锁对象经常被多个不同的线程竞争,
那偏向模式是多余的,有时禁止偏向锁优化反而能提升性能。

锁会偏向于第一个获得它的线程,在接下来的执行过程中,
假如该锁没有被其他线程所获取,没有其他线程来竞争该锁,
那么持有偏向锁的线程将永远不需要进行同步操作。
假设有两个线程竞争偏向锁,偏向锁失效会升级为轻量级锁。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lunJaEQO-1680311422332)(evernotecid://BCE3D193-8584-4CB1-94B3-46FF37A1AC6C/appyinxiangcom/12192613/ENResource/p3984)]

待验证:

  1. 当锁对象第一次被某个线程获取时,JVM将对象头里的Mark Word里的锁标志位修改为01,这样就进入了偏向锁模式,同时使用CAS操作把[获取到这个锁的线程的线程ID]记录在对象的Mark Word中;
  2. 如果CAS操作操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,JVM都将不再进行任何同步操作;
  3. 当有另外一个线程尝试获取这个锁时,偏向模式结束,根据锁对象目前是否处于被锁定状态,撤销偏向(Revoke Bias)后,恢复到无锁状态(锁标志位为01)或轻量级锁状态(锁标志位为00),之后进入轻量级锁的执行过程;

待验证:
在此线程之后的执行过程中,
如果再次进入或者退出同一段同步块代码,
并不再需要去进行加锁或者解锁操作,而是接下来做下面的动作:

  • Load-and-test,判断一下当前线程id是否与Markword当中的线程ID是否一致;
  • 如果一致,则说明此线程已经成功获得了锁,继续执行后续代码;
  • 如果不一致,则要检查一下对象是否还是可偏向,即“是否偏向锁”标志位的值;
  • 如果还是未偏向的状态,则利用CAS操作来竞争锁,也即是第一次获取锁时的操作;
轻量级锁

出现有两个线程来竞争锁的话,那么偏向锁就失效了,
此时会升级为轻量级锁。

待验证:
偏向锁失效之后,需要把锁撤销掉:

  1. 在一个安全点暂停当前拥有锁的线程;
  2. 遍历线程栈,如果存在锁记录的话,需要修复锁记录(Lock Record)和Mark Word,使其变成无锁状态;
  3. 唤醒当前线程,将当前锁升级成轻量级锁;

轻量级锁的加锁过程:

  1. 当代码进入同步块时,如果当前同步对象没有被锁定(锁标志位为01),JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用来存储锁对象目前的Mark Word的拷贝(使用Displaced前缀命名),然后把Lock Record中的owner指向当前对象;
  2. 之后JVM使用CAS操作尝试将对象的Mark Word更新为[指向当前线程的栈帧中的锁记录(Lock Record)的指针];
  3. 如果更新成功,表示当前线程获得对象的锁,加锁成功,并且对象Mark Word的锁标志位将转变为00,即表示此对象处于轻量级锁状态,接下来继续执行相关同步操作;
  4. 如果更新失败,JVM首先检查当前对象的Mark Word是否[指向当前线程的栈帧中的锁记录(Lock Record)的指针],如果指向则说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程抢占了,将继续锁升级,修改锁的状态,之后等待的线程也阻塞;

轻量级锁的解锁过程:

  1. 如果对象的Mark Word仍然[指向当前线程的栈帧中的锁记录(Lock Record)的指针],那就用CAS操作把对象当前的Mark Word和线程中复制的Displaced前缀替换回来;
  2. 如果替换成功,整个同步过程完成;
  3. 如果替换失败,说明有其他线程尝试过获取该锁,那就要在释放锁的同时,唤醒被挂起的线程(待验证);

如果存在2个以上的线程争用同一个锁(待验证),
那轻量级锁将升级为重量级锁,
锁标志位将转变为10,
Mark Word中存储的是指向重量锁(互斥量)的指针,
其他等待锁的线程都要进入阻塞状态。

1.2 ReentrantLock锁

在JDK1.5中,Java并发包提供了一种更为强大的线程同步机制,
通过显式定义同步锁对象来实现同步。

ReentrantLock(可重入锁)具有可重入性,
一个线程可以对已被加锁的ReentrantLock锁再次加锁,
也叫做递归锁,指的是同一线程 外层函数获得锁之后,
内层递归函数仍然有获取该锁的代码,但不受影响。

ReentrantLock对象会维持一个计数器来追踪lock()方法的嵌套调用,
线程在每次调用lock()加锁后,必须显式的调用unlock()来释放锁,
一段被锁保护的代码可以调用另一段被锁保护的代码。

与synchronized锁相似,他们都有重入性,性能相似(JDK1.6之后),
synchronized锁是原生语法层面的互斥锁,
ReentrantLock锁是API层面的互斥锁,
通过lock()、unlock()、配合[try/finally]使用。
此外,ReentrantLock增加了一些高级功能:

  • 等待可中断,当持有锁的线程长时间不释放时,其他线程可以选择放弃等待,有助于执行时间长的同步块;
  • ReentrantLock锁默认是非公平锁,但可通过构造方法来配置出公平锁,synchronized锁是是非公平锁;
  • 锁可以绑定多个条件,1个ReentrantLock对象可以同时绑定多个Condition对象;

每次只能有一个线程对lock对象加锁,
线程访问共享数据前必须先获得lock对象,
使用lock对象必须显式的加锁、释放锁。

Lock锁比Synchronized锁的锁定操作更多,
Lock锁允许更灵活的加锁结构,并支持condition对象。

Lock与ReadWriteLock(读写锁)是JDK1.5提供的两个根基接口,
其中ReadWriteLock允许对共享资源的并发访问。

Lock的实现类是ReentrantLock(可重入锁)。
ReadWriteLock的实现类是ReentrantReadWriteLock(可重入读写锁)。

(待验证)
首先明确一下,不是说ReentrantLock不好,只是ReentrantLock某些时候有局限。如果使用ReentrantLock,可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不一致,但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。
因为这个,才诞生了读写锁ReadWriteLock。ReadWriteLock是一个读写锁接口,

public class LockDemo1 {// 定义lock锁对象,每个lock对象对应一个要加锁的对象,能达到synchronized的效果。private final ReentrantLock lock = new ReentrantLock();public void doSomething1() {//开启锁lock.lock();System.out.println(Thread.currentThread().getName() + " do something1...");}public void doSomething2() {System.out.println(Thread.currentThread().getName() + " do something2...");// 释放锁,可以跨方法lock.unlock();}public static void main(String[] args) {LockDemo1 lockDemo1 = new LockDemo1();LockThread lockThread = new LockThread(lockDemo1);for (int i = 0; i < 20; i++) {new Thread(lockThread).start();}}
}class LockThread implements Runnable {LockDemo1 lockDemo1;public LockThread(LockDemo1 lockDemo1) {this.lockDemo1 = lockDemo1;}public void run() {lockDemo1.doSomething1();lockDemo1.doSomething2();}
}
ReentrantReadWriteLock

可重入读写锁,读读共享,读写,写写互斥。

ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,
实现了读写的分离,读锁是共享的,写锁是独占的,
读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,
提升了读写的性能。

在读的地方使用读锁,在写的地方使用写锁,灵活控制,
如果没有写锁的情况下,
读是无阻塞的,在一定程度上提高了程序的执行效率。

ReentrantLock源码

关于AQS的知识点,请阅读本文其他章节,此处不再赘述。

public class ReentrantLock implements Lock, java.io.Serializable {private static final long serialVersionUID = 7373984872572414699L;private final Sync sync;abstract static class Sync extends AbstractQueuedSynchronizer {private static final long serialVersionUID = -5179523762034025860L;abstract void lock();final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {// 0意味着当前共享资源处于空闲状态if (compareAndSetState(0, acquires)) {// 使用CAS去尝试抢锁,如果抢到则设置当前线程为持有锁的线程setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {// 判断已经持有锁的线程与当前线程是否相同,// 如果是同一个,则将state+1,// 这里就是ReentrantLock支持重入性的关键,// 到时候解锁的时候也是通过减去这个state计数。int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}protected final boolean tryRelease(int releases) {// 减少一次可重入次数int c = getState() - releases;// 判断当前线程是否是持有锁的线程if (Thread.currentThread() != getExclusiveOwnerThread())// 如果不是则直接抛异常throw new IllegalMonitorStateException();boolean free = false;if (c == 0) {// 0代表当前的资源处于空闲状态free = true;// 将持有锁的线程设置为nullsetExclusiveOwnerThread(null);}// 更新statesetState(c);return free;}protected final boolean isHeldExclusively() {return getExclusiveOwnerThread() == Thread.currentThread();}final ConditionObject newCondition() {return new ConditionObject();}final Thread getOwner() {return getState() == 0 ? null : getExclusiveOwnerThread();}final int getHoldCount() {return isHeldExclusively() ? getState() : 0;}final boolean isLocked() {return getState() != 0;}private void readObject(java.io.ObjectInputStream s)throws java.io.IOException, ClassNotFoundException {s.defaultReadObject();setState(0);}}// 非公平锁static final class NonfairSync extends Sync {private static final long serialVersionUID = 7316153563782823691L;final void lock() {// 通过CAS加锁if (compareAndSetState(0, 1))// 加锁成功,将当前线程设置为持有锁的线程setExclusiveOwnerThread(Thread.currentThread());else// 加锁失败acquire(1);}protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);}}// 公平锁static final class FairSync extends Sync {private static final long serialVersionUID = -3000897897090466540L;// 没有像非公平锁那样用CAS尝试加锁final void lock() {acquire(1);}protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {// 0意味着当前共享资源处于空闲状态// hasQueuedPredecessors()判断等待队列中是否有节点// 如果没有节点,再通过CAS进行加锁,// 如果成功,将当前线程设置为持有锁的线程if (!hasQueuedPredecessors() &&compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {// 判断已经持有锁的线程与当前线程是否相同,// 如果是同一个,则将state+1,// 这里就是ReentrantLock支持重入性的关键,// 到时候解锁的时候也是通过减去这个state计数。int nextc = c + acquires;if (nextc < 0)throw new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}}// 解锁public void unlock() {sync.release(1);}
}
public abstract class AbstractQueuedSynchronizerextends AbstractOwnableSynchronizerimplements java.io.Serializable {public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}protected boolean tryAcquire(int arg) {// 这里的tryAcquire()由子类重写来实现throw new UnsupportedOperationException();}protected boolean tryRelease(int arg) {// 这里的tryRelease()由子类重写来实现throw new UnsupportedOperationException();}// 将当前线程加入等待队列// 创建一个和当前线程绑定的Node节点,// 此时等待队列中的tail指针为空,// 直接调用enq(node)方法将当前线程加入等待队列尾部private Node addWaiter(Node mode) {Node node = new Node(Thread.currentThread(), mode);// Try the fast path of enq; backup to full enq on failureNode pred = tail;if (pred != null) {node.prev = pred;if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}enq(node);return node;}private Node enq(final Node node) {for (;;) {Node t = tail;if (t == null) { // Must initialize// 第一遍循环时tail指针为空,if (compareAndSetHead(new Node()))// 使用CAS操作设置head指针,将head指向一个新创建的Node节点tail = head;} else {node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}}// addWaiter()执行后,会返回当前线程创建的节点信息。// 继续往后执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {final Node p = node.predecessor();// 判断当前传入的Node对应的前置节点是否为head,如果是则尝试加锁。if (p == head && tryAcquire(arg)) {// 加锁成功过则将当前节点设置为head节点,setHead(node);// 然后空置之前的head节点,方便后续被GC。p.next = null;failed = false;return interrupted;}// 如果加锁失败或者Node的前置节点不是head节点,// 就会通过shouldParkAfterFailedAcquire()将head节点的waitStatus变为SIGNAL=-1,// 最后执行parkAndChecknIterrupt(),// 调用LockSupport.park()挂起当前线程。if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}}private final boolean parkAndCheckInterrupt() {LockSupport.park(this);// 返回当前线程的中断状态return Thread.interrupted();}private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {int ws = pred.waitStatus;if (ws == Node.SIGNAL)return true;if (ws > 0) {do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {compareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;}// 解锁方法的实现public final boolean release(int arg) {if (tryRelease(arg)) {Node h = head;if (h != null && h.waitStatus != 0)unparkSuccessor(h);return true;}return false;}
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HXbUjVg8-1680311422332)(evernotecid://BCE3D193-8584-4CB1-94B3-46FF37A1AC6C/appyinxiangcom/12192613/ENResource/p3996)]
执行完成之后,head、tail、t都指向第一个Node元素。

接着执行第二遍循环,进入else逻辑,此时已经有了head节点,
这里要操作的就是将线程二对应的Node节点挂到head节点后面。
此时队列中就有了2个Node节点:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zx990zBB-1680311422333)(evernotecid://BCE3D193-8584-4CB1-94B3-46FF37A1AC6C/appyinxiangcom/12192613/ENResource/p3997)]

2.非阻塞同步(轻量级、乐观锁)

互斥同步的痛点是[阻塞/唤醒]线程造成的性能问题,
互斥同步属于一种悲观策略,认为只要不进行一些同步措施就会出问题,
其中包括:加锁(虚拟机会优化掉一批不必要的加锁)、
用户态与[核心/内核]态转换、维护锁计数器、检查是否有被阻塞的线程、
线程唤醒等。

但是,有的共享数据不存在竞争,在这种情况下,同步措施就没有必要执行。
随着硬件指令集的发展,允许[操作]与[冲突检测]这2个步骤具备原子性。
硬件可以保证从语义上看起来需要多次操作的行为只通过1条指令就能完成。
例如:CAS指令。

我们可以采用基于冲突检测的乐观策略:
先进行操作,如果共享数据不存在竞争则操作成功。
如果共享数据存在竞争则使用补偿措施,例如:不断重试,直到成功。
这种乐观策略一般不需要把线程挂起。

2.1 原子类

我们在之前JDK版本若要并发的对Integer、Long、Double等这些Java原始类或引用类型来进行操作,
一般需要我们通过锁来控制并发,以防数据不一致。

在JDK1.5之后的并发包中的java.util.concurrent.atomic工具包,
这个包提供了Java原始/引用类型的映射类,
这些类的底层是通过一种无锁算法来实现的。

无锁算法:乐观锁机制,其实底层就是通过Unsafe类实现的一种比较并且交换的算法,大致的过程是,当希望修改的值与expectedValue相同时,则尝试将值更新为updateValue,更新成功返回true,否则返回false。

普通:

  • AtomicBoolean;
  • AtomicLong;
  • AtomicInteger;
  • AtomicIntegerArray;
  • AtomicLonngArray;

Reference:

  • AtomicReference;
  • AtomicReferenceArray;
  • AtomicMarkableReference;
  • AtomicStampedReference;

增强:

  • LongAccumulator;
  • DoubleAccumulator;
  • LongAdder;
  • DoubleAdder;
public class AtomicDemo1 {static AtomicInteger a = new AtomicInteger(0); // 使用原子类,运行结果是2000000
//    static int a = 0; // 不使用原子累,运行结果不是2000000static final int THREAD_COUNT = 20;static void increase(){a.incrementAndGet();
//        a++;}public static void main(String[] args) throws Exception{System.out.println("aaaaa");Thread[] threads = new Thread[THREAD_COUNT];for (int i = 0 ; i < THREAD_COUNT; i++){threads[i] = new Thread(new Runnable() {@Overridepublic void run() {for(int i = 0 ; i < 100000; i++){increase();}}});threads[i].start();}Thread.sleep(5 * 1000);System.out.println(a);}
}
原子类原理

关于CAS的知识点,请阅读本文其他章节,此处不再赘述。

原子类其内部实现是一个更为高效的方式CAS+volatile和native方法,
从而避免了synchronized的高开销,执行效率大为提升。

当然CAS一定要volatile变量配合,
这样才能保证每次拿到的变量是主内存中最新的那个值,
否则旧值A对某条线程来说,永远是一个不会变的值A,
只要某次CAS操作失败,永远都不可能成功。

addAndGet()允许参数是非1。

2.2 Volatile

我们可以把volatile看成是轻量级的synchronized,
它的使用成本更低,不会引起线程上下文的切换,
volatile关键字非常重要,被翻译为“易变的”,
在很多开源框架的源码种频繁出现。

在理解volatile关键字之前,我们必须要掌握一些基础概念:
Java内存模型,知道主内存与工作内存的关系,
以及并发编程中的三个特性,
原子性、可见性、有序性,CPU与虚拟机的指令重排等知识点。

我们可以使用volatile关键字来修饰一个共享变量,
可以保证共享变量的可见性和有序性,但是无法保证原子性。

  • 可见性,保证被volatile修饰的变量对所有线程的可见性,当前线程修改变量后,其他线程立即可见,但普通变量则不能立即可见,因为普通线程需要主内存与工作内存的同步。被volatile修饰的变量在工作内存中被改变后,立刻同步回主内存,并使其他线程的工作内存中的变量副本立刻失效,如果其他线程要使用当前变量时,必须再次从主内存中同步;
  • 有序性,被volatile修饰的变量禁止指令重排,代码的顺序与执行的顺序相同,普通变量则会进行指令重排,虽然可以提升效率,但可能导致执行顺序不可控,从而造成线程安全问题;
  • 原子性,volatile变量在并发下进行运算时,并不是线程安全的,因为运算这个过程并不是原子性操作。例如:volatile int a,然后多线程i++,这个i++并不是原子性操作,对于[不进行计算的赋值操作]是线程安全的;

例子:

public class VolatileDemo1 {
//    public static volatile boolean flag = false;public static boolean flag = false;public static void main(String[] args) {Thread t1 = new Thread() {public void run(){int i = 1;while(!flag){i++;}System.out.println("i = " + i);}};t1.start();try {Thread.sleep(1000);flag = true;System.out.println("flag = " + flag);t1.join();} catch (InterruptedException e) {e.printStackTrace();}}
}

主线程启动一个新线程,在新线程里不停的增加计数器的值,
直到flag被主线程设置为true才结束循环。

主线程sleep后将flag设置为true,
这个时候子线程应该检测到并且跳出循环,
我们期望看到的效果是1秒后停止,并且打印出实际值。
但真实情况是,陷入了死循环,使用volatile修饰后问题解决。

Volatile原理

线程A修改被volatile修饰的变量时,
会立刻使线程B工作内存的这个变量副本失效,
同时将新值刷新到主内存中。

使用volatile关键字修饰后,
在字节码指令层面会添加一个lock操作,
lock操作相当于一个内存屏障,
执行指令重排时不能把[处于下面的指令]重排到内存屏障上面的位置。

待确认:
lock操作使得当前CPU的高速缓存写入主存,
同时使其他CPU的高速缓存失效,
因此可以让volatile变量的修改对其他CPU立即可见。

volatile操作读操作与普通变量区别不大,但是写操作会慢一些,
因为需要插入一些内存屏障指令,来保证没有指令重排,
在线程安全的情况下加volatile会牺牲性能。

3.无同步

如果不涉及多线程共享数据,则不需要使用同步措施来保证正确性。

  • 可重入代码;
  • 线程本地存储;

可以尝试把共享数据的可见范围限制在同一个线程内,
例如:一个客户端请求对应一个服务器线程。

或者将数据ID按照哈希算法取模分段,不同线程处理不同段的数据。

未完待续

ThreadLocal

ThreadLocal代表一个线程局部变量,
是Java为线程安全提供的工具类,实现线程本地存储,
将数据存储在线程自己的局部变量中,避免多线程竞争。

通过把数据存储在ThreadLocal对象中(实际存储在调用set()的线程对象中),
就可以让每个线程创建一个ThreadLocal对象的副本,
每个线程都可以独立的改变自己变量副本中的值,
而不会和其他线程的副本起冲突,
就好像每一个线程都完全拥有该变量一样,
从而避免并发访问引起的线程安全问题。

ThreadLocal与其他的同步机制类似,
都是为了解决多线程对同一变量的访问冲突,
但ThreadLocal不能代替同步机制,它们的维度不一样。

同步机制是通过加锁的机制为了同步多个线程对相同资源的并发访问,
在竞争状态下获得共享数据,
而ThreadLocal是为了隔离多个线程的数据共享,
从根本上避免多个线程对共享资源的竞争。

ThreadLocal不能代替同步机制,
因为需要同步机制共享数据达到线程通信的目的。

此外ThreadLocal还有一个小功能,就是可以在一次线程调用中,
TheadLocal可以携带共享资源&变量跨越多个类与方法,
避免复杂参数传递,类似于全局变量的概念。

public class ThreadLocalDemo1 {public static void main(String[] args) {Person p = new Person();//启动两个线程,两个线程共享person对象new ThreadDemo1(p).start();new ThreadDemo2(p).start();}
}class ThreadDemo1 extends Thread {Person p;public ThreadDemo1(Person p) {this.p = p;}public void run() {System.out.println(this.getName() + " start " + p.getName());// 将线程局部变量赋值p.setName("张三");System.out.println(this.getName() + " end " + p.getName());}
}class ThreadDemo2 extends Thread {Person p;public ThreadDemo2(Person p) {this.p = p;}public void run() {System.out.println(this.getName() + " start " + p.getName());// 将线程局部变量赋值p.setName("李四");System.out.println(this.getName() + " end " + p.getName());}
}class Person {// 定义线程局部变量,每个线程都会保留该变量的一个副本,多个线程之间并不互相影响。private ThreadLocal<String> name = new ThreadLocal<>();public String getName() {return this.name.get();}public void setName(String name) {this.name.set(name);}
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N3ZtK2ft-1680311422333)(evernotecid://BCE3D193-8584-4CB1-94B3-46FF37A1AC6C/appyinxiangcom/12192613/ENResource/p4009)]

ThreadLocal原理

每个线程的ThreadLocal对象中都有一个ThreadLocalMap对象
(ThreadLocal.ThreadLocalMap threadLocals)
该对象可以理解成是一个轻量级的Map对象,
与Map的区别是桶里放的是entry而不是entry的链表,

这个ThreadLocalMap对象存储了一组Entry,
每个Entry都是[KV键值对],
以ThreadLocal为key(实际使用的是唯一threadLocalHashCode值),
以本地线程变量为value(调用ThreadLocal.set()设置的值),
ThreadLocal对象就是当前线程的ThreadLocalMap的访问入口,
每一个ThreadLocal对象都包含了一个独一无二的threadLocalHashCode值,
使用这个值就可以在[KV键值对]中找到对应的本地线程变量。

主要方法是get()和set(T a),
set之后在map里维护一个threadLocal -> a,get时将a返回。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qg08jT5a-1680311422334)(evernotecid://BCE3D193-8584-4CB1-94B3-46FF37A1AC6C/appyinxiangcom/12192613/ENResource/p3991)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ti2VbkId-1680311422334)(evernotecid://BCE3D193-8584-4CB1-94B3-46FF37A1AC6C/appyinxiangcom/12192613/ENResource/p3992)]
每个Thread线程会有一个threadlocals,
这是一个ThreadLocalMap对象。

通过这个对象,可以存储线程的私有变量,
就是通过ThreadLocal的set和get来操作

ThreadLocal本身不是一个容器,本身不存储任何数据,
实际存储数据的对象是ThreadLocalMap对象,
操作的过程就类似于Map的put和get。

这个ThreadLocalMap对象就是负责ThreadLocal真实存储数据的对象,
内部的存储结构是Entry数组,这个Entry就是存储Key和Value对象。

Key就是ThreadLocal实例本身,而Value就是我们要存储的真实数据,
而我们也从上面的源码中看到了,
存和取就是根据ThreadLocal实例来操作的。

public class ThreadLocal<T> {private final int threadLocalHashCode = nextHashCode();ThreadLocalMap getMap(Thread t) {return t.threadLocals;}public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);}public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}return setInitialValue();}private T setInitialValue() {T value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);return value;}void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}static class ThreadLocalMap {// 定义Entry类(弱引用类型),key为ThreadLocal对象,// value为Object,我们想要存储的任意对象static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}// 定义Entry数组,存储多个Entry对象private Entry[] table;private void set(ThreadLocal<?> key, Object value) {// We don't use a fast path as with get() because it is at// least as common to use set() to create new entries as// it is to replace existing ones, in which case, a fast// path would fail more often than not.Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode & (len-1);for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();if (k == key) {e.value = value;return;}if (k == null) {replaceStaleEntry(key, value, i);return;}}tab[i] = new Entry(key, value);int sz = ++size;if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();}}private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.get() == key)return e;elsereturn getEntryAfterMiss(key, i, e);}}

待整理:

ThreadLocal有什么用?
简单说ThreadLocal就是一种以空间换时间的做法,
在每个Thread里面维护了一个以开地址法实现的ThreadLocal.ThreadLocalMap,
把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了。

如果定义了一个ThreadLocal,每个线程往这个对象中读写是线程隔离的,互相之间不会影响,提供了一种将可变数据通过每个线程有自己独立的副本从而实现线程封闭的机制。

ThreadLocal可以理解为线程本地变量,
即每个定义的ThreadLocal对象,
每个线程对这个ThreadLocal中的读写是线程隔离的,互相之间不会影响

ThreadLocal内部有个static类型的ThreadLocalMap类,
Key是ThreadLocal,Value是存储的值。

举个例子:
现有ThreadLocal类的对象local,在每个线程的内部都有属于自己的ThreadLocalMap,可以简单的将它认为Key是ThreadLocal,Value为代码中放的值(实际上Key并不是ThreadLocal本身,而是它的一个弱引用),每个线程在往某个ThreadLocal里塞值的时候,都会往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。

底层存储结构是ThreadLocalMap,有自己的Key和Value,我们可以将ThreadLocal简单的视为Key,放入的值则是Value,实际上ThreadLocal中存放的是弱引用

Entry是ThreadLocalMap里定义的节点,
它继承了WeakReference类,
定义了一个类型为Object的value,用于存放我们放的值。

为什么要用弱引用?
为了防止内存泄露,如果使用强引用,
当引用ThreadLocal的外部其他对象全部失效后,
ThreadLocalMap内部还引用着ThreadLocal,
导致在GC分析中一直处于可达状态,没办法被回收。

弱引用是Java中四档引用的第三档,比软引用更弱一些,
如果一个对象没有强引用链可达,那么一般活不过下一次GC。
当某个ThreadLocal已经没有强引用可达,则随着它被垃圾回收,
在ThreadLocalMap里对应的Entry的键值会失效,
这为ThreadLocalMap本身的垃圾清理提供了便利。

每一个Thread对应一个ThreadLocalMap映射表,
ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key 的,
弱引用的对象在 GC 时会被回收,value 是真正需要存储的 Object。

ThreadLocalMap使用ThreadLocal的弱引用作为Key,
如果一个ThreadLocal没有外部强引用时,势必会被回收,
这样便会出现Key为null的Entry,
那么我们无法访问到这些Key为null的Entry的value值了

若线程再迟迟不结束,
这些key为null的Entry的value就会一直存在一条强引用链:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

如何防止内存泄漏?
每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
在使用线程池的情况下,没有及时清理ThreadLocal,
不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。
所以,使用ThreadLocal就跟加锁完要解锁一样,用完就需要清理。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XzgHYMHC-1680311422362)(evernotecid://BCE3D193-8584-4CB1-94B3-46FF37A1AC6C/appyinxiangcom/12192613/ENResource/p3993)]
线程的一些局部变量和引用使用的内存属于Stack(栈)区,
而普通的对象是存储在Heap(堆)区。

线程运行时,我们定义的TheadLocal对象被初始化,存储在Heap,
同时线程运行的栈区保存了指向该实例的引用,
也就是图中的ThreadLocalRef。

当ThreadLocal的[set()/get()]被调用时,
JVM会根据当前线程的引用也就是CurrentThreadRef找到其对应在堆区的实例,
然后查看其对用的TheadLocalMap实例是否被创建,
如果没有,则创建并初始化。

Map实例化之后,也就拿到了该ThreadLocalMap的句柄,
那么就可以将当前ThreadLocal对象作为key,进行存取操作。

图中的虚线,表示key对应ThreadLocal实例的引用是个弱引用。

无共享数据

未完待续

线程通信

由于线程的调度与执行存在[随机性/争抢性],
因此需要一些机制来保证线程的协作运行。
如果现在有存钱、取钱两个线程,要求不断重复存钱取钱操作,
存钱后立刻取钱,不允许有两次连续的存钱,
也不允许有两次连续的取钱,
那我们就需要这两个线程之间有通信机制。

等待通知

wait()、notify()、notifyAll()是Object类的三个方法,
任意一个对象都可以调用这三个方法,
但是必须在synchronized范围内并已经获取到synchronize锁,
并需要使用监视器的对象来调用。

方法名 描述
wait() 使当前线程进入等待状态并会释放synchronized锁(也可以设置超时时间,超时后自动恢复),使当前线程进入[等待队列/等待池],直到其他线程调用当前对象监视器的notify()或notifyAll()来唤醒。
notify() 唤醒在当前对象监视器上等待的单个线程(唤醒[等待队列/等待池]中的一个线程),如果当前有多个线程处于等待状态,则会随机唤醒其中一个线程,线程被唤起后等待被CPU调用。
notifyAll() 唤醒在当前对象监视器上等待的所有线程(唤醒对应的[等待队列/等待池]中的全部线程)。
/*** 如果现在有存钱、取钱两个线程,要求不断重复存钱取钱操作,存钱后立刻取钱,* 不允许有两次连续的存钱,也不允许有两次连续的取钱,那我们就需要这两个线程之间有通信机制。**/
public class NotifyDemo1 {public static void main(String[] args) {Item item = new Item();for (int i = 0; i < 10; i++) {new ThreadGet(item,"get"+i).start();new ThreadSave(item,"save"+i).start();}}
}/*** 被两个线程共享的对象所属的类*/
class Item {int count = 0 ;String flag = "save";
}/*** 存钱线程**/
class ThreadSave extends Thread {Item item;public ThreadSave(Item item,String name) {super(name);this.item = item;}public void run() {try {synchronized (item) {if ("get".equals(item.flag)) {item.wait();}if ("save".equals(item.flag)) {item.count++;System.out.println(Thread.currentThread().getName() + " 存钱后,金额=" + item.count);item.flag = "get";item.notifyAll();}}} catch (Exception e) {e.printStackTrace();}}
}
/***    取钱线程*/
class ThreadGet extends Thread {Item item;public ThreadGet(Item item,String name) {super(name);this.item = item;}public void run() {try {synchronized (item) {if ("save".equals(item.flag)) {item.wait();
//                  System.out.println("-------------------"+
//                          Thread.currentThread().getName() + "取钱被唤醒了,此刻的flag是"+item.flag);//取钱线程执行后调用item.notifyAll(),notifyAll()会唤醒所有处于item对象等待队列中的所有线程,即所有处于等待状态的存钱线程与取钱线程。//此刻,如果侥幸让一个取钱线程抢到了锁并执行,就会从item.wait()下面开始执行,在euqals判断时,因为flag=save而导致没有进入到if ("get".equals(item.flag)) 里,//因此这个线程执行完毕后,没有执行任何操作,那这个线程就相当于浪费掉了,save线程也是同理。//这就是所谓的“丢线程”}if ("get".equals(item.flag)) {item.count--;System.out.println(Thread.currentThread().getName() + " 取钱后,金额=" + item.count);item.flag = "save";item.notifyAll();}}} catch (Exception e) {e.printStackTrace();}}
}

注意:
当被唤醒的线程继续执行时,会继续执行object.wait()之后的代码,
而不会重新执行一次当前线程。
因此object.wait()的判断应该尽可能写在前面,如果object.wait()写在了代码的最后,
那即使线程被唤醒也是什么都做不了,因为刚被唤醒就执行结束了。

Condition

如果使用Lock锁而非Synchronized锁,就不存在监视器的概念了,
也不能再使用wait()、notify()、notifyAll()来进行线程通信。
取而代之的是使用Condition类来保证线程通信机制,
使用Condition可以让已得到Lock对象却无法继续执行的线程释放Lock对象,
也可以唤醒其他处于等待中的线程。

await(),signal(),await()是Condition类的方法,需要使用Condition对象来调用。

await():类似于wait(),导致当前线程进入等待状态,
直到其他线程调用Condition对象的signal()或signalAll()来唤醒该线程。

signal():类似于notify(),随机唤醒在当前Lock对象上等待的单个线程。

signalAll():类似于notifyAll(),唤醒在当前Lock对象上等待的全部线程。

public class ConditionDemo1 {public static void main(String[] args) {Bank b = new Bank();for (int i = 0; i < 10; i++) {new Thread(new SaveThread(b)).start();new Thread(new GetThread(b)).start();}}
}class Bank {Integer count = 10;String flag = "save";final Lock lock = new ReentrantLock();final Condition condition = lock.newCondition();public void save() {try {lock.lock();if ("get".equals(flag)) {condition.await();} else {count++;System.out.println(Thread.currentThread().getName() + " 存钱后,金额=" + count);flag = "get";condition.signalAll();}} catch (Exception e) {e.printStackTrace();} finally {lock.unlock();}}public void get() {try {lock.lock();if ("save".equals(flag)) {condition.await();} else {count--;System.out.println(Thread.currentThread().getName() + " 取钱后,金额=" + count);flag = "save";condition.signalAll();}} catch (Exception e) {e.printStackTrace();} finally {lock.unlock();}}
}class SaveThread implements Runnable {Bank bank;public SaveThread(Bank bank) {this.bank = bank;}public void run() {bank.save();}
}class GetThread implements Runnable {Bank bank;public GetThread(Bank bank) {this.bank = bank;}public void run() {bank.get();}
}

其他

线程组ThreadGroup

线程组可以对一批线程进行分类管理,相当于同时控制这批线程,
所有的线程都有指定的线程组。

如果没有显式指定,则线程属于默认线程组。
默认情况下父子线程属于一个线程组。

线程在运行中不能改变所属线程组,线程组允许拥有父线程组。
线程组与线程池并不是一个概念,请不要混淆。

public class ThreadGroupDemo1 {public static void main(String[] args) {//返回主线程所属的线程组ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();System.out.println("线程组名称 " + mainGroup.getName());System.out.println("线程组是否为后台线程 " + mainGroup.isDaemon());System.out.println("线程组的活动线程数目 " + mainGroup.activeCount());//创建新的线程组ThreadGroup newGroup = new ThreadGroup("newGroup");//设置后台线程newGroup.setDaemon(true);//设置线程优先级newGroup.setMaxPriority(Thread.MAX_PRIORITY);//设置异常处理new ThreadDemo(newGroup,"newThread1").start();new ThreadDemo(newGroup,"newThread2").start();//中断所有线程
//      newGroup.interrupt();}
}class ThreadDemo extends Thread {public ThreadDemo(ThreadGroup threadGroup, String name) {super(threadGroup, name);}public void run() {for (int i = 0; i < 100; i++) {System.out.println(this.getName() + " " + i);}}
}

一些代码

// 查看当前JVM进程中的活跃线程数量
Thread.activeCount();

并发包

从另外一个纬度来讲解Java多线程的知识点,
JUC并发包,即java.util.concurrent包,
是JDK自1.5之后引入的核心工具包。

整个JUC包可以大致按照如下划分:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4c2gNQxK-1680311422363)(evernotecid://BCE3D193-8584-4CB1-94B3-46FF37A1AC6C/appyinxiangcom/12192613/ENResource/p3979)]

基础概念

CAS

CAS,全称为Compare and Swap,即比较并替换,
它是一个处理器指令,
保证语义上需要多次操作的行为只通过一条指令便可完成。
这个概念很重要,在多个知识点中均有涉及。

CAS指令需要3个操作数:

  • 内存位置V,Java变量的内存地址;
  • 旧值A;
  • 新值B;

CAS指令执行时,当且仅当旧值A和内存位置V相符合时,
处理器才会用B更新V的值,否则什么都不做,这是一个原子操作。

JDK1.5后,程序中才能使用CAS,
在Java API调用中,与CAS相关的方法有:
compareAndSwap(),compareAndSet(),getAndIncrement()等等。

CAS广泛的被用于并发包的底层代码,例如:原子类,重入锁等等。

https://www.iteye.com/blog/flychao88-2269438
https://www.jianshu.com/p/e13a46e866ae
https://www.cnblogs.com/jiuya/p/10368129.html

在原子类中(例如AtomicBoolean),存在着compareAndSet():

public final boolean compareAndSet(boolean expect, boolean update) {int e = expect ? 1 : 0;int u = update ? 1 : 0;return unsafe.compareAndSwapInt(this, valueOffset, e, u);
}

ABA问题

一个变量V初次读取的时候是A值,
并且在准备赋值的时候检查到它仍然为A值,
如果在这段期间它的值曾经被改成了B,
后来又被改成了A,那CAS操作就会误认为它从来没有改变过。

为了解决ABA问题,J.U.C包提供了一个带标记的原子引用类:
AtomicStampedReference,
它可以通过控制变量值的版本来保证CAS的正确性,
或者可以考虑采用回互斥同步的方式。

AQS

AQS全称为AbstractQueuedSynchronizer,抽象队列同步器。
AQS是一个用于构建锁和其他同步组件的基础框架。

它使用了一个int类型的变量来表示共享资源的同步状态,
并提供了一组基本的操作来改变这个状态,包括获取锁和释放锁。

如果被请求的共享资源的锁还没有被任何线程占用,
就将获得共享资源的线程设置为[有效线程],
然后修改state为锁定状态,其它的线程立即可见。

private volatile int state;protected final int getState() {return state;
}protected final void setState(int newState) {state = newState;
}// 使用CAS方式来更新state
protected final boolean compareAndSetState(int expect, int update) {// See below for intrinsics setup to support thisreturn unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

AQS内部使用了一个FIFO(先进先出)的双向队列来维护获取锁失败的线程,当一个线程试图获取锁时,如果锁已经被其他线程占用,
那么该线程就会进入队列的尾部,等待获取锁,
需要注意的是,获取到锁的线程也在里面作为head节点存在。
队列中的每个节点在源码中使用Node表示,每个Node对应一个线程。

大家一起抢共享资源,抢到的就是有效线程,
放到双向队列的head头节点,没抢到的就依次往后排。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Bn19lN0X-1680311422363)(evernotecid://BCE3D193-8584-4CB1-94B3-46FF37A1AC6C/appyinxiangcom/12192613/ENResource/p3995)]

AQS定义了对[双向队列]所有的操作,
而只开放加锁tryAcquire()和解锁tryRelease()给开发者使用,
开发者可以重写这2个方法以实现自己的并发功能。

AQS提供了一些模板方法,供子类使用,
使得子类可以很容易地实现各种不同类型的锁。
例如:
ReentrantLock、ReentrantReadWriteLock、CountDownLatch、
Semaphore、ThreadPoolExecutor就是使用AQS实现的。

总之,AQS是Java中用于构建同步组件的基础框架,
它提供了同步状态维护和FIFO双向链表的实现,
并提供了一些模板方法供子类使用。

static final class Node {static final Node SHARED = new Node();static final Node EXCLUSIVE = null;// 表示线程获取锁的请求已被取消,// 超时或被中断,进入该状态后的节点不会再发生变化static final int CANCELLED =  1;// 表示线程已经准备好,等待资源释放// 后继节点入队时,会将前驱节点的状态更新为SIGNALstatic final int SIGNAL    = -1;// 表示节点在等待队列中,节点线程等待唤醒// 当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。static final int CONDITION = -2;// 当前线程处在SHARED情况下,该字段才会使用// 共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。static final int PROPAGATE = -3;// 当前节点在队列中的状态,可以被赋值为上面4个intvolatile int waitStatus;// 前驱指针volatile Node prev;// 后继指针volatile Node next;// 处于当前节点的线程volatile Thread thread;// 指向下一个处于CONDITION状态的节点Node nextWaiter;final boolean isShared() {return nextWaiter == SHARED;}// 返回前驱节点,没有的话抛出NPEfinal Node predecessor() throws NullPointerException {Node p = prev;if (p == null)throw new NullPointerException();elsereturn p;}Node() {}Node(Thread thread, Node mode) {this.nextWaiter = mode;this.thread = thread;}Node(Thread thread, int waitStatus) {this.waitStatus = waitStatus;this.thread = thread;}}

公平锁,当多个线程等待一个锁释放时,
必须按照申请锁的时间顺序来依次获得锁,
非公平锁则不用考虑这些。

抢占共享资源的两种方式:公平锁和非公平锁。
以ReentrantLock为例:

公平锁:
保证获取锁的线程按照先来后到的顺序获得锁。
公平锁在获取锁时,首先会进行tryAcquire()操作。
在tryAcquire中,会判断等待队列中是否已经有别的线程在等待了。
如果队列中已经有别的线程,则tryAcquire失败,将自己加入等待队列。
如果队列中没有别的线程,则进行获取锁的操作。

  • 优点:线程按照顺序获取锁,不会出现饿死现象(注:饿死现象是指一个线程的CPU执行时间都被其他线程占用,导致得不到CPU执行;
  • 缺点:整体吞吐效率相对非公平锁来说要低;

非公平锁:
不是先来后到的顺序,后来的可能先获得锁。
非公平锁在获取锁时,会直接抢着去加锁,
如果成功,则获取到锁,如果失败,就加入到等待队列中。

  • 优点:可以减少唤起线程上下文切换的消耗,整体吞吐量比公平锁高;
  • 缺点:在高并发环境下可能造成线程优先级反转和饿死现象;

原子类

该知识点已在本文其他章节描述,此处不再赘述;

并发锁

在早期的JDK版本中,只提供了synchronized、wait、notify、notifyAll等比较底层的多线程同步工具,
我们如果需要更复杂的多线程应用,
一般需要基于JDK提供的这些基础工具进行自定义封装。

JDK1.5之后,
提供了java.util.concurrent.locks这个包来对锁进行更多的补充和增强。

ReentrantLock

该知识点已在本文其他章节描述,此处不再赘述;

ReentrantReadWriteLock

该知识点已在本文其他章节描述,此处不再赘述;

StampedLock

带时间戳的读写锁,不可重入。

并发集合

我们经常会使用类似HashMap、ArrayList这些类,
但他们都是线程不安全的,只能用于单线程环境。

当多线程情况下,可以考虑使用下面这些线程安全的集合类框架,
按照类型划分为Map、List、Set、Queue。

以Concurrent开头与CopyOn开头的类都是线程安全的。
以Concurrent开头的集合类代表支持并发访问的集合,
他们可以支持多个线程并发写入访问,
这些写入线程的所有操作都是线程安全的,
但读取操作不必锁定,
采用更复杂的算法保证永远不会锁住整个集合,
因此在并发写入时拥有较好的性能。

Map

ConcurrentHashMap

高效的、线程安全的HashMap实现

ConcurrentSkipListMap

线程安全的、高效的、有序的map实现

List

CopyOnWriteArrayList

CopyOnWriteArrayList,顾名思义,
每次对数组进行写操作([插入/删除]元素)时,都会伴随一次数组的复制。

线程安全的、读多写少的ArrayList实现。

有点类似于读写分离的概念,
当线程对CopyOnWriteArrayLis执行[写操作]时,
该集合会在底层复制一份新的数组,之后对新数组执行[写操作],
并且会[加锁/阻塞],因此它是线程安全的。
当线程对CopyOnWriteArrayList执行[读操作]时,
线程将会直接读取原数组,不会[加锁/阻塞]。

由于CopyOnWriteArrayList写入时要频繁的复制数组,
性能较差,但由于[读/写]不操作同一个数组,
而且[读操作]也不需要加锁,因此[读操作]比较快,
但如果频繁的[写操作]会导致[读操作]时出现脏数据,
因此CopyOnWriteArrayList适合用在读多写少的场景,例如缓存。

CopyOnWriteArrayList原理
  • CopyOnWriteArrayList底层使用数组实现,添加元素时,
CopyOnWriteArrayList源码
private E get(Object[] a, int index) {return (E) a[index];
}public E get(int index) {return get(getArray(), index);
}public boolean add(E e) {final ReentrantLock lock = this.lock;// 使用ReentrantLock加锁lock.lock();try {// 获取当前数组Object[] elements = getArray();int len = elements.length;// 复制数组,新数组的size比原数组大1Object[] newElements = Arrays.copyOf(elements, len + 1);// 将要添加的元素放在新数组的末尾newElements[len] = e;// 将当前数组替换为新数组setArray(newElements);return true;} finally {// 解锁lock.unlock();}
}final Object[] getArray() {return array;
}final void setArray(Object[] a) {array = a;
}public E remove(int index) {final ReentrantLock lock = this.lock;lock.lock();try {Object[] elements = getArray();int len = elements.length;E oldValue = get(elements, index);int numMoved = len - index - 1;if (numMoved == 0)setArray(Arrays.copyOf(elements, len - 1));else {Object[] newElements = new Object[len - 1];System.arraycopy(elements, 0, newElements, 0, index);System.arraycopy(elements, index + 1, newElements, index,numMoved);setArray(newElements);}return oldValue;} finally {lock.unlock();}
}private static int indexOf(Object o, Object[] elements,int index, int fence) {if (o == null) {for (int i = index; i < fence; i++)if (elements[i] == null)return i;} else {for (int i = index; i < fence; i++)// 定位过程,感觉很lowif (o.equals(elements[i]))return i;}// 找不到就返回-1return -1;
}public boolean remove(Object o) {Object[] snapshot = getArray();// 找到要删除的元素在数组中的位置int index = indexOf(o, snapshot, 0, snapshot.length);return (index < 0) ? false : remove(o, snapshot, index);
}private static boolean eq(Object o1, Object o2) {return (o1 == null) ? o2 == null : o1.equals(o2);
}private boolean remove(Object o, Object[] snapshot, int index) {final ReentrantLock lock = this.lock;lock.lock();try {Object[] current = getArray();int len = current.length;// 重新定位一下index的值,防止误删其他元素  if (snapshot != current) findIndex: {// 找出index与当前数组长度中的最小值,用于遍历int prefix = Math.min(index, len);for (int i = 0; i < prefix; i++) {// 找到错位,重新赋值给index,跳出循环,重新判断if (current[i] != snapshot[i] && eq(o, current[i])) {index = i;break findIndex;}}if (index >= len)// 元素已被删除或不存在return false;if (current[index] == o)// 回马枪break findIndex;index = indexOf(o, current, index, len);if (index < 0)return false;}Object[] newElements = new Object[len - 1];// 复制前半段System.arraycopy(current, 0, newElements, 0, index);// 复制后半段System.arraycopy(current, index + 1,newElements, index,len - index - 1);setArray(newElements);return true;} finally {lock.unlock();}
}

Set

CopyOnWriteArraySet

ConcurrentSkipListSet

线程安全的、高效的、有序的set实现

Queue

BlockingQueue

基于队列的阻塞数据结构,
例如ArrayBlockingQueue和LinkedBlockingQueue

JDK1.5提供了BlockingQueue阻塞队列接口,
阻塞队列经常用于生产者与消费者的应用场景,
当生产者线程试图向BlockingQueue中放入元素时,
如果BlockingQueue已满,则生产者线程被阻塞,
当消费者线程试图从BlockingQueue取出元素时,
如果BlockingQueue已空,则消费者线程被阻塞。

通过两个线程交替向BlockingQueue中[放入/取出]元素,
可以控制线程通信。

BlockingQueue接口有5个实现类:

阻塞队列名称 描述
ArrayBlockingQueue 基于数组实现,有界队列。
LinkedBlockingQueue 基于链表实现,无界队列。
PriorityBlockingQueue 与PriorityQueue类似,该队列调用remove()、poll()、take()等方法取出元素时,并不是取出队列中存在时间最长的元素,而是队列中最小的元素,判断元素的大小根据元素本身大小来自然排序(实现Comparable接口),也可以使用Comparator进行定制排序。
SynchronousQueue 同步队列,对该队列的存取操作必须交替进行,每一个put必须等待一个take操作,否则不能继续添加元素。
DelayQueue 底层基于PriorityBlockingQueue实现,要求元素必须实现Delay接口,该接口有一个getDelay()方法,DelayQueue根据元素的getDelay()返回值进行排序。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6JgWRcE5-1680311422364)(evernotecid://BCE3D193-8584-4CB1-94B3-46FF37A1AC6C/appyinxiangcom/12192613/ENResource/p3977)]

public class BlockingQueueDemo {public static void main(String[] args) throws Exception {BlockingQueue<String> bq = new ArrayBlockingQueue<>(2);new Producer(bq).start();new Producer(bq).start();new Producer(bq).start();new Consumer(bq).start();//生产者必须等待消费者消费后才能继续执行}
}
/*** 生产者*/
class Producer extends Thread{BlockingQueue<String> bq ; public Producer(BlockingQueue<String> bq) {this.bq = bq;}public void run() {for(int i = 0 ; i < 10 ; i++) {try {bq.put("a");} catch (InterruptedException e) {e.printStackTrace();}finally {System.out.println(this.getName()+" 生产完毕"+bq);}}}
}
/*** 消费者*/
class Consumer extends Thread{BlockingQueue<String> bq ; public Consumer(BlockingQueue<String> bq) {this.bq = bq;}public void run() {for(int i = 0 ; i < 10 ; i++) {try {String take = bq.take();System.out.println(this.getName()+" 消费完毕 "+take+" "+bq);} catch (InterruptedException e) {e.printStackTrace();} }}
}

非阻塞队列

ConncurrentLinkedDeque
ConcurrentLinkedQueue

ConcurrentLinkedQueue是Java中并发包中,
一种高效率的线程安全的队列。

它采用了链接节点的方式实现,支持多线程的并发访问。

这个队列采用了非阻塞算法,在队列为空时,
从队列中获取元素的操作不会阻塞。
因此,它非常适用于需要高吞吐量和低延迟的场景。

除了常见的队列操作(例如添加、删除、检查队列是否为空)外,ConcurrentLinkedQueue还提供了一些高级特性,
例如使用CAS(compare-and-swap)操作来提高并发性能。

import java.util.concurrent.ConcurrentLinkedQueue;public class Main {public static void main(String[] args) {ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<>();queue.add(1);queue.add(2);queue.add(3);System.out.println("Queue size: " + queue.size());System.out.println("Removed element: " + queue.poll());System.out.println("Removed element: " + queue.poll());System.out.println("Removed element: " + queue.poll());System.out.println("Queue size: " + queue.size());}
}

并发工具

CountDownLatch

倒数计数器,构造时设定计数值,
当计数值归零后,所有阻塞线程恢复执行,其内部实现了AQS框架。

待确认:
CountDownLatch就像一个门,门上有N把锁,只有当锁同时都打开,
我们才能开门。这里这个门是一次性的,
用完之后不能重新再给这个门上锁,
为什么我要强调一次性呢?
因为下面还有不是一次性的CyclicBarrier。

CountDownLatch,使一个线程等待其他线程都达到相应的状态再执行,
完成工作的主要方法就是await()、countDown()。

内部是通过一个计数器实现的,计数器的初始值就是要等待线程的数量,
每当一个线程执行完毕调用countDown()之后计数器数值减一,
计数器数值变为0的时候,表示所有线程执行完毕,
调用await()方法等待的线程便会恢复工作。

CountDownLatch原理

CountDownLatch内部的主要方法是通过AQS来实现的,
AQS是一个多线程访问共享资源的同步器框架。

资源共享的方式有两种,即独占Exclusive和共享Share;
独占即只有一个线程能够执行,控制并发安全,例如ReentrantLock,
共享即多个线程可以同时执行,比如我们现在说的CountDownLatch。

CountDownLatch内部通过维护了一个被volatile修饰的state共享资源和一个FIFO线程等待队列来实现的,
自定义同步器时只需要实现共享资源state的获取和释放的方式即可。

CyclicBarrier

CyclicBarrier,循环栅栏,构造时设定等待线程数,
当所有线程都到达栅栏后,栅栏放行;

其内部通过ReentrantLock和Condition实现同步。

实现所有的线程一起等待某个事件的发生,
当某个事件发生时,所有线程一起开始往下执行。

待确认:
上面说到了一次性的锁,有时我们可能需要重复设置这个锁,
CyclicBarrier可以满足这个场景,
这个工具类是可以重复使用的,
通过reset来设置。

和CountDownLatch不同的是,
CyclicBarrier在指定数量的线程到达之前必须互相等待,
也是因为在等待的线程被释放之后可以重复使用。

在网上看到的一种说法叫做人满发车,很合适,
车没有满的时候车上乘客需要等待,
车到达目的地之后再返回出发点,重新等待发车。

CyclicBarrier内部是通过一个ReentrantLock的锁对象来控制的,
基于Condition条件队列来对线程进行阻塞。

内部也是一个计数器,每当线程到达屏障点的时候调用await()将自己阻塞,
计数器减一,
当计数器变为0的时候所有因为调用await()方法阻塞的线程都会被唤醒。

CyclicBarrier和CountDownLatch的区别:
两个看上去有点像的类,都在java.util.concurrent下,
都可以用来表示代码运行到某个点上,二者的区别在于:

  • CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch则不是,某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行;
  • CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务;
  • CyclicBarrier可重用,CountDownLatch不可重用;

Semaphore

信号量,类似于“令牌”,用于控制共享资源的访问数量;
其内部实现了AQS框架。

使用场景:
主要用于限流,可通过发放一定数量的许可来控制并发数量。

待确认:
我们在商场找停车位,车位一般是固定的,车位属于共享资源,
更多的车子可能会对这些固定数量的车位进行“抢夺”。
初始化的时候需要为这个许可集传入一个数值,
这个数值代表同一时刻能够访问共享资源的线程数量,
线程通过acquire()获得一个许可,然后对共享资源进行操作,
如果许可集分配完了,线程进入等待状态,
直到其他线程释放许可才有机会获得许可。

即初始化停车位数量(许可集),每个车子代表一个线程,
进入停车场会获得一个许可,占用共享资源。
一旦停车位全部被占,未分配到车位的车子进入等待状态,
等其它车子释放车位才有机会获得车位。

Semaphore内部也是通过AQS实现的,
当许可集的数量设置成1的时候可以来做一个互斥锁。

Exchanger

交换器,类似于双向栅栏,用于线程之间的配对和数据交换;
其内部根据并发情况有“单槽交换”和“多槽交换”之分。

待确认:
Exchanger,就是提供了两个线程互相交换数据的同步点,
即一个线程完成一定的事务之后想要和另一个线程交换数据,
则需要拿出数据,等待另一个线程的到来。

Exchanger,交换者,是一个用于线程间协作的工具类。
Exchanger用于进行线程间的数据交换。它提供一个同步点,在这个同步点两个线程可以交换彼此的数据。这两个线程通过exchange()交换数据,
如果第1个线程先执行exchange(),
它会一直等待第2个线程也执行exchange(),当两个线程都到达同步点时,
这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。

因此使用Exchanger的重点是[成对的]线程使用exchange(),当有一对线程达到了同步点,就会进行交换数据。因此该工具类的线程对象是[成对的]。

Phaser

多阶段栅栏,相当于CyclicBarrier的升级版,
可用于分阶段任务的并发控制执行;
其内部比较复杂,支持树形结构,以减少并发带来的竞争。

线程池

从自己整理的面试攻略上同步

线程池避免频繁创建销毁线程,减少系统开销,

不用以前的start()来启动线程,而是直接提交给线程池即可运行线程。

需要根据不同的业务划分不同的线程池,不然会存在一些耗时的业务影响了另一个业务导致这个业务崩了,然后都崩了的情况,所以要做好线程池隔离。

线程池是用于使用多线程的情况下,
用于减小经常创建和销毁线程这些造成的资源消耗,
线程池会提供一些活跃线程供用户随时取用;

也可以在新建ExecutorService时传入线程工厂ThreadFactory对象,
用来创建线程。

例子:


抛异常没有捕获的话可能会导致定时任务执行失灵,
详见:https://blog.csdn.net/qq_31279701/article/details/123352279

新线程的创建成本较高,使用线程池可以提高性能,
尤其是需要创建大量生命周期很短的线程时。
此外线程池能够有效的控制并发线程数量(最大线程数)。

名称 描述
Executor接口 线程池里的顶级接口,里面只有一个execute(Runnable command)
ExecutorService接口 继承Executor接口
AbstractExecutorService类 抽象类,实现ExecutorService接口
ThreadPoolExecutor类 继承AbstractExecutorService类
Executors类 工具类,调用ThreadPoolExecutor类,提供一些常用的线程池供用户调用

Executors

从JDK1.5开始支持线程池,
主要使用Executors工厂类来创建线程池,
有如下方法可以创建:

创建线程池方法 描述
newCachedThreadPool() 创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。线程被缓存在线程池中,返回ExecutorService对象。
newFixedThreadPool(int) 创建固定大小的线程池,具有固定线程数,可控制线程最大并发数,超出的线程会在队列中等待,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。返回ExecutorService对象。
newSingleThreadExecutor() 创建一个单线程的线程池,只有一个线程在执行任务,如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行,返回ExecutorService对象。
newScheduledThreadPool(int) 创建固定大小的线程池,可以指定线程数,可以指定延迟时间执行线程,即使线程是空闲的也会保存在线程池中,支持定时以及周期性执行任务,返回ScheduledExecutorService对象。
newSingleThreadScheduledExecutor() 只有单线程的线程池,可以指定延迟时间执行线程。返回ScheduledExecutorService对象。

Java 5+中的Executor接口定义一个执行线程的工具。
它的子类型即线程池接口是ExecutorService。

ThreadPoolExecutor

ThreadPoolExecutor是线程池常见实现类,
在构造时需要提供池大小等参数。

构造函数:

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler) {this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,Executors.defaultThreadFactory(), handler);}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uqgdWQBN-1680311422365)(evernotecid://BCE3D193-8584-4CB1-94B3-46FF37A1AC6C/appyinxiangcom/12192613/ENResource/p3999)]

线程池把任务的提交和任务的执行剥离开来,
使用懒加载机制(不是刚开始就创建全部线程),
当一个任务被提交到线程池之后:

  • 如果此时线程数小于[核心线程数],那么就会新建一个线程来执行任务,即使线程池中的所有线程都处于空闲状态也是如此;
  • 如果此时线程数大于[核心线程数],那么就会将任务塞入[任务队列]中,等待被执行;
  • 如果[任务队列]满了,并且此时线程数小于[最大线程数],那么会新建一个线程来执行任务;
  • 如果[任务队列]满了,并且线程数大于[最大线程数],那么会执行[拒绝策略];

我们向线程提交任务时可以使用execute()和submit(),
区别就是submit()可以返回一个Future对象,
通过Future对象可以了解任务执行情况,
可以取消任务的执行,还可获取执行结果或执行异常。
submit()最终也是通过execute()执行的。

默认任务拒绝策略(对应RejectedExecutionHandler接口的实现类):

  • ThreadPoolExecutor.AbortPolicy,终止策略(直接抛出异常终止);
  • ThreadPoolExecutor.CallerRunsPolicy,当任务添加到线程池被拒绝时,使用向线程池提交任务的那个线程处理被拒绝的任务;
  • ThreadPoolExecutor.DiscardPolicy,丢弃策略;
  • ThreadPoolExecutor.DiscardOldestPolicy,丢弃最老的策略;

handler

ThreadPoolExecutor源码

基于JDK8

public class ThreadPoolExecutor extends AbstractExecutorService {public void setCorePoolSize(int corePoolSize) {if (corePoolSize < 0)throw new IllegalArgumentException();// 计算当前corePoolSize与想要新设定的corePoolSize之间的差值int delta = corePoolSize - this.corePoolSize;// 设置新的corePoolSizethis.corePoolSize = corePoolSize;// 如果当前线程池中的线程数多于新corePoolSize,则回收多余的线程if (workerCountOf(ctl.get()) > corePoolSize)interruptIdleWorkers();else if (delta > 0) {// 如果当前线程池中的线程数少于新corePoolSize,并且当新值大于旧值时,看差值与工作队列中的任务数谁更小int k = Math.min(delta, workQueue.size());// 增加k个线程,如果在新增时工作队列为空的话就停止新增// 此时线程池内部是否需要这么多线程是不确定的,那么就按工作队列里面的任务数来,直接按任务数立刻新增线程,当任务队列为空了之后就终止新增,类似于懒加载机制。while (k-- > 0 && addWorker(null, true)) {if (workQueue.isEmpty())break;}}}public void setMaximumPoolSize(int maximumPoolSize) {if (maximumPoolSize <= 0 || maximumPoolSize < corePoolSize)throw new IllegalArgumentException();this.maximumPoolSize = maximumPoolSize;if (workerCountOf(ctl.get()) > maximumPoolSize)interruptIdleWorkers();}
}

ScheduledThreadPoolExecutor

ExecutorCompletionService

ForkJoinPool

JDK1.7提供了ForkJoinPool来支持将一个大任务分解成多个小任务来进行并行计算,
再把多个小任务的结果合并成总的计算结果。

Fork/Join框架类似于Hadoop框架的一种思想,
拆分处理再合并的思想。

Future

Future则是多线程中常用的一种异步执行任务的机制,
并在需要的时候取得结果。


源码

  • Thread类实现了Runnable接口;
  • 所有要被线程执行的类都需要实现Runnable接口并重写run();

练习

使用3个线程,要求三个线程顺序执行,
不允许使用sleep()强制让线程有顺序。(京东面试题)

线程A输出1、2、3,
线程B输出4、5、6,
线程C输出7、8、9,
线程A输出10、11、12,
线程B输出13、14、15,
以此类推,一直输出到1000为止。


总结

未完待续

明翰Java教学系列之多线程篇V0.2(持续更新)相关推荐

  1. 明翰英语教学系列之动词篇V0.3

    文章目录 传送门 前言 1. 动词概念 2. `动词的分类` 2.1 按照功能分类 2.1.1 实义动词&行为动词 2.1.2 `系动词&联系动词` 系动词的分类 `状态系动词` `感 ...

  2. 明翰英语教学系列之数词篇V0.2(持续更新)

    文章目录 传送门 什么是数词 基数词 基数词的单复数 序数词 数词应用 `表达[日期/时间]` 日期 年 月.日 年.月.日 `时间` 直接表达 间接表达 分数 小数 百分数 钱币 长度单位 重量单位 ...

  3. 明翰Java教学系列之认识Java篇V1.3(持续更新)

    文章目录 传送门 前言 什么是Java? Java之父 `Java的应用场景` Java部分特点 Java工作机制 JDK(Java Development Kit) JRE(Java Runtime ...

  4. 明翰英语教学系列之方法篇

    文章目录 传送门 前言 `学习路径&知识体系` 学习技巧 `构词规律` `词汇的偏旁部首` 词义前缀 否定前缀 `in-` mis- dis- de- un- con前缀 out前缀 sur前 ...

  5. 明翰Java教学系列之集合框架篇V0.2(持续更新)

    文章目录 传送门 前言 什么是集合框架 集合框架体系 Collection接口 `Set接口` `HashSet` LinkedHashSet TreeSet EnumSet Queue接口 Prio ...

  6. 明翰Java教学系列之进阶面向对象篇

    复习 1.Java有多少种数据类型,数据类型的分类?数据类型的大小排序?默认类型?默认值? 2.Java的工作机制? 3.Java中有多少种初始化数组的方式? 4.什么是变量,如何定义变量?Java中 ...

  7. 明翰英语教学系列之雅思阅读篇V0.9(持续更新)

    文章目录 传送门 6. 阅读 READING 6.1 阅读评分标准 6.2 阅读题型 `6.2.1 阅读填空题` `摘要填空题(Summary)` `无选项摘要填空` 1. 找定位词 2. 确定答案词 ...

  8. 明翰英语教学系列之时态与语态篇

    文章目录 前言 时态 现在 `一般现在时(Simple Present)` `现在进行时(Present Continuous)` `现在完成时(Present Perfect)` 现在完成进行时(P ...

  9. 明翰英语教学系列之冠词篇

    文章目录 前言 定冠词 定冠词的用法 1. `定冠词的特指用法` 1.1 `某个或者某些特定的人或物` 1.2 `上文提过的人或物(第二次提到)` 1.3 双方彼此都知道的人或物(默认) 1.4 形容 ...

最新文章

  1. easy-x库graphics.h图形库安装
  2. LaTeX 第五课:数学公式排版
  3. E0144“const char *“ 类型的值不能用于初始化 “char *“ 类型的实体
  4. 1.19 实例:Java求数组元素的最大和最小值
  5. 我们相信加密! 教程
  6. 蚂蚁集团上市 员工身价暴涨人均一套房?支付宝:没有 但会努力的
  7. Golang 接口相等比较注意要点
  8. PASCAL VOC 2012 and SBD (the augment dataset) 总结
  9. 2021 年 8 月程序员工资出炉啦!北京以18904元位居榜首
  10. 2013-2015阿里双十一技术网络文章总结
  11. 读书感受 之 《如何说客户才会听,怎么听客户才肯说》
  12. Mybatis---简单缓存了解
  13. 如何简单可靠地装系统-软碟通
  14. 有追求的品牌都应该去B站
  15. [mybatis] sql语句无错误,但是执行多条sql语句时,抛出java.sql.SQLSyntaxErrorException...
  16. 科学计算基础软件包NumPy入门讲座(4):操作数组
  17. 张丽俊最新演讲:要像竹子一样扎根,你终会一飞冲天
  18. C语言写个简单的串口调试助手
  19. YYKit - YYModel 使用方法
  20. coolwulf的乳腺癌网站介绍和操作方式

热门文章

  1. 及时雨计算机维修好吗,移动PC亟待解决几大问题!万幸有了这个“及时雨”
  2. python pandas 数据探索
  3. DeepMind再登Nature封面!推出AlphaTensor:强化学习发现矩阵乘法算法
  4. 一个人只有敢于承担责任,才有可能被赋予更大的责任。做不
  5. 【入侵检测】5.27quiz
  6. mac改变软件不跟随系统颜色模式
  7. 关于海康,宇视,天地伟业摄像头调试
  8. 小米8android系统版本,小米8系列获得Android P稳定版推送
  9. 《优雅是女人最美的外衣》-欧石楠
  10. 设计模式系列,六大设计原则