参考阅读:深入理解 Java 虚拟机——JVM 高级特性与最佳实践(第十三章)
原文链接:https://zeroclian.github.io/posts/632b531d.html
Synchronized 的了解
- 解决的是多个线程之间访问资源的同步性,保证被修饰的方法或代码块在任意时刻只能有一个线程 执行。
- 属于重量级锁,效率低下,JDK1.6之后,官方从 JVM 层面优化后,效率有所提升。
使用方式
- 修饰实例方法:作用于当前对象实例加锁,进入同步代码前要获取当前对象实例的锁。
- 修饰静态方法:给当前类加锁,作用于类的所有实例对象
- 修饰代码块:指定加锁对象,对给定对象加锁。
总结:synchronized
关键字加到static
静态方法和synchronized(class)
代码块上都是给Class类加锁。synchronized
关键字加到实例方法上是给对象实例上锁。尽量不要使用synchronized(String s)
因为JVM中,字符串常量池具有缓存功能。
synchronized在单例模式中的应用
双重校验锁实现对象单例(线程安全)
public class Singleton(){
private volatile static Singleton uniqueInstance;
private Singleton(){
}
public synchronized static SingletongetUniqueInstance({
//先判断对象是否已经实例过,没有实例化过才进⼊加锁代码
if(uniqueInstance == null){
///类对象加锁
synchronized (Singleton.class){
if(uniqueInstance == null){
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
-
uniqueInstance 采用 volatile 关键字修饰也是很有必要,uniqueInstance = new Singleton(); 这段代码其实是分三步走:
- 为 uniqueInstance 分配内存空间
- 初始化 uniqueInstance
- 将 uniqueInstance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执⾏顺序有可能变成 1→3→2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致⼀个线程获得还没有初始化的实例。例如,线程 T1 执⾏了 1 和 3,此时 T2 调用getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。
使用 volatile 可以禁止指令重排,保证多线程环境下也能正常运行。
底层原理
synchronized 同步代码块原理
Demo.java
public class Demo{
public void method(){
synchronized(this){
System.out.println("JavaStudys");
}
}
}
通过 JDK 自带的 javap 命令查看 Demo 类的相关字节码信息:
- 切到类对应目录执行
javac Demo.java
生成编译后的 .class 文件 - 执行
javap -c -s -v -l Demo.class
从图可知:
synchronized 同步代码块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令指向同步代码块的结束位置。
当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor 的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外⼀个线程释放为止。
(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因)
synchronized 修饰方法原理
Demo2.java
public class Demo2{
public synchronized void method(){
System.out.println("JavaStudys");
}
}
synchronized 修饰的方法并没有标识 monitorenter 和 monitorexit ,而是通过 ACC_SYNCHRONIZED 标识,指明该方法是一个同步方法,JVM 通过 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
JDK1.6 之后对 synchronized 做了哪些优化?
JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、自适应自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
推荐阅读:https://zeroclian.github.io/posts/1e489958.html
synchronized 与 ReentrantLock 的区别
两者都是可重入锁
“可重入锁”:自己可以再次获取自己的内部锁。比如一个线程获取了某个对象的锁,此时对象的锁还没释放,当其想要再次获取这个对象的内部锁的时候还是可以获取的,如果不可重入,就会造成死锁。同一个线程每次获取锁,锁计数器都会自增1,当计数器变为0时才可释放。
synchronized 依赖于JVM 而 ReentrantLock 依赖于API
前面讲过,JDK1.6在 JVM 层面对 synchronized 关键字做了很多优化,但这些都是在虚拟机层面,并没有暴露给我们。而 ReentrantLock 是在 JDK 层面(API),需要通过 lock() 和 unlock() 方法配合 try/finally 语句来完成,因此可以通过源代码来查看实现方式。
ReentrantLock 比 synchronized 增加了一些高级功能
-
等待可中断
ReentrantLock 提供了一种能够中断等待锁的线程机制,通 lock.lockInterruptibly() 来实现。即可以在等待过程选择放弃,改为处理其他事情。
-
可实现公平锁
synchronized 只能是非公平锁,ReentrantLock 可以指定公平锁还是非公平锁。所谓公平锁就是先等待先获取锁,ReentrantLock 默认是非公平,可以通过 ReentrantLock(boolean fair) 构造方法来制定是否公平。
-
可实现选择性通知(锁可以绑定多个条件)
synchronized关键字与
wait()
和notify()/notifyAll()
方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition
接⼝与newCondition()
方法。Condition
是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在⼀个Lock对象中可以创建多个Condition
实例(即对象监视器),线程对象可以注册在指定的Condition
中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()
方法进行通知时,被通知的线程是由 JVM 选择的,用 ReentrantLock 类结合Condition
实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition
接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有⼀个
Condition
实例,所有的线程都注册在它⼀个身上。如果执行notifyAll()
方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition
实例的signalAll()
方法 只会唤醒注册在该Condition
实例中的所有等待线程。
注意:JDK1.6之后性能不再是两者选择的标准