Java多线程之synchronized
- 1、前言
- 2、synchronized的两个用法
- 3、多线程访问同步方法的7种情况
- 4、Synchronized性质
- 5、Synchronized原理
- 6、常见面试问题
- 6.1、synchronized关键字使用注意点:锁对象不能为空、作用域不宜过大、避免死锁;
- 6.2、如何选择Lock和synchronized关键字?
- 6.3、多线程访问同步方法的各种具体情况。
- 6.4、多个线程等待同一个synchronized锁的时候,JVM如何选择下一个获取锁的是哪个线程?
- 6.5、Synchronized使得同时只有一个线程可以执行,性能较差,有什么办法可以提升性能?
- 6.6、我想更灵活地控制锁的获取和释放(现在释放锁的时机都被规定死了)怎么办?
- 6.7、构造方法可以使用synchronized关键字修饰么?
- 6.8、什么是锁的升级、降级?什么是JVM里的偏向锁、轻量级锁、重量级锁?
1、前言
1.1、一句话介绍synchronized
synchronized
关键字解决的是多个线程之间访问资源的同步性。synchronized
关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行,同时具有可重入和不可中断的性质。
1.2、synchronized的作用
能够保证在同一时刻最多只有一个线程执行该段代码,以达到保证并发安全的效果。
如果说一段代码被synchronized
修饰,那么被修饰的这段代码就会以原子的方式执行,而多个线程在执行这段代码的时候不会互相干扰,因为多个线程执行不会同时执行这段代码。
1.3、具体是如何做到不会同时执行呢?
它们会有一把锁,这把锁在第一个线程执行这段代码的时候就已经拿到这把锁,一旦拿到之后,就会独占这把锁;直到这个方法结束或者一定条件之后,它才会释放这把锁;在这把锁释放之前,其他的线程只能是等待阻塞,直到这把锁释放了,其他线程才可以去执行这段代码。
2、synchronized的两个用法
- 对象锁:包括方法锁(默认锁对象为this-当前实例对象)和同步代码块锁(自己指定锁对象),SynchronizedObjectCodeBlock2.java,SynchronizedObjectMethod3.java;
- 类锁:指
synchronized
修饰静态的方法或指定锁为Class对象,SynchronizedClassStatic4.java,SynchronizedClassClass5.java。
类锁的用法:Java类可能有多个对象,但只有一个class对象,效果是全局锁定。
本质:所谓的类锁,不过是Class对象的锁而已,由于class对象只有一个,所以不同线程之间无论是哪一个实例过来的,它都只能获取到唯一的一个锁(不同的实例之间会互斥,只能有一个线程在同一时刻访问这一个被类锁锁住的方法)。
与对象锁的区别:
- 类锁:即便是不同的Runnable实例,这个线程所对应的类锁仍然只有一个,只能有一个可以运行;
- 对象锁:如果是不同的实例创建出来,互相锁是不影响的,你也可以运行,我也可以运行,我们可以同时运行。
3、多线程访问同步方法的7种情况
3.1、两个线程同时访问一个对象的同步方法:
串行执行:同一个实例,锁是同一把锁
public class SynchronizedObjectMethod3 implements Runnable {
static SynchronizedObjectMethod3 instance = new SynchronizedObjectMethod3();
@Override
public void run() {
method();
}
public synchronized void method() {
System.out.println("我是对象锁的方法修饰符形式,我叫" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ",运行结束");
}
public static void main(String[] args) {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
}
运行结果:
我是对象锁的方法修饰符形式,我叫Thread-0
Thread-0,运行结束
我是对象锁的方法修饰符形式,我叫Thread-1
Thread-1,运行结束
finished
3.2、两个线程同时访问两个对象的同步方法:
同时并行执行,锁对象不是同一个,互不干扰
public class SynchronizedObjectCodeBlock2 implements Runnable {
static SynchronizedObjectCodeBlock2 instance1 = new SynchronizedObjectCodeBlock2();
static SynchronizedObjectCodeBlock2 instance2 = new SynchronizedObjectCodeBlock2();
@Override
public void run() {
synchronized (this) {
System.out.println("我是lock1,我叫"+Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+",lock1部分运行结束");
}
}
public static void main(String[] args) {
Thread t1 = new Thread(instance1);
Thread t2 = new Thread(instance2);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
}
我是lock1,我叫Thread-1
我是lock1,我叫Thread-0
Thread-1,lock1部分运行结束
Thread-0,lock1部分运行结束
finished
3.3、两个线程访问的是synchronized的静态方法:
会一个一个执行,锁生效。
public class SynchronizedClassStatic4 implements Runnable {
static SynchronizedClassStatic4 instance1 = new SynchronizedClassStatic4();
static SynchronizedClassStatic4 instance2 = new SynchronizedClassStatic4();
@Override
public void run() {
method();
}
//类锁的第一种形式static形式
public static synchronized void method() {
System.out.println("我是类锁的第一种形式:static形式。我叫" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ",运行结束");
}
public static void main(String[] args) {
Thread t1 = new Thread(instance1);
Thread t2 = new Thread(instance2);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
}
我是类锁的第一种形式:static形式。我叫Thread-0
Thread-0,运行结束
我是类锁的第一种形式:static形式。我叫Thread-1
Thread-1,运行结束
finished
3.4、同时访问同步方法与非同步方法:
同时开始,同时结束;synchronized关键字,只作用于指定的那个方法中,对于其他没有加synchronized关键字的方法,根本不受到影响。
public class SynchronizedYesAndNo6 implements Runnable {
static SynchronizedYesAndNo6 instance = new SynchronizedYesAndNo6();
@Override
public void run() {
if (Thread.currentThread().getName().equals("Thread-0")) {
method1();
} else {
method2();
}
}
public synchronized void method1() {
System.out.println("我是加锁的方法。我叫" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ",运行结束");
}
public void method2() {
System.out.println("我是没加锁的方法。我叫" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ",运行结束");
}
public static void main(String[] args) {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
}
我是加锁的方法。我叫Thread-0
我是没加锁的方法。我叫Thread-1
Thread-0,运行结束
Thread-1,运行结束
finished
3.5、访问同一个对象的不同的普通同步方法:
串行执行,对于同一个实例来讲,两个方法拿到的this是一样的,所以这两个方法没办法同时运行;
public class SynchronizedDifferentMethod7 implements Runnable {
static SynchronizedDifferentMethod7 instance = new SynchronizedDifferentMethod7();
@Override
public void run() {
if (Thread.currentThread().getName().equals("Thread-0")) {
method1();
} else {
method2();
}
}
public synchronized void method1() {
System.out.println("我是加锁的方法1。我叫" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ",运行结束");
}
public synchronized void method2() {
System.out.println("我是加锁的方法2。我叫" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ",运行结束");
}
public static void main(String[] args) {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
}
我是加锁的方法1。我叫Thread-0
Thread-0,运行结束
我是加锁的方法2。我叫Thread-1
Thread-1,运行结束
finished
3.6、同时访问静态synchronized和非静态synchronized方法:
同时运行,因为指定的锁对象不是同一个锁。
public class SynchronizedStaticAndNormal8 implements Runnable {
static SynchronizedStaticAndNormal8 instance = new SynchronizedStaticAndNormal8();
@Override
public void run() {
if (Thread.currentThread().getName().equals("Thread-0")) {
method1();
} else {
method2();
}
}
//类锁-锁住的是.class对象
public synchronized static void method1() {
System.out.println("我是静态加锁的方法1。我叫" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ",运行结束");
}
//对象锁-锁住的是this对象
public synchronized void method2() {
System.out.println("我是非静态加锁的方法2。我叫" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ",运行结束");
}
public static void main(String[] args) {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
}
我是静态加锁的方法1。我叫Thread-0
我是非静态加锁的方法2。我叫Thread-1
Thread-0,运行结束
Thread-1,运行结束
finished
3.7、方法抛出异常后,会释放锁
public class SynchronizedException9 implements Runnable {
static SynchronizedException9 instance = new SynchronizedException9();
@Override
public void run() {
if (Thread.currentThread().getName().equals("Thread-0")) {
method1();
} else {
method2();
}
}
public synchronized void method1() {
System.out.println("我是方法1。我叫" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
throw new RuntimeException();
//System.out.println(Thread.currentThread().getName() + ",运行结束");
}
public synchronized void method2() {
System.out.println("我是方法2。我叫" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ",运行结束");
}
public static void main(String[] args) {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
}
我是方法1。我叫Thread-0
我是方法2。我叫Thread-1
Exception in thread "Thread-0" java.lang.RuntimeException
at com.atu.SynchronizedException9.method1(SynchronizedException9.java:28)
at com.atu.SynchronizedException9.run(SynchronizedException9.java:15)
at java.lang.Thread.run(Thread.java:748)
Thread-1,运行结束
finished
总结:
- 一把锁只能同时被一个线程获取,没有拿到锁的线程必须等待(对应第1、5种情况);
- 每个实例都对应有自己的一把锁,不同实例之间互不影响;例外:锁对象是.class,以及synchronized修饰的是static方法的时候,所有对象共用同一把锁(对应第2,3,4,6种情况);
- 无论是方法正常执行完毕或者方法抛出异常,都会释放锁(对应第7种情况)。
注:目前进入到被synchronized
修饰的方法,而在这个方法里面去调用另外一个没有被synchronized
修饰的方法,那么这个时候是不线程安全的;因为一旦出了本方法,到了另一个方法中,由于另一个方法没有被synchronized
修饰,所以说这个方法可以同时被多个线程进行访问的。
4、Synchronized性质
- 可重入:同一线程的外层函数获得锁之后,内层函数可以直接再次获取该锁。好处是避免死锁,提升封装性。
- 不可中断:一旦这个锁已经被别人获得了,如果我还想获得,我只能选择等待或者阻塞,直到别的线程释放这个锁。
5、Synchronized原理
在java中,每一个对象有且仅有一个同步锁。这也意味着,同步锁是依赖于对象而存在。
当我们调用某对象的synchronized
方法时,就获取了该对象的同步锁。例如,synchronized(obj)
就获取了obj的同步锁的同步锁。不同线程对同步锁的访问是互斥的。也就是说,某时间点,对象的同步锁只能被一个线程获取到!通过同步锁,我们就能在多线程中,实现“对象/方法”的互斥访问。例如,现在有两个线程A和线程B,它们都会访问obj的同步锁。假设,在某一时刻,线程A获取到obj的同步锁并在执行一些操作;而此时,线程B也企图获取obj的同步锁——线程B会获取失败,它必须等待,直到线程A释放了该对象的同步锁之后,线程B才能获取到obj的同步锁,从而才可以运行。
5.1、synchronized 同步语句块的情况
加锁和释放锁的原理:深入JVM看字节码
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("synchronized 代码块");
}
}
}
通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行javac SynchronizedDemo.java
命令生成编译后的 .class 文件,然后执行javap -c -s -v -l SynchronizedDemo.class
。
synchronized
同步语句块的实现使用的是monitorenter
和 monitorexit
指令,其中 monitorenter
指令指向同步代码块的开始位置, monitorexit
指令则指明同步代码块的结束位置。
MonitorEnter(加锁):在执行时会让对象的锁计数加1,每个对象都和一个monitor
相关联,而一个monitor
的lock锁只能被一个线程在同一时间获得,一个线程在尝试获得与这个对象关联的monitor
所有权时,只会发生以下三种情况之一:
- 如果
monitor
计数器为0,这意味着目前还没有被获得,所以这个线程会立刻获得,然后把计数器加1,
一旦加1之后,别人在想进来就会看到这个信号,就知道它已经被别人所持有了;所以加1也就意味着当前线程是这个monitor
的所有者; - 如果说
monitor
已经拿到了锁的所有权,又重入了,这样会导致计数器累加,就是再加1,变成2,随着重入的次数会逐渐的往上走; - 如果
monitor
被其他线程所持有了,而我去获取它的时候,只能得到现在获取不了的信号,我就会进入阻塞状态;直到monitor
计数器变为0才会再次去尝试获取锁。
MonitorExit(释放):作用是释放对于monitor
的所有权,也就是释放锁(前提是已经拥有这个锁的所有权)。
释放的过程就是 将monitor
计数器减1,如果减为0了,那就意味着当前线程不在拥有对monitor
的所有权了,通俗讲就是解锁;如果说减完之后不是0,那意味着刚才是可重入进来的,当前线程还继续持有这把锁。
5.2、synchronized 修饰方法的的情况
public class SynchronizedDemo2 {
public synchronized void method() {
System.out.println("synchronized 方法");
}
}
synchronized
修饰的方法并没有 monitorenter
指令和 monitorexit
指令,取得代之的确实是ACC_SYNCHRONIZED
标识,该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED
访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
总结:synchronized
同步语句块的实现使用的是 monitorenter
和 monitorexit
指令,其中 monitorenter
指令指向同步代码块的开始位置, monitorexit
指令则指明同步代码块的结束位置。
synchronized
修饰的方法并没有 monitorenter
指令和 monitorexit
指令,取得代之的是 ACC_SYNCHRONIZED
标识,该标识指明了该方法是一个同步方法。
不过两者的本质都是对对象监视器 monitor 的获取。
5.2、可重入原理:加锁次数计数器
每个对象都自动含有一把锁,JVM负责跟踪对象被加锁的次数;线程第一次给对象加锁的时候,计数变为1。每当这个相同的线程再此对象上再次获得锁时,计数会递增。每当任务离开时,计数递减,当计数为0的时候,锁被完全释放。
5.3、可见性原理:Java内存模型(JMM)
本地内存:保存的变量是一个副本,也就是说是把主内存中的变量复制一份,放到本地内存。这样的好处就是可以加速程序运行。
线程A向线程B发送数据的步骤(线程之间想通信必须通过主内存):首先本地内存A会把自己修改过的变量内容放到主内存中,然后线程B从主内存中读取,整个过程是由JMM控制;JMM正是通过控制主内存与每个线程的本地内存之间的交互,来为我们的提供了内存可见性的保证。
5.5、Synchronized是如何做到可见性的实现的?
一旦一个代码块或者方法被我们Synchronized
关键字所修饰,那么它在执行完毕之后,被锁住的对象所做的任何修改都要在释放锁之前从线程内存写回到主内存当中,就是说不会存在一种情况叫做线程内存和主内存内容不一致。
5.6、Synchronized缺陷
- 效率低:锁的释放情况少、试图获得锁时不能设定超时、不能中断一个正在试图获得锁的线程;锁的释放情况少:当一个线程获得了对应的Synchronized锁的时候,并且在执行这段代码的时候,其他线程如果也想得到这把锁,只有等当前线程释放;而且只有两种情况才会释放锁:a、线程执行完这段代码,b、执行到一半发生异常;试图获得锁时不能设定超时:只能干巴巴的等待;不能中断一个正在试图获得锁的线程:相比之下lock有中断能力。
- 不够灵活(读写锁更灵活):加锁和释放的时机单一,每个锁仅有单一的条件 (某个对象),可能是不够的;
- 无法知道是否成功获取到锁
6、常见面试问题
6.1、synchronized关键字使用注意点:锁对象不能为空、作用域不宜过大、避免死锁;
锁对象不能为空:我们指定了一个对象作为我们的锁对象,它必须是一个实例对象,而不是一个空对象,这是因为这些锁的信息是保存在对象头中的,如果对象都没有,更不会有对象头,所以锁是不能工作的;
作用域不宜过大:这里的原则是锁的范围尽可能小,锁的时间能可能短,即能锁对象就不要锁类,能锁代码块,就不要锁方法。
6.2、如何选择Lock和synchronized关键字?
如果说synchronized关键字在程序中适用,那就优先适用,可以减少我们编写的代码。
6.3、多线程访问同步方法的各种具体情况。
- 两个线程同时访问一个对象的同步方法:串行执行,同一个实例,锁是同一把锁
- 两个线程同时访问两个对象的同步方法:并行执行,锁对象不是同一个,互不干扰
- 两个线程访问的是synchronized的静态方法:串行执行,锁生效。
- 同时访问同步方法与非同步方法:并行执行,同时开始,同时结束;synchronized关键字,只作用于指定的那个方法中,对于其他没有加synchronized关键字的方法,根本不受到影响。
- 访问同一个对象的不同的普通同步方法:串行执行,对于同一个实例来讲,两个方法拿到的this是一样的,所以这两个方法没办法同时运行;
- 同时访问静态synchronized和非静态synchronized方法:并行执行,因为指定的锁对象不是同一个锁;
- 方法抛出异常后,会释放锁。
6.4、多个线程等待同一个synchronized锁的时候,JVM如何选择下一个获取锁的是哪个线程?
内部锁调度机制:一个随机不可控的。
6.5、Synchronized使得同时只有一个线程可以执行,性能较差,有什么办法可以提升性能?
优化使用范围:让临界区在符合要求的情况下尽可能小;使用其他类型的lock。
6.6、我想更灵活地控制锁的获取和释放(现在释放锁的时机都被规定死了)怎么办?
自定义实现。
6.7、构造方法可以使用synchronized关键字修饰么?
构造方法不能使用 synchronized 关键字修饰。构造方法本身就属于线程安全的,不存在同步的构造方法一说。
6.8、什么是锁的升级、降级?什么是JVM里的偏向锁、轻量级锁、重量级锁?
所谓锁的升级、降级,就是 JVM 优化 synchronized 运行的机制,当 JVM 检测到不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。
偏向锁:减少同一线程获取锁的代价。
核心思想:如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也变为偏向锁结构,当该线程再次请求锁时,无需在做任何同步操作,即获取锁的过程只需检查Mark Word的锁标记为偏向锁以及当前线程id等于Mark Word的ThreadId即可,这样就省去了大量有关锁申请的操作。不适用于锁竞争比较激烈的多线程场合。
轻量级锁:轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。
适用场景:线程交替执行同步块。
若存在同一时间访问同一锁的情况,就会导致轻量级升级为重量级。
JVM利用CAS在对象的头上设置线程ID,表示这个对象偏向于当前线程,这就是偏向锁。偏向锁的作用就是为了在资源没有被多线程竞争的情况下尽量减少锁带来的性能开销。
当没有竞争出现时,默认会使用偏向锁。JVM 会利用 CAS 操作(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏向锁可以降低无竞争开销。如果有另外的线程试图锁定某个已经被偏向过的对象,JVM 就需要撤销(revoke)偏向锁,并升级到轻量级锁实现。轻量级锁依赖 CAS 操作 Mark Word 来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。
第三章 让线程停下来