二、線程安全
1.爲什麼有線程安全問題?
當多個線程同時共享同一個全局變量或靜態變量,做寫操作時,可能會發生數據衝突問題,也就是線程安全問題。但是做讀操作是不會發生數據衝突問題。
2. 如何解決多線程之間的線程安全問題?
使用同步synchronized或使用鎖(lock)。線程在執行的時候,必須先獲得鎖,一次只能允許一個線程獲得鎖,其他線程必須等待,代碼執行完後釋放鎖,讓其他線程去執行,相當與變成了單線程執行,保證了線程的原子性。
3.什麼是多線程之間同步?
線程之間共享同一個資源,相互之間不會產生干擾。
4.內置鎖
Java提供一種內置的鎖機制來支持原子性,每個java對象都可以看作是一個鎖,稱爲內置鎖。內置鎖爲互斥鎖:線程A獲得鎖後,線程B必須等待A執行完釋放鎖後才能獲得鎖。內置鎖使用synchronize關鍵字實現。實現方法如下:
(1)修飾需要同步的方法,此時充當鎖的對象爲——調用同步方法的對象。
(2)同步代碼塊,同步代碼塊的粒度比同步方法更細,並且充當鎖的對象不一定是this鎖,也可以是其他對象。使用更加靈活。
//同步方法
public synchronized void sale(){
if(count > 0){
System.out.println("正在出售第" + (100-count+1) + "張火車票");
count--;
}
}
//同步代碼塊
public synchronized void sale(){
synchronized(obj){ //參數爲任意對象
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(count > 0){
System.out.println("正在出售第" + (100-count+1) + "張火車票");
count--;
}
}
}
5.靜態同步函數
靜態同步函數就是被static和synchronize同時修飾的方法。靜態同步函數使用的鎖是該函數所屬的字節碼文件對象,可以用getClass()方法獲取也可以用當前類名.class表示。
public static void sale() {
synchronized (ThreadTrain3.class) {
if (trainCount > 0) {
System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - trainCount + 1) + "張票");
trainCount--;
}
}
}
總結:
synchronized 修飾方法使用鎖是當前this鎖。
synchronized 修飾靜態方法使用鎖是當前類的字節碼文件
6.多線程死鎖
什麼是多線程死鎖:同步中嵌套同步,線程之間相互等待,導致鎖無法釋放。線程A先獲取lock1,讓線程A休眠50ms,同時線程B先獲取lock2,然後去獲取lock1,但是lock1已經被線程A獲取,所以只能等待,線程A休眠結束後去獲取lock2,但是lock2被線程B佔用,所以也只能等待,就造成了死鎖。
以下代碼會造成死鎖,把sale()的synchronized去掉就不會死鎖或者把同步代碼塊的修飾去掉。
class threadTest2 implements Runnable {
private static int count = 100; //定義火車票總數
private static Object obj = new Object(); //靜態的變量存放在方法區,被共享
public boolean flag = true;
public void run() {
if(flag){
while(count > 0){
synchronized (obj) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
sale();
}
}
}
else {
while(count > 0){
sale();
}
}
}
//同步代碼塊
public synchronized void sale(){
//synchronized(obj){ //參數爲任意對象
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(count > 0){
System.out.println("正在出售第" + (100-count+1) + "張火車票");
count--;
}
//}
}
}
/**
*
* @author johson
* 模擬多線程死鎖問題,多線程死鎖是因爲在同步中嵌套了同步,把sale()的synchronized去掉就不會死鎖或者把同步代碼塊的修飾去掉
*
*/
public class test2 {
public static void main(String[] args) throws InterruptedException {
threadTest2 threadTest1 = new threadTest2();
//窗口1
Thread t1 = new Thread(threadTest1,"窗口1");
//窗口2
Thread t2 = new Thread(threadTest1,"窗口2");
t1.start();
Thread.sleep(40);
threadTest1.flag = false;
t2.start();
}
7.ThreadLocal
使用TreadLocal維護變量是,ThreadLocal爲每個使用變量的線程創建變量的副本,所以每一個線程都可以獨立的修改自己的副本,而不會影響到其他線程的副本。ThreadLocal是通過map來實現的。
這裏介紹ThreadLocal的四個方法:
(1)set方法:設置當前線程的線程局部變量的值
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
(2)get()方法:該方法返回當前線程所對應的線程局部變量。
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
(3)remove()方法:將當前局部變量的值刪除,目的是爲了減少內存的佔用,是jdk5.0新增的方法。
線程執行完成後,對線程的局部變量會自動被垃圾回收,所以remove()方法並不是必須要顯式調用的,但是調用了可以加快內存回收的速度。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
(4)initialValue()方法:返回線程局部變量的初始值。需要重寫這個方法。這個方法是一個延遲調用方法,在線程第一次調用get()或set()方法時纔會執行,並且只執行一次。
protected T initialValue() {
return null;
}
下面是一個ThreadLocal的demo
class Res{
//public Integer count = 0;
public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
//初始化
protected Integer initialValue() {
return 0;
};
};
public Integer getNumber(){
Integer count = threadLocal.get()+1;
threadLocal.set(count);
return count;
}
}
/**
* ThreadLocal測試
* @author johson
*
*/
public class test3 extends Thread{
private Res res;
public test3(Res res) {
this.res = res;
}
@Override
public void run() {
for(int i = 0;i < 3;i++){
System.out.println(Thread.currentThread().getName() + "," + res.getNumber());
}
}
public static void main(String[] args){
Res res = new Res();
test3 t = new test3(res);
test3 t1 = new test3(res);
t.start();
t1.start();
}
8.多線程的三大特性
(1)原子性:即一個操作或者多個操作 要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。
一個很經典的例子就是銀行賬戶轉賬問題:
比如從賬戶A向賬戶B轉1000元,那麼必然包括2個操作:從賬戶A減去1000元,往賬戶B加上1000元。這2個操作必須要具備原子性才能保證不出現一些意外的問題。
我們操作數據也是如此,比如i = i+1;其中就包括,讀取i的值,計算i,寫入i。這行代碼在Java中是不具備原子性的,則多線程運行肯定會出問題,所以也需要我們使用同步和lock這些東西來確保這個特性了。
原子性其實就是保證數據一致、線程安全一部分.
(2)可見性:兩個線程同時訪問一個變量,A線程修改了這個變量,在B線程中能立刻看到。
如果A線程修改了i的值,沒有及時刷新到主內存中,B線程使用的i還是之前的那個i,這就是線程的可見性問題。
(3)有序性:代碼的執行順序按照代碼的先後順序執行。代碼在cpu中運行的時候,代碼的運行順序會被改變,但是不會改變邏輯順序,會保證執行結果的一致。——這就是重排序。例如:
int a = 10; //語句1
int r = 2; //語句2
a = a + 3; //語句3
r = a*a; //語句4
則因爲重排序,他還可能執行順序爲 2-1-3-4,1-3-2-4
但絕不可能 2-1-4-3,因爲這打破了依賴關係。
顯然重排序對單線程運行是不會有任何問題,而多線程就不一定了,所以我們在多線程編程時就得考慮這個問題了。
9.java內存模型
共享內存模型指的就是Java內存模型(簡稱JMM),JMM決定一個線程對共享變量的寫入時,能對另一個線程可見。從抽象的角度來看,JMM定義了線程和主內存之間的抽象關係:線程之間的共享變量存儲在主內存(main memory)中,每個線程都有一個私有的本地內存(local memory),本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是JMM的一個抽象概念,並不真實存在。它涵蓋了緩存,寫緩衝區,寄存器以及其他的硬件和編譯器優化。
線程A和線程B如果要通信,必須經過以下兩個步驟:
(1)線程A將更新的變量刷新到主內存中
(2)線程B再去主內存中讀取線程A更新的變量
如上圖所示,本地內存A和B有主內存中共享變量x的副本。假設初始時,這三個內存中的x值都爲0。線程A在執行時,把更新後的x值(假設值爲1)臨時存放在自己的本地內存A中。當線程A和線程B需要通信時,線程A首先會把自己本地內存中修改後的x值刷新到主內存中,此時主內存中的x值變爲了1。隨後,線程B到主內存中去讀取線程A更新後的x值,此時線程B的本地內存的x值也變爲了1。
從整體來看,這兩個步驟實質上是線程A在向線程B發送消息,而且這個通信過程必須要經過主內存。JMM通過控制主內存與每個線程的本地內存之間的交互,來爲java程序員提供內存可見性保證。
總結:什麼是Java內存模型:java內存模型簡稱jmm,定義了一個線程對另一個線程可見。共享變量存放在主內存中,每個線程都有自己的本地內存,當多個線程同時訪問一個數據的時候,可能本地內存沒有及時刷新到主內存,所以就會發生線程安全問題。
10.volatile的作用
(1)保證此變量對所有的線程的可見性,當一個線程修改了這個變量的值,volatile 保證了新值能立即同步到主內存,以及每次使用前立即從主內存刷新。但普通變量做不到這點,普通變量的值在線程間傳遞均需要通過主內存(詳見:Java內存模型)來完成。
(2)禁止指令重排序優化。
11.Volatile和Synchronize的區別
(1)volatile雖然具有可見性但是並不能保證原子性。
(2)性能方面,synchronized關鍵字是防止多個線程同時執行一段代碼,就會影響程序執行效率,而volatile關鍵字在某些情況下性能要優於synchronized。
但是要注意volatile關鍵字是無法替代synchronized關鍵字的,因爲volatile關鍵字無法保證操作的原子性。