# Synchronized 关键字详解

作者:LYX (opens new window)编程导航星球 (opens new window) 编号 26376

# 一、Synchronized关键字的使用位置有哪些?

1.直接在方法上使用Synchronized关键字,可以是实例方法也可以是静态方法。如果是实例方法,对象锁是当前实例对象(this)等价于synchronized(this)的对象锁。如果是静态方法锁对象是当前类对应的Class对象等价于synchronized(当前类名.class)的对象锁。

实例方法使用synchronized代码:

public synchronized void testMethod() {

}
1
2
3

静态方法使用synchronized代码:

public static synchronized void testMethod() {

}
1
2
3

2.修饰一段代码块,格式为:

synchronized(对象锁){

//需要被同步的代码块

}
1
2
3
4
5

此时对象锁需要手动去指定,有以下三种类型对象锁。

synchronizedthis//对象锁是当前实例对象

X x = new X()

synchronized(x)		//对象锁是x对象

synchronizedX.class//对象锁是X类对应的Class对象
1
2
3
4
5
6
7

ps:(这里就没有代码详细介绍了,本篇重点在第二和第五部分)

# 二、Synchronized关键字的工作原理

当对一段代码使用synchronized关键字修饰后,会绑定上对应锁对象的监视器对象。在Java中每个Java对象都对应一个监视器(Monitor)对象,该监视器对象中包含了一个计数器和等待队列。当一个线程访问到代码块后,发现被synchronized同步过,就会查看锁对象对应的监视器对象中的计数器,如果是0说明没有被线程占用,如果不是0说明被其他线程占用便进入等待队列。线程成功进入代码块之后,便将计数器加1。线程从代码块出去之后,计数器便减1。如果计数器减为0说明锁被释放了,这个时候在等待队列的其他线程就可以进来进行访问了。

总结:Java对象锁中判断对象锁是否被释放或者占用的规则,判断监视器对象中的计数器是否为0

ps:线程是如何判断一段代码是否被synchronized同步过请参考四Synchronized在字节码指令中的原理

ps:这个计数器是否可以一直加加呢?请查看三Sychronized关键字可重入锁的实现原理

# 三、Synchronized关键字可重入锁的的实现原理

上文讲到线程进入同步代码块之后计数器会被加1,如果在同步代码块中又有一个synchronized修饰的同步代码块,而且它的对象锁还是同一个对象锁。进入到这个代码块计数器会再次加1。这个过程就是重入锁。“可重入锁”就是指可以再次获取自己的内部锁。然后这个线程依次离开这两个代码块计数器依次减一减一,最后计数器为0释放锁。

# 四、Synchronized在字节码指令中的原理

# 4.1synchronized修饰方法的字节码指令原理

在方法上使用synchronized关键字实现同步的原因是使用flag标记ACC_SYNCHRONIZED,当调用方法时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否设置,如果设置了,执行线程先尝试获取对象锁(判断计数器是否为0,为0则可以获取对象锁),如果获取到了对象锁,便再去执行方法,最后在方法完成时释放锁。

测试代码如下:

image-20231114132548538

在cmd中使用命令javap来将class文件转换成字节码指令,参数-v表示输出附加信息,参数-c表示对代码进行反汇编。

使用javap.exe 命令如下:

javap -c -v Mytest.class

生成这个class文件对应的字节码指令,指令的核心代码如下:

image-20231114132559250

在反编译的字节码指令中对public synchronized void testMethod()方法使用了flag标记ACC_SYNCHRONIZED,说明此方法是同步的。

# 4.2synchronized修饰代码块的字节码原理

如果使用synchronized修饰代码块,会在同步代码块的前后分别形成monitorenter和monitorexit这两个字节码指令。测试代码如下:

image-20231114132611069

在cmd中使用指令:

javap -c -v Mytest.class

生成这个class文件对应的字节码指令,指令的核心代码如下:

image-20231114132619639

由代码可知,在字节码中使用了monitorenter和monitorexit指令进行同步处理。

# 五、Synchronized使用不同类型的对象锁对应的线程同步情况

# 5.1

Synchronizedthis),多个线程执行以下情况都是互相同步的状态:

1.执行synchronized(this){}同步代码块

2.执行this所指对象中的用synchronized 修饰的实例方法

3.执行synchronizedthis所指对象){}同步代码块
1
2
3
4
5
6
7

代码测试如下:

image-20231114132631869

根据三种情况分别定义3个runnable任务

image-20231114132641887

image-20231114132658020

开启三个线程进行测试

image-20231114132709910

测试结果如下:

image-20231114132718734

从测试结果可以看出线程执行这三种情况下的代码都是同步执行的。

Thread-0执行情况1(synchronized(this){}同步代码块)时,线程Thread-1和Thread-2进入等待队列等待。Thread-0执行结束,释放锁。然后就是Thread-2执行情况3(synchronized(this所指对象){}同步代码块),Thread-1在等待队列等待。Thread-2执行结束,释放锁。最后就是Thread-1执行情况2(执行this所指对象中的synchronized 关键字修饰的实例方法)。

# 5.2

Synchronized(非this对象x),多个线程执行以下情况都是互相同步的状态:

1.执行synchronized(非this对象x){}同步代码块。

2.执行x对象中synchronized修饰的实例方法是呈同步效果。

3.执行x对象方法里面的synchronized(this){}代码块时也呈现同步效果。

这个和5.1所说的意思相同,就是反过来进行描述。

# 5.3

每一个 Java类都对应一个(Class类)的实例对象,这个对象在内存中是单例的。所以如果使用synchronized(X类名.class)作为对象锁,每个Java类只有一个这样对应的对象锁。

Synchronized(X类名.class),多个线程执行以下情况都是互相同步的状态:

1.执行synchronized(X类名.class){}同步代码块

2.执行x对象的synchronized 修饰的静态方法,记住这里的x对象包含所有的X对象实例。比如X x1 = new X(),X x2 = new X()。在这个时候x1对象的静态方法和x2对象的静态方法调用以及所有使用synchronized修饰的(X类名.class){}的代码块都是互相同步的。所以这里算是一个使用synchronized(X类名.class)作为对象锁的一个陷阱,一不小心就遗漏了。

因为Java中每个类对应的Class对象是单例的所以才会出现上面这么复杂的情况。

这里引发一个思考?

是不是只要多线程操作一个单例对象就要考虑是否会出现线程不安全问题?一个线程对应一个对象不会出现线程不安全问题。但是如果这个对象是单例的那么就是多个线程对应一个对象就可能会有线程不安全问题。

单例对象可能来源于:

1.jdk原生的系统类,可能某个原生的类返回的对象是单例的。

2.通过@Bean注解返回的对象

3.自己手写单例模式返回的对象。

这里又引发一个思考?

在Java中对象存在单例的,那么是否存在其他单例的东西。比如“单例方法”、“单例变量”。

还真存在“单例方法”“单例变量”。static关键字修饰的方法就是“单例方法”,因为实例方法是new一个对象对应一个实例方法的内存,但是static静态方法是不管你这个类new多少个对象对应的都是这个类中同一块内存中的静态方法。所以我这里称之为“单例方法”。

再思考一下

既然是静态方法和静态变量是单例的,那么多线程访问它们是不是就要考虑是否会出现线程不安全问题?

为什么它们是单例的就要考虑线程安全问题?

因为它们是单例的,那么在多线程访问时对于多个线程来说它们就是共享资源。既然是多线程操作共享资源那么就需要考虑线程安全问题。

多线程操作共享资源会有什么问题?

有很多问题例如数据覆盖,脏读,数据可见性等等。

再问一个问题,既然使用sychronized解决了上个问题中说的数据覆盖,脏读,数据可见性问题,那么使用sychronized会有什么问题吗?

例如线程阻塞导致的等待时间过长,还有死锁等等。尤其是线程阻塞这一点,synchronized在刚诞生之初使用synchronized对程序性能影响是很大的,尤其是在JDK5之前。在JDK6有专门针对synchronized进行过性能优化。如自旋锁、轻量级锁、偏向锁等。

总结一下

什么时候需要使用sychronized去同步代码?

在多线程操作共享资源的时候需要用到sychronized关键字。(这里延伸一下,准确地说是什么时候需要添加同步配置,因为使用synchronized只是同步配置中的一种,让java代码变得同步还有很多种配置方法,例如:Lock类,Cas,Volatile关键字,分布式锁,如果你写的java代码中有操作mysql数据库,本身mysql中也提供了大量的同步实现方法。但是具体使用哪种锁方式就要根据实现难易程度,各自锁的优缺点,具体场景,自己实验测试,团队理解成本等等方面考虑)

上一个问题说了共享资源要用到synchronized关键字,那么在Java中哪些是共享资源?

1.多线程操作的是同一个对象,在同一个对象中的实例变量实例方法

2.单例模式返回的对象实例。单例模式下返回的对象满足第一点,多线程操作的是同一个对象。

3.单例对象可能来源于:1.jdk原生的系统类,可能某个原生的类返回的对象是单例的。2.通过@Bean注解返回的对象3.自己手写单例模式返回的对象。

4.单例方法,单例变量:例如static修饰的方法和变量

备注:在@controller类下写的代码一定是被多线程访问的。所以这也是一个在@controller类下写代码的注意事项。前面我关于伙伴匹配系统加锁的笔记有说只要是@controller下写的代码一定都会是被多线程访问的。因为@controller里面的代码会被多线程执行,如果我们在@controller里面操作的对象都是来一个线程现场new一个对象没有关系,但是如果使用的对象是单例对象就要注意了。

那此时如何检验一个对象、一个静态成员(方法、变量)是否是线程安全呢?

1.阅读代码,比如你会发现我们很常用的System.out.println()方法就有加synchronized进行同步。是不是很惊奇!平常一直用的方法居然还有加同步配置。为什么要加因为System.out是一个静态变量(下面有源码截图)那么对于多线程来说它就是共享资源。只要是共享资源就要去衡量这个要不要加同步配置。(这里延伸一下同步配置不止有synchronize关键字,还有很多例如Volatile关键字,Lock类,CAS,分布式锁,如果是对mysql操作过程的同步mysql也要很多方法实现同步也不一定要使用synchronized,但是具体使用哪种锁方式就要根据实现难易程度,各自锁的优缺点,具体场景,自己实验测试,团队理解成本等等方面考虑)

下图代码是System.out.println()方法中加的同步配置,这里使用的是synchronized来进行同步

image-20231114133108224

如下所示System.out被static修饰是一个静态变量

image-20231114133116676

2.实验测试一个对象是否是线程安全的

比如测试这里的hashmap对象是否是线程全的:

image-20231114133126447

按理来说这里大小应该是20000,但是实际并不是20000,比20000小。这就说明这里的hashmap对象不是线程安全的。

3.查百度,看官网,直接问有经验的人,参考项目历史版本中的代码。

# 六、什么时候使用Synchronized关键字

使用Synchronized关键字,是为了让代码在多线程环境下同步执行。所以需要牢牢记住“共享”这两个字。只有共享资源的写访问才需要同步化,如果不是共享资源,那么就没有同步的必要。如果多线程对共享资源访问了,只是读没有涉及到对数据的写,那么就没有同步的必要。

# 七、Synchronized使用String字符串常量作为对象锁的坑

JVM具有String常量池的功能,所以如果使用字符串常量作为锁对象建议这样使用:

String  str  =  new  String(“a”);

synchronized(str){

//需要同步执行的代码

}

不建议这样使用

String  str  =  “a”;

synchronized(str){

//需要同步执行的代码

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 八、在Synchronized修饰的代码中,什么时候会释放锁

两种情况:

1.自然释放,就是正常一个线程结束完方法

2.出现异常,一个线程执行方法过程中如果出现异常也会释放锁

# 九、继承环境下的Synchronized的使用情况

父类

实例方法A,该方法有使用synchronized修饰。

子类继承父类

新定义实例方法B,该方法有使用synchronized修饰。

这个时候子类下的方法A和方法B多线程下操作是同步执行的,即子类名 子类对象名 = new 子类() ,使用子类对象名.方法A()和子类对象名.方法B()是同步执行的。

如果子类中重写了父类的方法A,必须要自己再单独去添加synchronized修饰。否则没有同步。这样设计的原因是为了代码可读性。

# 十、使用synchronized造成的死锁问题以及如何使用jdk命令查看死锁状态

Java线程死锁是一个经典的多线程问题,因为不同的线程都在等待根本不可能被释放的锁,导致所有的任务都无法继续完成。在多线程技术中,“死锁”是必须要避免的,因为这会造成线程的“假死”。

代码实现:

image-20231114133229307

定义两个runnable

image-20231114133239987

image-20231114133250004

进行测试:

image-20231114133259497

程序运行结果:

image-20231114133310067

从程序运行结果来看程序进入了死锁状态。

可以使用JDK自带的工具来检测是否有死锁现象

先使用jps获取运行的id

image-20231114133319552

然后使用jstack -l 73640命令,如下监测出死锁现象

image-20231114133327674

死锁是程序设计的bug,在设计程序时就要避免双方持有对方的锁,只要互相等待对方释放锁,就有可能出现死锁。

备注:本篇只代表个人理解,可能有理解不到位的地方。如有错误请指正。

最近更新: 11/17/2023, 10:27:15 AM
编程导航   |