這篇文章目的是爲了總結一下這段時間看《java併發編程藝術》學到的東西,嘗試用自己的話說出來對java多線程的理解和使用。
一、什麼是多線程,爲什麼要用多線程,多線程帶來的挑戰
多線程定義:
多線程(英語:multithreading),是指從軟件或者硬件上實現多個線程併發執行的技術。具有多線程能力的計算機因有硬件支持而能夠在同一時間執行多於一個線程,進而提升整體處理性能。具有這種能力的系統包括對稱多處理機、多核心處理器以及芯片級多處理(Chip-level multithreading)或同時多線程(Simultaneous multithreading)處理器。在一個程序中,這些獨立運行的程序片段叫作“線程”(Thread),利用它編程的概念就叫作“多線程處理(Multithreading)”。–來自百度詞條
爲什麼要使用多線程:總的來說是因爲現代計算機硬件的發展導致的,單核cpu性能的瓶頸已經難以突破,那麼就在一臺計算機上多加cpu唄,現在的個人電腦2核4核 的cpu已經是很普及了,更別說用作服務器的計算機了,所以近年來多線程發展如日中天。
使用多線程帶來性能上的提升,什麼是性能,我的理解就是同樣的時間做的事情多少,比如一個小時的時間學霸可以背60個單詞,我就只能記住10個單詞,但是如果我能在早上做早餐的時候,比如燒水的時候,我需要等待水燒開,注意這裏出現了一個重要信號等待,我的大腦(cpu)空閒下來了,這個時候再繼續背單詞是不是就能多背幾個呢,然後當水燒開時水壺報警音(信號),通知我水燒開了,我繼續做早餐。這樣我背單詞的性能是不是提高了呢?
多線程會出現什麼問題:
1、上下文切換會帶來額外的開支:繼續我背單詞的例子,我決定我一個早上只要空閒下來就去背單詞,於是等待燒水的時候我背了4個半單詞,第5個單詞我記住了一半前4個字母,這個時候水壺響了,我是不是得放下手中的單詞書,然後我還必須記住這本單詞書放在哪裏,背到了幾頁,背了幾個單詞,背到第幾個字母(其實這個就是我們的jvm中的程序計數器乾的活,記住我們的程序執行到了什麼位置,以便下次切換到該線程jvm知道從什麼地方執行,程序計數器是線程私有的),這個時候我去倒水做早餐,這就帶來了負擔,因爲我的大腦需要記住額外的東西,當頻繁燒水、背單詞兩個動作切換的時候我需要記住額外的單詞書的頁碼,和沒背完的單詞字母。但是確實提高了我總體的效率,我之前一個小時只能做早餐,但是我現在多記住了5個單詞啊。
2、死鎖:`public class DeadLockDemo {
static String A = “A”;
private static String B = “B”;
public static void main(String[] args) {
new DeadLockDemo().deadLock();
}
private void deadLock() {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (A) {
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (B) {
System.out.println("1");
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (B) {
synchronized (A) {
System.out.println("2");
}
}
}
});
t1.start();
t2.start();
}
}`
比如上面的代碼,AB互相鎖住,造成了死鎖,這是我們在進行多線程編程時一定要避免的
下面是書中提到的幾個方法:
1、 ·避免一個線程同時獲取多個鎖。
2、 ·避免一個線程在鎖內同時佔用多個資源,儘量保證每個鎖只佔用一個資源。
3、 ·嘗試使用定時鎖,使用lock.tryLock(timeout)來替代使用內部鎖機制。
4、 ·對於數據庫鎖,加鎖和解鎖必須在一個數據庫連接裏,否則會出現解鎖失敗的情況。
二、鎖
鎖是多線程編程中的重點內容,正所謂自由是相對的,有鎖纔能有自由,因爲併發編程涉及到對同一個共享變量的操作,爲避免造成變量修改混亂所以加鎖是必須的。
1、volatile和synchronized關鍵字:
volatile關鍵字:
volatile關鍵字的主要作用是保證共享變量的可見性,當A線程修改了共享變量c字段,B線程啓動時,B線程能感知到這個共享變量c變化,藉此來進行線程間的通信。
注意:volatile關鍵字並不能保證原子性,因爲它具有的只有可見性,對於i++這種操作相當於是①:先去讀i的值
②:將i+1賦值給i
假設i=1在進行①操作結束時②操作未開始,另外一個線程將i的值修改爲了5,那麼i+1操作依然是1+1(i原先的值),但是另一個線程已經修改i的值爲5了,這個時候正確的值應該是6。
public class VolatileTest {
public static volatile int i = 1;
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new VolatileRun1());
Thread thread2 = new Thread(new VolatileRun2());
thread2.start();
thread1.start();
thread2.join();
System.out.println(i);//2 預期值應該是6
}
static class VolatileRun1 implements Runnable {
@Override
public void run() {
i = 5;
}
}
static class VolatileRun2 implements Runnable {
@Override
public void run() {
//i++;
//模仿i++
int a = i;
SleepUtils.second(2);
i = a + 1;
}
}
}
volatile建立happens-before規則:
public class VolatileExample {
int a = 0;
volatile boolean flag = false;
public void writer() {
a = 1; // 1
flag = true; // 2
}
public void reader() {
if (flag) { // 3
int i = a; // 4
}
}
}
假設A、B兩個線程分別執行writer和reader兩個方法:
1)根據程序次序規則,1 happens-before 2;3 happens-before 4。
2)根據volatile規則,2 happens-before 3。
3)根據happens-before的傳遞性規則,1 happens-before 4。
2、synchronized關鍵字
java併發編程中synchronized一定是元老級的鎖,之前很多人提到synchronized想到的都是重量級鎖,但是1.6版本中加入了偏向鎖和輕量級鎖,降低了之前鎖的獲取和釋放帶來的性能損耗。
偏向鎖: 當一個線程獲取鎖的時候會在同步塊所在的對象頭中存儲一個偏向線程ID,這個線程再次進入的時候只需要簡單的對比一下存儲的線程ID是否是當前線程ID。如果是,則當前線程獲取鎖成功,否則,使用CAS來獲取鎖更新存儲的線程ID。
偏向鎖的撤銷: 偏向鎖的釋放是一種當競爭關係出現時纔會釋放的機制,當其他線程競爭時纔會釋放鎖。釋放時,會檢查持有偏向鎖的線程是否存活,如果不處於存活狀態則修改爲無鎖狀態,如果持有偏向鎖的線程存活則執行完。
輕量級鎖 當線程在進入同步快之前會在自己的幀棧中創建一份鎖記錄的空間來存儲對象頭的鎖信息(複製的),然後CAS操作將對象頭的信息修改爲指向線程鎖記錄的指針,如果CAS成功則當前線程獲取鎖,如果失敗,則說明其他線程競爭當前鎖,則當前線程最少經過一次自旋獲取鎖(就是自己檢查自己有沒有獲取鎖)。
輕量級鎖解鎖 輕量級鎖解鎖時會使用CAS操作將對象頭的鎖信息(指針)替換回之前複製的存儲在自己幀棧中的鎖信息,如果成功,則表示沒有發生競爭,如果失敗則鎖升級爲重量級鎖。
下面是各種鎖的優缺點對比:
總結一下就是:同步快的執行速度快慢決定了優先使用那種鎖
同步快執行很快(競爭不多)–>輕量級鎖
同步快執行很慢 (競爭很多)–>重量級鎖
synchronized關鍵字的使用
不同於volatile關鍵字只能使用在字段上,synchronized關鍵字可以修飾方法和代碼塊當然字段也可以。
1)·對於普通同步方法,鎖是當前實例對象。
2) ·對於靜態同步方法,鎖是當前類的Class對象。
3) ·對於同步方法塊,鎖是Synchonized括號裏配置的對象。
public class SynchonizedTest {
public synchronized void test1() {
for (int i = 0; i < 500; i++)
System.out.println("test1 加了synchronized的普通方法.。。。。。。。" + Thread.currentThread().getName());
}
public synchronized void test01() {
for (int i = 0; i < 500; i++)
System.out.println("test01 加了synchronized的普通方法");
}
public void test2() {
for (int i = 0; i < 500; i++)
System.out.println("test2 不加synchronized的普通方法");
}
public static synchronized void test3() {
for (int i = 0; i < 500; i++)
System.out.println("test3 加了synchronized的靜態方法 ");
}
public static synchronized void test4() {
for (int i = 0; i < 500; i++)
System.out.println("test4 加了synchronized的靜態方法 ");
}
public void test5() {
System.out.println("鎖當前實例對象同步代碼塊執行開始");
synchronized (this) {
for (int i = 0; i < 500; i++)
System.out.println("test5 當前對象實例鎖");
}
}
public static Integer i = 1;
public void test6() {
System.out.println("鎖靜態變量同步代碼塊開始執行");
synchronized (i) {
for (int i = 0; i < 500; i++)
System.out.println("test6 鎖靜態變量" + Thread.currentThread().getName());
}
}
public Integer j = 1;
public void test7() {
System.out.println("鎖普通變量同步代碼塊開始執行");
synchronized (j) {
for (int i = 0; i < 500; i++)
System.out.println("test6 鎖普通變量");
}
}
public static void t1() {
//1、測試同步實例方法 test1 加了synchronized的普通方法 test2 不加synchronized的普通方法
SynchonizedTest test1 = new SynchonizedTest();
new Thread(new Runnable() {
@Override
public void run() {
test1.test1();
System.out.println();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
test1.test2();
}
}).start();
//輸出:test1和test2交替輸出
//結論:同一個對象中加了synchronized的普通方法並不影響不加的普通方法
//test1和test2互不影響
}
public static void t2() {
SynchonizedTest test1 = new SynchonizedTest();
new Thread(new Runnable() {
@Override
public void run() {
test1.test1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
test1.test5();
// test5 當前對象實例鎖
}
}).start();
//輸出依次輸出test1和test5
//結論:鎖當前對象和普通方法上鎖 鎖的對象都是當前實例
}
public static void t3() {
SynchonizedTest test1 = new SynchonizedTest();
new Thread(new Runnable() {
@Override
public void run() {
test1.test1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
test1.test01();
}
}).start();
//輸出:test1和test01依次輸出
//結論:對於兩個不同的普通方法加了synchronized 鎖的對象是當前實例
}
public static void t4() {
SynchonizedTest test1 = new SynchonizedTest();
new Thread(new Runnable() {
@Override
public void run() {
test1.test1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
test1.test7();
}
}).start();
//輸出test1和test6交替輸出
//結論:普通方法加鎖鎖的是當前對象 普通成員變量加鎖鎖的是成員變量synchronized修飾的和實例對象無關
}
public static void t5() {
SynchonizedTest test1 = new SynchonizedTest();
new Thread(new Runnable() {
@Override
public void run() {
test1.test1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
test1.test4();
}
}).start();
//輸出test1和test4交替輸出
//結論:靜態變量加鎖和同步方法加鎖不會影響,兩個是獨立的
}
public static void t6() {
SynchonizedTest test1 = new SynchonizedTest();
new Thread(new Runnable() {
@Override
public void run() {
test1.test6();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
test1.test4();
}
}).start();
//輸出:test4和test6交替輸出
//靜態方法加鎖和靜態變量加鎖兩個互不影響,靜態方法加鎖鎖的是Class對象,靜態變量加鎖鎖的是synchronized修飾的變量
}
public static void t7() {
SynchonizedTest test1 = new SynchonizedTest();
SynchonizedTest test2 = new SynchonizedTest();
new Thread(new Runnable() {
@Override
public void run() {
test1.test6();
}
}, "test1").start();
new Thread(new Runnable() {
@Override
public void run() {
test2.test6();
}
}, "test2....").start();
//輸出:依次輸出test1線程和test2線程
//靜態方法加鎖鎖的是Class對象,不同對象實例依然屬於同一個Calss對象
}
public static void t8() {
SynchonizedTest test1 = new SynchonizedTest();
SynchonizedTest test2 = new SynchonizedTest();
new Thread(new Runnable() {
@Override
public void run() {
test1.test1();
}
}, "test1").start();
new Thread(new Runnable() {
@Override
public void run() {
test2.test1();
}
}, "test2....").start();
//輸出:交替輸出test1線程和test2線程
//普通方法鎖的是當前對象實例,當新創建兩個對象實例時兩個並不影響
}
以上代碼建議拷貝到自己idea中跑一跑,總結一點對於synchronized關鍵字的使用就是儘量使用同步代碼塊的方式。
volatile關鍵字和synchronized關鍵字對比
1、 volatile關鍵字是線程同步的輕量級實現,所以volatile性能肯定要比synchronized關鍵字要好。但是volatile關鍵字只能用於變量而synchronized可以修飾方法以及代碼塊。synchronized關鍵字在Java1.6之後進行了主要包括爲了減少獲得鎖和釋放鎖帶來的性能消耗而引起的偏向鎖和輕量級鎖以及其他各種優化之後執行效率由了顯著提升,實際開發中使用synchronized關鍵字的場景還是更多一些。
2、多線程訪問volatile關鍵字不會發生阻塞,而synchronized關鍵字可能會發生阻塞。
3、volatile可以保證數據的可見性,但不能保證數據的原子性,synchronized都能保證。
4、volatile關鍵字主要用於解決變量在多個線程之間的可見性,而synchronized關鍵字解決的是多個線程之間訪問資源的同步性。
缺點:這兩個關鍵字並不是使用得越多越好,多線程併發編程一定是比單線程慢的,當過多的使用volatile關鍵字時會引起頻繁的刷新共享內存造成性能下降,同樣的synchronized關鍵字會引起阻塞同樣會降低性能,並且阻塞不可被中斷、隱式的釋放鎖(不受我們控制)。所以兩個關鍵字的使用場景一定是在保證線程安全的情況下使用,單線程環境當然不需要。
線程優先級
在Java線程中,通過一個整型成員變量priority來控制優先級,優先級的範圍從1~10,在線 程構建的時候可以通過setPriority(int)方法來修改優先級,默認優先級是5,優先級高的線程分配時間片的數量要多於優先級低的線程。設置線程優先級時,針對頻繁阻塞(休眠或者I/O操 作)的線程需要設置較高優先級,而偏重計算(需要較多CPU時間或者偏運算)的線程則設置較 低的優先級,確保處理器不會被獨佔。
注意:很多操作系統並不會響應設置的優先級,針對不同的線程類型(cpu密集、io密集的)最好設置不同的線程池(設置不同的線程數量)進行處理。
Thread.join()的使用
如果一個線程A執行了thread.join()語句,其含義是:當前線程A等待thread線程終止之後才 從thread.join()返回。線程Thread除了提供join()方法之外,還提供了join(long millis)和join(long millis,int nanos)兩個具備超時特性的方法。這兩個超時方法表示,如果線程thread在給定的超時 時間裏沒有終止,那麼將會從該超時方法中返回。
線程間的通信
/**
* **經典範式
* synchronized(對象) {
* while(條件不滿足) {
* 對象.wait();
* }對應的處理邏輯 }
*
* synchronized(對象) {
* 改變條件
* 對象.notifyAll();
* }**
*/
public class WaitNotify {
private static Object lock = new Object();
private static Boolean flag = true;
public static void main(String[] args) {
Thread waitThread = new Thread(new WaitRuner(), "wait");
waitThread.start();
Thread notifyThread = new Thread(new NotifyRuner(), "notify");
notifyThread.start();
}
static class WaitRuner implements Runnable {
@Override
public void run() {
synchronized (lock) {
while (flag) {
try {
System.out.println(System.currentTimeMillis() + Thread.currentThread().getName());
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("第二次喚醒");
}
}
}
static class NotifyRuner implements Runnable {
@Override
public void run() {
synchronized (lock) {
System.out.println(System.currentTimeMillis() + Thread.currentThread().getName());
lock.notify();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = false;
}
}
}
}
等待/通知機制,是指一個線程A調用了對象O的wait()方法進入等待狀態,而另一個線程B 調用了對象O的notify()或者notifyAll()方法,線程A收到通知後從對象O的wait()方法返回,進而 執行後續操作。上述兩個線程通過對象O來完成交互,而對象上的wait()和notify/notifyAll()的 關係就如同開關信號一樣,用來完成等待方和通知方之間的交互工作。
通知等待經典範式:
- synchronized(對象) {
- while(條件不滿足) {
-
對象.wait(); }
- 對應的處理邏輯
- }
synchronized(對象) {
改變條件 -
對象.notifyAll();
- }
安全地終止線程
public class Shutdown {
public static void main(String[] args) throws InterruptedException {
//創建兩個線程
Runer one = new Runer();
Thread oneThred = new Thread(one, "one");
oneThred.start();
SleepUtils.second(1);
oneThred.interrupt();
Runer two = new Runer();
Thread twoThred = new Thread(two, "two");
twoThred.start();
SleepUtils.second(1);
two.chanl();
}
static class Runer implements Runnable {
private long i;
private volatile boolean on = true;
@Override
public void run() {
while (on && !Thread.currentThread().isInterrupted()) {
i++;
}
System.out.println(i + Thread.currentThread().getName());
}
public void chanl() {
on = false;
}
}
}
示例在執行過程中,main線程通過中斷操作(interrupt)和cancel()方法均可使CountThread得以終止。 這種通過標識位或者中斷操作的方式能夠使線程在終止時有機會去清理資源,而不是武斷地 將線程停止,因此這種終止線程的做法顯得更加安全和優雅。