java如何写线程外部类_廖雪峰Java读书笔记(六)--多线程(或称并发)
1. 多線程基礎(chǔ)
首先要明白一些概念:
進(jìn)程:把一個(gè)任務(wù)稱為一個(gè)進(jìn)程,瀏覽器就是一個(gè)進(jìn)程,視頻播放器是另一個(gè)進(jìn)程,類似的,音樂播放器和Word都是進(jìn)程。
線程:某些進(jìn)程內(nèi)部還需要同時(shí)執(zhí)行多個(gè)子任務(wù)。例如,我們在使用Word時(shí),Word可以讓我們一邊打字,一邊進(jìn)行拼寫檢查,同時(shí)還可以在后臺(tái)進(jìn)行打印,我們把子任務(wù)稱為線程。注:操作系統(tǒng)調(diào)度的最小單位是線程。
進(jìn)程和線程的關(guān)系就是:一個(gè)進(jìn)程可以包含一個(gè)或多個(gè)線程,但至少會(huì)有一個(gè)線程。每個(gè)進(jìn)程都有自己完整的變量,但一個(gè)進(jìn)程內(nèi)的線程共享數(shù)據(jù)。(The essential difference is that while each process has a complete set of its own variables, threads share the same data.)
Java用一個(gè)主線程來執(zhí)行main()方法,在main()方法內(nèi)部我們又可以啟動(dòng)多個(gè)線程。
2. 創(chuàng)建新線程
要?jiǎng)?chuàng)建一個(gè)新線程非常容易,我們需要實(shí)例化一個(gè)Thread實(shí)例,然后調(diào)用它的start()方法即可。
package MultiThr;
public class CreateThread {
public static void main(String[] args) {
Thread t = new Thread();
t.start(); // 啟動(dòng)新線程
}
}
但這個(gè)線程什么都沒有做。如果要?jiǎng)?chuàng)建一個(gè)做事的線程,應(yīng)該這樣做。
方法一:
來自于《Java核心技術(shù)》,其實(shí)是廖雪峰的方法二,但這個(gè)用到了lambda表達(dá)式。
實(shí)現(xiàn)Runnable接口,并實(shí)現(xiàn)其中的run方法。Runnable接口的源代碼如下 :
public interface Runnable{
void run();
}
這是個(gè)函數(shù)式接口,所以可以用lambda表達(dá)式來實(shí)現(xiàn)之:
Runnable r = () -> { /*在此處寫下要執(zhí)行的代碼*/ };
構(gòu)建一個(gè)Thread對(duì)象,并傳入剛才實(shí)現(xiàn)的Runnable接口。
Thread t = new Thread(r);
調(diào)用start()方法:
t.start();
注:在Runnable的實(shí)現(xiàn)中要catch (InterruptedException e)。
// 廖雪峰給出的例子
public class Main{
public static void main(String[] args){
Thread t = new Thread(new MyRunnable());
t.start();
}
class MyRunnable implements Runnable{
@Override
public void run(){
System.out.println("start new thread");
}
}
}
如果用《Java核心技術(shù)》的方法改寫上述代碼,是:
public class Main{
public static void main(String[] args){
Thread t = new Thread(new Runnable() ->{ // 這里用到了lambda表達(dá)式方法
try{
System.out.println("start new thread");
} catch (InterruptedException e){ }
}
);
t.start();
}
}
方法二:
廖雪峰的方法一,《Java核心技術(shù)》中的方法二。
操作:從Thread繼承一個(gè)類出來,并@Override其中的run()方法。
示例:
// 將上面的例子用此方法改寫如下:
public class Main{
public static void main(String[] args){
Thread t = new MyThread();
t.start();
}
}
class MyThread extends Thread{
@Override
public void run(){
/*task codes*/
}
}
小結(jié):
Thread(Runnable target):實(shí)例化新對(duì)象Thread并調(diào)用其中的run()方法。具體如何調(diào)用,看上面的博客文章;
void start():開始線程;
void run():一定要復(fù)寫之。可以開一個(gè)新類extends Thread,也可以實(shí)現(xiàn)public inteface Runnable接口;
static void sleep(long millis):線程睡眠一定時(shí)間
3. 線程狀態(tài)
線程有六態(tài),分別是:
New:新創(chuàng)建的線程,尚未執(zhí)行。
Runnable:運(yùn)行中的線程,正在執(zhí)行run()方法的Java代碼。Runnable的狀態(tài)是可執(zhí)行可不執(zhí)行,只要開始執(zhí)行沒結(jié)束,不管是在執(zhí)行中還是在休息,都是Runnable。
Blocked:運(yùn)行中的線程,因?yàn)槟承┎僮鞅蛔枞鴴炱?#xff1b;
Waiting:運(yùn)行中的線程,因?yàn)槟承┎僮髟诘却小:蜕厦娴腂locked狀態(tài)區(qū)別不大。
Timed Waiting:運(yùn)行中的線程,因?yàn)閳?zhí)行sleep()方法正在計(jì)時(shí)等待;
Terminated:線程已終止,因?yàn)閞un()方法執(zhí)行完畢。
當(dāng)線程啟動(dòng)后,它可以在Runnable、Blocked、Waiting和Timed Waiting這幾個(gè)狀態(tài)之間切換,直到最后變成Terminated狀態(tài),線程終止。
注:New和Terminated是兩頭,其他的是中間。
線程終止的原因有:
線程正常終止:run()方法執(zhí)行到return語句返回;
線程意外終止:run()方法因?yàn)槲床东@的異常導(dǎo)致線程終止;
對(duì)某個(gè)線程的Thread實(shí)例調(diào)用stop()方法強(qiáng)制終止(強(qiáng)烈不推薦使用)。
注:
沒捕獲到的異常也可以終止線程
別用stop()方法!
使用join()方法會(huì)使線程暫停,等其他線程結(jié)束了再來。也就是“讓道”。
4. 線程屬性
廖雪峰部分講到了兩個(gè):
中斷線程(interrupted status)
守護(hù)線程(daemon threads)
在《Java核心技術(shù)》中還有一個(gè),暫時(shí)不知如何翻譯,因?yàn)槲易x的是英文版:
handlers for uncaught exceptions
4.1 中斷線程
有兩種情況會(huì)讓線程中斷:
進(jìn)入return狀態(tài)
沒能捕捉異常
以下方法可以檢查線程是否設(shè)置了中斷狀態(tài),首先我們可以調(diào)用Thread.currentThread()方法來獲取當(dāng)前線程,然后調(diào)用isInterrupted()檢查是否設(shè)置了interrupted()狀態(tài)。但如果線程被鎖定便不能檢查中斷狀態(tài)了。這就是InterruptedException的來源。
在catch到InterruptedException之后,可以檢查一下Thread.currentThread().interrupt();也可以在方法之前就預(yù)先拋出Exception,像這樣:
public void run() throws InterruptedException
4.2 守護(hù)線程
Java程序入口就是由JVM啟動(dòng)main線程,main線程又可以啟動(dòng)其他線程。當(dāng)所有線程都運(yùn)行結(jié)束時(shí),JVM退出,進(jìn)程結(jié)束。如果有一個(gè)線程沒有退出,JVM進(jìn)程就不會(huì)退出。所以,必須保證所有線程都能及時(shí)結(jié)束。
注:也就是說,一切方法都要在main()方法中執(zhí)行。
守護(hù)線程是指為其他線程服務(wù)的線程。在JVM中,所有非守護(hù)線程都執(zhí)行完畢后,無論有沒有守護(hù)線程,虛擬機(jī)都會(huì)自動(dòng)退出。
注:守護(hù)線程除了服務(wù)其他線程以外沒有其他的作用。(《Java核心技術(shù)》)原文:A daemon is simply a thread that has no other role in life than to serve others.
設(shè)置守護(hù)線程的方法:
t.setDaemon(true)
4.3 為線程取名
可以給線程命名:
Thread t = new Thread(runnable);
t.setName("abc"); # 使用setName("name")方法
4.4 解決未捕獲的異常問題
線程也可以被未捕獲的異常終止,然后線程死亡。這時(shí)候就需要處理未捕獲的異常。可以實(shí)現(xiàn)Thread.UncaughtExceptionHandler接口來處理。如下:
void unchangedException(Thread t, Throwable e)
可以以線程中設(shè)置一個(gè)setUncaughtExceptionHandler方法,也可以設(shè)置靜態(tài)方法setDefaultUncaughtExceptionHandler。
ThreadGroup對(duì)象暫時(shí)不是很懂,在《Core Java》P747-748中。就暫時(shí)先翻譯一下:
ThreadGroup類實(shí)現(xiàn)了Thread.UncaughtExceptionHandler接口。其中的uncaughtException方法進(jìn)行以下操作:
如果線程組有父線程,那么會(huì)調(diào)用uncaughtException()方法;
否則,如果Thread.getDefaultUncaughtExceptionHandler方法返回一個(gè)非null的解決方案,便會(huì)調(diào)用uncaughtException方法;
如果Throwable是ThreadDeath的實(shí)例,那么什么也不會(huì)發(fā)生。
線程的名字及Throwable堆棧追蹤會(huì)被輸出 。
5. 線程同步(Synchronization)
前面說過,同一進(jìn)程內(nèi)的多個(gè)線程共享數(shù)據(jù),這樣會(huì)導(dǎo)致競爭情況(race condition)。也就是,當(dāng)多個(gè)線程同時(shí)運(yùn)行時(shí), 線程并不是非常有禮貌地排隊(duì)運(yùn)行,如果不加干預(yù),就是你運(yùn)行一會(huì)它運(yùn)行一會(huì)。如果是兩個(gè)線程同時(shí)運(yùn)行,并不是兩個(gè)各運(yùn)行相等的時(shí)間。要注意。
廖老師的例子:
// https://www.liaoxuefeng.com/wiki/1252599548343744/1306580844806178
public class Main {
public static void main(String[] args) throws Exception {
var add = new AddThread(); // 線程一
var dec = new DecThread(); // 線程二
// 兩個(gè)線程并不是“先來后到”式運(yùn)行的
add.start();
dec.start();
add.join();
dec.join();
System.out.println(Counter.count); // 最后的結(jié)果不一定是0
}
}
class Counter {
public static int count = 0;
}
class AddThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) { Counter.count += 1; }
}
}
class DecThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) { Counter.count -= 1; }
}
}
很可能的運(yùn)行模式如下圖:
從廖老師網(wǎng)站上截圖下來
那么,如果要讓線程之間可以禮讓地運(yùn)行,遵循“先來后到”的順序,怎么辦?就像這樣:
從廖老師網(wǎng)站截圖下來
從圖中可以看出,為保證代碼可以“先來后到”地運(yùn)行,需要通過lock(加鎖)與unlock(解鎖)操作實(shí)現(xiàn)。通過加鎖與解鎖的操作就可以保證一個(gè)線程執(zhí)行期間不會(huì)有其他的線程進(jìn)入此指令區(qū)間。即使在執(zhí)行期線程被操作系統(tǒng)中斷執(zhí)行,其他線程也會(huì)因?yàn)闊o法獲得鎖導(dǎo)致無法進(jìn)入此指令區(qū)間。只有執(zhí)行線程將鎖釋放后,其他線程才有機(jī)會(huì)獲得鎖并執(zhí)行。在專業(yè)術(shù)語中,此操作叫做代碼的原子性(atomic)。
有鎖與無鎖的區(qū)別(圖片來自《Java核心技術(shù)》)
有兩種方法可以實(shí)現(xiàn):ReetrantLock類型與synchronized關(guān)鍵字。《Java核心技術(shù)》先講的是前者,我看書有點(diǎn)懵逼,但廖老師先講的是后者,相對(duì)比較明白一些。
由上面可以得知,保證一段代碼的原子性,可以通過加鎖與解鎖的操作來實(shí)現(xiàn)。不過,在《Java核心技術(shù)》中作者也承認(rèn):
The Lock and Condition interfaces give programmers a high degree of control over locking. However, in most situations, you don't need that control -- you can use a mechanism that is built into the Java language.
其實(shí),“a mechanism that is built into the Java language”就是我們要說的synchronized關(guān)鍵字,在術(shù)語中稱為intrinsicLock。
兩種方法使用:
synchronized(lock)
public synchronized void method()
第一種使用方式表示用lock實(shí)例作為鎖,兩個(gè)線程在執(zhí)行各自的synchronized(Counter.lock) { ... }代碼塊時(shí),必須先獲得鎖,才能進(jìn)入代碼塊進(jìn)行。執(zhí)行結(jié)束后,在synchronized語句塊結(jié)束會(huì)自動(dòng)釋放鎖。第二種方法也不用寫unlock。 但是,它的缺點(diǎn)是帶來了性能下降。因?yàn)閟ynchronized代碼塊無法并發(fā)執(zhí)行。此外,加鎖和解鎖需要消耗一定的時(shí)間,所以,synchronized會(huì)降低程序的執(zhí)行效率。(廖雪峰語)
如何使用synchronized關(guān)鍵字鎖定對(duì)象呢?
找出修改共享變量的線程代碼塊;
選擇一個(gè)共享實(shí)例作為鎖;
使用synchronized(lockObject) { ... }。
注意:
如果要兩個(gè)線程對(duì)同一個(gè)對(duì)象先來后到地操作,那么兩個(gè)線程應(yīng)當(dāng)鎖住同一個(gè)對(duì)象;使用synchronized的時(shí)候,獲取到的是哪個(gè)鎖非常重要。鎖對(duì)象如果不對(duì),代碼邏輯就不對(duì)。
對(duì)幾個(gè)變量要進(jìn)行鎖操作,就設(shè)幾個(gè)鎖。
JVM規(guī)范定義了幾種原子操作。不需要同步:
基本類型(long和double除外)賦值,例如:int n = m;
引用類型賦值,例如:List list = anotherList
如果一個(gè)類被設(shè)計(jì)為允許多線程正確訪問,我們就說這個(gè)類就是“線程安全”的(thread-safe)。Java標(biāo)準(zhǔn)庫的java.lang.StringBuffer也是線程安全的。
如果是第二種方法,可以把整個(gè)方法變?yōu)橥酱a塊,鎖住的對(duì)象是this。
如果鎖住的是static方法,那么鎖住的是class.Class對(duì)象本身。
5.1 死鎖
首先我們要明白,Java線程鎖是可重入的鎖(廖雪峰語)。對(duì)同一個(gè)線程,鎖可以重復(fù)獲得,即JVM允許線程重復(fù)地獲取同一個(gè)鎖,這就是可重入鎖。例如:
public class Counter {
private int count = 0;
public synchronized void add(int n) { // add方法會(huì)獲取一個(gè)鎖
if (n < 0) {
dec(-n); // 同時(shí)dec方法也會(huì)獲得鎖
// JVM同時(shí)允許add()與dec()獲得鎖
} else {
count += n;
}
}
public synchronized void dec(int n) {
count += n;
}
}
那么死鎖是怎么發(fā)生的呢?通俗地說,一個(gè)已經(jīng)獲取鎖的對(duì)象還要再獲取另一個(gè)鎖,但另一個(gè)鎖已經(jīng)被其他對(duì)象把持,死鎖就發(fā)生了。(我自己說的)例如:(廖雪峰老師的例子)
public void add(int m) {
synchronized(lockA) { // 獲得lockA的鎖
this.value += m;
synchronized(lockB) { // 獲得lockB的鎖
this.another += m;
} // 釋放lockB的鎖
} // 釋放lockA的鎖
}
public void dec(int m) {
synchronized(lockB) { // 獲得lockB的鎖
this.another -= m;
synchronized(lockA) { // 獲得lockA的鎖
this.value -= m;
} // 釋放lockA的鎖
} // 釋放lockB的鎖
}
(用廖老師的方法)
分析如上例子:線程1和線程2分別執(zhí)行add()與dec()時(shí):
線程1:進(jìn)入add()方法,獲得lockA;
線程2:進(jìn)入dec()方法,獲得lockB;
然后順序執(zhí)行:
線程1:獲取lockB失敗,因?yàn)橐呀?jīng)被dec()獲取
線程2:獲取lockA失敗,因?yàn)橐呀?jīng)被add()獲取
于是死鎖(Deadlocks)就發(fā)生了。死鎖一旦形成就只能強(qiáng)制結(jié)束進(jìn)程。避免死鎖的方法是嚴(yán)格按照線程獲取鎖的順序來寫!
一旦死鎖發(fā)生,那么可以按Ctrl + \來查看所有線程。每個(gè)線程都有追蹤,告訴你在哪里鎖住了。
5.2 使用wait與notify
Java中synchronized解決了多線程競爭的問題,但并不解決多線程協(xié)調(diào)的問題。先看一個(gè)例子:
package MultiThr;
import java.util.*;
public class TaskQueue {
Queue queue = new LinkedList<>();
public synchronized void addTask(String s){
this.queue.add(s);
}
public synchronized String getTask(){
while (queue.isEmpty()){} // 實(shí)際上while循環(huán)不會(huì)停下來
return queue.remove();
}
}
理論上,如果任務(wù)隊(duì)列為空,就等待,直到線程里有一個(gè)任務(wù)就退出。但事實(shí)上不是這樣的:實(shí)際運(yùn)行中,因?yàn)榫€程在執(zhí)行while()循環(huán)時(shí),已經(jīng)在getTask()入口處獲得了this鎖,導(dǎo)致其他線程無法調(diào)用getTask()方法。最后的結(jié)果是getTask()陷入死循環(huán)。如何修改呢?
package MultiThr;
import java.util.*;
public class TaskQueue {
Queue queue = new LinkedList<>();
public synchronized void addTask(String s){
this.queue.add(s);
}
public synchronized String getTask() throws InterruptedException { // 拋出異常必須
while (queue.isEmpty()){
// 釋放this鎖
this.wait();
}
return queue.remove();
}
}
當(dāng)一個(gè)線程在this.wait()等待時(shí),它就會(huì)釋放this鎖,從而使得其他線程能夠在addTask()方法上獲得this鎖。當(dāng)wait()方法調(diào)用時(shí)會(huì)釋放線程鎖,返回后又重新獲得鎖。
當(dāng)我們用wait()方法讓線程進(jìn)入等待狀態(tài)后,有什么方法可以重新喚起線程嗎?notify()方法,可以喚醒一個(gè)正在this鎖等待的線程。方法如下:
public synchronized void addTask(String s) {
this.queue.add(s);
this.notify(); // 喚醒在this鎖等待的線程
}
5.3 再談ReentrantLock
由于《Java核心技術(shù)》一上來就講到了ReentrantLock,我有點(diǎn)不懂。現(xiàn)在學(xué)完之后再回來看ReentrantLock,似乎可以懂了。
首先,ReentrantLock在java.uitl.concurrent.locks,屬于并發(fā)編程。
上面的Counter類例子:
public class Counter {
private int count;
public void add(int n) {
synchronized(this) {
count += n;
}
}
}
用ReentrantLock改造一下是(廖老師例子):
import java.util.concurrent.locks.*;
public class Counter{
private int count;
private final Lock lock = new ReentrantLock(); // 在方法外圍定義一個(gè)鎖
public void add(int n){
lock.lock(); // 方法最一開始就加鎖
try{ // 要用try語句
count += n;
} finally{ lock.unlock(); } // 一定要釋放鎖
}
}
注1:(《Java核心技術(shù)》語) It is critically important that the unlock operation is enclosed in a finally clause. (譯:在finally語句中使用unlock語句釋放鎖)
注2:(《Java核心技術(shù)》語) You cannot use the try-with-resources statement. (譯:在ReentrantLock中,不能使用try帶括號(hào))
ReentrantLock也是可重入鎖,也就是說一個(gè)線程可以多次獲取同一個(gè)鎖,得鎖者運(yùn)行。
前面已經(jīng)說到用wait()方法和notifyAll()方法實(shí)現(xiàn)多線程協(xié)調(diào)。那么在ReentrantLock中有何方法?Condition類中提供的await()、signal()、signalAll()可以實(shí)現(xiàn)同樣的功能。
手?jǐn)]一遍廖老師的代碼:
import java.util.concurrent.locks.*;
public class TaskQueue{
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private Queue queue = new LinkedList<>();
public void addTask(String S){
lock.lock();
try{
queue.add(s);
condition.signalAll(); // notifyAll()
}finally{
lock.unlock();
}
}
public void getTask(){
lock.lock();
try{
while (queue.isEmpty()){
condition.await();
}finally{ lock.unlock(); }
return queue.remove();
}
}
}
5.4 讀寫鎖
不論是synchronized()方法也好,還是ReentrantLock也罷,都只能允許一個(gè)線程進(jìn)行讀寫,如果不寫入的話,其他線程讀取都困難,因?yàn)闆]有獲取鎖。但我們想要的效果是允許多個(gè)線程同時(shí)讀,但只要有一個(gè)線程在寫,其他線程就必須等待。換句話說,如果沒人寫,其他的都可以讀;如果寫了,其他的就不可讀了。這個(gè)問題可以用ReadWriteLock解決。
使用ReadWriteLock時(shí),適用條件是同一個(gè)數(shù)據(jù),有大量線程讀取,但僅有少數(shù)線程修改。比如論壇的帖子。
但它有一個(gè)問題:既某個(gè)線程要寫的時(shí)候,需要釋放讀鎖才能寫。于是悲觀鎖發(fā)生了。
示例:
package MultiThr;
import java.util.concurrent.locks.*;
import java.util.*;
public class CounterRW {
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock rLock = rwLock.readLock();
private final Lock wLock = rwLock.writeLock();
private int[] counts = new int[10];
// 把讀寫操作分別用讀鎖和寫鎖來加鎖,在讀取時(shí),多個(gè)線程可以同時(shí)獲得讀鎖,這樣就大大提高了并發(fā)讀的執(zhí)行效率。
public void inc(int index){
wLock.lock(); // 寫鎖
try{
counts[index] += 1;
}finally{
wLock.unlock();
}
}
public int[] get(){
rLock.lock(); // 讀鎖
try{
return Arrays.copyOf(counts, counts.length);
}finally{ rLock.unlock(); }
}
}
5.5 悲觀鎖與樂觀鎖
先來看廖雪峰老師的簡版定義:
悲觀鎖:如果有線程正在讀,寫線程需要等待讀線程釋放鎖后才能獲取寫鎖,即讀的過程中不允許寫,這是一種悲觀的讀鎖。
樂觀鎖:讀的過程中大概率不會(huì)有寫入,因此被稱為樂觀鎖。
再來看看技術(shù)博客中如何定義:
悲觀鎖:總是假設(shè)最壞的情況,每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人會(huì)修改,所以每次在拿數(shù)據(jù)的時(shí)候都會(huì)上鎖,這樣別人想拿這個(gè)數(shù)據(jù)就會(huì)阻塞直到它拿到鎖。synchronized與ReentrantLock還有ReadWriteLock都屬于悲觀鎖范圍。相對(duì)更安全,但效率不高。
樂觀鎖:顧名思義,就是很樂觀,每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人不會(huì)修改,所以不會(huì)上鎖,但是在更新的時(shí)候會(huì)判斷一下在此期間別人有沒有去更新這個(gè)數(shù)據(jù),可以使用版本號(hào)等機(jī)制。
5.6 StampedLock
這是Java 8開始引進(jìn)的一種讀寫鎖,讀的過程也允許獲取寫鎖后寫入。但需要一些代碼判斷是否有寫入。例:
package MultiThr;
import java.util.*;
import java.util.concurrent.locks.*;
public class Point {
public final StampedLock stampedLock = new StampedLock();
private double x;
private double y;
public void move(double deltaX, double deltaY){
long stamp = stampedLock.writeLock(); // 獲取寫鎖
try{
x += deltaX;
y += deltaY;
}finally{
stampedLock.unlockWrite(stamp); // 釋放寫鎖
}
}
public double distanceFromOrigin(){
long stamp = stampedLock.tryOptimisticRead();
double currentX = x;
double currentY = y;
if (!stampedLock.validate(stamp)){ // 檢查是否有其他寫鎖發(fā)生
stampedLock.readLock(); // 這是個(gè)悲觀鎖
try{
currentX = x;
currentY = y;
} finally{
stampedLock.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY + currentY);
}
}
5.7 Concurrent集合與Atomic操作以及volatile關(guān)鍵字
首先應(yīng)明白何為“線程安全”。
線程安全就是多線程訪問時(shí),采用了加鎖機(jī)制,當(dāng)一個(gè)線程訪問該類的某個(gè)數(shù)據(jù)時(shí),進(jìn)行保護(hù),其他線程不能進(jìn)行訪問直到該線程讀取完,其他線程才可使用。
引用地址
Concurrent集合:由于默認(rèn)類并非線程安全,所以調(diào)用時(shí)為了實(shí)現(xiàn)線程安全可以加鎖。但Java的并發(fā)機(jī)制已經(jīng)為我們寫好了,即Concurrent集合,線程安全類。對(duì)照表如下:
interface
non-thread-safe
thread-safe
List
ArrayList
CopyOnWriteArrayList
Map
HashMap
ConcurrentHashMap
Set
HashSet / TreeSet
CopyOnWriteArraySet
Queue
ArrayDeque / LinkedList
ArrayBlockingQueue / LinkedBlockingQueue
Deque
ArrayDeque / LinkedList
LinkedBlockingQueue
volatile關(guān)鍵字:不加鎖也可以實(shí)現(xiàn)同步的一種機(jī)制,使得field可以被同其他線程同步。
例:
private volatile boolean done;
public boolean isDone(){ return done; }
public void setDone(){ done = true; }
6. 線程池
定義:能接受大量小任務(wù)并進(jìn)行分發(fā)處理,使用ExecutorSerivce接口來表示。
ExecutorService有三個(gè)常用實(shí)現(xiàn):
FixedThreadPool:固定大小的線程池
CachedThreadPool:根據(jù)任務(wù)動(dòng)態(tài)調(diào)整的線程池
SingleThreadExecutor:僅單線執(zhí)行的線程池
ScheduledThreadPool:定期反復(fù)執(zhí)行的線程池
示例:
package MultiThr;
import java.util.concurrent.*;
public class ThrPool {
// 創(chuàng)建一個(gè)固定大小的線程池
ExecutorService es = Executors.newFixedThreadPool(4);
for(int i = 0; i <= 5; i++){
es.submit(new Task("" + i));
}
// 關(guān)閉線程池
es.shutdown(); // 使用shutdown關(guān)閉線程池的時(shí)候,線程池會(huì)等待當(dāng)前任務(wù)完成
// shutdownNow(): 立即停止當(dāng)前正在執(zhí)行的任務(wù)
// awaitTermination()則會(huì)等待指定的時(shí)間讓線程池關(guān)閉
}
class Task implements Runnable{
private final String name;
public Task(String name){
this.name = name;
}
@Override
public void run(){
System.out.println("start task" + name);
try{
Thread.sleep(1000);
}catch(InterruptedException e){}
System.out.println("end task" + name);
}
}
6.1 動(dòng)態(tài)調(diào)整的線程池
在簡介中說過CachedThreadPool可以實(shí)現(xiàn)這一功能。扒一下CachedThreadPool的源碼:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
所以可以這樣寫:
int min = 4 ;
int max = 10;
ExecutorService es = new ThreadPoolExecutor(min, max, 60L, TimeUnit.SECONDS, new SynchronousQueue());
6.2 定期反復(fù)執(zhí)行的線程池
例如:每秒刷新證券價(jià)格的任務(wù)就可以通過這種線程池來執(zhí)行。依然要通過Executors類來創(chuàng)建。
ScheduledExecutorService ses = Executors.newScheduledThreadPool(4);
一次性任務(wù):
ses.schedule(new Task("one-time"), 1, TimeUnit.SECONDS);
以固定每3秒執(zhí)行一次。FixedRate是指任務(wù)總是以固定時(shí)間間隔觸發(fā),不管任務(wù)執(zhí)行多長時(shí)間:
// 2秒后開始執(zhí)行定時(shí)任務(wù),每3秒執(zhí)行一次
ses.scheduleAtFixedRate(new Task("fixed-rate"), 2, 3, TimeUnit.SECONDS);
每次任務(wù)執(zhí)行間隔3秒。FixedDelay是指,上一次任務(wù)執(zhí)行完畢后,等待固定的時(shí)間間隔,再執(zhí)行下一次任務(wù):
// 2秒后開始執(zhí)行定時(shí)任務(wù),每個(gè)任務(wù)之間間隔3秒
ses.scheduleWithFixedDelay(new Task("fixed-delay"), 2, 3, TimeUnit.SECONDS);
7. Runnable接口
在前作說過創(chuàng)建新任務(wù)可以實(shí)現(xiàn)Runnable接口。但這個(gè)接口有一個(gè)問題:是一個(gè)void方法,并無返回值,所以執(zhí)行一些有返回值的任務(wù)時(shí)候就有所不便。
解決這個(gè)問題的方法是使用Callable接口。與Runnable相比多一個(gè)返回值。示例:
class Task implements Callable{
public String call() throws Exception{
return longTimeCalculation();
}
}
從以上代碼可以看出,Callable是一個(gè)泛型接口,<>當(dāng)中標(biāo)注要返回的類型,實(shí)現(xiàn)call方法可以返回指定的結(jié)果。
8. Future類型
一個(gè)Future類型的實(shí)例代表一個(gè)未來能獲取結(jié)果的對(duì)象,可以獲取異步執(zhí)行的結(jié)果。ExecutorService.submit()方法可以返回一個(gè)Future()類型。
ExecutorService es = Executors.newFixedThreadPool(4);
// 定義任務(wù)
Callable task = new Task();
// 提交任務(wù)并獲得Future
Future f = es.submit(task);
// 從Future返回異步執(zhí)行的結(jié)果
String s = f.get();
Future的方法有:
get() :獲取結(jié)果
get(long timeout, TimeUnit unit):獲取結(jié)果,但只等待指定的時(shí)間
cancel(boolean mayInterruptIfRunning): 取消當(dāng)前任務(wù)
isDone():判斷任務(wù)是否完成
9. Fork/Join
其思想為x分法:如果一個(gè)任務(wù)比較大,那就把它分為x部分執(zhí)行。
示例代碼(先粘貼,回去打):
public class Main {
public static void main(String[] args) throws Exception {
// 創(chuàng)建2000個(gè)隨機(jī)數(shù)組成的數(shù)組:
long[] array = new long[2000];
long expectedSum = 0;
for (int i = 0; i < array.length; i++) {
array[i] = random();
expectedSum += array[i];
}
System.out.println("Expected sum: " + expectedSum);
// fork/join:
ForkJoinTask task = new SumTask(array, 0, array.length);
long startTime = System.currentTimeMillis();
Long result = ForkJoinPool.commonPool().invoke(task);
long endTime = System.currentTimeMillis();
System.out.println("Fork/join sum: " + result + " in " + (endTime - startTime) + " ms.");
}
static Random random = new Random(0);
static long random() {
return random.nextInt(10000);
}
}
class SumTask extends RecursiveTask {
static final int THRESHOLD = 500;
long[] array;
int start;
int end;
SumTask(long[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
if (end - start <= THRESHOLD) {
// 如果任務(wù)足夠小,直接計(jì)算:
long sum = 0;
for (int i = start; i < end; i++) {
sum += this.array[i];
// 故意放慢計(jì)算速度:
try {
Thread.sleep(1);
} catch (InterruptedException e) {
}
}
return sum;
}
// 任務(wù)太大,一分為二:
int middle = (end + start) / 2;
System.out.println(String.format("split %d~%d ==> %d~%d, %d~%d", start, end, start, middle, middle, end));
SumTask subtask1 = new SumTask(this.array, start, middle);
SumTask subtask2 = new SumTask(this.array, middle, end);
invokeAll(subtask1, subtask2);
Long subresult1 = subtask1.join();
Long subresult2 = subtask2.join();
Long result = subresult1 + subresult2;
System.out.println("result = " + subresult1 + " + " + subresult2 + " ==> " + result);
return result;
}
}
10. ThreadLocal
在同一線程中傳遞同一對(duì)象。回去慢慢更。
與50位技術(shù)專家面對(duì)面20年技術(shù)見證,附贈(zèng)技術(shù)全景圖總結(jié)
以上是生活随笔為你收集整理的java如何写线程外部类_廖雪峰Java读书笔记(六)--多线程(或称并发)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 我的名字和党员的党案的名字最后的一个字是
- 下一篇: 房价为什么会下跌?