一、Synchronized关键字
synchronized 内置锁是一种对象锁,作用粒度是对象而非引用变量,可将任何非空对象当做“锁”,可用来修饰方法或代码块以保证同一时刻最多只有一个线程执行被修饰方法或代码块。synchronized 修饰的地方不相同,但它的锁都在一个对象上,它持有相同对象锁的地方发产生互斥,而不是只有当前所指的代码块或者方法体。
相对于显示锁,synchronized 不需要加锁和解锁的操作。synchronized 语法简单方便,高并发情况下也同样适用。
二、Synchronized的用法
(1)同步代码块
指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象,synchronized(Object),Object可为任意非空的对象。这里创建一个byte类型的数组作为加锁时使用的对象,如下的代码:
private byte[] lock = new byte[1];
public void syncCodeSegment(){
synchronized(lock){
// do task
}
}
(2)同步实例方法
作用于当前实例对象,进入同步代码前要获得当前实例的锁。类的对象可以有多个,所以每个对象有其独立的对象锁,互不干扰。加锁的对象为当前对象this。
public synchronized void syncInstanceMethod(){
// do task
}
(3)修饰静态方法
作用于当前类的类对象(Class对象),进入同步代码前要获得当前类的类对象的锁。每个类只有一个 Class 对象,所以每个类只有一个类锁。Class数据存在于永久代,因此,静态方法锁相当于该类的一个全局锁。
public static synchronized void syncStaticMethod(){
// do task
}
三、Synchronized的作用
确保线程互斥的访问同步代码;
保证共享变量的修改能够及时可见,其实是通过Java内存模型中的 “对一个变量unlock操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值” 来保证的;
有效解决重排序问题,即 “一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”。
四、实现原理分析
当一个线程访问同步代码块时,首先是需要得到锁才能执行同步代码,当同步代码执行结束或抛出异常时必须要释放锁,那么它是如何来实现这个机制的呢?
分析 synchronized 的实现原理,需要借助JDK自带的反汇编器 javap。javap 的作用是根据 class 字节码文件,解析出当前类对应的 code 区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。通过这些信息可以找到一些与 synchronized 实现原理相关的内容。
如下代码,使用 synchronized 关键字修饰代码块,代码块中输出一句话:
public class SyncDemo {
/**
* 修饰代码块
*/
public void test1(){
synchronized (this){
System.out.println("修饰代码块");
}
}
// ...忽略修饰实例方法和修饰静态方法
}
现在使用javap反编译该类的代码,来看看它的字节码。找到该类class文件所在的文件夹,并在该文件夹下打开命令窗口,然后执行命令:javap -v SyncDemo.class,反编译结果如下(图中未截取反编译全部内容):

monitorenter:同步代码互斥的入口
当线程执行 monitorenter 指令时,就会尝试获取占有 monitor 对象。如果 monitor 的计数器等于0,也就是还没有线程占有 monitor 对象,那么当前线程就会占有这个对象,并且将 monitor 的计数器加1。如果 monitor 的计数器不为1,也就是已经有线程占有了 monitor 对象。这样需要进一步判断占有 monitor 对象的线程是不是当前线程,如果占有 monitor 对象的线程不是当前线程,那么当前线程进入阻塞状态,等待monitor 对象的计数器变为0,再尝试获取占有 monitor 对象;如果占有 monitor 对象的线程就为当前线程,也就是当前线程重新进入,那就将 monitor 对象的计数器加1,也就是 synchronized 的可重入性。
monitorexit:同步代码互斥的出口
当执行 monitorexit 指令时,monitor 的计数器减1。如果减1后进入数为0,那线程就释放锁,也就不在占有 monitor 对象。这时其他被这个 monitor 阻塞的线程可以尝试去获取占有 monitor 对象。
上图中 monitorexit 指令出现两次,第一次是同步代码正常执行完成的出口;第二次是同步代码出现异常时的出口。
上文分析了 synchronized 用于同步代码块时,是基于 monitor 对象实现同步的。那 synchronized 用于同步方法时,又是怎么实现的呢?还是熟悉的味道,先来一段代码:
public class SyncDemo {
// ...忽略修饰代码块的方法
/**
* 修饰实例方法
*/
public synchronized void test2(){
System.out.println("修饰实例方法");
}
// ...忽略修饰静态方法的方法
}
来看看 test2 方法反编译之后的结果,结果如下图:

从图中发现并没有出现 monitorenter 和 monitorexit 指令,然而,常量池中多了 ACC_SYNCHRONIZED标示符,JVM就是根据该标示符来实现方法的同步的。
当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
由此可知,本质上,两种同步方式没有区别。synchronized 的关键字底层是通过一个 monitor 的对象来完成同步的。Java 中每个对象都有一个 monitor 与之关联,当且当 monitor 被占有后,它处于锁定状态。synchronized 在JVM里的实现都是 基于进入和退出 monitor 对象来实现方法同步和代码块同步。
五、使用Synchronized注意的问题
(1)与 moniter 关联的对象不能为空
(2)synchronized 作用域太大
(3)避免不同的 monitor 企图锁相同的方法
(4)避免多个锁的交叉导致死锁
六、参考资料
【1】https://www.jianshu.com/p/e62fa839aa41
【2】https://blog.csdn.net/u011212394/article/details/82228321
【3】https://www.jianshu.com/p/6a8997560b05
【4】张振华 Java并发编程从入门到精通[M] 清华大学出版社
本文详细介绍了Java中的Synchronized关键字,包括其作用、用法、实现原理以及使用时需要注意的问题。Synchronized用于实现线程互斥,可修饰方法或代码块,基于monitor对象实现同步,确保共享变量的可见性和防止重排序。它通过monitorenter和monitorexit指令进行加锁和解锁,并在静态方法上使用ACC_SYNCHRONIZED标志。在使用中要注意避免空对象、过大作用域、锁对象混乱及死锁等问题。
398

被折叠的 条评论
为什么被折叠?



