synchronized关键字
关于synchronized关键字的一些问题
synchronized是什么?有什么用?
synchronized
是 Java
中的一个关键字,翻译成中文是同步的意思,主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
在 Java 早期版本中,synchronized
属于
重量级锁,效率低下。这是因为监视器锁(monitor)是依赖于底层的操作系统的
Mutex Lock
来实现的,Java
的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。
不过,在 Java 6 之后, synchronized
引入了大量的优化如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销,这些优化让
synchronized
锁的效率提升了很多。因此,
synchronized
还是可以在实际项目中使用的,像 JDK
源码、很多开源框架都大量使用了 synchronized
。
关于偏向锁多补充一点:由于偏向锁增加了 JVM
的复杂性,同时也并没有为所有应用都带来性能提升。因此,在 JDK15
中,偏向锁被默认关闭(仍然可以使用 -XX:+UseBiasedLocking
启用偏向锁),在 JDK18
中,偏向锁已经被彻底废弃(无法通过命令行打开)。
如何使用synchronized?
synchronized
关键字的使用方式主要有下面 3 种:
- 修饰实例方法
- 修饰静态方法
- 修饰代码块
1、修饰实例方法 (锁当前对象实例)
给当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁 。
1 | synchronized void method() { |
2、修饰静态方法 (锁当前类)
给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。
这是因为静态成员不属于任何一个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享。
1 | synchronized static void method() { |
静态 synchronized
方法和非静态 synchronized
方法之间的调用互斥么?不互斥!如果一个线程 A 调用一个实例对象的非静态
synchronized
方法,而线程 B
需要调用这个实例对象所属类的静态 synchronized
方法,是允许的,不会发生互斥现象,因为访问静态 synchronized
方法占用的锁是当前类的锁,而访问非静态 synchronized
方法占用的锁是当前实例对象锁。
3、修饰代码块 (锁指定对象/类)
对括号里指定的对象/类加锁:
synchronized(object)
表示进入同步代码库前要获得 给定对象的锁。synchronized(类.class)
表示进入同步代码前要获得 给定 Class 的锁
1 | synchronized(this) { |
总结:
synchronized
关键字加到static
静态方法和synchronized(class)
代码块上都是是给 Class 类上锁;synchronized
关键字加到实例方法上是给对象实例上锁;- 尽量不要使用
synchronized(String a)
因为 JVM 中,字符串常量池具有缓存功能。
构造方法可以用synchronized修饰吗?
先说结论:构造方法不能使用 synchronized 关键字修饰。
构造方法本身就属于线程安全的,不存在同步的构造方法一说。
synchronized底层原理
synchronized同步语句块的情况
1 | public class SynchronizedDemo { |
通过 JDK 自带的 javap
命令查看
SynchronizedDemo
类的相关字节码信息:首先切换到类的对应目录执行
javac SynchronizedDemo.java
命令生成编译后的 .class
文件,然后执行javap -c -s -v -l SynchronizedDemo.class
。
从上面我们可以看出:synchronized
同步语句块的实现使用的是 monitorenter
和
monitorexit
指令,其中 monitorenter
指令指向同步代码块的开始位置,monitorexit
指令则指明同步代码块的结束位置。
上面的字节码中包含一个 monitorenter
指令以及两个
monitorexit
指令,这是为了保证锁在同步代码块代码正常执行以及出现异常的这两种情况下都能被正确释放。它在底层使用了隐式的try
- finally,以防正常代码在执行过程中抛了异常。
当执行 monitorenter
指令时,线程试图获取锁也就是获取
对象监视器 monitor
的持有权。
Monitor
被翻译为监视器,是由jvm
提供,C++
语言实现。
在执行monitorenter
时,会尝试获取对象的锁,如果锁的计数器为
0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。
对象锁的的拥有者线程才可以执行 monitorexit
指令来释放锁。在执行 monitorexit
指令后,将锁计数器设为
0,表明锁被释放,其他线程可以尝试获取锁。
如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
synchronized修饰方法的情况
1 | public class SynchronizedDemo2 { |
synchronized
修饰的方法并没有 monitorenter
指令和 monitorexit
指令,取得代之的确实是
ACC_SYNCHRONIZED
标识,该标识指明了该方法是一个同步方法。JVM 通过该
ACC_SYNCHRONIZED
访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
如果是实例方法,JVM 会尝试获取实例对象的锁。如果是静态方法,JVM 会尝试获取当前 class 的锁。
总结
synchronized
同步语句块的实现使用的是
monitorenter
和 monitorexit
指令,其中
monitorenter
指令指向同步代码块的开始位置,monitorexit
指令则指明同步代码块的结束位置。
synchronized
修饰的方法并没有 monitorenter
指令和 monitorexit
指令,取得代之的确实是
ACC_SYNCHRONIZED
标识,该标识指明了该方法是一个同步方法。
不过两者的本质都是对对象监视器 monitor 的获取。
JDK1.6后的synchronized底层做了哪些优化?锁升级原理?
在 Java 6 之后, synchronized
引入了大量的优化如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销,这些优化让
synchronized
锁的效率提升了很多(JDK18
中,偏向锁已经被彻底废弃,前面已经提到过了)。
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
synchronized
锁升级是一个比较复杂的过程,面试也很少问到,如果你想要详细了解的话,可以看看这篇文章:浅析 synchronized
锁升级的原理与实现
synchronized和volatile有什么区别
synchronized
关键字和 volatile
关键字是两个互补的存在,而不是对立的存在!
volatile
关键字是线程同步的轻量级实现,所以volatile
性能肯定比synchronized
关键字要好 。但是volatile
关键字只能用于变量而synchronized
关键字可以修饰方法以及代码块 。volatile
关键字能保证数据的可见性,但不能保证数据的原子性。synchronized
关键字两者都能保证。volatile
关键字主要用于解决变量在多个线程之间的可见性,而synchronized
关键字解决的是多个线程之间访问资源的同步性。