在后面的文章《双刃剑 - 了解多线程带来的平安问题》中,咱们提到了多线程状况下存在的线程平安问题。本文将以这个问题为背景,介绍如何通过应用 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 你能够留神下:
- Java 中的
synchronized
关键字用于解决多线程访问共享资源时的同步,以解决 线程烦扰 和内存一致性 问题; - 你能够通过 代码块(code block) 或者 办法(method) 来应用
synchronized
关键字; synchronized
的原理基于 对象中的锁 ,当线程须要进入synchronized
润饰的办法或代码块时,它须要先 取得 锁并在执行完结后 开释 它;- 当线程进入 非动态(non-static)同步办法时,它取得的是对象实例(Object level)的锁。而线程进入 动态 同步办法时,它所取得的是类实例(Class level)的锁,两者没有必然关系;
- 如果
synchronized
中应用的对象是 null,将会抛出NullPointerException
谬误; synchronized
对办法的性能有肯定影响,因为线程要期待获取锁;- 应用
synchronized
时尽量应用代码块,而不是整个办法,免得阻塞整个办法; - 尽量不要应用 String 类型和 原始类型 作为参数。这是因为,JVM 在解决字符串、原始类型时会对它们进行优化。比方,你本来是想对不同的字符串进行加锁,然而 JVM 认为它们是同一个,很显然这不是你想要的后果。
对于 synchronized
的可见性、指令排序等底层原理,咱们会在前面的阶段中具体介绍。
以上就是文本的全部内容,祝贺你又上了一颗星!✨
夫子的试炼
- 手写代码体验
synchronized
的不同用法。
参考资料
- https://docs.oracle.com/javas…
- https://javagoal.com/synchron…
对于作者
关注公众号【庸人技术笑谈】,获取及时文章更新。记录平凡人的技术故事,分享有品质(尽量)的技术文章,偶然也聊聊生存和现实。不贩卖焦虑,不抛售课程。