導語
上面說到了多線程的簡單實作,編寫了幾個入門的小例子,這裡我們來研究一下關于執行個體變量和線程安全的問題。在自定義的線程類中的執行個體變量針對其他線程可以有共享和不共享之分,下多個線程之間進行互動的時候會産生線程安全的問題。下面就來看看會有什麼技術點。
不共享資料的情況
如圖在進行資料操作的時候,互相之間的資料不會産生影響,都在自己的一塊記憶體中進行操作。
下面就通過一個小例子來看看不共享情況
自定義實作類
public class MyThread extends Thread {
private int count = 5;
public MyThread(String name){
super();
this.setName(name);
}
@Override
public void run() {
super.run();
while (count>0){
count--;
System.out.println("由 "+currentThread().getName()+" 計算,count="+count);
}
}
}
運作類
public class Run {
public static void main(String[] args) {
MyThread a = new MyThread("a");
MyThread b = new MyThread("b");
MyThread c = new MyThread("c");
a.start();
b.start();
c.start();
}
}
運作結果
通過上面的例子可以看到,一共建立了3個線程,每個線程都有各自的count變量,自己減少自己的count變量值,這樣所實作的變量就是不共享的,也就是說在JVM配置設定給線程獨立的記憶體中進行運作。是以不會存在多個線程共同通路同一個變量的情況。
下面就來看看多個線程通路同一個變量的情況
共享資料情況
共享資料的情況就是多個線程可以通路同一個變量,例如在實作搶票軟體功能的時候,多線程共同處理同一個火車上的火車票操作。
共享資料小例子
建立自定義線程
public class MyThread extends Thread {
private int count = 5;
@Override
public void run() {
super.run();
count--;
System.out.println("由 "+currentThread().getName()+ " 計算, count = "+count);
}
}
測試類
public class Run {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread a = new Thread(myThread,"A");
Thread b = new Thread(myThread,"B");
Thread c = new Thread(myThread,"C");
Thread d = new Thread(myThread,"D");
Thread e = new Thread(myThread,"E");
a.start();
b.start();
c.start();
d.start();
e.start();
}
}
測試結果
從上面的結果中可以看到有些線程處理的資料居然是同一個資料,還有一個情況是每次運作的結果是不一樣的。從這個角度上看對于count變量的通路并不是安全的在之前的之前的部落格中分析過這種情況出現的原因。這裡簡單的說一下操作步驟
- 1、擷取原有的值
- 2、計算i-1的值
- 3、對i進行指派
在這個三個步驟任意的一個步驟都有可能出現問題,并且導緻結果不同。有興趣的讀者可以研究一下或者看看部落客的高并發系列。那麼如果我們将自定義的Thread修改為如下,還會出現錯誤麼!!
public class MyThread extends Thread {
private int count = 5;
@Override
synchronized public void run() {
super.run();
count--;
System.out.println("由 "+currentThread().getName()+ " 計算, count = "+count);
}
}
運作結果
會看到結果是正确的。也就是說加入了synchronized關鍵字之後線上程進入run()方法之前會以排隊的方式進行處理,當一個線程調用run方法之前首先判斷run方法是不是被鎖上了,如果被鎖則說明有其他線程正在處理這個資料,那就要等到這個線程處理完成之後才會輪到排隊線程進行處理。而這段被synchronized關鍵字标注的代碼塊被稱為是互斥區或者是臨界區。
當一個線程想要執行同步方法裡面的代碼時,線程首先嘗試去擷取鎖,如果能夠拿到這把鎖,那麼這個線程就可以執行synchronize裡面的代碼,如果不能拿到這把鎖,那麼這個線程就會不斷嘗試擷取這把鎖,直到拿到為止,而且多個線程是同時争搶這把鎖。
面試題
一個局部變量i=1,兩個線程同時執行,是否是線程安全的?
情況一 屬于自定義線程類中的變量
public class MyThread extends Thread {
private int i = 5;
@Override
public void run() {
i--;
System.out.println(i);
}
}
測試類
public class Test {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start();
t2.start();
}
}
情況二 屬于自定義線程類中的某個方法的局部變量
public class MyThread extends Thread {
public void print(){
int a = 5;
a--;
System.out.println(currentThread().getName() + " "+a);
}
@Override
public void run() {
print();
}
}
情況三 屬于繼承了Runnable接口的線程類變量
public class MyRunnable implements Runnable {
private int i = 5;
private String name ;
public MyRunnable(String name){
this.name = name;
}
@Override
public void run() {
System.out.println("name " + name +getClass().getName()+ " "+i--);
}
}
測試類
public class Run {
public static void main(String[] args) throws InterruptedException {
MyRunnable r1 = new MyRunnable("TE");
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r1);
t1.start();
TimeUnit.SECONDS.sleep(10);
t2.start();
}
}
有一個共享變量如何保證線程安全 ?
- 使用synchronize關鍵字
- 使用顯式鎖
- 使用單一對象模式
總結
這篇部落客要分析了兩件事情,也就是結尾的時候所提供的兩個面試題。怎樣實作多線程線程安全是很多人在研究的一個課題,在分布式系統中,在高并發場景下,這些都是不可避免的問題。