为什么写本篇
多线程的概念比较多,每一个知识点又牵连着其他知识点,所以要融汇贯通不是一件容易的事情。并且有一些非常基本的事情很多书都不会写,自己查也不一定好运能获得这些基本知识点。而正是这些基本的知识点明白了,才能逐渐搭建起来整个多线程的世界观。
多线程编写不好会导致一些很诡异的问题,这是由于CPU线程调度的不可控性(不可预期性)和一些代码优化编译混排引起的执行顺序上的混乱导致的。所以我们在编写多线程的时候,需要刻意的使用一些方法或者关键字来告诉机器我们希望的执行逻辑是怎么样子的,从而使得我们的程序逻辑是可控或者可预期的。
知识点1:无论synchronized关键字写在哪里,本质上synchronized只能作用在堆内存里面的某一个实例对象或者类.class(其本质也是一个存在在堆内存中的java.lang.Class类的对象)上(或者说给某一个对象上锁,或者说占据某一个对象的monitor)。
public class Sample1 {
private Integer count = 0;
private Object o = new Object();
public void add(int n) {
synchronized(o) {
// 多线程调用同一个sample对象的add方法的时候,希望同时只能有一个线程进行add操作
for (int i = 0; i < n; i++) {
this.count++;
System.out.println(Thread.currentThread().getName() + ":" + this.count);
try {
Thread.sleep(10l);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
// main function
Sample1 sample1 = new Sample1()
new Thread(() -> {sample1.add(10);}, "t1").start();
new Thread(() -> {sample1.add(10);}, "t2").start();
上面的同步锁加在o上面,但是每次弄synchronized锁的时候,都需要弄一个没有什么其他作用的object对象o出来。好像很麻烦。所以可以写成:
public class Sample2 {
private Integer count = 0;
public void add(int n) {
synchronized(this) {
// 多线程调用同一个sample对象的add方法的时候,希望同时只能有一个线程进行add操作
for (int i = 0; i < n; i++) {
this.count++;
System.out.println(Thread.currentThread().getName() + ":" + this.count);
try {
Thread.sleep(10l);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
Sample2和Sample1其实是一样的效果。
人们又发现整个add函数synchronized从头到底,于是又写的更简单:
public class Sample3 {
private Integer count = 0;
synchronized public void add(int n) {
// 多线程调用同一个sample对象的add方法的时候,希望同时只能有一个线程进行add操作
for (int i = 0; i < n; i++) {
this.count++;
System.out.println(Thread.currentThread().getName() + ":" + this.count);
try {
Thread.sleep(10l);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
同理,对于静态函数,以下两种写法也是等效的。
public class Sample3 {
private static Integer count = 0;
synchronized public static void add() {
count++
}
}
public class Sample4 {
private static Integer count = 0;
public static void add() {
synchronized(Sample4.class) {
count++;
}
}
}
知识点2:在运行时,两个线程只有synchronized同一个内存对象,才会有竞争关系。一个获得锁,一个被block住从而进入blocked状态。
public class Sample5 {
private Integer count = 0;
synchronized public void add(int n) {
// 多线程调用同一个sample对象的add方法的时候,希望同时只能有一个线程进行add操作
for (int i = 0; i < n; i++) {
this.count++;
System.out.println(Thread.currentThread().getName() + ":" + this.count);
try {
Thread.sleep(10l);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// main function
Sample5 sampleA = new Sample5()
Sample5 sampleB = new Sample5()
new Thread(() -> {sampleA.add(10);}, "t1").start();
new Thread(() -> {sampleB.add(10);}, "t2").start();
有同学说:“这两个线程都是调用Sample5的add方法呀,他们应该一个执行,一个卡住呀。“ 这个是错误的。首先synchronzed不是作用在方法上,这种写法只是作用在this上的简写。而线程t1和线程t2的Sample5对象是两个,一个是sampeA,一个sampleB,而this指的显然不是同一个内存对象。因此t1和t2线程并不会形成竞争关系。同理下面的例子中,两个线程也不会有竞争关系。
public class Sample6 {
private Integer count = 0;
private static Integer total = 0;
synchronized public void add(int n) {
// 多线程调用同一个sample对象的add方法的时候,希望同时只能有一个线程进行add操作
for (int i = 0; i < n; i++) {
this.count++;
System.out.println(Thread.currentThread().getName() + ":" + this.count);
try {
Thread.sleep(10l);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
synchronized public static void sum(int n) {
for (int i = 0; i < n; i++) {
total++;
System.out.println(Thread.currentThread().getName() + ":" + total);
try {
Thread.sleep(10l);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// main function
Sample6 sample = new Sample6()
new Thread(() -> {sample.add(10);}, "t1").start();
new Thread(() -> {Sample6.sum(10);}, "t2").start();
上面的例子中,t1线程锁的是sample的this对象(Sample6的一个实例对象),而t2线程锁的是Sample6.class对象(一个java.lang.Class的实例对象)。这两个对象不是同一个对象,因此互不干涉。
知识点3: 一个线程进入synchronized(obj)代码块后,只要它没有出代码块,其他线程就肯定会卡在其他synchronized(obj)的地方被blocked住。先抛出结论:以上这句话是错误的。
通常有人会认为线程A出了synchronized(obj)的代码块之后,才会释放obj的锁,其他synchronized(obj)的线程才会继续执行下去。其实线程A如果在synchronized(obj)代码块中调用obj.wait()的话,线程A也会释放obj的锁,并且线程A会进入waiting的状态卡在这一步。这个时候其他线程B就可以进入其他synchronized(obj)的代码块了。如果线程B的代码块里面也有调用obj.wait(),那么线程B也进入waiting状态。obj的锁被释放。这个时候线程A和线程B都卡在synchronized(obj)的代码块中的obj.wait()这里,虽然他们在代码块中,但是却不占用obj的锁。
如果这个时候有线程C执行了obj.notifyAll()的话,那么就会唤醒线程A和线程B,从而让线程A和线程B可以继续执行各自obj.wait()后面的代码(严重注意:虽说这里写是唤醒线程A和线程B,其实是把线程A和线程B的状态改为Runnable,由于线程A和线程B无法马上获得obj的锁,所以线程A和线程B又会马上变为blocked状态,直到线程C释放了obj的锁,线程A和线程B才会去竞争obj的锁,竞争赢的线程才能继续执行,竞争输的线程还得继续保持blocked状态。)。
如果线程C没有执行notifyAll,而是执行notify,那么线程A和线程B中的一个会被唤醒。至于是哪一个,取决于CPU的线程调度器,你无法控制。
知识点4:obj.wait(), obj.notify(), obj.notifyAll()执行的前提是该线程先占用obj的锁(monitor)。
如果在synchronized(obj)代码块之外调用obj.wait(), obj.notify(), obj.notifyAll()的话,会抛出java.lang.IllegalMonitorStateException
知识点5: 在synchronized(obj)的代码块中,调用obj.wait(30000l)和Thread.sleep(30000l)的共同点和区别是什么?
两者都能使当前线程状态从RUNNABLE变为TIMED_WAITING。不同点是wait会让当前线程释放monitor,而sleep不会让当前线程释放monitor。假设两者的状态成为TIMED_WAITING后,30秒过后,sleep的线程百分百能继续执行下去。而wait()的线程要再去竞争obj的锁才能执行下去(有可能30秒内发生了很多其他事情,导致占有obj的monitor的线程已经是其他线程了)。