1 Synchronized
1.1 引言
在多线程并发编程中Synchronized
一直是元老级角色,很多人都会称呼它为重量级锁,但是随着Java SE1.6
对Synchronized
进行了各种优化之后,有些情况下它并不那么重了,为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程
术语 | 英文 | 说明 |
---|---|---|
CAS | Compare and Swap | 比较并设置。用于在硬件层面上提供原子性操作。在 Intel 处理器中,比较并交换通过指令cmpxchg 实现。比较是否和给定的数值一致,如果一致则修改,不一致则不修改 |
1.2 概念理解
1.2.1 不同锁对象
Java
中的每一个 对象
都可以作为 锁
对于同步方法,锁是当前实例对象(this)
对于静态同步方法,锁是当前对象的Class对象
,又因为Class
的相关数据存储在永久带PermGen
(jdk1.8
则是metaspace
),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁
,会锁所有调用该方法的线程
对于同步方法块,锁是Synchonized
括号里配置的对象。当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁,synchronized
是自动释放的
1.2.2 对象锁和类锁概念区别
java
的对象锁和类锁:在锁的概念上基本上和内置锁是一致的,但是,两个锁实际是有很大的区别的,对象锁
是用于对象实例方法
,或者一个对象实例上的,类锁
是用于类的静态方法
或者一个类的class对象
上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象
,所以 不同对象实例的对象锁是互不干扰的
,但是每个类 只有一个类锁
。但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的
1.2.3 同步概念
JVM
规范规定JVM
基于进入和退出Monitor
对象来实现方法同步和代码块同步,但两者的实现细节不一样。
代码块同步是使用monitorenter
和monitorexit
指令实现,而方法同步是使用另外一种方式实现的,细节在JVM
规范里并没有详细说明,但方法的同步同样可以使用这两个指令来实现。monitorenter
指令是在编译后插入到同步代码块的开始位置,而monitorexit
是插入到方法结束处和异常处, JVM
要保证每个monitorenter
必须有对应的monitorexit
与之配对。任何对象都有一个 monitor
与之关联,并且一个monitor
被持有后,它将处于锁定状态。线程执行到 monitorenter
指令时,将会尝试获取对象所对应的 monitor
的所有权,即尝试获得对象的锁
1.2.4 Synchronized概念
synchronized
是Java
中的关键字,是一种同步锁。它修饰的对象有以下几种:
- 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号
{}
括起来的代码,作用的对象是调用这个代码块的对象; - 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
- 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
- 修改一个类,其作用的范围是
synchronized
后面括号括起来的部分,作用主的对象是这个类的所有对象
注意
:synchronized
关键字是不能继承的,也就是说,基类的方法synchronized f(){}
在继承类中并不自动是synchronized f(){}
,而是变成了f(){}
。继承类需要显式的指定它的某个方法为synchronized
方法
1.3 原理
1.3.1 Synchronized实现原理
Synchronized
实现如下图所示;
它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。
-
Contention List
:竞争队列,所有请求锁的线程首先被放在这个竞争队列中; -
Entry List
:Contention List
中那些有资格成为候选资源的线程被移动到Entry List
中; -
Wait Set
:那些调用wait
方法被阻塞的线程被放置在这里; -
OnDeck
:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck
; -
Owner
:当前已经获取到所有资源的线程被称为Owner
; -
!Owner
:当前释放锁的线程。
JVM
每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck
),但是并发情况下,ContentionList
会被大量的并发线程进行CAS
访问,为了降低对尾部元素的竞争,JVM
会将一部分线程移动到EntryList
中作为候选竞争线程。Owner
线程会在unlock
时,将Contention List
中的部分线程迁移到Entry List
中,并指定Entry List
中的某个线程为OnDeck
线程(一般是最先进去的那个线程)。Owner
线程并不直接把锁传递给OnDeck
线程,而是把锁竞争的权利交给OnDeck
,OnDeck
需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM
中,也把这种选择行为称之为竞争切换
。
OnDeck
线程获取到锁资源后会变为Owner
线程,而没有得到锁资源的仍然停留在EntryList
中。如果Owner
线程被wait
方法阻塞,则转移到WaitSet
队列中,直到某个时刻通过notify
或者notifyAll
唤醒,会重新进去EntryList
中。
处于ContentionList
、EntryList
、WaitSet
中的线程都处于阻塞状态,该阻塞是由操作系统来完成的
Synchronized
是非公平锁。 Synchronized
在线程进入Contention List
时,等待的线程会先尝试自旋获取锁,如果获取不到就进入Contention List
,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占OnDeck
线程的锁资源
1.3.2 Java对象头
点此了解Java对象头和各种锁基础理解
1.4 实际操作
同步机制可以使用synchronized
关键字实现。
当synchronized
关键字修饰一个方法的时候,该方法叫做同步方法。
当synchronized
方法执行完或发生异常时,会自动释放锁
1.4.1 对象锁
1.4.1.1 使用同一对象锁
同一个object使用synchronized
会有以下几种情况:
- 两个方法都没有
synchronized
修饰,调用时都可进入:方法A和方法B都没有加synchronized
关键字时,调用方法A的时候可进入方法B; - 一个方法有
synchronized
修饰,另一个方法没有,调用时都可进入:方法A加synchronized
关键字而方法B没有加时,调用方法A的时候可以进入方法B; - 两个方法都加了
synchronized
修饰,一个方法执行完才能执行另一个:方法A和方法B都加了synchronized
关键字时,调用方法A之后,必须等A执行完成才能进入方法B;
当一个对象中有2个方法同时用synchronized
修饰,那么当线程一
在访问方法1
时,其他线程是否可以访问方法二
?
答案:由于对象的内置锁
(监视器锁)是唯一的
,所以当线程一
在访问对象的方法1
时,持有了该对象的内置锁
,那么在线程一
释放该内置锁之前,其他线程是无法获取该对象内置锁,所以其他线程无法访问方法二
- 两个方法都加了
synchronized
修饰,其中一个方法加了wait()
方法,调用时都可进入:方法A和方法B都加了synchronized
关键字时,且方法A加了wait()方法时,调用方法A的时候可以进入方法B; - 一个添加了
synchronized
修饰,一个添加了static
修饰,调用时都可进入:方法A加了synchronized
关键字,而方法B为static
静态方法时,调用方法A的时候可进入方法B; - 两个方法都是静态方法且还加了
synchronized
修饰,一个方法执行完才能执行另一个:方法A和方法B都是static
静态方法,且都加了synchronized
关键字,则调用方法A之后,需要等A执行完成才能进入方法B; - 两个方法都是静态方法且还加了
synchronized
修饰,分别在不同线程调用不同的方法,还是需要一个方法执行完才能执行另一个:方法A和方法B都是static
静态方法,且都加了synchronized
关键字,创建不同的线程分别调用A和B,需要等A执行完成才能执行B(因为static
方法是单实例的,A持有的是Class
锁,Class
锁可以对类的所有对象实例起作用) - 同一个
object
中多个方法都加了synchronized
关键字的时候,其中调用任意方法之后需等该方法执行完成才能调用其他方法,即同步的
,阻塞的
;
对于object
中使用synchronized(this)
同步代码块的场景也是如此,synchronized
锁定的都是当前对象
下面例子中Worker worker = new Worker();
就是使用同一对象的例子
package cn.jzh.test.thread;
public class Worker {
public synchronized void executeA(String name) {
for (int i = 0; i < 10; i++) {
System.out.println(name + "-executeA-" + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void executeB(String name) {
for (int i = 0; i < 10; i++) {
System.out.println(name + "-executeB-" + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private class SynchronizerWorkerA extends Thread {
public void run() {
executeA("thread1");
}
}
private class SynchronizerWorkerB extends Thread {
public void run() {
executeB("thread2");
}
}
public static void main(String args[]) {
Worker worker = new Worker();
worker.new SynchronizerWorkerA().start();
worker.new SynchronizerWorkerB().start();
}
}
执行结果永远是执行完一个线程的输出再执行另一个线程的。
说明:
如果一个对象有多个synchronized
方法,某一时刻某个线程已经进入到了某个synchronized
方法,那么在该方法没有执行完毕前,其他线程是无法访问该对象的任何synchronized
方法的。
当synchronized
关键字修饰一个方法的时候,该方法叫做同步方法
。Java
中的每个对象都有一个锁(lock
),或者叫做监视器(monitor
),当一个线程访问某个对象的synchronized
方法时,将该对象上锁,其他任何线程都无法再去访问该对象的synchronized
方法了(这里是指所有的同步方法,而不仅仅是同一个方法),直到之前的那个线程执行方法完毕后(或者是抛出了异常),才将该对象的锁释放掉,其他线程才有可能再去访问该对象的synchronized
方法。注意:
这时候是给对象上锁,如果是不同的对象,则各个对象之间没有限制关系。
尝试在代码中构造第二个线程对象时传入一个新的对象,则两个线程的执行之间没有什么制约关系
1.4.1.2 使用不同对象锁
使用不同object
时,如果是对象锁,那么由于不同对象实例的对象锁是互不干扰的
,多线程是并行执行,且不会按顺序执行了,如果是类锁,那么还会按顺序执行
下面例子中不再使用Worker worker = new Worker();
,而是使用new Worker()
每次使用新对象,不同对象的线程执行结果就没有什么影响
package cn.jzh.test.thread;
public class Worker {
public synchronized void executeA(String name) {
for (int i = 0; i < 10; i++) {
System.out.println(name + "-executeA-" + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void executeB(String name) {
for (int i = 0; i < 10; i++) {
System.out.println(name + "-executeB-" + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private class SynchronizerWorkerA extends Thread {
public void run() {
executeA("thread1");
}
}
private class SynchronizerWorkerB extends Thread {
public void run() {
executeB("thread2");
}
}
public static void main(String args[]) {
//Worker worker = new Worker();
new Worker().new SynchronizerWorkerA().start();
new Worker().new SynchronizerWorkerB().start();
}
}
1.4.4.3 Synchronized块
Synchronized块
块锁和方法锁一样,都是使得两个线程的执行顺序进行,而不是并发进行,当一个线程执行时,将object
对象锁住,另一个线程就不能执行对应的块。synchronized
方法实际上等同于用一个synchronized
块包住方法中的所有语句,然后在synchronized
块的括号中传入this
关键字。当然,如果是静态方法,需要锁定的则是class对象
。
可能一个方法中只有几行代码会涉及到线程同步问题,所以synchronized块
比synchronized方法
更加细粒度地控制了多个线程的访问,只有synchronized块
中的内容不能同时被多个线程所访问,方法中的其他语句仍然可以同时被多个线程所访问(包括synchronized块之前的和之后的)。注意
:被synchronized
保护的数据应该是私有的。
synchronized
方法是一种粗粒度
的并发控制,某一时刻,只能有一个线程执行该synchronized
方法;synchronized
块则是一种细粒度
的并发控制,只会将块中的代码同步,位于方法内、synchronized
块之外的其他代码是可以被多个线程同时访问到的
1.4.2 类锁
如果是静态方法的情况,即便是向两个线程传入不同的对象,这两个线程仍然是互相制约的,必须 先执行完一个,再执行下一个
,如下使用两个static
修饰
package cn.jzh.test.thread;
public class Worker {
public static synchronized void executeA(String name) {
for (int i = 0; i < 10; i++) {
System.out.println(name + "-executeA-" + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static synchronized void executeB(String name) {
for (int i = 0; i < 10; i++) {
System.out.println(name + "-executeB-" + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private class SynchronizerWorkerA extends Thread {
public void run() {
executeA("thread1");
}
}
private class SynchronizerWorkerB extends Thread {
public void run() {
executeB("thread2");
}
}
public static void main(String args[]) {
//Worker worker = new Worker();
new Worker().new SynchronizerWorkerA().start();
new Worker().new SynchronizerWorkerB().start();
}
}
结论:
如果某个synchronized
方法是static
的,那么当线程访问该方法时,它锁的并不是synchronized
方法所在的对象,而是synchronized
方法所在的类所对应的Class对象
。Java
中,无论一个类有多少个对象,这些对象会对应唯一一个Class对象
,因此当线程分别访问同一个类的两个对象的两个static
,synchronized
方法时,它们的执行顺序也是顺序的,也就是说一个线程先去执行方法,执行完毕后另一个线程才开始