写于:2020-02-03
在 《线程安全与数据同步-概念》中提到共享数据在线程间的问题。
针对该问题,JDK 提供了 synchronized 关键字来解决。
一、什么是 synchronized
synchronized 关键字可以实现一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象对多个线程是可见,那么对该对象的所有读或者写都将通过同步的方式进行,具体如下:
- synchronized 关键字提供了一种锁的机制,能够确保共享变量的互斥访问,从而防止数据不一致问题的出现
- synchronized 关键字保证线程对共享变量的更新操作马上刷入主内存中。
- synchronized 遵守 hapens-before 规则。
二、synchronized 使用
1、同步代码块的方式
案例如下
public class SimpleThread{
private final Object lock = new Object();
public void sayHello(){
synchronized(lock){
.....
}
}
}
2、同步方法的方式
直接在方法上加上 synchronized
关键字
案例如下:
public class SimpleThread{
public synchronized void sayHello(){
.......
}
}
三、深入 synchronized 关键字
测试代码如下:
public class SimpleThread {
public static void main(String[] args) throws InterruptedException {
SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
// 三个线程:T1,T2,T3
IntStream.rangeClosed(1,3).forEach(loopTimes ->{
new Thread(synchronizedDemo::shareData,"T" + loopTimes).start();
});
}
public static class SynchronizedDemo{
private final static Object mutex = new Object();
public void shareData(){
synchronized (mutex){
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
多个线程对使用 synchronized 的共享资源进行同时访问。
1、线程堆栈分析
使用 jstack 命令分析
step1、jps 获取线程号
$ jps
6032 JConsole
2980
11928 Jps
12268 Launcher
2716 SimpleThread
step2、jstack 查看 2716 堆栈信息
jstack 2716
查看对应的信息如下:
通过堆栈信息能够知道,等待和持有的是同一个锁对象。
2、JVM 指令分析
通过 javap 指令对代码的 class 文件进行反编译
javap 指令进行反编译
javap -c SimpleThread$SynchronizedDemo.class
反编译后的代码如下
代码中需要关注的就是两个 JVM 指令:monitorenter 和 monitorexit
monitorenter
每个对象都与一个 monitor 关联,一个 monitor 的 lock 的锁只能被一个线程在同一时间获得。
小贴士: monitor 存在计数器: 当计数器为0时,表示该 monitor 的 lock 还没被获取。 当计数器 >= 1,表示该 monitor 的 lock 被同一个线程多次获取。(锁重入)
线程获取 monitor 的可能存在几种情况:
a、monitor 计数器此时为 0 ,线程获取到 monitor 的 lock ,monitor 计数器 +1
b、同一个线程再一次获取到 monitor 的 lock,monitor 计数器累加
c、monitor 的 lock 被另一个线程持有,当前线程进入阻塞状态,知道 monitor 计数器变为 0,然后在尝试获取 monitor 的 Lock
monitorexit
monitorenter 是通过对 monitor 计数器的累加,来表示被某个线程持有了该 monitor 的 lock。
而 monitorexit 通过对 monitor 计数器的递减,来表示对该 monitor 的 lock 的释放。
四、 this monitor 和 class monitor
synchronized 是一种锁机制,同步代码块的方式可以任意指定锁对象,那么同步方法被锁的又是什么?
1、this : 当前对象实例
代码验证
普通的方法加 synchronized 被锁的是 this 当前对象实例
通过代码的方式进行验证,代码如下:
public class ThisLockValid {
public static void main(String[] args) {
ThisLock thisLock = new ThisLock();
new Thread(()->{
thisLock.m1();
},"Thread-1").start();
new Thread(()->{
thisLock.m2();
},"Thread-2").start();
new Thread(()->{
thisLock.m3();
},"Thread-3").start();
}
}
// 验证逻辑
// 默认为 This 锁,也就是 ThisLock 对象锁。获取同一个锁,方法调用需等待。
class ThisLock{
public synchronized void m1(){
System.out.println(Thread.currentThread().getName() + ",m1");
try {
TimeUnit.SECONDS.sleep(70);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void m2(){
System.out.println(Thread.currentThread().getName() + ",m2");
try {
TimeUnit.SECONDS.sleep(70);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void m3(){
synchronized (this){
System.out.println(Thread.currentThread().getName() + ",m3");
try {
TimeUnit.SECONDS.sleep(70);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
代码中三个方法 m1、m2、m3。
其中,m1,m2 直接在方法上加了 synchronized 关键字,而 m3 使用同步代码块的方式显示指定 this 对象为锁对象。
通过代码执行验证,当 m3 执行时,m1 和 m2 都需要进行等待,验证 普通方法加上 synchronized ,默认锁的是 this 对象。
通过堆栈信息验证
通过堆栈信息,发现 Thread1 和 Thread2 和 Thread3 持有的是同一个 monitor 的 lock。验证 普通方法加上 synchronized ,默认锁的是 this 对象。
2、 class:当前 class
静态方法加上 synchronized 被锁的是 class 。
代码验证
通过如下代码验证
public static class SynchronizedDemo{
public synchronized static void m1(){
System.out.println(Thread.currentThread().getName() + ":m1 开始执行");
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized static void m2(){
System.out.println(Thread.currentThread().getName() +":m2 开始执行");
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void m3(){
synchronized (SynchronizedDemo.class){
System.out.println(Thread.currentThread().getName() +":m3 开始执行");
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
代码中三个方法 m1、m2、m3。
其中 m1、m2 是加了 synchronized 关键字的静态方法,m3是通过同步代码块指定 class 为锁对象的方法。
通过如下代码执行验证
public class SimpleThread {
public static void main(String[] args) throws InterruptedException {
SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
new Thread(()->{
synchronizedDemo.m1();
},"T1").start();
new Thread(()->{
synchronizedDemo.m2();
},"T2").start();
new Thread(()->{
synchronizedDemo.m3();
},"T3").start();
}
}
通过运行能够知道,m1,m2,m3 都需要进行争抢锁来获取执行权。从而验证静态方法加上 synchronized 锁的 class 。
通过堆栈信息验证
通过堆栈信息,发现 T1 和 T1 和 T3 持有的是同一个 monitor 的 lock。从而验证静态方法加上 synchronized 锁的 class 。
五、使用 synchronized 需要注意的几个问题
1、与 monitor 关联的对象不能为空
例如:
// 错误案例
public static class SynchronizedDemo{
private final static Object mutex = null;
public void shareData(){
synchronized (mutex){
......
}
}
}
2、synchronized 作用域太大
synchronized 存在排他性,被 synchronized 包围的区域,线程只能串行的执行,如果 synchronized 作用域越大,运行效率越低。
譬如下面代码:
public class Task implements Runnable{
@Override
public synchronized void run() {
}
}
上述代码中,Runnable 中 run 方法整块都在 synchronized 同步代码块中,此时即使创建在多线程也没用,引用多个线程的执行同时串行执行的。
synchronized 应该尽可能的只作用于共享资源的读写作用域。
3、不同对象对应的 monitor 是不一样的
例如如下代码
public class SimpleThread {
public static void main(String[] args) throws InterruptedException {
// 三个线程:T1,T2,T3
IntStream.rangeClosed(1,3).forEach(loopTimes ->{
new Thread(SynchronizedDemo::new,"T" + loopTimes).start();
});
}
public static class Task implements Runnable{
private final Object lock = new Object();
@Override
public void run() {
synchronized(lock){
}
}
}
}
代码中创建的三个线程中对应的 lock 对象是不同的,所以 monitor 也是不一样的。因此起不到互斥的作用。
4、多个锁的交叉导致死锁
案例代码如下
public class SimpleThread {
public static void main(String[] args) throws InterruptedException {
SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
IntStream.rangeClosed(1,3).forEach(loopTimes ->{
new Thread(synchronizedDemo::read,"T" + loopTimes).start();
});
IntStream.rangeClosed(4,6).forEach(loopTimes ->{
new Thread(synchronizedDemo::write,"T" + loopTimes).start();
});
}
public static class SynchronizedDemo{
private final static Object mutex_read = new Object();
private final Object mutex_write = new Object();
public void read(){
synchronized (mutex_read){
synchronized (mutex_write){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public void write(){
synchronized (mutex_write){
synchronized (mutex_read){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
jconsole 查看死锁