Java并发学习之synchronized原理及使用小结
synchronized工作原理及使用小结
为确保共享变量不会出现并发问题,通常会对修改共享变量的代码块用synchronized加锁,确保同一时刻只有一个线程在修改共享变量,从而避免并发问题
本篇将集中在synchronized关键字的工组原理以及使用方式上
I. 工作原理
以一个case进行分析,源码如下
在加锁的代码块, 多了一个monitorenter,monitorexit
每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权
执行monitorexit的线程必须是objectref所对应的monitor的所有者。
指令执行时,monitor的进入数减1
如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者
其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权
锁
谈到synchronized就不可避免的要说到锁这个东西,基本上在网上可以搜索到一大批的关于偏向锁,轻量锁,重量锁的讲解文档,对这个东西基本上我也不太理解,多看几篇博文之后,简单的记录一下
先抛一个结论:轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能
1. 偏向锁
获取过程
判断是否为可偏向状态
是,则判断线程ID是否指向当前线程
竞争成功, 则设置线程ID为当前线程, 并执行代码块;
竞争失败,说明多线程竞争啦,问题严重了,当偏向锁到达安全点时,将偏向锁升级为轻量锁
是,即表示这个偏向锁就是这个线程持有, 直接执行代码块
否,通过CAS操作竞争锁
释放过程
当偏向锁遇到其他线程尝试竞争时,持有偏向锁的线程会释放,并升级为轻量锁
到达安全点, 暂停拥有偏向锁的线程,判断锁对象是否处于被锁的状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
2. 轻量锁
“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。
但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。
在解释轻量级锁的执行过程之前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁
3. 转换
简单来讲,单线程时,使用偏向锁,如果这个时候,又来了一个线程访问这个代码块,那么就要升级为轻量锁,如果这个线程在访问代码块同时,又来了一个线程来访问这个代码块,那么就要升级为重量锁了。下面更多的显示了这些变动时,标记位的随之改变
II. 三中使用姿势
1. 三种方法说明
-
修饰实例方法
多个线程访问同一个实例的加锁方法时,会出现锁的竞争
-
修饰静态方法
多个线程访问类的加锁方法时,会出现锁的竞争
-
修饰代码块
多线程访问到同一个代码块时,会出现竞争的问题
2. 几个疑问
一个case:TestDemo方法定义如下
线程1访问a方法时,线程2访问a方法会被阻塞;若此时线程2访问b方法会被阻塞么?访问c,d, e方法呢?
线程1访问c方法时,线程2访问c方法会被阻塞,若此时线程2访问d方法会被阻塞么,访问a,b,e方法呢?
线程1进入f方法内部的同步代码块,此时线程2若访问f会被阻塞;那么线程2访问g方法会如何?访问a,b,c,d,e方法又是怎样?
对上面的问题,核心的一点就是synchronized是否只作用于修饰的代码块or方法上
3. 测试验证
TestDemo的具体实现如下
测试一实例加锁方法的访问测试
上面的测试case主要覆盖:
两个线程访问一个实例的同一个加锁方法(期望阻塞,顺序执行)
线程1访问实例1的加锁方法,线程2访问实例2的加锁方法(无阻塞,并发执行)
两个线程访问一个实例的两个加锁方法
两个线程,一个访问实例的加锁方法,一个访问静态加锁方法
输出结果如下
验证结果:
同一个实例中加锁的方法,只要有一个线程已经获取到了锁,其他线程再去访问时,都会被阻塞(即此时的锁,是一个实例共享同一把锁;不同的实例,锁不同)
一个线程获取到一个实例中的加锁方法的锁时,另一个线程依然可以访问静态加锁方法(即实例的锁与静态方法的锁是不同的,两者不影响)
测试case二:静态加锁方法测试
上面的测试主要覆盖
两个线程访问相同的静态加锁方法(期待阻塞)
三个线程,两个访问不同实例的静态加锁方法,一个访问实例加锁方法
输出结果如下
验证结果:
不同的线程访问静态同步方法时,会阻塞(即静态同步方法,共享一把锁;所有的实例访问静态同步方法依然是共享这把锁)
静态同步方法的锁和实例同步方法的锁不同,两者没有关系,不会相互影响
测试case三:同步代码块
基本上和上面的相同,同步代码块分为静态同步代码块(共享类锁);非静态同步代码块(共享实例锁)
III. 小结
synchronized三中使用姿势,修饰静态方法,实例方法,(静态/非静态)代码块
静态同步方法,静态同步代码块共享同一把锁(简易称为类锁),所有这些同步代码的访问,都会去竞争类锁,从而出现阻塞
一个实例中的同步方法,非静态同步代码块共享一把锁(简易成为实例锁),所有访问同一个实例中的这些同步代码时,都会竞争实例锁,从而出现阻塞
不同的实例拥有不同的实例锁,彼此相互没有影响
实例锁和类锁没有影响,不会造成彼此阻塞
synchronized底层主要是通过偏向锁,轻量级锁和重量级锁组合来实现线程同步的功能
几个锁的简要说明为:单线程时,使用偏向锁,如果这个时候,又来了一个线程访问同步代码块,那么就要升级为轻量锁,如果这个线程在访问代码块同时,又来了一个线程来访问这个代码块,那么就要升级为重量锁了