目录
Synchronized
锁的是什么
在一些业务简单或某些单机系统不要求高性能高效率问题的情况下,可以使用 synchronized
进行线程安全处理,那么 synchronized
它到底锁的是什么
同步方法(非静态)测试示例一
public class SynchronizedTest {
public synchronized void test() {
System.out.println("test 方法开始调用...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test 方法结束调用...");
}
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new Thread(() -> {
SynchronizedTest synchronizedTest = new SynchronizedTest();
synchronizedTest.test();
}).start();
}
}
}
结果:
test 方法开始调用...
test 方法开始调用...
test 方法开始调用...
test 方法结束调用...
test 方法结束调用...
test 方法结束调用...
- 上面的程序起动了三个线程,同时运行
SynchronizedTest
类中的test()
方法,虽然test()
方法加上了synchronized
,但是查看结果,貌似synchronized
没起作用 - 很明显,在
for
循环下面,我们创建了3
个SynchronizedTest
实例。所以我们怀疑synchronized
没起作用,与这3
个SynchronizedTest
实例有关
同步方法(非静态)测试示例二
public class SynchronizedTest {
public synchronized void test() {
System.out.println("test 方法开始调用...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test 方法结束调用...");
}
public static void main(String[] args) {
SynchronizedTest synchronizedTest = new SynchronizedTest();
for (int i = 0; i < 3; i++) {
new Thread(synchronizedTest::test).start();
}
}
}
结果:
test 方法开始调用...
test 方法结束调用...
test 方法开始调用...
test 方法结束调用...
test 方法开始调用...
test 方法结束调用...
- 很明显,这一次在
for
循环下面,我们创建了1
个SynchronizedTest
实例,而且synchronized
起作用了。达到了预期效果 - 结论:在非静态的方法上,加上
synchronized
时,它锁得是方法所在类的实例
同步方法(静态)测试示例二
public class SynchronizedTest {
public static synchronized void test() {
System.out.println("test 方法开始调用...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test 方法结束调用...");
}
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new Thread(SynchronizedTest::test).start();
}
}
}
结果:
test 方法开始调用...
test 方法结束调用...
test 方法开始调用...
test 方法结束调用...
test 方法开始调用...
test 方法结束调用...
- 在一个静态的方法上,加上
synchronized
时,synchronized
起作用了。达到了预期效果 - 结论:在静态的方法上,加上
synchronized
时,它锁得是静态方法所在类的Class
对象
同步代码块测试示例一
public class SynchronizedTest {
public synchronized void test() {
synchronized (this) {
System.out.println("test 方法开始调用...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test 方法结束调用...");
}
}
public static void main(String[] args) throws ClassNotFoundException {
for (int i = 0; i < 3; i++) {
SynchronizedTest synchronizedTest = new SynchronizedTest();
new Thread(synchronizedTest::test).start();
}
}
}
结果:
test 方法开始调用...
test 方法开始调用...
test 方法开始调用...
test 方法结束调用...
test 方法结束调用...
test 方法结束调用...
- 上面的程序起动了三个线程,同时运行
SynchronizedTest
类中的test()
方法,虽然test()
方法加上了synchronized
,但是查看结果,貌似synchronized
没起作用 - 很明显,在
for
循环下面,我们创建了3
个SynchronizedTest
实例。所以我们怀疑synchronized
没起作用,与这3
个SynchronizedTest
实例有关
同步代码块测试示例二
public class SynchronizedTest {
public synchronized void test() {
synchronized (this) {
System.out.println("test 方法开始调用...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test 方法结束调用...");
}
}
public static void main(String[] args) throws ClassNotFoundException {
SynchronizedTest synchronizedTest = new SynchronizedTest();
for (int i = 0; i < 3; i++) {
new Thread(synchronizedTest::test).start();
}
}
}
结果:
test 方法开始调用...
test 方法结束调用...
test 方法开始调用...
test 方法结束调用...
test 方法开始调用...
test 方法结束调用...
- 在使用
synchronized (this) { }
同步代码块起时作用了。达到了预期效果 - 结论:在使用
synchronized (this) { }
同步代码块时,它锁得是当前所在类的实例
同步代码块测试示例三
public class SynchronizedTest {
public synchronized void test() {
synchronized (SynchronizedTest.class) {
System.out.println("test 方法开始调用...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test 方法结束调用...");
}
}
public static void main(String[] args) throws ClassNotFoundException {
for (int i = 0; i < 3; i++) {
SynchronizedTest synchronizedTest = new SynchronizedTest();
new Thread(synchronizedTest::test).start();
}
}
}
- 在使用
synchronized (类.class) { }
同步代码块起时作用了。达到了预期效果 - 虽然在
for
循环下面,我们创建了3
个SynchronizedTest
实例,显然没有干扰到预期效果,说明与这3
个SynchronizedTest
实例无关 - 结论:在使用
synchronized (类.class) { }
同步代码块时,它锁得是当前所在类的Class
对象
Synchronized
锁总结
同步方法
- 在非静态的方法上,加上
synchronized
时,它锁得是方法所在类的实例 - 在静态的方法上,加上
synchronized
时,它锁得是静态方法所在类的Class
对象
同步代码块
- 在使用
synchronized (this) { }
同步代码块时,它锁得是当前所在类的实例 - 在使用
synchronized (类.class) { }
同步代码块时,它锁得是当前所在类的Class
对象
Synchronized
原理
JVM
中的同步是基于进入和退出 Monitor
监视器对象实现的。每个对象实例都会有一个 Monitor
,Monitor
可以和对象一起创建、销毁
- 当多个线程同时访问一段同步代码时,多个线程会先被存放在
EntryList
集合(阻塞队列)中,处于BLOCKED
阻塞状态的线程,都会被加入到该队列中 - 接下来当线程获取到对象的
Monitor
监视器时,Monitor
监视器是依靠底层操作系统的Mutex Lock
来实现互斥的,线程申请Mutex
成功,则持有该Mutex
,其它线程将无法获取到该Mutex
- 如果线程调用
wait()
方法,就会释放当前持有的Mutex
,并且该线程会进入WaitSet
集合(等待队列)中,等待下一次被唤醒。此时线程会处于WAITING
等待状态或者TIMEDWAITING
超时等待状态 - 如果当前线程顺利执行完方法,也将释放
Mutex
总的来说,就是同步锁在这种实现方式中,因 Monitor
监视器是依赖于底层的操作系统实现的,存在用户态与内核态之间的切换(可以理解为上下文切换),所以增加了性能开销
Synchronized
优化升级
为了提升性能,jdk 1.6
引入了偏向锁、轻量级锁、重量级锁,来减少锁竞争带来的线程上下文切换
- 在
jdk 1.6
中,锁对象一共有4
种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁,这几个状态会随着竞争情况逐渐升级 - 锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率
偏向锁
经过 HotSpot
的作者大量的研究发现,大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁
偏向锁升级为轻量级锁
- 当线程
A
访问同步代码块并获取锁对象时,会在Java
对象头和栈帧中记录偏向锁的线程ID
,因为偏向锁不会主动释放锁,因此以后线程A
再次获取锁的时候,需要比较当前线程的线程ID
和Java
对象头中的线程ID
是否一致 - 如果一致,说明还是线程
A
获取锁对象,则无需使用CAS
来加锁、解锁 - 如果不一致,说明线程
B
要竞争锁对象,而偏向锁不会主动释放,因此还是存储的线程A
的线程ID
,那么需要查看Java
对象头中记录的线程A
是否存活 - 如果没有存活,那么锁对象被重置为无锁状态,线程
B
可以竞争将其设置为偏向锁** - 如果存活,那么立刻查找线程
A
的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程A
,撤销偏向锁,升级为轻量级锁
偏向锁的释放
偏向锁的释放采用只有竞争才会释放锁的机制,线程是不会主动去释放偏向锁,需要等待其他线程来竞争,然后其他线程使用 CAS
替换掉原来的线程的线程 ID
轻量级锁
轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要 CPU
从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋着等待锁释放
轻量级锁升级为重量级锁
- 线程
A
获取轻量级锁时,会先把锁对象的对象头MarkWord
复制一份到线程A
的栈帧中创建用于存储锁记录的空间,然后使用CAS
把锁对象的对象头中的内容替换为线程A
存储的锁记录的地址 - 如果在线程
A
复制对象头的同时(在线程B
的CAS
之前),线程B
也准备获取锁,复制了对象头到线程B
的锁记录空间中,但是在线程B
在CAS
的时候,发现线程A
已经把锁对象的对象头换了,线程B
的CAS
失败,那么此时线程B
就尝试使用自旋锁来等待线程A
释放锁 - 但是如果自旋的时间太长也不行,因为自旋是要消耗
CPU
的,因此自旋的次数是有限制的,如果自旋次数到了线程A
还没有释放锁,或者线程A
还在执行,线程B
还在自旋等待,这时又有一个线程C
过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU
空转
为了避免无用的自旋,轻量级锁一旦膨胀为重量级锁就不会再降级为轻量级锁了;偏向锁升级为轻量级锁也不能再降级为偏向锁。一句话就是锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态
锁粗化
按理来说,同步块的作用范围应该尽可能小,仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,缩短阻塞时间,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁
但是加锁解锁也需要消耗资源,如果存在一系列的连续加锁解锁操作,可能会导致不必要的性能损耗
锁粗化就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁,避免频繁的加锁解锁操作