synchronized关键字
# synchronized关键字
# synchronized介绍
synchronized 关键字解决了多个线程之间访问资源的同步性,它可以保证被它修饰的方法或代码块在任意时刻只能由一个线程运行。
在 Java 早期版本,synchronized 是重量级锁,因为监视器锁 Monitor 依赖于底层的操作系统的 Mutex Lock 来实现的,Java 线程是映射到操作系统的原生线程上的,如果要挂起或唤醒一个线程,都需要操作系统帮我完成,而操作系统线程之间的切换需要从用户态转换为内核态,这个转换过程时间较长,时间成本相对较高。
Java6 对 synchronized 进行了优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等减小锁操作的开销。锁主要存在四种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,它们会随着竞争的激励而逐渐升级。锁可以升级,但不可以降级,这种策略是为了提高获得锁和释放锁的效率。所以,不论是各种开源框架还是JDK源码都大量使用 synchronized 关键字。
# synchronized使用
修饰实例方法:作用域当前对象实例,进入同步代码块前要获得当前对象实例的锁。
class Test{ public synchronized void test() { ... } } // 等价于 class Test{ public void test() { synchronized(this) { ... } } }
1
2
3
4
5
6
7
8
9
10
11
12
13修饰静态方法:给当前类加锁,会作用类的所有对象实例,进入同步代码块前要获得当前 class 的锁。如果,线程 A 调用实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的是当前类的锁,访问非静态 synchronized 方法占用的锁是当前实例对象锁。
class Test{ public synchronized static void test() { } } // 等价于 class Test{ public static void test() { synchronized(Test.class) { } } }
1
2
3
4
5
6
7
8
9
10
11
12
13修饰代码块:指定加锁对象,给对象/类加锁。
synchronized(this|object)
表示进入同步代码库前要获得给定对象的锁。synchronized(类.class)
表示进入同步代码前要获得 当前 class 的锁
注意:构造方法不可以使用,因为构造方法本身就是线程安全的,不存在同步的构造方法。
# synchronized 修饰同步代码块的底层原理
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("synchronized 代码块");
}
}
}
2
3
4
5
6
7
synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令指向同步代码块的结束位置。
Monitorenter 和 Monitorexit 指令,会让对象在执行,使其锁计数器加1或者减1。每一个对象在同一时间只与一个 monitor (锁)相关联,而一个 monitor 在同一时间只能被一个线程获得,一个对象在尝试获得与这个对象相关联的 Monitor 锁的所有权的时候,monitorenter 指令会发生如下3中情况之一:
- monitor 计数器为 0,意味着目前还没有被获得,那这个线程就会立刻获得然后把锁计数器 +1,一旦 +1,别的线程再想获取,就需要等待
- 如果这个 monitor 已经拿到了这个锁的所有权,又重入了这把锁,那锁计数器就会累加,变成 2,并且随着重入的次数,会一直累加
- 这把锁已经被别的线程获取了,等待锁释放
monitorexit 指令:释放对于monitor的所有权,将 monitor 的计数器减1,如果减完以后,计数器不是0,则代表刚才是重入进来的,当前线程还继续持有这把锁的所有权,如果计数器变成 0,则代表当前线程不再拥有该 monitor 的所有权,即释放锁。
monitor 是基于 c++ 实现的,wait/notify 等方法也依赖于 monitor 对象,这也就是为什么只有在同步块或方法中才能调用wait/notify 等方法,否则会抛出 java.lang.IllegalMonitorStateException 的异常的原因。
# synchronized 修饰方法的底层原理
public class SynchronizedDemo2 {
public synchronized void method() {
System.out.println("synchronized 方法");
}
}
2
3
4
5
没有 monitorenter 和 monitorexit 指令,取而代之的是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。如果是实例方法,JVM 会尝试获取实例对象的锁;如果是静态方法,JVM 会尝试获取当前 class 的锁。
# synchronized 和 ReentrantLock的区别
相同点:
- 都是可重入锁
- 都保证了可见性和互斥性
- 都可以用于控制多线程共享对象访问
不同点:
- synchronized 是 java 关键字,依赖于 JVM,而 RentrantLock 是 Lock 接口下的实现类,依赖于 API。
- synchronized 隐式的获取锁和释放锁,ReentrantLock 显式的获取和释放锁,在使用时避免程序异常无法释放锁,需要在 finally 控制块中进行解锁操作。
- synchronized 是非公平的,ReentrantLock 默认也是非公平的,但是可以通过修改参数来实现公平锁。
- ReentrantLock 可以实现选择性通知(绑定多个条件)。synchronized 关键字与 wait()、notify()、notifyAll() 实现等待/通知,随机唤醒线程;reetrantLock 类可以借助于 Condition 实现选择性通知,精确唤醒的线程。(sync、wait、notify —— lock、await、signal)
- synchronized 不可中断,除非抛出异常或者正常运行完成;ReentrantLock 等待可中断,可以设置超时或使用 interrupt() 方法中断。
- synchronized 的加锁顺序和解锁顺序是相反的,而 Lock 可以自定义顺序。
- 简单的环境下 synchronized 效率高,复杂场景下(使用线程池) lock 效率高。
- synchronized 锁只能同时被一个线程拥有,Lock 锁没有这个限制(读写锁)