volatile 关键字可以保证共享变量的可见性和有序性,但不能保证原子性,要想同时满足三者,可以使用 synchronized 关键字。synchronized能够确保同一时刻只有一个线程可以执行某个代码块或方法,从而避免多个线程同时问共享资源时圬引发的线程安全问题。
基本使用
- 对象锁
通过synchronized代码块,指定锁定对象(自定义锁对象或锁对象为this,当前实例对象)
public class SynchronizedPrinciple {
// 自定义锁对象
Object lock = new Object();
public void method1() {
// synchronized(this) 当前实例对象锁
synchronized (lock) {
}
}
}
通过synchronized修饰普通方法,锁对象默认为this
public class SynchronizedPrinciple {
//synchronized修饰普通方法,锁对象默认为this
public synchronized void method2() {
}
}
- 类锁
通过synchronize修饰静态的方法,默认的锁就是当前所在的Class类
public class SynchronizedPrinciple {
// synchronized用在静态方法上,默认的锁就是当前所在的Class类
public static synchronized void method2() {
}
}
通过synchronized代码块,指定锁对象为Class对象
public class SynchronizedPrinciple {
public void method1() {
synchronized (SynchronizedPrinciple.class) {
}
}
}
原理分析
通过java命令,反编译上述的java代码,查看.class文件的信息
在Java中,每一个对象都有一个关联的监视器,监视器用于实现同步方法和synchronized代码块。 当一个线程进入一个同步方法或同步代码块时,它必须首先获得该对象的监视器。
-
monitorenter指令:它的作用是尝试获取对象的内部锁(监视器锁)。如果锁未被占用,则当前线程可以获得锁(锁计数器加1)并继续执行;如果锁已被其他线程占用,则当前线程将被阻塞,直到锁被释放。
-
monitorexit指令:它的作用是当线程执行完synchronized块的代码或抛出异常时,monitorexit指令会释放之前获取的锁(锁计数器减1),允许其他等待的线程有机会获取锁并进入同步块。
锁的优化
在之前Synchronized的效率是非常低的,因为在多线程下无论线程竞争锁是否激烈它都会创建重量级锁。1.6版本中synchronized引入了大量的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销。
- 自旋锁
自旋锁是指线程在短时间内尝试获取锁时,不会立即进行阻塞,而是在循环中不断尝试获取锁,这样可以避免线程频繁地进入和退出阻塞状态(涉及线程上下文切换换),从而减少线程调度带来的开销。 但如果自旋时间过长,会让CPU空转,会占用CPU资源,反而降低低性能。
- 自适应自旋锁
自适应自旋锁与自旋锁的区别是:自适应自旋锁的自旋次数不是固固定的,而是根据前一次自旋锁的获取情况以及锁的拥有者的状态来动态调整。
- 锁消除
锁消除是指JVM通过对代码的逃逸分析,判断某些同步块所使用的锁对象不会逃逸出线程,从而消除这些不不必要的锁操作。
- 锁粗化
通常情况下,我们都是将同步块的范围尽量缩小,也就是锁粒度尽量缩小,以减少锁的竞争。然而,如果在一个短时间内,对象被频繁地加锁和解锁,反而会增加性能开销。锁粗化通过扩展同步块的范围,将多个连续的加锁和解锁操作合并为一个,从而减少锁操作的频率,提升性能。
Synchronied同步锁,共有四种状态:无锁、偏向锁、轻量级锁、重量级锁,它会随着竞争情况逐渐升级。
- 偏向锁
只有一个线程访问,不存在多线程争用的情况,就会使用偏向锁,线程不需要触发同步就能获得锁,降低获得锁的代价,只有当其他线程请求锁时,偏向锁才会撤销,改为轻量级锁等其他形式的锁。
- 轻量级锁
如果另一个线程尝试获取已被标记为偏向锁的锁,JVM会将锁升级为轻量级锁。此时,线程会通过自旋尝试获取锁,避免立即进入阻塞状态,减少操作系统调度的开销。 偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。
- 重量级锁
如果轻量级锁仍然无法有效处理线程争用,JVM会进一步将锁升级为重量级锁。此时,线程会进入操作系统级别的等待队列,涉及到内核态切换,开销较大。