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

八种使用场景:

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

  1. 两个线程同时访问同一个对象的同步方法
  2. 两个线程同时访问两个对象的同步方法
  3. 两个线程同时访问(一个或两个)对象的静态同步方法
  4. 两个线程分别同时访问(一个或两个)对象的同步方法和非同步方法
  5. 两个线程访问同一个对象中的同步方法,同步方法又调用一个非同步方法
  6. 两个线程同时访问同一个对象的不同的同步方法
  7. 两个线程分别同时访问静态synchronized和非静态synchronized方法
  8. 同步方法抛出异常后,JVM会自动释放锁的情况

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

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

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

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

    代码验证

    1. public class Condition2 implements Runnable {
    2. // 创建两个不同的对象
    3. static Condition2 instance1 = new Condition2();
    4. static Condition2 instance2 = new Condition2();
    5. @Override
    6. public void run() {
    7. method();
    8. }
    9. private synchronized void method() {
    10. System.out.println("线程名:" + Thread.currentThread().getName() + ",运行开始");
    11. try {
    12. Thread.sleep(4000);
    13. } catch (InterruptedException e) {
    14. e.printStackTrace();
    15. }
    16. System.out.println("线程:" + Thread.currentThread().getName() + ",运行结束");
    17. }
    18. public static void main(String[] args) {
    19. Thread thread1 = new Thread(instance1);
    20. Thread thread2 = new Thread(instance2);
    21. thread1.start();
    22. thread2.start();
    23. while (thread1.isAlive() || thread2.isAlive()) {
    24. }
    25. System.out.println("测试结束");
    26. }
    27. }

    运行结果

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

    1. 线程名:Thread-0,运行开始
    2. 线程名:Thread-1,运行开始
    3. 线程:Thread-0,运行结束
    4. 线程:Thread-1,运行结束
    5. 测试结束

    代码分析

    「问题在此:」
    两个线程(thread1、thread2),访问两个对象(instance1、instance2)的同步方法(method()),两个线程都有各自的锁,不能形成两个线程竞争一把锁的局势,所以这时,synchronized修饰的方法method()和不用synchronized修饰的效果一样(不信去把synchronized关键字去掉,运行结果一样),所以此时的method()只是个普通方法。
    「如何解决这个问题:」
    若要使锁生效,只需将method()方法用static修饰,这样就形成了类锁,多个实例(instance1、instance2)共同竞争一把类锁,就可以使两个线程串行执行了。这也就是下一个场景要讲的内容。

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

    这个场景解决的是场景二中出现的线程不安全问题,即用类锁实现:
    「两个线程同时访问(一个或两个)对象的静态同步方法,是线程安全的。」

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

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

    1. public class Condition4 implements Runnable {
    2. static Condition4 instance = new Condition4();
    3. @Override
    4. public void run() {
    5. //两个线程访问同步方法和非同步方法
    6. if (Thread.currentThread().getName().equals("Thread-0")) {
    7. //线程0,执行同步方法method0()
    8. method0();
    9. }
    10. if (Thread.currentThread().getName().equals("Thread-1")) {
    11. //线程1,执行非同步方法method1()
    12. method1();
    13. }
    14. }
    15. // 同步方法
    16. private synchronized void method0() {
    17. System.out.println("线程名:" + Thread.currentThread().getName() + ",同步方法,运行开始");
    18. try {
    19. Thread.sleep(4000);
    20. } catch (InterruptedException e) {
    21. e.printStackTrace();
    22. }
    23. System.out.println("线程:" + Thread.currentThread().getName() + ",同步方法,运行结束");
    24. }
    25. // 普通方法
    26. private void method1() {
    27. System.out.println("线程名:" + Thread.currentThread().getName() + ",普通方法,运行开始");
    28. try {
    29. Thread.sleep(4000);
    30. } catch (InterruptedException e) {
    31. e.printStackTrace();
    32. }
    33. System.out.println("线程:" + Thread.currentThread().getName() + ",普通方法,运行结束");
    34. }
    35. public static void main(String[] args) {
    36. Thread thread1 = new Thread(instance);
    37. Thread thread2 = new Thread(instance);
    38. thread1.start();
    39. thread2.start();
    40. while (thread1.isAlive() || thread2.isAlive()) {
    41. }
    42. System.out.println("测试结束");
    43. }
    44. }

    运行结果

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

    1. 线程名:Thread-0,同步方法,运行开始
    2. 线程名:Thread-1,普通方法,运行开始
    3. 线程:Thread-0,同步方法,运行结束
    4. 线程:Thread-1,普通方法,运行结束
    5. 测试结束

    结果分析

    问题在于此:method1没有被synchronized修饰,所以不会受到锁的影响。即便是在同一个对象中,当然在多个实例中,更不会被锁影响了。结论: :::info 「非同步方法不受其它由synchronized修饰的同步方法影响」 ::: 可能想到一个类似场景:多个线程访问同一个对象中的同步方法,同步方法又调用一个非同步方法,这个场景会是线程安全的吗?

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

    来实验下这个场景,用两个线程调用同步方法,在同步方法中调用普通方法;再用一个线程直接调用普通方法,看看是否是线程安全的? ```java public class Condition8 implements Runnable {

    static Condition8 instance = new Condition8();

    @Override
    public void run() {

    1. if (Thread.currentThread().getName().equals("Thread-0")) {
    2. //直接调用普通方法
    3. method2();
    4. } else {
    5. // 先调用同步方法,在同步方法内调用普通方法
    6. method1();
    7. }

    }

    // 同步方法
    private static synchronized void method1() {

    1. System.out.println("线程名:" + Thread.currentThread().getName() + ",同步方法,运行开始");
    2. try {
    3. Thread.sleep(2000);
    4. } catch (InterruptedException e) {
    5. e.printStackTrace();
    6. }
    7. System.out.println("线程:" + Thread.currentThread().getName() + ",同步方法,运行结束,开始调用普通方法");
    8. method2();

    }

    // 普通方法
    private static void method2() {

    1. System.out.println("线程名:" + Thread.currentThread().getName() + ",普通方法,运行开始");
    2. try {
    3. Thread.sleep(4000);
    4. } catch (InterruptedException e) {
    5. e.printStackTrace();
    6. }
    7. System.out.println("线程:" + Thread.currentThread().getName() + ",普通方法,运行结束");

    }

    public static void main(String[] args) {

    1. // 此线程直接调用普通方法
    2. Thread thread0 = new Thread(instance);
    3. // 这两个线程直接调用同步方法
    4. Thread thread1 = new Thread(instance);
    5. Thread thread2 = new Thread(instance);
    6. thread0.start();
    7. thread1.start();
    8. thread2.start();
    9. while (thread0.isAlive() || thread1.isAlive() || thread2.isAlive()) {
    10. }
    11. System.out.println("测试结束");

    }

}

  1. <a name="pr5ns"></a>
  2. #### 运行结果

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

  1. <a name="vGDSe"></a>
  2. #### 结果分析
  3. 可以看出,普通方法被两个线程并行执行,不是线程安全的。这是为什么呢?<br />因为如果非同步方法,有任何其他线程直接调用,而不是仅在调用同步方法时,才调用非同步方法,此时会出现多个线程并行执行非同步方法的情况,线程就不安全了。<br />对于同步方法中调用非同步方法时,要想保证线程安全,就必须保证非同步方法的入口,仅出现在同步方法中。但这种控制方式不够优雅,若被不明情况的人直接调用非同步方法,就会导致原有的线程同步不再安全。所以不推荐在项目中这样使用,但要理解这种情况,并且要用语义明确的、让人一看就知道这是同步方法的方式,来处理线程安全的问题。<br />所以,最简单的方式,是在非同步方法上,也加上`synchronized`关键字,使其变成一个同步方法,这样就变成了《场景五:两个线程同时访问同一个对象的不同的同步方法》,这种场景下,大家就很清楚的看到,同一个对象中的两个同步方法,不管哪个线程调用,都是线程安全的了。<br />所以结论是:
  4. :::info
  5. **「两个线程访问同一个对象中的同步方法,同步方法又调用一个非同步方法,仅在没有其他线程直接调用非同步方法的情况下,是线程安全的。若有其他线程直接调用非同步方法,则是线程不安全的。」**
  6. :::
  7. <a name="161154a7"></a>
  8. ### 场景六:两个线程同时访问同一个对象的不同的同步方法
  9. 这个场景也是在探讨对象锁的作用范围,对象锁的作用范围是对象中的所有同步方法。所以,当访问同一个对象中的多个同步方法时,结论是:
  10. :::info
  11. **「两个线程同时访问同一个对象的不同的同步方法时,是线程安全的。」**
  12. :::
  13. ```java
  14. public class Condition5 implements Runnable {
  15. static Condition5 instance = new Condition5();
  16. @Override
  17. public void run() {
  18. if (Thread.currentThread().getName().equals("Thread-0")) {
  19. //线程0,执行同步方法method0()
  20. method0();
  21. }
  22. if (Thread.currentThread().getName().equals("Thread-1")) {
  23. //线程1,执行同步方法method1()
  24. method1();
  25. }
  26. }
  27. private synchronized void method0() {
  28. System.out.println("线程名:" + Thread.currentThread().getName() + ",同步方法0,运行开始");
  29. try {
  30. Thread.sleep(4000);
  31. } catch (InterruptedException e) {
  32. e.printStackTrace();
  33. }
  34. System.out.println("线程:" + Thread.currentThread().getName() + ",同步方法0,运行结束");
  35. }
  36. private synchronized void method1() {
  37. System.out.println("线程名:" + Thread.currentThread().getName() + ",同步方法1,运行开始");
  38. try {
  39. Thread.sleep(4000);
  40. } catch (InterruptedException e) {
  41. e.printStackTrace();
  42. }
  43. System.out.println("线程:" + Thread.currentThread().getName() + ",同步方法1,运行结束");
  44. }
  45. //运行结果:串行
  46. public static void main(String[] args) {
  47. Thread thread1 = new Thread(instance);
  48. Thread thread2 = new Thread(instance);
  49. thread1.start();
  50. thread2.start();
  51. while (thread1.isAlive() || thread2.isAlive()) {
  52. }
  53. System.out.println("测试结束");
  54. }
  55. }

运行结果

是线程安全的。

  1. 线程名:Thread-1,同步方法1,运行开始
  2. 线程:Thread-1,同步方法1,运行结束
  3. 线程名:Thread-0,同步方法0,运行开始
  4. 线程:Thread-0,同步方法0,运行结束
  5. 测试结束

结果分析

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

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

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

代码实现

  1. public class Condition6 implements Runnable {
  2. static Condition6 instance = new Condition6();
  3. @Override
  4. public void run() {
  5. if (Thread.currentThread().getName().equals("Thread-0")) {
  6. //线程0,执行静态同步方法method0()
  7. method0();
  8. }
  9. if (Thread.currentThread().getName().equals("Thread-1")) {
  10. //线程1,执行非静态同步方法method1()
  11. method1();
  12. }
  13. }
  14. // 重点:用static synchronized 修饰的方法,属于类锁,锁对象为(*.class)对象。
  15. private static synchronized void method0() {
  16. System.out.println("线程名:" + Thread.currentThread().getName() + ",静态同步方法0,运行开始");
  17. try {
  18. Thread.sleep(4000);
  19. } catch (InterruptedException e) {
  20. e.printStackTrace();
  21. }
  22. System.out.println("线程:" + Thread.currentThread().getName() + ",静态同步方法0,运行结束");
  23. }
  24. // 重点:synchronized 修饰的方法,属于方法锁,锁对象为(this)对象。
  25. private synchronized void method1() {
  26. System.out.println("线程名:" + Thread.currentThread().getName() + ",非静态同步方法1,运行开始");
  27. try {
  28. Thread.sleep(4000);
  29. } catch (InterruptedException e) {
  30. e.printStackTrace();
  31. }
  32. System.out.println("线程:" + Thread.currentThread().getName() + ",非静态同步方法1,运行结束");
  33. }
  34. //运行结果:并行
  35. public static void main(String[] args) {
  36. //问题原因: 线程1的锁是类锁(*.class)对象,线程2的锁是方法锁(this)对象,两个线程的锁不一样,自然不会互相影响,所以会并行执行。
  37. Thread thread1 = new Thread(instance);
  38. Thread thread2 = new Thread(instance);
  39. thread1.start();
  40. thread2.start();
  41. while (thread1.isAlive() || thread2.isAlive()) {
  42. }
  43. System.out.println("测试结束");
  44. }

运行结果

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

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

本场景探讨的是synchronized释放锁的场景: :::info 「只有当同步方法执行完或执行时抛出异常这两种情况,才会释放锁。」 ::: 所以,在一个线程的同步方法中出现异常的时候,会释放锁,另一个线程得到锁,继续执行。而不会出现一个线程抛出异常后,另一个线程一直等待获取锁的情况。这是因为JVM在同步方法抛出异常的时候,会自动释放锁对象。

代码实现

  1. public class Condition7 implements Runnable {
  2. private static Condition7 instance = new Condition7();
  3. @Override
  4. public void run() {
  5. if (Thread.currentThread().getName().equals("Thread-0")) {
  6. //线程0,执行抛异常方法method0()
  7. method0();
  8. }
  9. if (Thread.currentThread().getName().equals("Thread-1")) {
  10. //线程1,执行正常方法method1()
  11. method1();
  12. }
  13. }
  14. private synchronized void method0() {
  15. System.out.println("线程名:" + Thread.currentThread().getName() + ",运行开始");
  16. try {
  17. Thread.sleep(4000);
  18. } catch (InterruptedException e) {
  19. e.printStackTrace();
  20. }
  21. //同步方法中,当抛出异常时,JVM会自动释放锁,不需要手动释放,其他线程即可获取到该锁
  22. System.out.println("线程名:" + Thread.currentThread().getName() + ",抛出异常,释放锁");
  23. throw new RuntimeException();
  24. }
  25. private synchronized void method1() {
  26. System.out.println("线程名:" + Thread.currentThread().getName() + ",运行开始");
  27. try {
  28. Thread.sleep(4000);
  29. } catch (InterruptedException e) {
  30. e.printStackTrace();
  31. }
  32. System.out.println("线程:" + Thread.currentThread().getName() + ",运行结束");
  33. }
  34. public static void main(String[] args) {
  35. Thread thread1 = new Thread(instance);
  36. Thread thread2 = new Thread(instance);
  37. thread1.start();
  38. thread2.start();
  39. while (thread1.isAlive() || thread2.isAlive()) {
  40. }
  41. System.out.println("测试结束");
  42. }
  43. }

运行结果

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

结果分析

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