面试官:能说说 Synchronized 吗?
答:Synchronized 是Java的一个关键字,使用于多线程并发环境下,可以用来修饰实例对象和类对象,确保在同一时刻只有一个线程可以访问被Synchronized修饰的对象,并且能确保线程间的共享变量及时可见性,还可以避免重排序,从而保证线程安全。
面试官:你背书呢?可以再具体的深入一点吗?
答:行!
相信很多 Android程序员跟我一样,最开始接触到 Synchronized 这个关键字是在创建单例的时候,如:
public class SingleTon {
private static volatile SingleTon instance;
public static SingleTon getInstance() {
if (instance == null) {
//同步锁,保证同一时刻只有一个线程进入该代码块。
synchronized (SingleTon.class) {
if (instance == null) {
instance = new SingleTon();
}
}
}
return instance;
}
}
这里前辈们告诉我们,这叫同步锁,保证同一时刻只有一个线程进入同步锁修饰的代码块,从而保证在多线程的环境下也只会创建一个 SingleTon 实例,达到单例效果。
那除了单例,Synchronized 的其他使用方法及其原理,你有额外了解过吗?今天就让我们来重头学习一遍吧!
从Java语法上来说,它有三种使用方法,分别是:
再来看一段代码:
public class SynchronizedTestRunnable implements Runnable {
@Override
public void run() {
a();
b();
}
public void a() {
System.out.println(Thread.currentThread().getName() + " a start on " + getCurrentTime());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " a end " + getCurrentTime());
}
public void b() {
System.out.println(Thread.currentThread().getName() + " b start " + getCurrentTime());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " b end " + getCurrentTime());
}
public static String getCurrentTime() {
String dateFormat = "yyyy-MM-dd hh:mm:ss";
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat);
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
return simpleDateFormat.format(calendar.getTime());
}
public static void main(String[] args) {
SynchronizedTestRunnable synchronizedTestRunnable = new SynchronizedTestRunnable();
new Thread(synchronizedTestRunnable).start();
new Thread(synchronizedTestRunnable).start();
}
}
其执行结果为:
Thread-1 a start on 2020-11-24 11:49:04
Thread-0 a start on 2020-11-24 11:49:04
Thread-0 a end 2020-11-24 11:49:07
Thread-1 a end 2020-11-24 11:49:07
Thread-0 b start 2020-11-24 11:49:07
Thread-1 b start 2020-11-24 11:49:07
Thread-0 b end 2020-11-24 11:49:08
Thread-1 b end 2020-11-24 11:49:08
根据执行结果可以看到两个线程会同时执行同一个 runnable 中的方法 a() 与 b(),并且不存在顺序关系,接下来我们试试加上 Synchronized 关键字。
给普通方法 a() 与 b() 分别都加上 Synchronized 关键字修饰,如下:
public class SynchronizedTestRunnable implements Runnable {
@Override
public void run() {
a();
b();
}
public synchronized void a() {
System.out.println(Thread.currentThread().getName() + " a start on " + getCurrentTime());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " a end " + getCurrentTime());
}
public synchronized void b() {
System.out.println(Thread.currentThread().getName() + " b start " + getCurrentTime());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " b end " + getCurrentTime());
}
public static String getCurrentTime() {
String dateFormat = "yyyy-MM-dd hh:mm:ss";
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat);
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
return simpleDateFormat.format(calendar.getTime());
}
public static void main(String[] args) {
SynchronizedTestRunnable synchronizedTestRunnable = new SynchronizedTestRunnable();
new Thread(synchronizedTestRunnable).start();
new Thread(synchronizedTestRunnable).start();
}
}
其执行结果为:
Thread-0 a start on 2020-11-24 11:50:10
Thread-0 a end 2020-11-24 11:50:13
Thread-0 b start 2020-11-24 11:50:13
Thread-0 b end 2020-11-24 11:50:14
Thread-1 a start on 2020-11-24 11:50:14
Thread-1 a end 2020-11-24 11:50:17
Thread-1 b start 2020-11-24 11:50:17
Thread-1 b end 2020-11-24 11:50:18
两个线程按顺序同步执行同一个 runnable 中的方法 a() 与 b(),达到同步锁效果。
思考一下:那要是两个线程分别执行两个 runnable 呢?如:
public static void main(String[] args) {
SynchronizedTestRunnable synchronizedTestRunnable = new SynchronizedTestRunnable();
SynchronizedTestRunnable synchronizedTestRunnable2 = new SynchronizedTestRunnable();
new Thread(synchronizedTestRunnable).start();
new Thread(synchronizedTestRunnable2).start();
}
其执行结果为:
Thread-1 a start on 2020-11-24 11:51:02
Thread-0 a start on 2020-11-24 11:51:02
Thread-1 a end 2020-11-24 11:51:05
Thread-0 a end 2020-11-24 11:51:05
Thread-1 b start 2020-11-24 11:51:05
Thread-0 b start 2020-11-24 11:51:05
Thread-0 b end 2020-11-24 11:51:06
Thread-1 b end 2020-11-24 11:51:06
根据结果来看,虽然我们为方法 a() 与 方法 b() 都加上了 Synchronized 修饰,但是由于 Thread-0 与 Thread-1 执行的是两个runnable,所以两个线程还是同时并发执行了方法a() 与 方法 b(),这是为什么呢?思考一下,后面解释。
在 2.1 修饰普通方法 中我们用 Synchronized 修饰普通方法,但是我们发现,当我们用两个线程分别执行两个 runnable时,同步锁失效了,两个线程还是同时并发执行。
现在,我们稍微修改一下上述代码,将方法a() 与方法 b() 修改成静态方法,如下:
public class SynchronizedTestRunnable implements Runnable {
@Override
public void run() {
a();
b();
}
public static synchronized void a() {
System.out.println(Thread.currentThread().getName() + " a start on " + getCurrentTime());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " a end " + getCurrentTime());
}
public static synchronized void b() {
System.out.println(Thread.currentThread().getName() + " b start " + getCurrentTime());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " b end " + getCurrentTime());
}
public static String getCurrentTime() {
String dateFormat = "yyyy-MM-dd hh:mm:ss";
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat);
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
return simpleDateFormat.format(calendar.getTime());
}
public static void main(String[] args) {
SynchronizedTestRunnable synchronizedTestRunnable = new SynchronizedTestRunnable();
SynchronizedTestRunnable synchronizedTestRunnable2 = new SynchronizedTestRunnable();
new Thread(synchronizedTestRunnable).start();
new Thread(synchronizedTestRunnable2).start();
}
}
其执行结果为:
Thread-0 a start on 2020-11-24 11:51:46
Thread-0 a end 2020-11-24 11:51:49
Thread-0 b start 2020-11-24 11:51:49
Thread-0 b end 2020-11-24 11:51:50
Thread-1 a start on 2020-11-24 11:51:50
Thread-1 a end 2020-11-24 11:51:53
Thread-1 b start 2020-11-24 11:51:53
Thread-1 b end 2020-11-24 11:51:54
同样是两个线程执行两个runnble,但是与 2.1 修饰普通方法 的结果不同的是,两个线程变成了同步顺序执行,这是为什么呢?就加了两个 static 关键字,思考一下,往后看,后面解释。
前面介绍的都是用 Synchronized 关键字来修饰方法,但是很多时候我们只需要同步一小块代码,而不需要同步整个方法,从而减小系统开销。现在,我们接着再修改一下代码,就将方法 a() 的 Thread.sleep(3000) 锁住,方法 b() 还是普通方法,如下:
public class SynchronizedTestRunnable implements Runnable {
@Override
public void run() {
a();
b();
}
public void a() {
System.out.println(Thread.currentThread().getName() + " a start on " + getCurrentTime());
synchronized (this) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " a end " + getCurrentTime());
}
public void b() {
System.out.println(Thread.currentThread().getName() + " b start " + getCurrentTime());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " b end " + getCurrentTime());
}
public static String getCurrentTime() {
String dateFormat = "yyyy-MM-dd hh:mm:ss";
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat);
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
return simpleDateFormat.format(calendar.getTime());
}
public static void main(String[] args) {
SynchronizedTestRunnable synchronizedTestRunnable = new SynchronizedTestRunnable();
new Thread(synchronizedTestRunnable).start();
new Thread(synchronizedTestRunnable).start();
}
}
其执行结果为:
Thread-1 a start on 2020-11-24 11:52:35
Thread-0 a start on 2020-11-24 11:52:35
Thread-1 a end 2020-11-24 11:52:38
Thread-1 b start 2020-11-24 11:52:38
Thread-1 b end 2020-11-24 11:52:39
Thread-0 a end 2020-11-24 11:52:41
Thread-0 b start 2020-11-24 11:52:41
Thread-0 b end 2020-11-24 11:52:42
两个线程执行同一个runnable,Thread-0 与 Thread-1 同时进入方法 a(),但是由于锁的存在,Thread-0 与 Thread-1 存在竞争关系,这里 Thread-1 先获取到锁,所以会往下执行,而 Thread-0 则阻塞,直到 Thread-1 执行完方法 a(),Thread-0 才开始执行方法 a()。
我们想要搞清楚这三个方法的区别,就需要知道它们本质上锁的到底是什么对象。比如 2.2 Synchronized 修饰静态方法 中,锁住的是类对象,所以在多线程中,尽管new了多个实例对象,但是本质上是属于同一个类对象,所以还是存在同步关系。而 2.1 Synchronized 修饰普通方法 中,锁住的是类的实例对象,所以在多线程中,如果多个线程执行同一个runnable,就存在同步关系,而如果new了多个实例对象,且线程间各自执行不同的runnable,线程之间就不存在同步关系了。
修饰方法:
a() 修饰普通方法,锁住类的实例对象 | b() 修饰静态方法,锁住类对象 |
---|---|
修饰代码块:
锁住当前类的实例对象 | 锁住当前类对象 | 锁住任意实例对象 |
---|---|---|
上面我们介绍了 Synchronized 的三种使用方法,分别是1. 修饰普通方法;2. 修饰静态方法;3. 修饰代码块。接下来为了更好的理解Synchronized 的工作原理,我们反编译一下这三种方法。
//1.修饰普通方法
public synchronized void a() {
System.out.println(Thread.currentThread().getName() + " a start on " + getCurrentTime());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " a end " + getCurrentTime());
}
//2.修饰静态方法
public static synchronized void b() {
System.out.println(Thread.currentThread().getName() + " b start " + getCurrentTime());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " b end " + getCurrentTime());
}
//3.修饰代码块
public void c() {
System.out.println(Thread.currentThread().getName() + " c start " + getCurrentTime());
try {
synchronized (this) {
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " c end " + getCurrentTime());
}
方法 a()、 b()、 c() 反编译的结果如下所示:
a() 修饰普通方法 | b() 修饰静态方法 | c() 修饰代码块 |
---|---|---|
结果分析:(注意看上图我标红的地方)
说了这么多获取 monitor所有权、释放 monitor所有权,那 monitor 又是什么呢?
他被称为内置锁(intrinsic lock)或监视器锁(monitor lock),它其实是一种互斥锁,也就是同一时刻最多只有一个线程可以持有这种锁,当线程A想去获取一个由线程B持有的锁时,线程A必须等待或阻塞,直到线程B释放该锁,如果线程B永远不释放持有的该锁,那么线程A也将永远的等待或阻塞着。monitor lock 存在于 Java对象头中,获得 monitor lock 的唯一途径就是进入由这个锁(Synchronized)保护的同步代码块或方法中。
《Java并发编程实战》是这么介绍的:
每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁和监视器锁。线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁,而无论是通过正常的控制路径退出,还是通过从代码块中抛出异常退出。获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。
总结一下:
我们用 Synchronized 修饰的同步方法中,是通过监视器锁来实现同步效果的,而这个监视器锁存在于 Java对象头中。
想要知道 Java对象头,我们就得先了解 Java对象是什么。
关于Java对象,在《深入理解Java虚拟机》中是这么介绍的:在 HotSpot 虚拟机里,对象在堆内存中的存储布局可以分划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
这里我们主要关注对象头的 Mark Word,用于存储对象自身的运行时数据,如哈希码HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分的数据长度在32位和64位的虚拟机(未开启压缩指针)中分别为32个比特和64个比特,官方称为 Mark Word。
引用《深入理解Java虚拟机》中的插图,帮助理解,如下所示:
对象头的最后两位存储了锁的标志位,如00是轻量级锁,10位重量级锁。随着锁级别的不同,对象头里存储的内容不同,具体如上图所示。所以这里也验证了每个Java对象都可以做一个实现同步的锁。
通过上面的学习,我们知道了用 Synchronized 修饰的同步方法,本质上是通过获取Java对象头中的监视器锁来实行同步的,接下来我们来看看底层虚拟机是通过怎样的方式来实现同步的(再来回忆一下,同一时刻只有一个线程可以进入到同步方法中)。
来看看底层虚拟机源码
ObjectMonitor() {
_header = NULL;
_count = 0; //用来记录当前线程获取该锁的次数
_waiters = 0, //等待线程数
_recursions = 0; //锁的重入次数
_object = NULL;
_owner = NULL; //表示持有ObjectMonitor对象的线程
_WaitSet = NULL; //线程队列:存放处于wait状态的线程
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //线程队列:存放正在等待锁释放而处于block状态的线程
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0; //前一个持有者的线程ID
}
当多个线程同时访问同一同步代码时,先获取到 monitor 的线程会先成为 _owner,_count加一,而其他线程则进入 _EntryList 队列中,处于阻塞状态,直到当前线程 _owner 释放了 monitor(此时_count为0),这些处于 _EntryList 中阻塞的线程才会被唤醒然后去竞争 monitor,新竞争到 monitor 的线程就会成为新的 _owner。
获取到 monitor 的线程在调用 wait() 方法后,_owner 会释放 monitor,_count减一,该线程会加入到 _WaitSet 队列中,直到调用 notify()/notifyAll() 方法出队列,再次获取到 monitor。
_EntryList 与 _WaitList 的区别
注意:处于 _EntryList 队列中的线程是还没有进入到同步方法中的,而处于 _WaitList 队列的线程是已经进入到了同步方法中,但是由于某些条件(调用了wait()方法)暂时释放了 monitor,等待某些条件(调用notify()/notifyAll()方法)再次获取到 monitor。
这个问题也称之为锁阻塞与等待阻塞的区别,锁阻塞是被动地,还没进入到同步方法中,而等待阻塞是主动的,已经进入到了同步方法中,只是等待另一个获取到monitor锁的线程调用 notify() 唤醒。
上述回答中的 wait()/notify()/notifyAll() 方法也被称之为监控条件(Monitor Condition) ,它与 monitor 是相互关联的,所以你想使用监控条件必须先获取到 monitor,所以 wait()/notify()/notifyAll() 方法必须用在同步方法中(因为Synchronized修饰的同步方法中可以获取到 monitor),否则会抛出 IllegalMonitorStateException 异常。
进一步思考:那 _EntryList 与 _WaitList 里的线程会一起竞争monitor吗?还是说 _WaitList 里的线程会比 _EntryList 里的线程优先获取到 monitor 呢?
会公平竞争monitor
参考Does Java monitor’s wait set has a priority over entry set?
由小伙伴提出的问题延伸,欢迎大家提出有疑问的地方,让我们共同进步呀 ~
想知道类锁与对象锁有什么区别,我们就要先知道类对象与类的实例对象有什么区别,因为他们四个的对应关系为:
类对象 --> 类锁
类的实例对象 --> 对象锁
我们在编写完 .java 文件后,JVM 是不能直接运行该文件的,需要先将 .java 文件编译成 .class 文件后,JVM 再把 class 文件加载到内存中,创建一个 Class对象,并且存在 JVM数据区中的方法区中,被所有线程所共享,这时候才可以使用这个 .java 文件,也就是这个类。
这个Class对象,就是我们说的类对象,而我们刚刚说的类锁,就是这个类对象实现的。
总结一下:
一个Java类可以有很多的实例对象,但只有一个类对象,不管是类对象还是类的实例对象,都是 Java对象,所以类锁与对象锁其实都是内置锁,都是通过 Java对象头中的 Mark Word 中的标志位来实现的,其实现原理也是一样的。
OK,到这里是不是对 Synchronized 有了进一步的理解,其实还可以进阶 Synchronized 的锁优化,篇幅及能力关系,这里就不展开了。
参考文献:
《Java并发编程实战》
《深入理解Java虚拟机》
synchronized实现原理
其实分享文章的最大目的正是等待着有人指出我的错误,如果你发现哪里有错误,请毫无保留的指出即可,虚心请教。
另外,如果你觉得文章不错,对你有所帮助,请给我点个赞,就当鼓励,谢谢~Peace~!