synchronized 是 java的内置锁。在多线程,数据一致性的场景下经常会使用到,而面试时也经常会问到,所以我这里做一个总结。理解synchronized的时候不要只从字节码的角度理解,还要清楚它的同步机制的实现,它是由它的等待队列和同步队列来实现的;synchronized是锁监视器,它一般会监视一个对象锁,每个对象的对象头都有一个锁记录指针,它指向一个Monitor,所以这才有Object::wait()和Object::notifyAll(),我们可以把这两个方法看成是等待队列和同步队列的开关,持有锁的线程执行Object::wait()就会释放锁,把本线程加入等待队列,如果持有锁的线程执行Object::notifyAll(),会先执行完临界代码,释放锁,同时把等待队列里的所有线程都移出去,共同竞争一把锁,竞争锁失败的线程又会进入到同步队列。另外,synchronized的等待队列和同步队列里的线程的状态转换关系,这又和线程的状态有关。所以要理解synchronized得清楚Object对象锁,线程状态转移关系,它们不是各自独立的,我们要在这些东西之间要建立关联关系来理解它们。和synchronized差不多的Lock (AQS) 也是如此。
底层实现
synchronized 底层是由两个字节码指令来实现的,分别是 monitorEnter和 monitorExit,它们包裹的代码块就是需要保护的临界资源。synchronized是非公平锁,是抢占式的,可能会导致线程饥饿,另外,当synchronized包裹的代码块执行的时候抛出异常,synchronized会自动释放锁,所以synchronized 的字节码底层实际上是有两个 monitorExit ,另外的一个monitorExit 是在出现异常时自动释放锁的。这个可以通过 javap -c XX.class 文件验证。
public class Test4 {
private static Object lock = new Object();
public static void main(String[] args) {
synchronized (lock){
}
}
}
生成的字节码如下,看到了 monitorexit 和 athrow,处理异常。
Compiled from "Test4.java"
public class com.yzp.algo.Test4 {
public com.yzp.algo.Test4();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field lock:Ljava/lang/Object;
3: dup
4: astore_1
5: monitorenter
6: aload_1
7: monitorexit
8: goto 16
11: astore_2
12: aload_1
13: monitorexit
14: aload_2
15: athrow
16: return
Exception table:
from to target type
6 8 11 any
11 14 11 any
static {};
Code:
0: new #3 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: putstatic #2 // Field lock:Ljava/lang/Object;
10: return
}
同步队列和等待队列
我们经常使用synchronized来处理线程同步,synchronized一般会监视一个Object对象,因为每一个对象都是一把锁,对象的对象头有一个锁记录信息,每个对象都和一个Monitor对象相关联,Monitor对象记录了持有锁的线程信息,等待队列等。Monitor对象包含以下三个字段:
_owner 记录当前持有锁的线程
_EntryList 是一个队列,记录所有阻塞等待锁的线程
_WaitSet 也是一个队列,记录调用 wait() 方法并还未被通知的线程
当线程持有锁的时候,线程id等信息会拷贝进owner字段,其余线程会进入阻塞队列entrylist,当持有锁的线程执行wait方法,会立即释放锁进入waitset,当线程释放锁的时候,owner会被置空,公平锁条件下,entrylist中的线程会竞争锁,竞争成功的线程id会写入owner,其余线程继续在entrylist中等待。synchronized通常会结合Object::wait()和Object::notifyAll()进行使用。
参考: https://www.cnblogs.com/ZoHy/p/11313155.html
![](https://i-blog.csdnimg.cn/blog_migrate/d8910ef25b21fd8d4aa697d52f434954.png)
synchronized有两个队列,一个是同步队列,一个是等待队列。它们都是唯一的。
当A线程持有锁时B线程想抢锁,B线程自旋一段时间后抢不到锁就会进去synchronized的同步队列,当A线程释放了锁,B线程就从同步队列里出来获取锁。如果线程A调用了wait() 方法,那么线程A就会释放锁,并进入synchronized的等待队列,此时如果线程B获得了锁,当它执行结束后调用notifyAll()方法,那么线程A就会从synchronized的等待队列里移除,并可以去抢锁。
举例一个我面试过的百度题目:使用两个线程,交替打印0到99。
下面是我写的代码,线程A和线程B分别打印50次。线程A打印完后就唤醒线程B,并进入synchronized的等待队列,等待被线程B唤醒;线程B打印完后就唤醒线程A,并进入synchronized的等待队列,等待被线程A唤醒。因为线程A和线程B想要执行,就必须获得一把锁,在这把锁看来,同一时刻,线程A和线程B必定有一个是被阻塞的,它处于这把锁的等待队列( 按理说没抢到锁的线程会在同步队列,但这里已经释放锁的线程通过wait()方法把自己阻塞了而进入等待队列 ),另一个在运行,然后不停地轮流在这个这把锁的等待队列交换进交换出。
public class Test3 {
private static volatile int val = 0;
private static Object lock = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
synchronized (lock) {
if (val%2==1){
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
if (val % 2 == 0) {
System.out.println( Thread.currentThread().getName() + "打印:" + val);
val++;
lock.notifyAll();
} else {
lock.notifyAll();
}
}
}
}
}, "thread A");
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
synchronized (lock) {
if (val%2==0){
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
if (val % 2 == 1) {
System.out.println(Thread.currentThread().getName() + "打印:" + val);
val++;
lock.notifyAll();
}
}
}
}
}, "thread B");
thread1.start();
thread2.start();
}
}
![](https://i-blog.csdnimg.cn/blog_migrate/d8cb84ed9f0213b1214d3bf33a3bd096.png)
其余的写法: Java并发编程:两个线程交替打印0-100的奇偶数_java 多线程获取0-100中奇数_Java后端何哥的博客-CSDN博客
交替打印 ABC
各个线程相互通知协作版
public static class DemoTask2 implements Runnable {
private static Object obj = new Object();
private static int count = 0;
private static final int MAX = 30;
private int index;
public static void main(String[] args) {
ArrayList<Thread> list = new ArrayList<>();
list.add(new Thread(new DemoTask2(0)));
list.add(new Thread(new DemoTask2(1)));
list.add(new Thread(new DemoTask2(2)));
list.forEach(Thread::start);
}
@SneakyThrows
@Override
public void run() {
while (count < MAX) {
synchronized (obj) {
if (count >= MAX){
break;
}
if ((count % 3 != index)){
obj.wait();
}else {
System.out.print((char) ('A' + index));
count++;
obj.notifyAll();
}
}
}
}
public DemoTask2(int index) {
this.index = index;
}
}
各个线程抢锁版:
public static class DemoTask implements Runnable {
private static Object obj = new Object();
private int index;
private static int count = 0;
private static final int MAX = 30;
public DemoTask(int index) {
this.index = index;
}
public static void main(String[] args) throws Exception {
ArrayList<Thread> list = new ArrayList<>();
list.add(new Thread(new DemoTask(0)));
list.add(new Thread(new DemoTask(1)));
list.add(new Thread(new DemoTask(2)));
list.forEach(Thread::start);
}
@Override
public void run() {
while (count < MAX) {
synchronized (obj) {
if (count < MAX && (count % 3 == index)) {
System.out.print((char) ('A' + count % 3));
count++;
}
}
}
}
}
锁升级和编译优化
synchronized是可重入锁。synchronized是悲观锁,会因为线程争用加剧导致锁升级,由无锁变为偏向锁,轻量级锁,重量级锁,这个过程不可逆。
synchronized在编译时期会根据逃逸分析进行优化,可能会导致锁粗化和锁消除。锁粗化会把多个加锁释放锁的过程合并为一个,导致锁的作用范围增大,减小了多个加锁释放锁的消耗。锁消除是编译器通过分析后认为没有数据争用,不需要加锁,从而把加锁变为不加锁的过程。
Lock接口也是锁,作用和synchronized差不多,volatile 关键字可以看做是轻量级锁,虽然它不保证原子性。
Lock的底层实现是AQS,一把锁对应一个同步队列,它有一个同步队列,就是CLH队列,多个等待队列,newCondition()就是一把锁,就创建一个等待队列,可以根据一个condition实现精确的通知等待机制。