有些代碼在單個線程環境下執行正確,如果同樣的代碼在多個線程下同時執行可能就會出現問題,這個就是線程安全問題(或者稱線程不安全問題),簡而言之就是:線程安全問題是由于多線程出現的問題,原因是在多線程條件下存在數據共享。?
線程安全問題
1. 觀察線程不安全
? ? ? ?下面這個例子是兩個線程同時對一個變量進行自增50000次,正常情況下如果我們對一個變量自增兩次五萬次,結果應該是十萬,但下圖的結果卻是其他數字,這種實際情況與預期不符的現象就是線程不安全。相反如果我們改變一些順序就可以出現預期的結果了,只是兩個線程不再是同時執行。
在這之前需要了解一個知識點,站在cpu的角度看count++,它其實是分成三步的(第一步:load 把數據從內存讀到cpu上;第二步:add?把寄存器中的數據加1;第三步:save?把寄存器中的數據保存到內存中)
2. 出現線程不安全的原因
? 2.1 操作系統對線程的調度是隨機的
什么是調度順序?每一步的執行順序。
t1可能先執行load、add、save, 然后再是t2的 load 、add、save;也可能是 t1 可能先執行load、add, 然后是t2的 load 、add、save,最后是 t1 的save等等很多情況
為什么調度順序不一樣產生的結果不一樣呢?我們再進行深度剖析。挑選一個同時的調度順序進行展開敘述:每個線程都有自己的cpu,方塊代表內存,橢圓代表cpu;
? ? 以下這個是一個線程一個線程執行的,先進行t1再進行t2的情況,最終的結果是2,結果這個沒有問題(這里每個線程我只進行一次自增,剩下的自增結果是類似的)
? ? 接著下面這個調度順序是兩個線程同時進行出現不同的結果,同樣是自增一次,最后內存結果確實1,這就是調度順序引起的線程不安全
? 2.2 多個線程修改同一個變量
t1和t2同時對count進行修改自增 也引起了線程不安全,如果是一個一個執行就不存在線程安全問題(看上圖)。
? 2.3?修改操作不是原子的
也因為自增是分成三步的導致調度順序不同,產生的結果不同。
? 2.4 內存可見性
? 2.5 指令重排性
3. 解決線程不安全
想要解決線程安全問題,就需要從以上原因入手。線程調度隨機是由系統解決的這個無法改變;同時修改一個變量有時可以通過調換代碼順序進行解決,有些情況下不可以;操作不是原子可以通過加鎖實現(給每一個步驟都加鎖變成原子),剩下的內存可見性和指令重排序在這個代碼中不存在,另外討論。
加鎖(synchronized)
在已經加鎖的狀態下,另一個線程嘗試同樣加這個鎖,就會產生鎖沖突(也叫鎖競爭),后面那個線程就會阻塞,直到前面那個線程解鎖。
將上面代碼進行加鎖得到以下代碼。
接著進行深度的理解每一步的過程
等待中也就意味著阻塞,然而阻塞也就避免了每個線程的三步進行"串行",于是線程安全問題也就解決掉了。
synchronized不僅可以修飾代碼塊,還可以修飾實例方法和靜態方法,下面是實例方法
public class Demo02 {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {counter.insert();}});Thread t2 = new Thread(()->{for (int i = 0; i < 50000; i++) {counter.insert();}});t1.start();t2.start();t1.join();t2.join();System.out.println("counter.count:"+counter.count);}
}
class Counter{public int count ;//方法1:
// synchronized public void insert(){ //修飾實例方法
// count++;
// }//上面這中寫法等價于下面這種,方法二:public void insert(){synchronized(this){count++;}}
}
靜態方法