关于java:并发王者课-青铜4synchronized用法初体验

41次阅读

共计 4212 个字符,预计需要花费 11 分钟才能阅读完成。

在后面的文章《双刃剑 - 了解多线程带来的平安问题》中,咱们提到了多线程状况下存在的线程平安问题。本文将以这个问题为背景,介绍如何通过应用 synchronized 关键字解这一问题。当然,在青铜阶段,咱们仍不会过多地形容其背地的原理,重点还是先体验并了解它的用法

一、从场景中体验 synchronized

是谁击败了主宰

在峡谷中,击败主宰能够取得高额的经济收益。因而,在条件容许的状况下,大家都会争相击败主宰。于是,哪吒和敌方的兰陵王开始抢夺主宰。按规矩,谁是击败主宰的最初一击,谁便是胜利的一方

假如主宰的初始血量是 100,咱们通过代码来模仿下:

public class Master {
    // 主宰的初始血量
    private int blood = 100;

    // 每次被击打后血量减 5
    public int decreaseBlood() {
        blood = blood - 5;
        return blood;
    }

    // 通过血量判断主宰是否还存活
    public boolean isAlive() {return blood > 0;}
}

咱们定义了哪吒和兰陵王两个线程,让他们同时攻打主宰:

 public static void main(String[] args) {final Master master = new Master();
        Thread neZhaAttachThread = new Thread() {public void run() {while (master.isAlive()) {
                    try {int remainBlood = master.decreaseBlood();
                        if (remainBlood == 0) {System.out.println("哪吒击败了主宰!");
                        }
                    } catch (InterruptedException e) {e.printStackTrace();
                    }
                }
            }
        };

        Thread lanLingWangThread = new Thread() {public void run() {while (master.isAlive()) {
                    try {int remainBlood = master.decreaseBlood();
                        if (remainBlood == 0) {System.out.println("兰陵王击败了主宰!");
                        }
                    } catch (InterruptedException e) {e.printStackTrace();
                    }
                }
            }
        };
        neZhaAttachThread.start();
        lanLingWangThread.start();}

上面是运行的后果:

兰陵王击败了主宰!哪吒击败了主宰!Process finished with exit code 0

两人居然都取得了主宰!很显然,咱们不可能承受这样的后果。然而,细看代码,你会发现这个神奇的后果其实一点也不意外,两个线程在对 blood 做并发减法时出了谬误,因为代码中压根没有必要的并发安全控制。

当然,解决办法也比较简单,在 decreaseBlood 办法上增加 synchronized 关键字即可:

public synchronized int decreaseBlood() {
       blood = blood - 5;
       return blood;
}

为什么加上 synchronized 关键字就能够了呢?这就须要往下看理解 Java 中的 同步 了。

二、意识 synchronized

1. 了解 Java 对象中的锁

在了解 synchronized 之前,咱们先简略了解下 的概念。在 Java 中,每个对象都会有一把锁。当多个线程都须要拜访对象时,那么就须要通过取得锁来取得许可,只有取得锁的线程能力拜访对象,并且其余线程将进入期待状态,期待其余线程开释锁。如下图所示:

2. 了解 synchronized 关键字

依据 Sun 官文文档的形容,synchronized关键字提供了一种预防 线程烦扰 内存一致性谬误 的简略策略,即如果一个对象对多个线程可见,那么该对象变量(final润饰的除外)的读写都须要通过 synchronized 来实现。

你可能曾经留神到其中的两个要害名词:

  • 线程烦扰(Thread Interference):不同线程中运行但作用于雷同数据的两个操作交织时,就会产生烦扰。这意味着这两个操作由多个步骤组成,并且步骤程序重叠;
  • 内存一致性谬误(Memory Consistency Errors):当不同的线程对应为雷同数据的视图不统一时,将产生内存一致性谬误。内存一致性谬误的起因很简单,侥幸的是,咱们不须要具体理解这些起因,所须要的只是防止它们的策略。

从竞态的角度讲,线程烦扰对应的是Read-modify-write,而内存一致性谬误对应的则是Check-then-act

联合 synchronized 的概念能够了解为,锁是多线程平安的根底机制,而 synchronized 是锁机制的一种实现。

三、synchronized 的四种用法

1. 在实例办法中应用 synchronized

public synchronized int decreaseBlood() {
       blood = blood - 5;
       return blood;
}

留神这段代码中的 synchronized 字段,它示意 以后办法每次能且仅能有一个线程拜访。另外,因为以后办法是实例办法,所以如果该对象存在多个实例的话,不同的实例能够由不同的线程拜访,它们之间并无协作关系。

然而,你可能曾经想到了,如果以后线程中有两个 synchronized 办法,不同的线程是否能够拜访不同的 synchronized 办法呢?

答案是:不能

这是因为每个 实例内的同步办法,能且仅能有一个线程拜访

2. 在静态方法中应用 synchronized

public static synchronized int decreaseBlood() {
       blood = blood - 5;
       return blood;
}

与实例办法的 synchronized 不同,静态方法的 synchronized 是基于以后办法所属的类,即 Master.class,而每个类在虚拟机上有且只有一个类对象。所以,对于同一类而言,每次有且只能有一个线程能拜访动态synchronized 办法。

当类中蕴含有多个动态的 synchronized 办法时,每次也依然有且只能有一个线程能够拜访其中的办法。

留神:synchronized 在实例办法和静态方法中的利用能够看出,synchronized办法是否能容许其余线程的进入,取决于 synchronized 的参数。每个不同的参数,在同一时刻都只容许一个线程拜访。基于这样的认知,上面的两种用法就很容易了解了。

3. 在实例办法的代码块中应用 synchronized

public int decreaseBlood() {synchronized(this) {
       blood = blood - 5;
       return blood;
    }
}

在某些状况下,你不须要在整个办法层面应用 synchronized,毕竟这样的形式粒度较大,容易产生阻塞。此时,在代码块中应用synchronized 就是十分不错的抉择,如下面代码所示。

方才曾经提到,synchronized的并发限度取决于其参数,在下面这段代码中的参数是 this,即以后类的实例对象。而在后面的public synchronized int decreaseBlood() 中,synchronized的参数也是以后类的实例对象。因而,上面这两段代码是等同的:

public int decreaseBlood() {synchronized(this) {
       blood = blood - 5;
       return blood;
    }
}

public synchronized int decreaseBlood() {
       blood = blood - 5;
       return blood;
}

4. 在静态方法的代码块中应用 synchronized

同理,上面这两个办法的成果也是等同的。

public static int decreaseBlood() {synchronized(Master.class) {
       blood = blood - 5;
       return blood;
    }
}

public static synchronized int decreaseBlood() {
       blood = blood - 5;
       return blood;
}

四、synchronized 小结

后面,咱们曾经介绍了 synchronized 的几种常见用法,不用死记硬背,你只有记住 synchronized 能够承受任何 非 null对象作为参数,而每个参数在同一时刻能且只能容许一个线程拜访即可。此外,还有一些具备理论指导意义的 Tips 你能够留神下:

  1. Java 中的 synchronized 关键字用于解决多线程访问共享资源时的同步,以解决 线程烦扰 内存一致性 问题;
  2. 你能够通过 代码块(code block) 或者 办法(method) 来应用 synchronized 关键字;
  3. synchronized的原理基于 对象中的锁 ,当线程须要进入synchronized 润饰的办法或代码块时,它须要先 取得 锁并在执行完结后 开释 它;
  4. 当线程进入 非动态(non-static)同步办法时,它取得的是对象实例(Object level)的锁。而线程进入 动态 同步办法时,它所取得的是类实例(Class level)的锁,两者没有必然关系;
  5. 如果 synchronized 中应用的对象是 null,将会抛出NullPointerException 谬误;
  6. synchronized对办法的性能有肯定影响,因为线程要期待获取锁;
  7. 应用 synchronized尽量应用代码块,而不是整个办法,免得阻塞整个办法;
  8. 尽量不要应用 String 类型和 原始类型 作为参数。这是因为,JVM 在解决字符串、原始类型时会对它们进行优化。比方,你本来是想对不同的字符串进行加锁,然而 JVM 认为它们是同一个,很显然这不是你想要的后果。

对于 synchronized 的可见性、指令排序等底层原理,咱们会在前面的阶段中具体介绍。

以上就是文本的全部内容,祝贺你又上了一颗星!✨

夫子的试炼

  • 手写代码体验 synchronized 的不同用法。

参考资料

  • https://docs.oracle.com/javas…
  • https://javagoal.com/synchron…

对于作者

关注公众号【庸人技术笑谈】,获取及时文章更新。记录平凡人的技术故事,分享有品质(尽量)的技术文章,偶然也聊聊生存和现实。不贩卖焦虑,不抛售课程。

正文完
 0