一.JMM(Java内存模型)
参考
- 老刘-JMM面试包过
- HollisChuang-Java内存模型
- 《Java并发编程实战》Chapter-16
- 《深入理解Java虚拟机》Chapter-12.3.6
背景:由于CPU和主内存间存在数量级的速率差,CPU-多级缓存-主存这种硬件架构来作为缓冲,CPU将常用的数据放在高级缓存中,运算结束后CPU再将结果同步到主存中。同样的,也会从主存中获取数据,然后在各个缓存中进行计算,但是这样会引入缓存一致性的问题。此外为了提升CPU的执行效率,还会进行处理器优化,包括指令重排序。由此,缓存一致性会产生可见性问题,CPU时间片可能会造成原子性问题,指令重排序会造成有序性问题。为了解决这个问题,就想到在物理机器上定义一套内存模型,规范内存的读写操作,解决多线程通过主存通信时存在的原子性,可见性以及有序性问题,保证程序在各种平台下对内存的访问都能得到一致的访问效果
Java中,内存模型是一种规范,定义了很多内容包括:
1.所有的变量都存储在主存中,每个线程都有自己的工作内存,保存了来自主存的变量的拷贝,每个线程只能操作自己工作内存中的变量,而无法直接操作主存以及访问其他线程的变量。线程之前的通信需要通过主存完成。
2.线程与主存之间的通信有8个操作,可以浓缩为4个
主 → read load 工作 → store write 主
3.JMM是一种规范,说明了某个线程的内存操作在哪些情况下对于其他线程时可见的,其中包括这些操作是按照 happens-before的偏序关系进行排序,从而保证了Java内存模型中各个操作的有序性。
**happens-before **(先行发生原则):JMM中定义两个操作偏序关系
- 程序顺序规则:在一个线程内,按照控制流的顺序,书写在前面的操作先行发生在后面的操作;
- 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作;
- volatile变量规则:对一个volatile的写操作要先发生于后面对同一变量的读操作;
- 线程启动规则:Thread对象的 start() 先发生于此线程的每一个动作;
- 线程终止规则:线程中所有的操作都先发生在对这个线程的终止检测;
- 线程中断规则:对线程interrupt()方法的调用先发生于被中断线程的代码检测到中断事件的发生;
- 对象终结规则:一个对象的初始化完成先行发生于它的finalize()方法的开始;
- 传递性:操作A先行发生于B,B先发生于C,则A先行发生于C。
4.内存模型也封装了底层的实现后提供给开发者一些关键字和并发包,比如 volatile synchronized,以及JUC包。
二.JUC并发包:
1.并发工具类
闭锁 CountDownLatch:处理一个线程等待多个线程执行完成之后才能执行的场景,等待的是事件,主要操作有 countDown/await,有一个计时器(实质是AQS中的state变量),不可重用;(Future/ FutureTask也能实现闭锁效果,见 极客Java并发编程《Future》煮茶叶的例子)
循环栅栏 Cyclic Barrier:一组线程之间互相等待,有计数器,屏障点,可选的冲出屏障后的任务,用到了可重入锁,会自动重置可重复使用;
信号量 :许可证 acqure release 实现资源池 和有界阻塞队列
2.Lock显式锁
可重入锁,可重入读写锁等
3.原子类
涉及到CAS的相关内容
4.并发容器
ConcurrentHashMap(1.7和1.8底层源码) CopyandWriteArrayList BlockingQueue(Array- Linked- Priority-)
5.Executor 框架
1.基于 Executors的静态方法之一构建的线程池:
- newFixedThreadPool:固定长度
- newCachedThreadPool:可缓存,回收空闲的线程池
- newSingleThreadExecutor:单线程
- newScheduledThreadPool:固定长度,延迟/定时
- 。。。
涉及到线程池 ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler){}
里面涉及到 阻塞队列
4种拒绝饱和策略:
- AbortPolicy:默认的,拒绝并抛出异常;
- Discard:抛弃任务不报异常;
- DiscardOldest:会抛弃下一个要执行的任务,然后尝试重新提交新的任务;
- CallerRuns:在一个调用了Execute的线程种执行该任务
设置策略:
- CPU密集的任务:设置线程少,阻塞队列长,能降低CPU使用率,但是限制吞吐量,有个经验公式 核数+1
- IO密集任务:线程数 = CPU核数*(1+平均等待时间/平均工作时间)
三.Synchronized底层实现原理
参考
- 做个好人君-死磕synchronized底层实现
- 《深入理解Java虚拟机》Chapter13 13.3
key:Java对象头和Monitor是实现syn的基础。
3.1 Synchronized
synchronized 是一个关键字,用于修饰方法或代码块,能够将其锁起来。这个锁是互斥锁,一次只允许一个线程进入被锁的代码块中,从而在并发环境下实现同步功能。
传统意义上的 syn重量级锁是内置锁(monitor锁,可以是任何一个Java对象) 依赖 系统的同步函数,在Linux上使用的是mutex互斥锁,这些同步函数涉及到用户态和内核态的切换,进程的上下文切换,成本较高。在没有多线程竞争,或者两个线程接近交替执行的情况下,使用 重量级锁 效率低下。
-
就是为了解决上面两个场景的效率低下的问题,JDK1.6后引入了两种新锁机制:偏向锁,轻量级锁。而二者的实现与JVM中对象的对象头有关。对象头中有一个MarkWord,存储了对象锁状态信息(锁标志位,偏向锁信息)。
偏向锁:当JVM开启偏向锁模式,只有一个线程获取锁时,锁对象Markword通过CAS操作存储该线程的ID,等该线程再次想获取锁的时候,就无需 CAS操作直接获取;
轻量级锁:当有另一个线程竞争偏向锁,交替执行同步代码块时,偏向锁结束,升级为轻量级锁,JVM会在当前线程的栈帧中创建一个lock record,通过CAS操作将Lock Record的地址存储在对象头的markword中,操作成功则说明 该线程成功获取这个锁,如果失败说明至少有一个线程在竞争,它会先检查Markword是否指向当前线程的栈帧,是就是重入,直接进同步块。不是就说明已经有另一个线程抢占了锁对象。如果出现两个以上竞争同一把锁,就膨胀为重量级锁。
此时的Markword存储的是指向重量级锁的指针,后面等待锁的线程也必须进入阻塞状态。
3.2 synchronized优化(JVM 1.6)
自旋锁 && 自适应自旋锁:避免线程挂起和恢复的开销,让请求锁的线程等一会(自旋),但不放弃cpu的执行时间,看看锁会不会马上被释放。虽然避免了线程切换的开销,但是要占用CPU时间。自适应自旋锁是指 自旋次数不固定,而是根据前一次在同一个锁的自旋时间及锁的拥有着的状态来决定。
锁消除 JVM即时编译器运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除,如果判断数据不会被其他线程访问到,就认为是线程私有的,就无需加锁
锁粗化: 在编写代码的时候,会尽量将同步块的作用范围限制得尽量小-只有在涉及到共享数据的才进行同步。但是如果一系列连续操作都对同一个对象反复加锁解锁会导致不必要的性能损耗,因此如果JVM检测到这种情况就会将同步的范围扩展到整个操作序列的外部,这样只需要加一次锁。
四.ReentrantLock (aka: Rel)
- 手动显式添加锁-释放锁(要在finally中释放);
- 提供可轮询,可定时,可中断的锁获取方式,其中可轮询和可定时能够避免死锁的发生;
- 提供公平锁的实现;
- 非块结构加锁。
与Synchronized相比,ReL是类,前者是关键字,且能提供以上的额外功能。同时实现机制不同:
- sync操作的是 MarkWord,lock调用是 Unsafe类的 park()方法
五. Volatile VS Synchromized的区别
volatile:修饰的变量一旦被某个线程修改,其他线程立即得到通知,并从主存中获取最新的值,可以实现可见性和有序性
-
通过lock关键字 和 CPU锁定内存来实现可见性;
当写一个volatile变量时,JMM会把该线程的工作内存的共享变量值更新到主内存中;
当读一个volatile变量时,JMM会把该线程的工作内存设置为无效
-
通过内存屏障实现有序性
通过插入内存屏障指令禁止在内存屏障前后的指令执行重排序优化
区别
- volatie本质告诉jvm当前变量在工作内存中值不确定,需要从主存中读取,sync则是锁住当前变量,只有当前线程能读取,在 重量级锁实现中,其他线程只能阻塞等待;
- v 修饰变量,sync 修饰级别包括 变量 方法 类
- volatile只能实现 变量 修改的可见性,以及有序性,不能保证原子性,因此不能保证线程安全;而Volatile都可能保证,因此线程安全;
- volatile不会造成线程阻塞,后者可能导致线程阻塞;
- volatile修饰 变量不会被编译器优化,而sync标记的变量可以被编译器优化
六.死锁
定义
类型
-
锁顺序死锁以及动态锁顺序死锁):两个线程获得锁的顺序交替进行,顺序不固定导致;
解决办法:固定加锁的顺序,比如利用 System.identityHashCode的方法来确定获取锁的顺序
-
协作对象之间发生的死锁:两个线程隐性的从不同顺序获取两个锁
解决:开放调用,即调用某个方法时不需要持有锁,需要使同步代码块仅用于保护涉及共享状态的操作