Java多线程访问Synchronized同步方法的八种使用场景

简介

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

八种使用场景:

接下来,我们来通过代码实现,分别判断以下场景是不是线程安全的,以及原因是什么。

  1. 两个线程同时访问同一个对象的同步方法

  2. 两个线程同时访问两个对象的同步方法

  3. 两个线程同时访问(一个或两个)对象的静态同步方法

  4. 两个线程分别同时访问(一个或两个)对象的同步方法和非同步方法

  5. 两个线程访问同一个对象中的同步方法,同步方法又调用一个非同步方法

  6. 两个线程同时访问同一个对象的不同的同步方法

  7. 两个线程分别同时访问静态synchronized和非静态synchronized方法

  8. 同步方法抛出异常后,JVM会自动释放锁的情况

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

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

两个线程同时访问同一个对象的同步方法,是线程安全的。
1
复制代码

我们在前文中已经讲过了。代码和详细讲解在《Java中synchronized实现对象锁的两种方式及原理解析》中的第二部分《方法锁》中,在此就不再重述了。

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

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

两个线程同时访问两个对象的同步方法,是线程不安全的。
1
复制代码

代码验证:

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("测试结束");
 }
}
123456789101112131415161718192021222324252627282930

运行结果:

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

关注公众号:程序员白楠楠,领取2020年末总结面试题

线程名:Thread-0,运行开始
线程名:Thread-1,运行开始
线程:Thread-0,运行结束
线程:Thread-1,运行结束
测试结束
12345

代码分析:

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

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

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

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

两个线程同时访问(一个或两个)对象的静态同步方法,是线程安全的。
1

关于此方法的代码实现和详细讲解,参考文章《Java中synchronized实现类锁的两种方式及原理解析》中的第二部分《静态方法锁的方式实现类锁》,在此不再重述。

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

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

两个线程分别同时访问(一个或两个)对象的同步方法和非同步方法,是线程不安全的。
1
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("测试结束");
 }

}
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950

运行结果:

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

线程名:Thread-0,同步方法,运行开始
线程名:Thread-1,普通方法,运行开始
线程:Thread-0,同步方法,运行结束
线程:Thread-1,普通方法,运行结束
测试结束
12345
复制代码

结果分析

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

非同步方法不受其它由synchronized修饰的同步方法影响
1

你可能想到一个类似场景:多个线程访问同一个对象中的同步方法,同步方法又调用一个非同步方法,这个场景会是线程安全的吗?

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

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

public class Condition8 implements Runnable {

 static Condition8 instance = new Condition8();

 @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("测试结束");
 }

}
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253

运行结果:

线程名:Thread-0,普通方法,运行开始
线程名:Thread-1,同步方法,运行开始
线程:Thread-1,同步方法,运行结束,开始调用普通方法
线程名:Thread-1,普通方法,运行开始
线程:Thread-0,普通方法,运行结束
线程:Thread-1,普通方法,运行结束
线程名:Thread-2,同步方法,运行开始
线程:Thread-2,同步方法,运行结束,开始调用普通方法
线程名:Thread-2,普通方法,运行开始
线程:Thread-2,普通方法,运行结束
测试结束
1234567891011

结果分析:

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

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

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

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

所以结论是:

两个线程访问同一个对象中的同步方法,同步方法又调用一个非同步方法,仅在没有其他线程直接调用非同步方法的情况下,是线程安全的。若有其他线程直接调用非同步方法,则是线程不安全的。
1

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

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

两个线程同时访问同一个对象的不同的同步方法时,是线程安全的。
1
public class Condition5 implements Runnable {
 static Condition5 instance = new Condition5();

 @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("测试结束");
 }
}
12345678910111213141516171819202122232425262728293031323334353637383940414243444546

运行结果:

是线程安全的。

线程名:Thread-1,同步方法1,运行开始
线程:Thread-1,同步方法1,运行结束
线程名:Thread-0,同步方法0,运行开始
线程:Thread-0,同步方法0,运行结束
测试结束
12345

结果分析:

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

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

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

两个线程分别同时访问静态synchronized和非静态synchronized方法,线程不安全。
1

代码实现:

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();
  }
 }

 // 重点:用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("测试结束");
 }
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748

运行结果:

线程名:Thread-0,静态同步方法0,运行开始
线程名:Thread-1,非静态同步方法1,运行开始
线程:Thread-1,非静态同步方法1,运行结束
线程:Thread-0,静态同步方法0,运行结束
测试结束
12345

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

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

只有当同步方法执行完或执行时抛出异常这两种情况,才会释放锁。
1

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

代码实现:

public class Condition7 implements Runnable {

 private 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();
  }
 }

 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("测试结束");
 }

}
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950

运行结果:

线程名:Thread-0,运行开始
线程名:Thread-0,抛出异常,释放锁
线程名:Thread-1,运行开始
Exception in thread "Thread-0" java.lang.RuntimeException
 at com.study.synchronize.conditions.Condition7.method0(Condition7.java:34)
 at com.study.synchronize.conditions.Condition7.run(Condition7.java:17)
 at java.lang.Thread.run(Thread.java:748)
线程:Thread-1,运行结束
测试结束
123456789

结果分析:

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

总结

本文总结了并用代码实现和验证了synchronized各种使用场景,以及各种场景发生的原因和结论。我们分析的理论基础都是synchronized关键字的锁对象究竟是谁?多个线程之间竞争的是否是同一把锁?根据这个条件来判断线程是否是安全的。所以,有了这些场景的分析锻炼后,我们在以后使用多线程编程时,也可以通过分析锁对象的方式,判断出线程是否是安全的,从而避免此类问题的出现。

小编总结了2020面试题,这份面试题的包含的模块分为19个模块,分别是: Java 基础、容器、多线程、反射、对象拷贝、Java Web 、异常、网络、设计模式、Spring/Spring MVC、Spring Boot/Spring Cloud、Hibernate、MyBatis、RabbitMQ、Kafka、Zookeeper、MySQL、Redis、JVM 。

关注公众号:程序员白楠楠,获取上述资料。

(0)

相关推荐

  • 多线程(二)线程的同步

    举例:创建三个卖票窗口,共有一百张票,完成售票 问题:卖票的过程中,出现了重票.错票 --> 出现了线程的安全问题 问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来, ...

  • Java并发编程之内置锁(synchronized)

    简介 synchronized在JDK5.0的早期版本中是重量级锁,效率很低,但从JDK6.0开始,JDK在关键字synchronized上做了大量的优化,如偏向锁.轻量级锁等,使它的效率有了很大的提 ...

  • Synchronized和Lock的区别

    引言 在多线程中,为了使线程安全,我们经常会使用synchronized和Lock进行代码同步和加锁,但是具体两者有什么区别,什么场景下适合用什么可能还不大清楚,主要的区别大致如下: 区别 1.syn ...

  • Java多线程(1):3种常用的实现多线程类的方法

    (1) 继承java.lang.Thread类(Thread也实现了Runnable接口) 继承Thread类的方法是比较常用的一种,如果说你只是想起一条线程.没有什么其它特殊的要求,那么可以使用Th ...

  • 从零开始学Java(六)Java八种基本数据类型详解

    数据类型 1.数据类型有什么用? 数据类型用来声明变量,程序在运行过程中根据不同的数据类型分配不同大小的空间. int i = 10; double d = 1.23; i变量和d变量类型不同,空间大 ...

  • 这八种字,就叫好字!

    这八种字,就叫好字!

  • JAVA多线程学习笔记整理

    多线程: 三种创建方法 继承Thread类,以线程运行内容重写run方法,创建Thread对象并用start方法启动该线程. (匿名内部类) (Lambda表达式) 实现Runable接口,以线程运行 ...

  • 最贵的黑酸枝是什么品种的?八种最贵黑酸枝特性介绍!

    最贵的黑酸枝是什么品种的?八种最贵黑酸枝特性介绍!趣历史小编给大家提供详细的相关内容. 说到黑酸枝其实大家也知道的很多,世界上黑酸枝很多很多,但是大致的可以分为七大类,所以下面我们一起来盘点看看,下面 ...

  • 孩子拥有这八种能力,比成绩更重要!现在社...

    孩子拥有这八种能力,比成绩更重要! 现在社会日益竞争,小时候比吃饭,读书比学习,比结婚,比工作,比经济,比地位!但是有些能力并不是学习成绩好就可以获得. 这8种能力每个人在18岁时就应具备的能力,也许 ...

  • 总结八种选股策略,买股八大经典形态 ,再...

    总结八种选股策略,买股八大经典形态 ,再忙也要花5分钟看完 1.要选股性活的,回避呆滞品种: 2.要看底部量.所谓量为价先,看量不能单纯看能,更要看量比,看持续程度: 3.要选筹码集中洗盘彻底的品种: ...

  • 2021高考化学要点盘点!那些渗透到选择题中的八种典型元素化合物

    高中化学了,尤其是化学元素那,简直是太难学了.元素化合物的性质应用在高考试卷中,贯穿始终,无论是化学基本概念.化学基本理论.化学计算.化学实验等各个方面,其题型灵活.作用之大.运用之广! 而且同学们查 ...

  • 八种情况,少喝茶!

    无论男女老少都喜欢在周末休闲的时候,慢慢地泡一壶茶,享受假日的休闲.不过,不是每个人都适合喝茶养生的. 我们来看一下,哪些状况下的我们需要和茶叶隔离起来. 发烧忌喝茶 茶叶中咖啡碱不但能使人体体温升高 ...