写点什么

源码解析 Synchronized 同步方法的八种使用场景,收藏

用户头像
996小迁
关注
发布于: 2020 年 12 月 22 日
源码解析Synchronized同步方法的八种使用场景,收藏

简介

本文将介绍 7 种同步方法的访问场景,我们来看看这八种情况下,多线程访问同步方法是否还是线程安全的。这些场景是多线程编程中经常遇到的,而且也是面试时高频被问到的问题,所以不管是理论还是实践,这些都是多线程场景必须要掌握的场景。


场景一:两个线程同时访问同一个对象的同步方法

分析:这种情况是经典的对象锁中的方法锁,两个线程争夺同一个对象锁,所以会相互等待,是线程安全的。


代码实现


package com.syn;
/** * @author :biws * @date :Created in 2020/12/21 22:27 * @description:两个线程同时访问同一个对象的同步方法 */
public class Condition1 implements Runnable {
private static Condition1 instance = new Condition1();
@Override public void run() { method(); }
//关键:synchronized可以保证此方法被顺序执行,线程1执行完4秒钟后,线程2再执行4秒。不加synchronized,线程1和线程2将同时执行 private synchronized void method() { System.out.println("线程:" + Thread.currentThread().getName() + ",运行开始"); try { //模拟执行一段操作,耗时4秒钟 Thread.sleep(4000); System.out.println("线程:" + Thread.currentThread().getName() + ",运行结束"); } catch (InterruptedException e) { e.printStackTrace(); } }
public static void main(String[] args) { // 模拟:同一个对象下,两个线程,同步执行一个方法(串行执行则为线程安全,并行执行,则为线程不安全,) Thread thread1 = new Thread(instance); Thread thread2 = new Thread(instance); thread1.start(); thread2.start(); while (thread1.isAlive() || thread2.isAlive()) {
} System.out.println("测试结束"); }}
复制代码


运行结果



结果分析


发现运行结果中,多个线程也是串行执行的,效果跟同步代码块锁是一致的。


场景二:两个线程同时访问两个对象的同步方法

这种场景就是对象锁失效的场景,原因出在访问的是两个对象的同步方法,那么这两个线程分别持有的两个线程的锁,所以是互相不会受限的。加锁的目的是为了让多个线程竞争同一把锁,而这种情况多个线程之间不再竞争同一把锁,而是分别持有一把锁


代码验证:


package com.syn;
/** * @author :biws * @date :Created in 2020/12/21 22:10 * @description:两个线程同时访问两个对象的同步方法 */
public class Condition2 implements Runnable { // 创建两个不同的对象 static Condition2 instance1 = new Condition2(); static Condition2 instance2 = new Condition2();
@Override public void run() { method(); }
private synchronized void method() { System.out.println("线程名:" + Thread.currentThread().getName() + ",运行开始"); try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程:" + Thread.currentThread().getName() + ",运行结束"); }
public static void main(String[] args) { Thread thread1 = new Thread(instance1); Thread thread2 = new Thread(instance2); thread1.start(); thread2.start(); while (thread1.isAlive() || thread2.isAlive()) { } System.out.println("测试结束"); }}
复制代码


运行结果:


两个线程是并行执行的,所以线程不安全。



代码分析:


「问题在此:」两个线程(thread1、thread2),访问两个对象(instance1、instance2)的同步方法(method()),两个线程都有各自的锁,不能形成两个线程竞争一把锁的局势,所以这时,synchronized 修饰的方法 method()和不用 synchronized 修饰的效果一样(不信去把 synchronized 关键字去掉,运行结果一样),所以此时的 method()只是个普通方法。


「如何解决这个问题:」若要使锁生效,只需将 method()方法用 static 修饰,这样就形成了类锁,多个实例(instance1、instance2)共同竞争一把类锁,就可以使两个线程串行执行了。


场景三:两个线程同时访问(一个或两个)对象的静态同步方法

这个场景解决的是场景二中出现的线程不安全问题,即用类锁实现


代码实现


package com.syn;
/** * @author :biws * @date :Created in 2020/12/21 22:32 * @description:静态方法锁的方式实现类锁 */

public class Condition3 implements Runnable {
// 这两个实例,声明为static,是因为要在main方法中测试,与方法锁无关 static Condition3 instance1 = new Condition3(); static Condition3 instance2 = new Condition3();
// 关键: static synchronized两个关键字同时使用 private static synchronized void method() { System.out.println("线程名:" + Thread.currentThread().getName() + ",运行开始"); try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程:" + Thread.currentThread().getName() + ",运行结束"); }
@Override public void run() { method(); }
public static void main(String[] args) { Thread thread1 = new Thread(instance1); Thread thread2 = new Thread(instance2); thread1.start(); thread2.start(); try { //两个线程必须都执行完毕后 thread1.join(); thread2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("两个线程都已经执行完毕"); }}
复制代码


结果展示



场景四:两个线程分别同时访问(一个或两个)对象的同步方法和非同步方法

这个场景是两个线程其中一个访问同步方法,另一个访问非同步方法,此时程序会不会串行执行呢,也就是说是不是线程安全的呢?我们可以确定是线程不安全的,如果方法不加 synchronized 都是安全的,那就不需要同步方法了。


package com.syn;
/** * @author :biws * @date :Created in 2020/12/21 22:12 * @description:两个线程同时访问(一个或两个)对象的静态同步方法 */
public class Condition4 implements Runnable {
static Condition4 instance = new Condition4();
@Override public void run() { //两个线程访问同步方法和非同步方法 if (Thread.currentThread().getName().equals("Thread-0")) { //线程0,执行同步方法method0() method0(); } if (Thread.currentThread().getName().equals("Thread-1")) { //线程1,执行非同步方法method1() method1(); } }
// 同步方法 private synchronized void method0() { System.out.println("线程名:" + Thread.currentThread().getName() + ",同步方法,运行开始"); try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程:" + Thread.currentThread().getName() + ",同步方法,运行结束"); }
// 普通方法 private void method1() { System.out.println("线程名:" + Thread.currentThread().getName() + ",普通方法,运行开始"); try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程:" + Thread.currentThread().getName() + ",普通方法,运行结束"); }
public static void main(String[] args) { Thread thread1 = new Thread(instance); Thread thread2 = new Thread(instance); thread1.start(); thread2.start(); while (thread1.isAlive() || thread2.isAlive()) { } System.out.println("测试结束"); }
}
复制代码


运行结果:


两个线程是并行执行的,所以是线程不安全的。



结果分析


「问题在于此:」 method1 没有被 synchronized 修饰,所以不会受到锁的影响。即便是在同一个对象中,当然在多个实例中,更不会被锁影响了。


场景五:两个线程访问同一个对象中的同步方法,同步方法又调用一个非同步方法

我们来实验下这个场景,用两个线程调用同步方法,在同步方法中调用普通方法;再用一个线程直接调用普通方法,看看是否是线程安全的?


package com.syn;
/** * @author :biws * @date :Created in 2020/12/21 22:14 * @description:两个线程访问同一个对象中的同步方法,同步方法又调用一个非同步方法 */

public class Condition5 implements Runnable {
static Condition5 instance = new Condition5();
@Override public void run() { if (Thread.currentThread().getName().equals("Thread-0")) { //直接调用普通方法 method2(); } else { // 先调用同步方法,在同步方法内调用普通方法 method1(); } }
// 同步方法 private static synchronized void method1() { System.out.println("线程名:" + Thread.currentThread().getName() + ",同步方法,运行开始"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程:" + Thread.currentThread().getName() + ",同步方法,运行结束,开始调用普通方法"); method2(); }
// 普通方法 private static void method2() { System.out.println("线程名:" + Thread.currentThread().getName() + ",普通方法,运行开始"); try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程:" + Thread.currentThread().getName() + ",普通方法,运行结束"); }
public static void main(String[] args) { // 此线程直接调用普通方法 Thread thread0 = new Thread(instance); // 这两个线程直接调用同步方法 Thread thread1 = new Thread(instance); Thread thread2 = new Thread(instance); thread0.start(); thread1.start(); thread2.start(); while (thread0.isAlive() || thread1.isAlive() || thread2.isAlive()) { } System.out.println("测试结束"); }
}
复制代码


运行结果:



结果分析:


我们可以看出,普通方法被两个线程并行执行,不是线程安全的。这是为什么呢?


因为如果非同步方法,有任何其他线程直接调用,而不是仅在调用同步方法时,才调用非同步方法,此时会出现多个线程并行执行非同步方法的情况,线程就不安全了。


对于同步方法中调用非同步方法时,要想保证线程安全,就必须保证非同步方法的入口,仅出现在同步方法中。但这种控制方式不够优雅,若被不明情况的人直接调用非同步方法,就会导致原有的线程同步不再安全。所以不推荐大家在项目中这样使用,但我们要理解这种情况,并且我们要用语义明确的、让人一看就知道这是同步方法的方式,来处理线程安全的问题。


所以,最简单的方式,是在非同步方法上,也加上 synchronized 关键字,使其变成一个同步方法,这样就变成了《场景五:两个线程同时访问同一个对象的不同的同步方法》,这种场景下,大家就很清楚的看到,同一个对象中的两个同步方法,不管哪个线程调用,都是线程安全的了。


场景六:两个线程同时访问同一个对象的不同的同步方法

这个场景也是在探讨对象锁的作用范围,对象锁的作用范围是对象中的所有同步方法。所以,当访问同一个对象中的多个同步方法时,结论是:


package com.syn;
/** * @author :biws * @date :Created in 2020/12/21 22:16 * @description:两个线程同时访问同一个对象的不同的同步方法 */
public class Condition6 implements Runnable { static Condition6 instance = new Condition6();
@Override public void run() { if (Thread.currentThread().getName().equals("Thread-0")) { //线程0,执行同步方法method0() method0(); } if (Thread.currentThread().getName().equals("Thread-1")) { //线程1,执行同步方法method1() method1(); } }
private synchronized void method0() { System.out.println("线程名:" + Thread.currentThread().getName() + ",同步方法0,运行开始"); try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程:" + Thread.currentThread().getName() + ",同步方法0,运行结束"); }
private synchronized void method1() { System.out.println("线程名:" + Thread.currentThread().getName() + ",同步方法1,运行开始"); try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程:" + Thread.currentThread().getName() + ",同步方法1,运行结束"); }
//运行结果:串行 public static void main(String[] args) { Thread thread1 = new Thread(instance); Thread thread2 = new Thread(instance); thread1.start(); thread2.start(); while (thread1.isAlive() || thread2.isAlive()) { } System.out.println("测试结束"); }}
复制代码


运行结果:



结果分析:


两个方法(method0()和 method1())的 synchronized 修饰符,虽没有指定锁对象,但默认锁对象为 this 对象为锁对象, 所以对于同一个实例(instance),两个线程拿到的锁是同一把锁,此时同步方法会串行执行。这也是 synchronized 关键字的可重入性的一种体现。


场景七:两个线程分别同时访问静态 synchronized 和非静态 synchronized 方法

这种场景的本质也是在探讨两个线程获取的是不是同一把锁的问题。静态 synchronized 方法属于类锁,锁对象是(*.class)对象,非静态 synchronized 方法属于对象锁中的方法锁,锁对象是 this 对象。两个线程拿到的是不同的锁,自然不会相互影响。


代码实现:

package com.syn;
/** * @author :biws * @date :Created in 2020/12/21 22:20 * @description:两个线程分别同时访问静态synchronized和非静态synchronized方法 */

public class Condition7 implements Runnable { static Condition7 instance = new Condition7();
@Override public void run() { if (Thread.currentThread().getName().equals("Thread-0")) { //线程0,执行静态同步方法method0() method0(); } if (Thread.currentThread().getName().equals("Thread-1")) { //线程1,执行非静态同步方法method1() method1(); } }
// 重点:用static synchronized 修饰的方法,属于类锁,锁对象为(*.class)对象。 private static synchronized void method0() { System.out.println("线程名:" + Thread.currentThread().getName() + ",静态同步方法0,运行开始"); try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程:" + Thread.currentThread().getName() + ",静态同步方法0,运行结束"); }
// 重点:synchronized 修饰的方法,属于方法锁,锁对象为(this)对象。 private synchronized void method1() { System.out.println("线程名:" + Thread.currentThread().getName() + ",非静态同步方法1,运行开始"); try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程:" + Thread.currentThread().getName() + ",非静态同步方法1,运行结束"); }
//运行结果:并行 public static void main(String[] args) { //问题原因: 线程1的锁是类锁(*.class)对象,线程2的锁是方法锁(this)对象,两个线程的锁不一样,自然不会互相影响,所以会并行执行。 Thread thread1 = new Thread(instance); Thread thread2 = new Thread(instance); thread1.start(); thread2.start(); while (thread1.isAlive() || thread2.isAlive()) { } System.out.println("测试结束"); }}
复制代码


运行结果:



场景八:同步方法抛出异常后,JVM 会自动释放锁的情况

本场景探讨的是 synchronized 释放锁的场景:


所以,在一个线程的同步方法中出现异常的时候,会释放锁,另一个线程得到锁,继续执行。而不会出现一个线程抛出异常后,另一个线程一直等待获取锁的情况。这是因为 JVM 在同步方法抛出异常的时候,会自动释放锁对象。


代码实现:

package com.syn;
/** * @author :biws * @date :Created in 2020/12/21 22:22 * @description:同步方法抛出异常后,JVM会自动释放锁的情况 */

public class Condition8 implements Runnable {
private static Condition8 instance = new Condition8();
@Override public void run() { if (Thread.currentThread().getName().equals("Thread-0")) { //线程0,执行抛异常方法method0() method0(); } if (Thread.currentThread().getName().equals("Thread-1")) { //线程1,执行正常方法method1() method1(); } }
private synchronized void method0() { System.out.println("线程名:" + Thread.currentThread().getName() + ",运行开始"); try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } //同步方法中,当抛出异常时,JVM会自动释放锁,不需要手动释放,其他线程即可获取到该锁 System.out.println("线程名:" + Thread.currentThread().getName() + ",抛出异常,释放锁"); throw new RuntimeException();
}
private synchronized void method1() { System.out.println("线程名:" + Thread.currentThread().getName() + ",运行开始"); try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程:" + Thread.currentThread().getName() + ",运行结束"); }
public static void main(String[] args) { Thread thread1 = new Thread(instance); Thread thread2 = new Thread(instance); thread1.start(); thread2.start(); while (thread1.isAlive() || thread2.isAlive()) { } System.out.println("测试结束"); }
}
复制代码


运行结果:



结果分析:


可以看出线程还是串行执行的,说明是线程安全的。而且出现异常后,不会造成死锁现象,JVM 会自动释放出现异常线程的锁对象,其他线程获取锁继续执行。


希望可以对大家有帮忙,喜欢的小伙伴可以关注 公众号:小迁不秃头,每天不定时更新,干货满满!

用户头像

996小迁

关注

我就是我 2020.10.13 加入

5年Java开发经验

评论

发布
暂无评论
源码解析Synchronized同步方法的八种使用场景,收藏