未完待续。。。
本文主要探究Java 中的锁,包括:
- synchronized要从下面三个方面去理解
- Volatile需要刨根问底
- 并发中的基础的概念
- 并发中的CAS和AQS
- Concurrent包主要看这几个类
- CountDownLatch
- CyclicBarrier
Synchronized要从下面三个方面去理解
-
【代码表现上】、【 锁升级】、【 锁在不同系统层面的实现】
-
1、代码表现上
- (1)Synchronized锁对象和同步方法时:只对该对象有效(对象锁:锁this或者锁其他对象);具体见Synchronized方法锁、对象锁、类锁区别
- (2)Synchronized锁类和静态方法时:对所有对象生效(类锁:static、对象.class)。
- (3)是否互斥执行(发生阻塞), 主 要 判 断 是 不 是 同 一 把 锁 就 行 \color {red}{主要判断是不是同一把锁就行} 主要判断是不是同一把锁就行,比如一个类中两个方法,一个static锁,一个非static锁,那么不会发生阻塞,因为不是同一把锁,记住,类的class对象归根到底也是对象。
-
2、锁升级
- 刚创建对象的时候,对象头的markword未存放锁信息,含有该对象的hashcode。
- 无锁 -> 偏向锁:在对象头的markword中存放线程ID。
- 偏向锁 -> 轻量级锁:竞争的线程,采用CAS方式,将自己线程栈中的LR(Lock Record)对象存放到对象头的markword中,此时对象的hashcode存在于线程栈的LR中。
- 轻量级锁 -> 重量级锁:
- 操作:去操作系统申请锁,此时状态由用户态转换成内核态。
- 升级条件:自旋超过一定次数(比如10次),或者自旋线程超过CPU核数的一半。1.6以后进行改进,加入了自适应自旋。
- 为什么要引入重量级锁:因为轻量级锁的CAS操作相当的耗CPU资源。
-
3、锁在不同系统层面的实现
- java层:关键字:Synchronized
- .class层:monitorenter 和 monitorexit
- 运行态:自动锁升级、锁消除、锁粗化、逃逸分析
- 汇编层:lockxchg
Volatile需要刨根问底
-
volatile能干什么?
- 1、保证此变量对所有线程的可见性
- 2、禁止指令重排序优化
-
volatile是如何保证此变量对所有线程的可见性?
- 理解一:CPU修改数据,首先是对缓存的修改,然后再同步回主存,在同步回主存的时候,如果其他CPU也缓存了这个数据,就会导致其他CPU缓存上的数据失效(通过嗅探总线数据传播,检查缓存对应的主存地址是否被修改过),这样,当其他CPU再去它的缓存读取这个数据的时候发现缓存已失效,就必须从主存重新获取。
- 理解二:使用 volatile 关键字会强制将修改的值立即写入主存;
- 使用 volatile 关键字的话,当线程 2 进行修改时,会导致线程 1 的工作内存中缓存变量 stop 的 缓 存 行 无 效 \color{red}缓存行无效 缓存行无效(反映到硬件层的话,就是CPU 的 L1 或者 L2 缓存中对应的缓存行无效);由于线程1的工作内存中缓存变量 stop 的缓存行无效,所以线程 1 再次读取变量 stop 的值时会去主存读取
-
volatile既然能保证可见性,为什么不能保证原子性?
- 为什么volatile能保证有序性不能保证原子性
- 当 i=5 的时候A,B两个线程同时读入了 i 的值, 然后A线程执行了 temp = i + 1的操作, 要注意,此时的 i 的值还没有变化,然后B线程也执行了 temp = i + 1的操作,注意,此时A,B两个线程保存的 i 的值都是5,temp 的值都是6, 然后A线程执行了 i = temp (6)的操作,此时i的值会立即刷新到主存并通知其他线程保存的 i 值失效, 此时B线程需要重新读取 i 的值那么此时B线程保存的 i 就是6,同时B线程保存的 temp 还仍然是6, 然后B线程执行 i=temp (6),所以导致了计算结果比预期少了1。
-
volatile是如何防止指令重排序优化的
- java层:加入volatile关键字
- 字节码层:ACC_VOLATILE
- JVM层:加入内存屏障
- hotspot层:锁总线
-
内存屏障又是什么?
- 是一种指令,通过在每个volatile读写操作前后加入相应的屏障,完成对值的控制
-
DCL锁为什么要加volatile?
- DCL的问题出现在new一个新的对象,不是原子操作,存在指令重排,其冲突存在于多线程,一个创建对象的时候指令重排,但是对象没有init,另一个线程调用了该对象,出现对象未初始化错误。
- DCL单例模式为什么还需要加volatile
-
volatile是否有锁?
- 有,其在编译层的实现是lock
-
volatile的应用场景
- 1、在原子性操作的场景,可以使用volatile代替其他锁,效率比较高
并发中的基础的概念
-
两个规则
- as-if-serial规则:as-if-serial规则是指不管如何重排序(编译器与处理器为了提高并行度),(单线程)程序的结果不能被改变。这是编译器、Runtime、处理器必须遵守的语义。
- happens-before规则:某些操作是一定严格区分执行顺序的,比如线程起停、加解锁、volatile
-
缓存一致性协议
- 当 CPU 写数据时,如果发现操作的变量是共享变量,即在其他 CPU 中也存在该变量的副本,会发出信号通知其他 CPU 将该变量的缓存行置为无效状态,因此当其他 CPU 需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
-
程序局部性原理。
- 时间局部性(Temporal Locality):如果一个信息项正在被访问,那么在近期它很可能还会被再次访问。比如循环、递归、方法的反复调用等。
- 空间局部性(Spatial Locality):如果一个存储器的位置被引用,那么将来他附近的位置也会被引用。
-
缓存行
- 每个缓存里面都是由缓存行组成的,缓存系统中是以缓存行(cache line)为单位存储的。缓存行是2的整数幂个连续字节,一般为32-256个字节。
- 最常见的缓存行大小是64个字节。当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享
并发中的CAS和AQS
- 【CAS】
- 【AQS】
Concurrent包主要看这几个类
-
【locks】
- ReentrantLock
- ReadWriteLock
-
【atomic】
-
【BlockingQueue】
-
【CountDownLatch】
- 作用:多线程控制工具类,统一管理多个线程达到某种状态。
- 主要方法:
- CountDownLatch(int count) //实例化一个倒计数器,count指定计数个数
- countDown() // 计数减一
- await() //等待,当计数减到0时,所有线程并行执行
- 在每个方法的finally中加入latch.countDown(),当计数器的值变为0时,在CountDownLatch上await()的线程就会被唤醒。
- CountDownLatch典型用法:
- 1、某一线程在开始运行前等待n个线程执行完毕。将CountDownLatch的计数器初始化为new CountDownLatch(n),每当一个任务线程执行完毕,就将计数器减1 countdownLatch.countDown(),当计数器的值变为0时,在CountDownLatch上await()的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。
- 2、实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的CountDownLatch(1),将其计算器初始化为1,多个线程在开始执行任务前首先countdownlatch.await(),当主线程调用countDown()时,计数器变为0,多个线程同时被唤醒。
- 注意事项:
CountDownLatch是一次性的,计算器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当CountDownLatch使用完毕后,它不能再次被使用。 - 参考博客:CountDownLatch的理解和使用
-
【CyclicBarrier】
- 作用:控制多线程的统一终点,类似于可循环利用的CountDownLatch
- 主要方法:
- await()
- 注意事项:
- 线程调用 await() 表示自己已经到达栅栏
- BrokenBarrierException 表示栅栏已经被破坏,破坏的原因可能是其中一个线程 await() 时被中断或者超时
- 参考博客:CyclicBarrier 使用详解
-
【Semaphore】