Java 并发 - 理论基础
为什么需要多线程④
CPU、内存、I/O 设备的速度是有极大差异的,为了合理利用 CPU 的高性能
CPU 增加了缓存,导致 可见性问题
操作系统增加了进程、线程,以分时复用 CPU, 导致原子性问题
编译程序优化指令执行次序,导致 有序性问题
并发出现问题的根源
并发三要素
可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到。
原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
有序性:即程序执行的顺序按照代码的先后顺序执行
编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新 安排语句的执行顺序。
指令级并行的重排序
内存系统的重排序
JAVA是怎么解决并发问题
- 可以通过什么保证原子性,可见性,有序性
- JMM是通过什么来保证有序性的
Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法
方法包括:
volatile、synchronized 和 final 三个关键字
共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存
通过volatile关键字来保证一定的“有序性”
Happens-Before 规则
可以通过synchronized和Lock保证原子性,可见性,有序性
JMM是通过Happens-Before 规则来保证有序性的
Happens-Before 规则
- 单一线程原则
在一个线程内,在程序前面的操作先行发生于后面的操作。
- 管程锁定规则
一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。
- volatile 变量规则
对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
- 线程启动规则
Thread 对象的 start() 方法调用先行发生于此线程的每一个动作。
- 线程加入规则
Thread 对象的结束早于 join() 方法返回。
A线程调用B线程.join,等B线程结束唤醒A
- 对象终结规则
一个对象的初始化完成早于它 finalize() 方法的开始。
- 线程中断规则
对线程 interrupt() 早于interrupted() 方法
- 传递性
A早于B,B早于C,A早于C
线程安全
- 可以将共享数据按照安全程度的强弱顺序分成以下五类:
不可变、绝对线程安全、相对线程安全、线程兼容和线程对立
- 不可变
多线程环境下,应当尽量使对象成为不可变,来满足线程安全
不可变的类型
final修饰的基本数据类型
String
枚举类型
Number 部分子类(Long 和 Double 等数值包装类型,BigInteger 和 BigDecimal 等大数据类型)
对于集合类型,可以使用什么方法来获取一个不可变的集合
Collections.unmodifiableXXX()
对原始的集合进行拷贝,需要对集合进行修改的方法都直接抛出异常。
- 绝对线程安全
不管运行时环境如何,调用者都不需要任何额外的同步措施
- 相对线程安全
保证对这个对象单独的操作是线程安全,调用不用做额外措施,但特殊连续调用需要同步手段
Vector、HashTable、Collections 的 synchronizedCollection() 方法包装的集合
- 线程兼容
对象本身并不是线程安全,通过同步手段可以同步,例 ArrayList 和 HashMap
- 线程对立
怎么都不能同步
线程安全的实现方法
互斥同步
- 最大问题
线程阻塞和唤醒所带来的性能问题
- 属于什么的并发策略,哪些操作
加锁,用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒
- synchronized 和 ReentrantLock
非阻塞同步
CAS
- 属于什么类
Unsafe 类
- 属于什么类
- 是什么
操作和冲突检测原子操作
检测到冲突
不断地重试,直到成功为止
实现
CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B
AtomicInteger
- compareAndSet()
- getAndIncrement()
ABA
变量初次读取是 A ,被改成 B,后来又改回 A,那 CAS 操作就认为它没有被改过
J.U.C 包提供了一个带有标记的类 什么来解决这个问题
AtomicStampedReference
通过控制变量值的版本来保证 CAS 的正确性
- 解决 ABA 问题,改用什么可能会比原子类更高效
传统的互斥同步
无同步方案
- 栈封闭
多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的
- 线程本地存储
使用 java.lang.ThreadLocal 类来实现线程本地存储功能
每个 Thread 都有一个 ThreadLocal.ThreadLocalMap 对象,Thread 类中就定义了 ThreadLocal.ThreadLocalMap 成员
- 可重入代码
可在任意时刻中断去执行其他,再接着执行不会出错
不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法
Java 并发 - 线程基础
线程状态转换
- 新建(New)
新建未启动
- 可运行(Runnable)② ``` 可能正在运行,也可能正在等待 CPU 时间片。
包含了操作系统线程状态中的 Running 和 Ready。
-
阻塞(Blocking)
-
无限期等待(Waiting)
-
进入方法和退出方法
1.没有设置 Timeout 参数的 Object.wait() 方法 Object.notify() / Object.notifyAll() 2.没有设置 Timeout 参数的 Thread.join() 方法 被调用的线程执行完毕 3.LockSupport.park() 方法
-
是什么
-
限期等待(Timed Waiting)
-
是什么
-
进入方法和退出方法
1.Thread.sleep() 方法 时间结束 2.设置了 Timeout 参数的 Object.wait() 方法 时间结束 / Object.notify() / Object.notifyAll() 3.设置了 Timeout 参数的 Thread.join() 方法 时间结束 / 被调用的线程执行完毕 4.LockSupport.parkNanos() 方法 5.LockSupport.parkUntil() 方法
-
睡眠和挂起是用来描述,而阻塞和等待用来描述(主动和被动)。
-
死亡(Terminated)
- 二种情况
<a name="a17fcdd0"></a>
## 线程使用方式
-
实现 Runnable 接口
-
实现 Callable 接口
- 与 Runnable 相比,Callable 可以有什么,返回值通过。
与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。
-
继承 Thread 类
-
需要实现,因为
需要实现 run() 方法,因为 Thread 类也实现了 Runable 接口
-
当调用 start() 方法启动一个线程时
虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的 run() 方法
-
实现接口会更好一些,因为
Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口; 类可能只要求可执行就行,继承整个 Thread 类开销过大。
<a name="5d01373d"></a>
## 基础线程机制
-
Executor
-
是什么
Executor管理多个异步任务的执行,无需程序员显式地管理线程的生命周期,异步就是多个任务互不干扰,不用同步.
-
三种Executor
CachedThreadPool: 一个任务创建一个线程 FixedThreadPool: 所有任务只能使用固定大小的线程 SingleThreadExecutor: 相当于大小为 1 的 FixedThreadPool
-
Daemon
-
是什么
程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分
-
当所有非守护线程结束时
程序也就终止,同时会杀死所有守护线程
-
main() 属于
非守护线程
-
使用什么 方法将一个线程设置为守护线程
使用 setDaemon() 方法将一个线程设置为守护线程
-
sleep()
-
使用方法,需要
Thread.sleep(millisec) 方法,sleep() 可能会抛出 InterruptedException,需要在本地进行处理
-
yield()
重要的已经执行完,可以切换到其他线程,只是对线程调度器的一个建议
<a name="aada9e72"></a>
## 线程中断
-
什么时候会中断
程序结束或异常时
-
InterruptedExcept
调用 interrupt() 中断线程 线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,提前结束该线程 不能中断 I/O 阻塞和 synchronized 锁阻塞
-
interrupted()
线程run()执行无限循环操作 没有执行 sleep() 等会抛出 InterruptedException 的操作 interrupt() 方法就无法使线程提前结束 但interrupt() 方法会设置线程的中断标记,调用 interrupted() 方法会返回 true
-
Executor 的中断操作
-
shutdown() 方法
等待线程都执行完毕之后再关闭
-
shutdownNow()
调用每个线程的 interrupt() 方法
-
只想中断 Executor 中的一个线程
使用 submit() 方法来提交一个线程,它会返回一个 Future<?> 对象 调用该对象的 cancel(true) 方法
<a name="9d7ab9a6"></a>
## 线程互斥同步
-
两种锁机制
控制多个线程对共享资源的互斥访问 JVM 实现的 synchronized JDK 实现的 ReentrantLock
-
synchronized
只作用于同一个对象
-
同步一个代码块
-
同步一个方法
同步一个类public synchronized void func () { // … }
-
同步一个类
public void func() { synchronized (SynchronizedExample.class) { // … } } 两个线程调用同一个类的不同对象上的这种同步语句,也会进行同步
-
同步一个静态方法
public synchronized static void fun() { // … }
-
ReentrantLock
- 是什么
是 java.util.concurrent(J.U.C)包中的锁
-
比较
-
锁的实现
-
性能
新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同
-
等待可中断
ReentrantLock 可中断,而 synchronized 不行
-
公平锁
多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁 synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的
-
锁绑定多个条件
一个 ReentrantLock 可以同时绑定多个 Condition 对象
-
使用选择
除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized synchronized原生支持,ReentrantLock 不是所有的 JDK 版本都支持 使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放
<a name="7c1ae05f"></a>
## 线程之间的协作
-
join()
在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束
-
wait() notify() notifyAll()
- 属于
- 只能用在
-
wait() 和 sleep() 的区别
wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法; wait() 会释放锁,sleep() 不会。
-
await() signal() signalAll()
-
在哪调用
java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调 可以在 Condition 上调用 await() 方法使线程等 其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程 相比于 wait() 这种等待方式,await() 可以指定等待的条件
private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); condition.await(); condition.signalAll();
<a name="b605494c"></a>
# synchronized详解
<a name="2f543f28"></a>
## Synchronized的使用
-
synchronized是通过实现的
软件(JVM)
-
注意
- 一把锁只能
- 实例对象的锁对象
- *.class或synchronized修饰的static方法锁对象
- synchronized修饰的方法结束时②
- 锁对象不能
- 作用域
- 同步时选择
1.一把锁只能同时被一个线程获取 2.实例对象都有自己的锁(this),锁对象是*.class或synchronized修饰的是static方法,所有对象公用一把锁 3.synchronized修饰的方法,正常结束或异常结束都会释放锁 4.锁对象不能为空,因为锁的信息都保存在对象头里 5.作用域不宜过大,影响程序执行的速度,控制范围过大,编写代码也容易出错 6.在能选择的情况下,既不要用Lock也不要用synchronized关键字,用java.util.concurrent包中的各种各样的类,如果不用该包下的类,在满足业务的情况下,可以使用synchronized关键字,因为代码量少,避免出错
-
对象锁②
方法锁(默认锁对象为this) 同步代码块锁(自己指定锁对象)
-
类锁
synchronize修饰静态的方法或指定锁对象为Class对象
<a name="d10931c0"></a>
## Synchronized原理分析
-
加锁和释放锁的原理
Monitorenter和Monitorexit指令会让对象在执行,使其锁计数器加1或者减1 每一个对象在同一时间只与一个monitor(锁)相关联,而一个monitor在同一时间只能被一个线程获得 一个对象在尝试获得与这个对象相关联的Monitor锁的所有权的时候,monitorenter指令会发生如下3中情况之一: monitor计数器为0, +1 如果这个monitor已经拿到了这个锁的所有权,又重入了这把锁, +1 这把锁已经被别的线程获取了,等待锁释放 monitorexit指令:monitor的计数器-1
![](https://pdai.tech/_images/thread/java-thread-x-key-schronized-2.png#alt=img)
-
可重入原理
如果这个monitor已经拿到了这个锁的所有权,又重入了这把锁, +1
-
保证可见性的原理
在释放锁之前一定会将数据写回主内存 在获取锁之后一定从主内存中读取数据
<a name="af719013"></a>
## JVM中锁的优化
-
JDK锁的优化
JDK1.6以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁”。
-
JVM中monitorenter和monitorexit字节码
JVM中monitorenter和monitorexit字节码依赖于底层的操作系统的Mutex Lock来实现
-
锁优化⑤
-
锁的类型
- 可以
- 锁膨胀方向
-
自旋锁与自适应自旋锁
- 默认的自旋次数
-
锁消除
虚拟机即时编译器对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除 例:Java API中有很多方法 例:String是一个不可变类,对字符串的连接操作总是通过生成的新的String对象来进行的, 因此Javac编译器会对String连接做自动优化。在JDK 1.5之前会使用StringBuffer对象的连续append()操作,在JDK 1.5及以后的版本中,会转化为StringBuidler对象的连续append()操作。
-
锁粗化
-
轻量级锁
-
轻量级锁加锁
1.JVM在线程栈帧中创建锁记录Lock Record(锁记录用来存储锁对象目前的Mark Word拷贝,对象Mark Word标记字段为01表示未锁) 2.CAS操作把锁对象目前的Mark Word拷贝到线程栈桢锁记录 3.将Mark Word更新为指向锁记录的指针,把对象Mark Word标记字段更新为00
![](https://pdai.tech/_images/thread/java-thread-x-key-schronized-7.png#alt=img)
-
偏向锁
-
偏向锁的撤销
<br />![](https://pdai.tech/_images/thread/java-thread-x-key-schronized-8.png#alt=img)
![](https://pdai.tech/_images/thread/java-thread-x-key-schronized-9.png#alt=img)
- 锁的优缺点对比
- 锁 优点 缺点 使用场景
<a name="00399da4"></a>
## Synchronized与Lock
-
synchronized的缺陷
1.效率低 2.不够灵活 3.无法知道是否成功获得锁
-
Lock解决相应问题
-
4个方法
-
Synchronized只有锁只与一个条件(是否获取锁)相关联,不灵活解决办法
-
多线程竞争一个锁时,其余未得到锁的线程只能不停的尝试获得锁,而不能中断
ReentrantLock的lockInterruptibly()方法可以优先考虑响应中断 一个线程等待时间过长,它可以中断自己,然后ReentrantLock响应这个中断,不再让这个线程继续等待 有了这个机制,使用ReentrantLock时就不会像synchronized那样产生死锁了。
<a name="b44656a7"></a>
# 关键字: volatile详解
<a name="8c0f0a87"></a>
## volatile的作用详解
-
防重排序
防止指令重排序
实例化一个对象其实可以分为三个步骤 分配内存空间。 初始化对象。 将内存空间的地址赋值给对应的引用
操作系统可以对指令进行重排序,可能变成 分配内存空间。 将内存空间的地址赋值给对应的引用。 初始化对象
-
实现可见性
-
保证原子性:单次读/写
-
共享的long和double变量的为什么要用volatile?
目前各种平台下的商用虚拟机都选择把 64 位数据的读写操作作为原子操作来对待,因此我们在编写代码时一般不把long 和 double 变量专门声明为 volatile多数情况下也是不会错的。
<a name="94e9f561"></a>
## volatile 的实现原理
-
volatile 可见性实现
-
volatile 变量的内存可见性是基于
volatile 变量的内存可见性是基于内存屏障(Memory Barrier)实现
内存屏障,又称内存栅栏,是一个 CPU 指令。 在程序运行时,为了提高执行性能,编译器和处理器会对指令进行重排序,JMM 为了保证在不同的编译器和 CPU 上有相同的结果,通过插入特定类型的内存屏障来禁止+ 特定类型的编译器重排序和处理器重排序,插入一条内存屏障会告诉编译器和 CPU:不管什么指令都不能和这条 Memory Barrier 指令重排序
-
在 volatile 修饰的共享变量进行写操作的时候
对声明了 volatile 的变量进行写操作,JVM 就会向处理器发送一条 lock 前缀的指令,将这个变量所在缓存行的数据写回到系统内存 在 volatile 修饰的共享变量进行写操作的时候会多出 lock 前缀的指令 0x000000000295158c: lock cmpxchg %rdi,(%rdx)
-
lock 前缀的指令在多核处理器下会引发两件事情
将当前处理器缓存行的数据写回到系统内存。 写回内存的操作会使在其他 CPU 里缓存了该内存地址的额数据无效
-
lock 指令
lock 前缀会使处理器执行当前指令时产生一个 LOCK# 信号,会对总线进行锁定 后来由高速缓存锁代替总线锁来处理
-
volatile 有序性实现
-
volatile 的 happens-before 关系
对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。
-
volatile 禁止重排序
<br />内存屏障指令
内存屏障指令 1.StoreStore 屏障 禁止上面的普通写和下面的 volatile 写重排序。 2.StoreLoad 屏障 防止上面的 volatile 写与下面可能有的 volatile 读/写重排序。 3.LoadLoad 屏障 禁止下面所有的普通读操作和上面的 volatile 读重排序。 4.LoadStore 屏障 禁止下面所有的普通写操作和上面的 volatile 读重排序。
![](https://pdai.tech/_images/thread/java-thread-x-key-volatile-3.png#alt=img)
![](https://pdai.tech/_images/thread/java-thread-x-key-volatile-4.png#alt=img)
<a name="710e0c96"></a>
## volatile 的应用场景
-
使用 volatile 必须具备的条件
对变量的写操作不依赖于当前值。 该变量没有包含在具有其他变量的不变式中。 只有在状态真正独立于程序内其他内容时才能使用 volatile。
-
模式1:状态标志
-
模式2:一次性安全发布
-
模式3:独立观察
-
模式4:volatile bean 模式
-
模式5:开销较低的读-写锁策略
-
模式6:双重检查
<a name="676075dd"></a>
# 关键字: final详解
<a name="588b95d1"></a>
## final基础使用
-
修饰类
-
继承
-
final类中的所有方法
-
final类型的类如何拓展
组合 在新类中创建旧类的实例对象调用实例方法.
-
修饰方法
- private 方法
- final方法可以
-
修饰参数
- 无法在方法中
- 主要用来
-
修饰变量
-
所有的final修饰的字段都是编译期常量吗
不是 Random r = new Random(); final int k = r.nextInt();
-
static final
- 只占据
- 赋值时机
- 属于这个类
-
blank final
- 赋值时机
<a name="f754dbac"></a>
## final域重排序规则
-
final域为基本类型
-
写final域重排序规则②
禁止对final域的写重排序到构造函数之外
-
读final域重排序规则
在读一个对象的final域之前,一定会先读这个包含这个final域的对象的引用
-
final域为引用类型
-
对final修饰的对象的成员域写操作
构造方法中引用类型赋值完后才能将构造出的对象赋值给应用
-
对final修饰的对象的成员域读操作
只有构造方法中的写入先于其他的读操作
<a name="43607421"></a>
## final再深入理解
-
final的实现原理②
写final域会要求编译器在final域写之后,构造函数返回前插入一个StoreStore屏障。读final域的重排序规则会要求编译器在读final域的操作前插入一个LoadLoad屏障。
-
为什么final引用不能从构造函数中“溢出”
public FinalReferenceEscapeDemo() { a = 1; //1 referenceDemo = this; //2 }
本来在引用对象对所有线程可见时,其final域已经完全初始化成功。 但引用对象this溢出,读取对象时可能没初始化完
-
使用 final 的限制条件和局限性
-
当声明一个 final 成员时,必须
当声明一个 final 成员时,必须在构造函数退出前设置它的值
-
final指向引用对象
<a name="ed6380ef"></a>
# JUC - 类汇总和学习指南
<a name="81012872"></a>
## JUC主要包含哪几部分
<a name="f4298388"></a>
## Lock框架和Tools类
-
接口: Condition
Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用
-
接口: Lock
Lock实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作接口:
-
接口: ReadWriteLock
维护了一对相关的锁,一个用于只读操作,另一个用于写入操作 只要没有 writer,读取锁可以由多个 reader 线程同时保持 写入锁是独占的
-
核心抽象类(int): AbstractQueuedSynchonizer
实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供一个框架
-
锁常用类: LockSupport
用来创建锁和其他同步类的基本线程阻塞原语 功能和”Thread中的 Thread.suspend()和Thread.resume()有点类似” LockSupport中的park() 和 unpark() 的作用分别是阻塞线程和解除阻塞线程 但是park()和unpark()不会遇到“Thread.suspend 和 Thread.resume所可能引发的死锁”问题。
-
锁常用类: ReentrantLock
可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大
-
锁常用类: ReentrantReadWriteLock
ReentrantReadWriteLock是读写锁接口ReadWriteLock的实现类,它包括Lock子类ReadLock和WriteLock。ReadLock是共享锁,WriteLock是独占锁。
-
锁常用类: StampedLock
java8在java.util.concurrent.locks新增的一个API StampedLock控制锁有三种模式(写,读,乐观读)
-
工具常用类: CountDownLatch
同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待
-
工具常用类: CyclicBarrier
同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点
-
工具常用类: Phaser
JDK 7新增的一个同步辅助类,它可以实现CyclicBarrier和CountDownLatch类似的功能,而且它支持对任务的动态调整,并支持分层结构来达到更高的吞吐量。
-
工具常用类: Semaphore
-
工具常用类: Exchanger
线程协作的工具类, 主要用于两个线程之间的数据交换 它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据 这两个线程通过exchange()方法交换数据,当一个线程先执行exchange()方法后,它会一直等待第二个线程也执行exchange()方法
<a name="bcbd3f58"></a>
## Collections: 并发集合
-
Queue: ArrayBlockingQueue
-
Queue: LinkedBlockingQueue
-
Queue: LinkedBlockingDeque
-
Queue: ConcurrentLinkedQueue
-
Queue: ConcurrentLinkedDeque
是双向链表实现的无界队列,该队列同时支持FIFO和FILO两种操作方式
-
Queue: DelayQueue
延时无界阻塞队列,使用Lock机制实现并发访问。队列里只允许放可以“延期”的元素,队列中的head是最先“到期”的元素。如果队里中没有元素到“到期”,那么就算队列中有元素也不能获取到。
-
Queue: PriorityBlockingQueue
无界优先级阻塞队列,使用Lock机制实现并发访问。priorityQueue的线程安全版,不允许存放null值,依赖于comparable的排序,不允许存放不可比较的对象类型。
-
Queue: SynchronousQueue
没有容量的同步队列,通过CAS实现并发访问,支持FIFO和FILO。
-
Queue: LinkedTransferQueue
JDK 7新增,单向链表实现的无界阻塞队列,通过CAS实现并发访问,队列元素使用 FIFO(先进先出)方式。LinkedTransferQueue可以说是ConcurrentLinkedQueue、SynchronousQueue(公平模式)和LinkedBlockingQueue的超集, 它不仅仅综合了这几个类的功能,同时也提供了更高效的实现。
-
List: CopyOnWriteArrayList
-
Set: CopyOnWriteArraySet
对其所有操作使用内部CopyOnWriteArrayList的Set。即将所有操作转发至CopyOnWriteArayList来进行操作,能够保证线程安全。在add时,会调用addIfAbsent,由于每次add时都要进行数组遍历,因此性能会略低于CopyOnWriteArrayList
-
Set: ConcurrentSkipListSet
一个基于ConcurrentSkipListMap 的可缩放并发 NavigableSet 实现。set 的元素可以根据它们的自然顺序进行排序,也可以根据创建 set 时所提供的 Comparator 进行排序,具体取决于使用的构造方法
-
Map: ConcurrentHashMap
-
Map: ConcurrentSkipListMap
线程安全的有序的哈希表(相当于线程安全的TreeMap);映射可以根据键的自然顺序进行排序,也可以根据创建映射时所提供的 Comparator 进行排序,具体取决于使用的构造方法
<a name="12cfc650"></a>
## Atomic: 原子类
-
是什么
多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成 借助硬件的相关指令来实现的
-
基础类型:AtomicBoolean,AtomicInteger,AtomicLong
-
数组:AtomicIntegerArray,AtomicLongArray,BooleanArray
-
引用:AtomicReference,AtomicMarkedReference,AtomicStampedReference
-
FieldUpdater:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater是FieldUpdater原子类。
<a name="eabe3319"></a>
## Executors: 线程池
- 接口: Executor
将任务提交与每个任务将如何运行的机制(包括线程使用的细节、调度等)分离开来的方法 通常使用 Executor 而不是显式地创建线程。
-
ExecutorService
- 继承自
- 提供了什么,以及
- 关闭 ExecutorService会导致
-
ScheduledExecutorService
ScheduledExecutorService继承自ExecutorService接口,可安排在给定的延迟后运行或定期执行的命令
-
AbstractExecutorService
AbstractExecutorService继承自ExecutorService接口,其提供 ExecutorService 执行方法的默认实现。此类使用 newTaskFor 返回的 RunnableFuture 实现 submit、invokeAny 和 invokeAll 方法,默认情况下,RunnableFuture 是此包中提供的 FutureTask 类。
-
FutureTask
- 线程安全由什么来保证。
FutureTask 为 Future 提供了基础实现,如获取任务执行结果(get)和取消任务(cancel)等。如果任务尚未完成,获取任务执行结果时将会阻塞。一旦执行结束,任务就不能被重启或取消(除非使用runAndReset执行计算)。FutureTask 常用来封装 Callable 和 Runnable,也可以作为一个任务提交到线程池中执行。除了作为一个独立的类之外,此类也提供了一些功能性函数供我们创建自定义 task 类使用。FutureTask 的线程安全由CAS来保证。
-
核心: ThreadPoolExecutor
-
核心: ScheduledThreadExecutor
-
核心: Fork/Join框架
-
工具类: Executors
<a name="b7c6001d"></a>
# JUC原子类: CAS, Unsafe和原子类详解
- java原子类本质上使用
java原子类本质上使用的是CAS,而CAS底层是通过Unsafe类实现的
<a name="CAS"></a>
## CAS
-
是什么
- 是一条
- 实现方式
- AtomicInteger类
- 简单解释
-
CAS使用示例
java中为我们提供了AtomicInteger 原子类(底层基于CAS进行更新数据的),
-
CAS 问题
-
ABA问题
-
解决思路②
变量前面追加上版本号 Atomic包里提供了一个类AtomicStampedReference来解决ABA问题 compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标 志是否等于预期标志 如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
-
循环时间长开销大
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销
-
只能保证一个共享变量的原子操作
-
从Java 1.5开始
从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作
<a name="aa008d20"></a>
## UnSafe类详解
-
是什么
主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等 Unsafe提供的API大致可分为内存操作、CAS、Class相关、对象操作、线程调度、系统信息获取、内存屏障、数组操作等几类
![](https://pdai.tech/_images/thread/java-thread-x-atomicinteger-unsafe.png#alt=img)
-
Unsafe与CAS
-
Unsafe只提供了3种CAS方法
compareAndSwapObject、compareAndSwapInt和compareAndSwapLong。都是native方法。
-
Unsafe底层(了解)
-
Unsafe其它功能
Unsafe 提供了硬件级别的操作
<a name="AtomicInteger"></a>
## AtomicInteger
-
AtomicInteger 底层用的是
-
源码解析
-
原子更新基本类型
AtomicBoolean: 原子更新布尔类型。 AtomicInteger: 原子更新整型。 AtomicLong: 原子更新长整型。
-
原子更新数组
AtomicIntegerArray: 原子更新整型数组里的元素。 AtomicLongArray: 原子更新长整型数组里的元素。 AtomicReferenceArray: 原子更新引用类型数组里的元素。 这三个类的最常用的方法是如下两个方法: get(int index):获取索引为index的元素值。 compareAndSet(int i,E expect,E update): 如果当前值等于预期值,则以原子方式将数组位置i的元素设置为update值
-
原子更新引用类型
- 首先
- 然后
- 然后
AtomicReference: 原子更新引用类型。 AtomicStampedReference: 原子更新引用类型, 内部使用Pair来存储元素值及其版本号。 AtomicMarkableReferce: 原子更新带有标记位的引用类型
-
原子更新字段类
- 基于
AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。 AtomicLongFieldUpdater: 原子更新长整型字段的更新器。 AtomicStampedFieldUpdater: 原子更新带有版本号的引用类型。 AtomicReferenceFieldUpdater: 上面已经说过此处不在赘述。
-
AtomicIntegerFieldUpdater 的使用稍微有一些限制和约束⑤
1.字段必须是volatile类型的 2.字段的描述类型(修饰符public/protected/default/private)是与调用者与操作对象字段的关系一致。也就是说调用者能够直接操作对象字段,那么就可以反射进行原子操作。但是对于父类的字段,子类是不能直接操作的 3.只能是实例变量,不能是类变量,也就是说不能加static关键字 4.只能是可修改变量,不能使final变量 5.对于AtomicIntegerFieldUpdater和AtomicLongFieldUpdater只能修改int/long类型的字段,不能修改其包装类型(Integer/Long)。如果要修改包装类型就需要使用AtomicReferenceFieldUpdater。
<a name="58cc6f29"></a>
## AtomicStampedReference解决CAS的ABA问题
-
主要维护
主要维护包含一个对象引用以及一个可以自动更新的整数”stamp”的pair对象来解决ABA问题
-
java中还有哪些类可以解决ABA的问题
- 不是维护,而是
AtomicMarkableReference,它不是维护一个版本号,而是维护一个boolean类型的标记,标记值有修改,了解一下。
<a name="d7fcd70c"></a>
# JUC锁: LockSupport详解
<a name="91a43520"></a>
## LockSupport简介
LockSupport用来创建锁和其他同步类的基本线程阻塞原语 当调用LockSupport.park时,表示当前线程将会等待,直至获得许可 当调用LockSupport.unpark时,必须把等待获得许可的线程作为参数进行传递,好让此线程继续运行
<a name="29b09910"></a>
## LockSupport源码分析
-
类的属性
-
类的构造函数
-
核心函数分析
-
park函数
- 是什么
- 下列情况发生之前都会被阻塞③
- 重载版本
- 有参版本其中一个setBlocker函数调用两次
- 无参重载版本
-
unpark函数
- 是什么
- 安全性
-
parkNanos函数
禁用当前线程,并最多等待指定的等待时间 public static void parkNanos(Object blocker, long nanos) { if (nanos > 0) { // 时间大于0 // 获取当前线程 Thread t = Thread.currentThread(); // 设置Blocker setBlocker(t, blocker); // 获取许可,并设置了时间 UNSAFE.park(false, nanos); // 设置许可 setBlocker(t, null); } }
-
parkUntil
在指定的时限前禁用当前线程
-
unpark函数
- 判空
<a name="f87c16b4"></a>
## LockSupport示例说明
-
使用wait/notify实现线程同步
- 先调用notify()再调用wait
```java
class MyThread extends Thread {
public void run() {
synchronized (this) {
System.out.println("before notify");
notify();
System.out.println("after notify");
}
}
}
public class WaitAndNotifyDemo {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
synchronized (myThread) {
try {
myThread.start();
// 主线程睡眠3s
Thread.sleep(3000);
System.out.println("before wait");
// 阻塞主线程
myThread.wait();
System.out.println("after wait");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
使用park/unpark实现线程同步
- 先调用unpark,然后调用park
- 中断响应
比较
Thread.sleep()和Object.wait()的区别
Thread.sleep()和Condition.await()的区别
- Object.wait()和Condition.await()原理
- Condition.await()底层
- 阻塞当前线程之前还干了两件事
Thread.sleep()和LockSupport.park()的区别
- 唤醒方式
- 异常
- native方法
Object.wait()和LockSupport.park()的区别
执行位置
异常
唤醒后继续执行后续内容吗?
wait前执行notify
park前执行unpark
线程不会被阻塞,直接跳过park()
- park()/unpark()
park()/unpark()底层的原理是“二元信号量”,你可以把它相像成只有一个许可证的Semaphore,只不过这个信号量在重复执行unpark()的时候也不会再增加许可证,最多只有一个许可证
- LockSupport.park()会释放锁资源吗?
不会,park()只负责阻塞线程,释放锁资源再Condition的await()方法.
JUC锁: 锁核心类AQS详解
AbstractQueuedSynchronizer简介
AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器
- AQS 核心思想
如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态
如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中
- CLH队列
虚拟的双向队列,不存在队列实例
AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配
同步状态怎么表示
资源线程的排队
同步状态值的修改③
procted类型的getState,setState,compareAndSetState进行操作
- AQS 对资源的共享方式 ```
- 独占
- 公平锁
- 非公平锁
- 共享 ```
- 自定义同步器在实现时
自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可
AQS底层使用了模板方法模式
- 自定义同步器时需要重写下面几个AQS提供的模板方法
isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
- 自定义同步器时需要重写下面几个AQS提供的模板方法
AbstractQueuedSynchronizer数据结构
- 底层的数据结构
Sync queue,即同步队列,是双向链表
Condition queue不是必须的,其是一个单向链表,只有当使用Condition时,才会存在此单向链表
AbstractQueuedSynchronizer源码分析
继承自,实现了,可以进行
AbstractOwnableSynchronizer抽象类中,可以设置②
AbstractQueuedSynchronizer类有两个内部类
类的内部类 - Node类
- 节点状态
// CANCELLED,值为1,表示当前的线程被取消
// SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,也就是unpark
// CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列中
// PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行
// 值为0,表示当前节点在sync队列中,等待着获取锁
- 节点状态
类的内部类 - ConditionObject类
Condition接口定义了条件操作规范
public interface Condition {
// 等待,当前线程在接到信号或被中断之前一直处于等待状态
void await() throws InterruptedException;
// 等待,当前线程在接到信号之前一直处于等待状态,不响应中断
void awaitUninterruptibly();
//等待,当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态
long awaitNanos(long nanosTimeout) throws InterruptedException;
// 等待,当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。此方法在行为上等效于: awaitNanos(unit.toNanos(time)) > 0
boolean await(long time, TimeUnit unit) throws InterruptedException;
// 等待,当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态
boolean awaitUntil(Date deadline) throws InterruptedException;
// 唤醒一个等待线程。如果所有的线程都在等待此条件,则选择其中的一个唤醒。在从 await 返回之前,该线程必须重新获取锁。
void signal();
// 唤醒所有等待线程。如果所有的线程都在等待此条件,则唤醒所有线程。在从 await 返回之前,每个线程都必须重新获取锁。
void signalAll();
}
- 类的属性
头结点head,尾结点tail,状态state、自旋时间spinForTimeoutThreshold
AbstractQueuedSynchronizer抽象的属性在内存中的偏移地址
类的构造方法
类的核心方法 - acquire方法
以什么方式获取资源,中断怎么处理
acquireQueued方法的整个的逻辑
1.判断结点的前驱是否为head并且是否成功获取(资源)。
2.若步骤1均满足,则设置结点为head,之后会判断是否finally模块,然后返回。
3.若步骤2不满足,则判断是否需要park当前线程,是否需要park当前线程的逻辑是判断结点的前驱结点的状态是否为SIGNAL,若是,则park当前结点,否则,不进行park操作。
4.若park了当前线程,之后某个线程对本线程unpark后,并且本线程也获得机会运行。那么,将会继续进行步骤①的判断。
- 类的核心方法 - release方法
AbstractQueuedSynchronizer总结
- 每一个结点都是由前一个结点唤醒
- 当结点发现前驱结点是head并且尝试获取成功,则会轮到该线程运行
- condition queue中的结点向sync queue中转移是通过signal操作完成的
- 当结点的状态为SIGNAL时,表示后面的结点需要运行
JUC锁: ReentrantLock详解
ReentrantLock源码分析
类的继承关系
- 实现了
- 类的内部类③
- 类的属性
- 类的构造函数②
JUC锁: ReentrantReadWriteLock详解
ReentrantReadWriteLock数据结构
- 底层是基于什么来实现的
ReentrantLock和AbstractQueuedSynchronizer
ReentrantReadWriteLock源码分析
- 实现了什么接口②
实现了ReadWriteLock接口
类的内部类⑤
内部类 - Sync类
类的继承关系
类的内部类② ``` HoldCounter和ThreadLocalHoldCounter HoldCounter主要有两个属性,count和tid,其中count表示某个读线程重入的次数,tid表示该线程的tid字段的值,该字段可以用来唯一标识一个线程
ThreadLocalHoldCounter重写了ThreadLocal的initialValue方法,ThreadLocal类可以将线程与对象相关联。在没有进行set的情况下,get到的均是initialValue方法里面生成的那个HolderCounter对象 ```
类的属性
类的构造函数
内部类 - Sync核心函数分析
- sharedCount函数
- exclusiveCount函数
- tryRelease函数
- tryAcquire函数
- 类的属性
JUC集合: ConcurrentHashMap详解
为什么HashTable慢
使用了synchronized关键字对put等操作进行加锁
而synchronized关键字加锁是对整个对象进行加锁
也就是说在进行put等修改Hash表的操作时,锁住了整个Hash表
ConcurrentHashMap - JDK 1.7
- 数据结构
ConcurrentHashMap 是一个 Segment 数组
Segment 通过继承 ReentrantLock 来进行加锁
初始化
- initialCapacity: 初始容量
这个值指的是整个 ConcurrentHashMap 的初始容量,实际操作的时候需要平均分给每个 Segment
- initialCapacity: 初始容量
- loadFactor: 负载因子
Segment 数组不可以扩容,所以这个负载因子是给每个 Segment 内部使用
Segment 数组长度,且
Segment[i] 的默认大小为 ,负载因子是 ,得出初始阈值为
只初始化了
put 过程分析
初始化槽: ensureSegment
获取写入锁: scanAndLockForPut
扩容: rehash
get 过程分析
并发问题分析
ConcurrentHashMap - JDK 1.8
- 加锁采用
加锁则采用CAS和synchronized实现
数据结构
- 初始化
通过提供初始容量,计算了 sizeCtl
sizeCtl = 【 (1.5 * initialCapacity + 1),然后向上取最近的 2 的 n 次方】
- 初始化
- putVal流程
1.判断key value 不为空
2.计算hash值
3.根据对应位置的节点的类型来赋值,或者helpTransfer,或者增长链表,或者给红黑树增加节点
4.检查满足阈值就"红黑树化"
5.返回oldVal
- get流程
1.计算hash值
2.找到对应的位置,根据情况进行:
直接取值
红黑树里找值
遍历链表取值
返回找到的结果
JUC集合: CopyOnWriteArrayList详解
CopyOnWriteArrayList源码分析
- 类的继承关系④
CopyOnWriteArrayList实现了List接口,List接口定义了对列表的基本操作;同时实现了RandomAccess接口,表示可以随机访问(数组具有随机访问的特性);同时实现了Cloneable接口,表示可克隆;同时也实现了Serializable接口,表示可被序列化。
类的内部类
- COWIterator类
- 类的属性
JUC集合: ConcurrentLinkedQueue详解
ConcurrentLinkedQueue数据结构
ConcurrentLinkedQueue源码分析
- 类的继承关系
JUC集合: BlockingQueue详解
JUC线程池: FutureTask详解
FutureTask简介
- 常用来封装,也可以
FutureTask 常用来封装 Callable 和 Runnable,也可以作为一个任务提交到线程池中执行
- FutureTask 的线程安全由什么来保证
FutureTask 的线程安全由CAS来保证
FutureTask类关系
FutureTask源码解析
- Callable接口
- Future接口