什么是synchronized

synchronized是Java提供的一个关键字,用于方法或者代码块,保证并发安全。

synchronized使用场景

同步代码块(原子性)

synchronized可以用在方法上,或者用在代码块。

可锁的对象可以是普通对象,或者是类(也就是Class对象)。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SynchronizedTest {

static final Object monitor = new Object();

public static void main(String[] args) {
synchronized (monitor) {
System.out.println("对象锁同步代码块访问");
}
synchronized (SynchronizedTest.class) {
System.out.println("类锁同步代码块访问");
}
}
}

线程通信(可见性)

线程交替打印1~100

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class SynchronizedTest {

private static final Object monitor = new Object();
private static volatile int count = 0;
private static final int MAX = 100;

public static void main(String[] args) {
Thread t1 = new Thread(new Print(), "thread-1");
Thread t2 = new Thread(new Print(), "thread-2");
t1.start();
t2.start();
}

static class Print implements Runnable {

@Override
public void run() {
while (count <= MAX) {
synchronized (monitor) {
try {
if (count <= MAX) {
System.out.println(Thread.currentThread().getName() + ": " + count);
count++;
}
monitor.notify();
monitor.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}

}
}
}

synchronized原理

字节码层面

  • 同步代码块使用monitorenter和monitorexit实现
  • 同步方法使用ACC_SYNCHRONIZED实现

数据结构方面

和ReentrantLock有着类似的实现结构:

  • monitor:锁对象,有一个count计数器用来实现重入
  • value:互斥量
  • Owner:持有锁的线程
  • EntryList:同步队列,没有获取到锁的线程会在这里阻塞
  • WaitSet:条件队列,持有锁的线程调用了wait()之后会被放到条件队列,并唤醒同步队列的第一个阻塞节点

image-20240325185308438

synchronized锁升级

synchronized的锁有四种状态:无锁、偏向锁、轻量级锁、重量级锁

image-20240325191122481

对于没有锁定的对象,锁标志位是01,此时可能是无锁或者偏向锁状态,对于轻量级锁,锁标志位是00,对于重量级锁,锁标志位是10。

在JDK6以后引入了偏向锁以及轻量级锁来优化synchronized的性能,这才使得synchronized的性能和ReentrantLock的性能差不多。

轻量级锁

轻量级锁是为了只有两个线程来交替获取同一个锁的状况进行的优化,对于没有锁定的对象,会先在本线程的栈帧生成一个锁记录(Lock Record)的空间,用于拷贝当前的Mark Word,随后用CAS把Mark Word指向栈帧的指针指向本线程:

  • 如果成功了,则代表当前线程获取到该对象的锁,并且修改锁标志位为00。
  • 如果失败了,则说明至少有一条线程在争抢锁,先检查Mark Word当前指针是否指向本线程的栈帧,是则直接进入同步代码块执行,如果有多条线程争抢,则锁需要膨胀到重量级锁。

对于轻量级锁的释放,同样用到CAS操作,把栈帧的锁记录(Lock Record)替换回Mark Word:

  • 如果成功了,则说明同步过程完成
  • 如果失败了,也就是锁膨胀了,说明其它(第三个或者更多)线程尝试获取过该锁,需要唤醒同步队列的其它线程

总的来说,就是当有竞争,会先暂停当前同步代码块的线程,修改当前的锁标志位和对象头,等待当前同步代码块线程执行完成,再进行争抢

偏向锁

偏向锁是为了消除无竞争的情况下的一个锁优化,当线程获取一个无锁状态的对象,会先用CAS尝试修改Thread ID设置为自己的线程ID,如果设置成功,同时会把偏向锁标志位设置为1,此后的线程访问这个同步代码块将不用再执行同步操作。

当有其他线程尝试获取锁的时候,偏向锁失效,发生锁升级为轻量级锁,撤销偏向锁标志位为0,后续操作在轻量级锁已经叙述过。

重量级锁

利用操作系统进程通信信号量的互斥量来实现,锁标志位为10,在多线程竞争锁的时候,重量级锁更加合适,避免了线程空转占用CPU

总结

一开始是无锁状态;

当A线程代码运行到synchronized这里,会先进入偏向锁;

当A线程执行同步区代码期间,有B线程来试图访问同步区代码,(A线程在退出同步区代码的时候会CAS撤销偏向锁,如果失败就说明锁需要继续膨胀为轻量级锁),然后两个线程去抢锁;

这个时候如果还有第三个甚至更多线程试图访问同步区代码,(A线程在退出同步区代码的时候会CAS撤销轻量级锁,如果失败就说明锁需要继续膨胀为重量级锁),然后几个线程去抢锁,Mark Word指针指向Monitor;

synchronized锁优化

锁自旋

锁自旋(不断重试)代替锁阻塞(放弃CPU时间片,直接挂起)

锁消除

逃逸分析(锁的对象是本地变量,不会共享),某个代码块只会被单一线程访问

锁粗化

不要连续的加锁和解锁,适当加大锁的粒度可能效率更高,避免在循环次数很多的循环内部使用synchronized

原子性

单线程执行,不存在并发

可见性

  1. 进入同步代码块之前通过读内存屏障,会先从主存同步数据值
  2. 退出同步代码块之前会通过写内存屏障,将数据从线程本地内存同步到主存

有序性

同步代码块内部通过内存屏障,禁止指令重排