文章首發於公衆號,歡迎訂閱
實現多線程的方法
方法一:繼承Thread
類
public class MyThread extends Thread {
@Override
public void run() {
super.run();
// ......
}
}
// 使用MyThread
MyThread thread = new MyThread();
thread.start();
方法二:實現Runnable
接口
public class MyRunnable implements Runnable {
@Override
public void run() {
// ......
}
}
// 使用MyRunnable
Runnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
使用方法二的好處是:
- 從代碼耦合角度看,
run
方法代碼中的任務應該與Thread
類解耦,因此方法二更好。 - 從資源節約角度看,方法一每次想新建一個任務都需要新建一個獨立的線程,而新建一個獨立的線程損耗比較大,需要創建、執行、銷燬。使用方法二的話,後續可以使用線程池這樣的工具提高性能。
- 方法一不支持多繼承,如果線程類已經有一個父類了,這時不能再繼承自
Thread
類了,因爲 Java 不支持多繼承,但是可以實現Runnable
接口來處理。
兩種方法本質對比:
- 方法一:最終調用的是
target.run()
- 方法二:最終調用的是我們自己重寫的
run()
問題:同時使用方法一和方法二會發生什麼?
請看如下代碼:
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("我來自Runnable");
}
}) {
@Override
public void run() {
System.out.println("我來自Thread");
}
}.start();
}
輸出:
我來自Thread
爲什麼只執行下面這個?如果沒有下面這個,那麼必然執行上面 run
方法,因爲在 Thread
類中的 run
方法中,有這樣三行代碼:
private Runnable target;
@Override
public void run() {
if (target != null) {
target.run();
}
}
此時 target
確實不爲 null
,那麼就執行上面的 run
方法。但是如果下面用戶重寫了 Thread
的 run
方法,那麼此時就執行子類的 run
方法了。
準確說,創建線程只有一種方式那就是構造Thread類,而實現線程的執行單元有兩種方式:
- 方法一:實現
Runnable
接口的run
方法,並把Runnable
實例傳給Thread
類 - 方法二:繼承
Thread
類,重寫Thread
類 的run
方法
線程的啓動
請看如下代碼:
public static void main(String[] args) {
Runnable runnable = () -> {
System.out.println(Thread.currentThread().getName());
};
// run啓動
runnable.run();
// start啓動
new Thread(runnable).start();
}
輸出:
main
Thread-0
start() 方法原理解讀
- 啓動新線程檢查線程狀態
- 加入線程組
- 調用
start0()
run() 方法原理解讀
上面提到的三行代碼
問題:一個線程兩次調用start
方法會出現什麼情況?爲什麼?
會拋出異常,原因是start
方法會檢查線程狀態threadStatus
,threadStatus
初始值爲0,啓動後threadStatus
就不是 0 了,如果第二次繼續調用start
方法的話,由於threadStatus
不爲 0 就會拋出異常。
線程的停止
停止線程使用interrupt
來通知,而不是強制。
線程停止的三種情況
- 一般情況
通過Thread.currentThread().isInterrupted()
方法來判斷。
public class S1 implements Runnable {
@Override
public void run() {
int num = 0;
while (!Thread.currentThread().isInterrupted() && num <= Integer.MAX_VALUE / 2) {
if (num % 10000 == 0) {
System.out.println(num + "是10000的倍數");
}
num++;
}
System.out.println("任務運行結束了");
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new RightWayStopThreadWithoutSleep());
thread.start();
Thread.sleep(2000);
thread.interrupt();
}
}
- 線程被阻塞的情況
通過try...catch
捕獲可以響應interrupt
中斷的方法。
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
int num = 0;
try {
while (num <= 300 && !Thread.currentThread().isInterrupted()) {
if (num % 100 == 0) {
System.out.println(num + "是100的倍數");
}
num++;
}
// 可以響應interrupt中斷
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(500);
thread.interrupt();
}
- 線程在每次迭代後阻塞
和上面相比,可以省略!Thread.currentThread().isInterrupted()
的判斷。
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
int num = 0;
try {
while (num <= 10000) {
if (num % 100 == 0) {
System.out.println(num + "是100的倍數");
}
num++;
Thread.sleep(10);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(5000);
thread.interrupt();
}
下面這種情況是錯誤的,while 內 try…catch 會導致線程無法停止。
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
int num = 0;
while (num <= 10000 && !Thread.currentThread().isInterrupted()) {
if (num % 100 == 0) {
System.out.println(num + "是100的倍數");
}
num++;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(5000);
thread.interrupt();
}
因爲當 sleep
方法響應中斷後,就會清除中斷標誌,所以 while 中判斷無法退出。
解決方法是在 catch 中加入一行Thread.currentThread().interrupt();
最佳實踐
- 傳遞中斷
public class S1 implements Runnable {
@Override
public void run() {
while (true && !Thread.currentThread().isInterrupted()) {
System.out.println("go");
try {
throwInMethod();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("保存日誌");
e.printStackTrace();
}
}
}
private void throwInMethod() throws InterruptedException {
Thread.sleep(2000);
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new S1());
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}
- 恢復中斷
public class S2 implements Runnable {
@Override
public void run() {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("Interrupted,程序運行結束");
break;
}
reInterrupt();
}
}
private void reInterrupt() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new S2());
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}
可以響應中斷的方法
Object.wait
Thread.sleep
Thread.join
java.util.concurrent.BlockingQueue.take/put
java.util.concurrent.locks.Lock.locklnterruptibly
java.util.concurrent.CountDownLatch.await
java.util.concurrent.CyclicBarrier.await
java.util.concurrent.Exchanger.exchange
java.nio.channels.InterruptibleChannel
java.nio.channels.Selector
線程停止的一種錯誤方式
這裏我們不談stop/suspend
,因爲這兩個方法已經被禁用。
我們講的是用volatile
設置boolean
標記位的方法。這個方法在一般情況下使用是沒有問題的,但是如果用在比如像消費者生產者這種場景下,由於BlockingQueue.take/put
方法有可能會一直阻塞,從而導致線程無法停止,而take/put
方法是可以響應interrupt
中斷的,因此只推薦使用interrupt
方法作爲線程停止的方式。
如何處理不可中斷的阻塞
按照特定的方法,比如像ReentrantLock
本身的lock
方法無法響應中斷,但是可以使用能響應中斷的lockInterruptibly
方法。
線程的生命週期
線程的 6 個狀態:
- New 已創建但還尚未啓動的新線程
- Runnable 可運行
- Blocked 被阻塞
- Waiting 等待
- Timed waiting 限期等待
- Terminated 終止
轉化示意圖如下:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-kGBxVe9A-1586831784737)(https://i.loli.net/2020/02/20/XUTgNWwmzhC9oqR.png)]
一般而言,把 Blocked (被阻塞)、Waiting (等待)、Timed_waiting (計時等待)都稱爲阻塞狀態。
Thread 和 Object 類中的重要方法詳解
wait、notify、notifyAll 方法
wait
方法遇到以下 4 種情況時會被喚醒:
- 另一個線程調用這個對象的
notify
方法且剛好被喚醒的是本線程; - 另一個線程調用這個對象的
notifyAll
方法; - 過了
wait(time)
規定的超時時間,如果傳入 0 就是永久等待; - 線程自身調用了
interrupt
notify
方法只應該被擁有該對象 monitor 的線程調用。一旦線程被喚醒,線程便會從對象的“等待線程集合”中被移除,所以可以重新參與到線程調度當中。
案例:
1、生產者消費者模型
public class ProducerConsumerModel {
public static void main(String[] args) {
EventStorage eventStorage = new EventStorage();
Producer producer = new Producer(eventStorage);
Consumer consumer = new Consumer(eventStorage);
new Thread(producer).start();
new Thread(consumer).start();
}
}
class Producer implements Runnable {
private EventStorage storage;
public Producer(EventStorage storage) {
this.storage = storage;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
storage.put();
}
}
}
class Consumer implements Runnable {
private EventStorage storage;
public Consumer(EventStorage storage) {
this.storage = storage;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
storage.take();
}
}
}
class EventStorage {
private int maxSize;
private LinkedList<Date> storage;
public EventStorage() {
maxSize = 10;
storage = new LinkedList<>();
}
public synchronized void put() {
while (storage.size() == maxSize) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
storage.add(new Date());
System.out.println("倉庫裏有了" + storage.size() + "個產品。");
notify();
}
public synchronized void take() {
while (storage.size() == 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("拿到了" + storage.poll() + ",現在倉庫還剩下" + storage.size());
notify();
}
}
2、兩個線程交替打印0~100的奇偶數
- 用
synchronized
關鍵字實現
public class PrintOddEvenSyn1 {
private static int count;
private static final Object lock = new Object();
// 新建2個線程
// 1個只處理偶數,第二個只處理奇數(用位運算)
// 用synchronized來通信
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
while (count < 100) {
synchronized (lock) {
// 位運算提高效率
if ((count & 1) == 0) {
System.out.println(Thread.currentThread().getName() + ":" + count++);
}
}
}
}
}, "偶數").start();
new Thread(new Runnable() {
@Override
public void run() {
while (count < 100) {
synchronized (lock) {
if ((count & 1) == 1) {
System.out.println(Thread.currentThread().getName() + ":" + count++);
}
}
}
}
}, "奇數").start();
}
}
這種方法雖然能實現功能,但是效率太低,因爲每次都是第一個線程或者第二個線程中的其中一個搶到鎖,有可能其中一個一直搶到鎖,那麼就白白等待浪費時間。
- 用
wait
和notify
方法實現
public class PrintOddEvenSyn2 {
private static int count = 0;
private static final Object lock = new Object();
public static void main(String[] args) {
new Thread(new TurningRunner(), "偶數").start();
new Thread(new TurningRunner(), "奇數").start();
}
// 1.拿到鎖,我們就打印
// 2.打印完,喚醒其他線程,自己就休眠
static class TurningRunner implements Runnable {
@Override
public void run() {
while (count <= 100) {
synchronized (lock) {
// 拿到鎖就打印
System.out.println(Thread.currentThread().getName() + ":" + count++);
lock.notify();
if (count <= 100) {
try {
// 如果任務還沒結束,就讓出當前的鎖,並休眠
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}
問題:爲什麼線程通信的 wait、notify 和 notifyAll 被定義在 Object 類中,而 sleep 被定義在 Thread 類中?
因爲 wait
、notify
和 notifyAll
是鎖級別的操作,而鎖是綁定到某個對象中的,而不是綁定到線程 Thread
。我們經常會遇到某個線程持有多個鎖,並且這些鎖之間是相互配合的,如果把 wait
、notify
和 notifyAll
方法定義在 Thread
中,那麼就無法靈活配合了。
sleep 方法
sleep
方法可以讓線程進入 Waiting 狀態,並且不佔用 CPU 資源,但是不會釋放鎖,直到規定時間後再執行,休眠期間如果被中斷,會拋出異常並清除中斷標誌。
sleep
方法和 wait
方法比較
相同:
- 都會阻塞當前線程
- 都可以響應中斷
不同:
wait
必須在同步方法中執行,而sleep
不需要wait
會釋放鎖,而sleep
不會wait
可以不傳入參數,表示直到自己被喚醒,而sleep
必須傳入參數wait
屬於Object
類,sleep
屬於Thread
類
join 方法
在很多情況下,主線程創建並啓動子線程,如果子線程中要進行大量的耗時運算,主線程往往將早於子線程結束之前結束。這時,如果主線程想等待子線程執行完成之後再結束,比如子線程處理一個數據,主線程要取得這個數據中的值,就要用到 join
方法。
join
方法的作用是使所屬的線程對象 x 正常執行 run
方法中的任務,而使當前線程 z 進行無限期的阻塞 ,等待線程 x 銷燬後再繼續執行線程 z 後面的代碼。
在 join
期間,當前線程處於 Waiting 狀態。
在部使用 wait
方法進行等待,synchronized
使用的是對象監視器做同步。
join(time)
和 sleep(time)
的區別:join
內部使用 wait
等待,最終會底層會調用 notifyAll
釋放鎖,而 sleep
不能。
在 join
過程中,如果當前線程對象被中斷則當前線程出現異常。
public class JoinInterrupt {
public static void main(String[] args) {
Thread mainThread = Thread.currentThread();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
// 主線程被中斷
mainThread.interrupt();
Thread.sleep(5000);
System.out.println("Thread1 finished.");
} catch (InterruptedException e) {
System.out.println("子線程中斷");
}
}
});
thread1.start();
System.out.println("等待子線程運行完畢");
try {
thread1.join();
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()+"主線程中斷了");
// 由於主線程已經中斷,所以讓子線程也中斷防止發生意外
thread1.interrupt();
}
System.out.println("子線程已運行完畢");
}
}
用 synchronized
+ wait
實現 join
的功能:
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "執行完畢");
}
});
thread.start();
System.out.println("開始等待子線程運行完畢");
// thread.join();
// 使用Thread對象作爲鎖
synchronized (thread) {
thread.wait();
}
System.out.println("所有子線程執行完畢");
}
這是因爲 Thread
對象會在自己的線程死亡時調用 this.notifyAll()
,因此一般不建議使用 Thread
的對象作爲鎖。
yield 方法
作用:釋放自己剩餘的時間片。JVM 不保證遵循,因此我們一般不使用。
sleep
與 yield
方法的區別在於,當線程調用 sleep
方法時調用線程會被阻塞掛起指定的時間,在這期間線程調度器不會去調度該線程 。 而調用 yield
方法時,線程只是讓出自己剩餘的時間片,並沒有被阻塞掛起,而是處於就緒狀態,線程調度器下一次調度時就有可能調度到當前線程執行 。
線程屬性
線程 ID
標識不同的線程,無法修改。
線程 Name
清晰有意義的名字。
守護線程(Daemon)
守護線程是一種特殊的線程,典型的守護線程就是垃圾回收線程,當進程中不存在非守護線程了,則垃圾回收線程就會自動銷燬。當最後一個非守護線程結束,守護線程才隨着 JVM 一同結束工作。
普通線程和守護線程區別:
普通線程是執行我們的邏輯的,守護線程是服務於我們的,且隨着 JVM 一起結束工作。
線程優先級
Java 中線程的優先級分爲 1~10 這 10 個等級,如果小於 1 或大於 10,那麼會拋出異常。
使用 3 個常量來預置定義優先級的值
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
- 繼承性
在 Java 中,線程優先級具有繼承性,當 A 線程啓動 B 線程,那麼 B 線程的優先級和 A 線程相同。當優先級被修改後再繼承的話,會繼承修改後的優先級
- 規則性
高優先級總是大部分先執行完
- 隨機性
高優先級總是大部分先執行完,但不代表高優先級一定全部都先執行完
線程的異常處理
對於 checked exception 我們一般在程序中直接 try…catch 即可,但是遇到 unchecked exception,我們就需要自定義一個異常處理器專門去處理非檢查型異常,示例如下:
public class MyThreadExceptionHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("An exception has been captured");
System.out.printf("Exception: %s: %s\n", e.getClass().getName(), e.getMessage());
System.out.println("Stack Trace");
e.printStackTrace(System.out);
System.out.printf("Thread status: %s\n", t.getState());
}
}
public class MyThread implements Runnable {
@Override
public void run() {
int a = Integer.parseInt("a");
System.out.println(a);
}
}
public class Run {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.setUncaughtExceptionHandler(new MyThreadExceptionHandler());
thread.start();
}
}
//輸出
An exception has been captured
Exception: java.lang.NumberFormatException: For input string: "a"
Stack Trace
java.lang.NumberFormatException: For input string: "a"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Integer.parseInt(Integer.java:580)
at java.lang.Integer.parseInt(Integer.java:615)
at t10.MyThread.run(MyThread.java:11)
at java.lang.Thread.run(Thread.java:748)
Thread status: RUNNABLE
線程組的異常處理如下:
public class MyThreadGroup extends ThreadGroup {
public MyThreadGroup(String name) {
super(name);
}
// ThreadGroup默認已經實現了Thread.UncaughtExceptionHandler
@Override
public void uncaughtException(Thread t, Throwable e) {
super.uncaughtException(t, e);
// 一旦線程組中有線程發生異常,就中斷整個線程組
this.interrupt();
}
}
public class MyThread extends Thread {
private String num;
public MyThread(ThreadGroup group, String name, String num) {
super(group, name);
this.num = num;
}
@Override
public void run() {
int numInt = Integer.parseInt(num);
while (this.isInterrupted() == false) {
System.out.println("while循環中:" + Thread.currentThread().getName());
}
}
}
public class Run {
public static void main(String[] args) {
MyThreadGroup group = new MyThreadGroup("我的線程組");
MyThread[] myThread = new MyThread[10];
for (int i = 0; i < myThread.length; i++) {
myThread[i] = new MyThread(group, "線程" + (i + 1), "1");
myThread[i].start();
}
MyThread thread = new MyThread(group, "報錯線程", "a");
thread.start();
}
}
只要有一個線程發生異常,那麼此線程組中的所有線程都會中斷。
多線程安全問題
什麼是線程安全
當多個線程訪問一個對象時,如果不用考慮這些線程在運行時環境下的調度和交替執行,也不需要進行額外的同步,或者在 ,調用這個對象的行爲都可以獲得正確的結果,那這個對象是線程安全的。
多線程安全問題三要素
- 是否存在多線程環境
- 是否存在共享的數據
- 是否有多條語句同時操作這條數據
案例1-計數出錯
public class CountingError implements Runnable {
static final CountingError INSTANCE = new CountingError();
int index = 0;
static AtomicInteger realIndex = new AtomicInteger();
static AtomicInteger wrongCount = new AtomicInteger();
static volatile CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
final boolean[] marked = new boolean[10000000];
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(INSTANCE);
Thread thread2 = new Thread(INSTANCE);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("表面上結果是" + INSTANCE.index);
System.out.println("真正運行的次數" + realIndex.get());
System.out.println("錯誤次數" + wrongCount.get());
}
@Override
public void run() {
marked[0] = true;
// 等待直到兩個線程都忙完了再繼續下一次
for (int i = 0; i < 100000; i++) {
try {
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
index++;
realIndex.incrementAndGet();
synchronized (INSTANCE) {
// 由於synchronized也保證線程可見性,所以只憑marked[index]無法判斷,需要根據前一位的
// 舉例:
// 假設沒有發生多線程問題,即index++沒有問題
// 線程2先使得index++,線程2 index爲1,線程1 index爲2
// 線程1先進入synchronized代碼塊,使得marked[2] = true;
// 由於synchronized間接保證可見性,線程2此時看到的index爲2。那麼marked[index]就會誤判錯誤,其實應該判斷marked[index - 1]
if (marked[index] && marked[index - 1]) {
System.out.println("發生錯誤" + index);
wrongCount.incrementAndGet();
}
marked[index] = true;
}
}
}
}
案例2-死鎖
死鎖的產生具備以下四個條件:
- 互斥條件:指線程對己經獲取到的資源進行排它性使用, 即該資源同時只由一個線程佔用。如果此時還有其他線程請求獲取該資源,則請求者只能等待,直至佔有資源的線程釋放該資源。
- 請求並持有條件: 指一個線程己經持有了至少一個資源 , 但又提出了新的資源請求 ,而新資源己被其他線程佔有,所 以當前線程會被阻塞,但阻塞的同時並不釋放自己己經獲取的資源。
- 不可剝奪條件: 指線程獲取到的資源在自己使用完之前不能被其他線程搶佔,只有在自己使用完畢後才由自 己釋放該資源。
- 環路等待條件:指在發生死鎖時,必然存在一個線程→資源的環形鏈,即線程集合 { T0,T1,T2 ,…,Tn} 中的 T0 正在等待一個 T1 佔用的資源,T1 正在等待 T2 佔用的資源,……Tn 正在等待己被 T0 佔用的資源。
示例1:
public class DeadLockThread implements Runnable {
public String name;
public Object lock1 = new Object();
public Object lock2 = new Object();
public void setName(String name) {
this.name = name;
}
@Override
public void run() {
if (name.equals("a")) {
synchronized (lock1) {
System.out.println("name=" + name);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("lock1->lock2");
}
}
}
if (name.equals("b")) {
synchronized (lock2) {
System.out.println("name=" + name);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("lock2->lock1");
}
}
}
}
}
public class Run {
public static void main(String[] args) {
try {
DeadLockThread deadLockThread = new DeadLockThread();
deadLockThread.setName("a");
Thread thread1 = new Thread(deadLockThread);
thread1.start();
Thread.sleep(1000);
deadLockThread.setName("b");
Thread thread2 = new Thread(deadLockThread);
thread2.start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
示例2:
public class DeadLockThread implements Runnable {
int flag = 1;
static Object o1 = new Object();
static Object o2 = new Object();
public static void main(String[] args) {
DeadLockThread d1 = new DeadLockThread();
DeadLockThread d2 = new DeadLockThread();
d1.flag = 1;
d2.flag = 0;
new Thread(d1).start();
new Thread(d2).start();
}
@Override
public void run() {
System.out.println("flag = " + flag);
if (flag == 1) {
synchronized (o1) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println("1");
}
}
}
if (flag == 0) {
synchronized (o2) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println("0");
}
}
}
}
}
對象逸出
1、方法返回一個 private 對象(private 的本意是不讓外部訪問)
public class Error1 {
private Map<String, String> states;
public Error1() {
states = new HashMap<>();
states.put("1", "週一");
states.put("2", "週二");
states.put("3", "週三");
states.put("4", "週四");
}
public Map<String, String> getStates() {
return states;
}
public Map<String, String> getStatesImproved() {
return new HashMap<>(states);
}
public static void main(String[] args) {
Error1 error1 = new Error1();
Map<String, String> states = error1.getStates();
System.out.println(states.get("1"));
states.remove("1");
System.out.println(states.get("1"));
}
2、還未完成初始化(構造函數沒完全執行完畢)就把對象提供給外界
- 在構造函數中未初始化完畢就進行 this 賦值
public class Error2 {
static Point point;
public static void main(String[] args) throws InterruptedException {
new PointMaker().start();
// 因爲時間不多,導致結果不同
// Thread.sleep(10);
Thread.sleep(105);
if (point != null) {
System.out.println(point);
}
}
}
class Point {
private final int x, y;
public Point(int x, int y) throws InterruptedException {
this.x = x;
Error2.point = this;
Thread.sleep(100);
this.y = y;
}
@Override
public String toString() {
return x + "," + y;
}
}
class PointMaker extends Thread {
@Override
public void run() {
try {
new Point(1, 1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 隱式逸出—註冊監聽事件
public class Error3 {
int count;
public Error3(MySource source) {
source.registerListener(new EventListener() {
@Override
public void onEvent(Event e) {
// 0
System.out.println("\n我得到的數字是" + count);
}
});
for (int i = 0; i < 10000; i++) {
System.out.print(i);
}
count = 100;
}
public static void main(String[] args) {
MySource mySource = new MySource();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
mySource.eventCome(new Event() {
});
}
}).start();
Error3 error3 = new Error3(mySource);
}
static class MySource {
private EventListener listener;
void registerListener(EventListener eventListener) {
this.listener = eventListener;
}
void eventCome(Event e) {
if (listener != null) {
listener.onEvent(e);
} else {
System.out.println("還未初始化完畢");
}
}
}
interface EventListener {
void onEvent(Event e);
}
interface Event {
}
}
- 構造函數中運行線程
public class Error4 {
private Map<String, String> states;
public Error4() {
new Thread(new Runnable() {
@Override
public void run() {
states = new HashMap<>();
states.put("1", "週一");
states.put("2", "週二");
states.put("3", "週三");
states.put("4", "週四");
}
}).start();
}
public Map<String, String> getStates() {
return states;
}
public static void main(String[] args) throws InterruptedException {
Error4 error4 = new Error4();
// 如果沒有等待,就會因爲還沒有初始化完成導致空指針異常
Thread.sleep(1000);
System.out.println(error4.getStates().get("1"));
}
}
解決這兩類逸出的方法:
1、返回副本
public class Solution1 {
private Map<String, String> states;
public Solution1() {
states = new HashMap<>();
states.put("1", "週一");
states.put("2", "週二");
states.put("3", "週三");
states.put("4", "週四");
}
public Map<String, String> getStates() {
// 返回states的副本
return new HashMap<>(states);
}
public static void main(String[] args) {
Solution1 solution1 = new Solution1();
System.out.println(solution1.getStates().get("1"));
solution1.getStates().remove("1");
System.out.println(solution1.getStates().get("1"));
}
}
2、工廠模式
對隱式逸出—註冊監聽事件修改爲工廠模式:
public class Error3Fixed {
int count;
private EventListener listener;
private Error3Fixed(MySource source) {
listener = new EventListener() {
@Override
public void onEvent(Error3Fixed.Event e) {
System.out.println("\n我得到的數字是" + count);
}
};
for (int i = 0; i < 10000; i++) {
System.out.print(i);
}
count = 100;
}
public static Error3Fixed getInstance(MySource source) {
Error3Fixed safeListener = new Error3Fixed(source);
source.registerListener(safeListener.listener);
return safeListener;
}
public static void main(String[] args) {
MySource mySource = new MySource();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
mySource.eventCome(new Error3Fixed.Event() {
});
}
}).start();
Error3Fixed error3Fixed = Error3Fixed.getInstance(mySource);
}
static class MySource {
private EventListener listener;
void registerListener(EventListener eventListener) {
this.listener = eventListener;
}
void eventCome(Error3Fixed.Event e) {
if (listener != null) {
listener.onEvent(e);
} else {
System.out.println("還未初始化完畢");
}
}
}
interface EventListener {
void onEvent(Error3Fixed.Event e);
}
interface Event {
}
}
需要考慮線程安全的情況
1、訪問共享的變量或資源,會有併發風險,比如對象的屬性、靜態變量、共享緩存、數據庫等。
2、所有依賴時序的操作,即使每一步操作都是線程安全的,還是存在併發問題。
-
read-modify-write 操作:一個線程讀取了一個共享數據,並在此基礎上更新該數據。
-
check-then-act 操作:一個線程讀取了一個共享數據,並在此基礎上決定其下一個的操作。
3、不同的數據之間存在捆綁關係的時候,如 IP 和端口號。
4、我們使用其他類的時候,如果對方沒有聲明自己是線程安全的,那麼大概率會存在併發問題比如 HashMap 沒有聲明自己是併發安全的,所以我們併發調用 HashMap 的時候會出錯。
多線程帶來的性能問題
調度-上下文切換
在多線程編程中,線程個數一般都大於 CPU 個數,而每個 CPU 同一時刻只能被一個線程使用,爲了讓用戶感覺多個線程是在同時執行的, CPU 資源的分配採用了時間片輪轉的策略 ,也就是給每個線程分配一個時間片,線程在時間片內佔用 CPU 執行任務。當前線程使用完時間片後,就會處於就緒狀態並讓出 CPU 讓其他線程佔用 , 這就是上下文切換 ,從當前線程的上下文切換到了其他線程 。 那麼就有一個問題,讓出 CPU 的線程等下次輪到自己佔有 CPU 時如何知道自己之前運行到哪裏了?所以在切換線程上下文時需要保存當前線程的執行現場 , 當再次執行時根據保存的執行現場信息恢復執行現場 。
線程上下文切換時機有:當前線程的 CPU 時間片使用完處於就緒狀態時,當前線程被其他線程中斷時。
如何減少上下文切換
減少上下文切換的方法有無鎖併發編程、CAS 算法、使用最少線程以及使用協程。
- 無鎖併發編程。多線程競爭鎖時,會引起上下文切換,所以多線程處理數據時,可以使用一些辦法來避免使用鎖,如將數據的 ID 按照 Hash 算法取模分段,不同的線程處理不同段的數據。
- CAS 算法。Java 的 Atomic 包使用 CAS 算法來更新數據,而不需要加鎖。
- 使用最少線程。避免創建不需要的線程,比如任務很少,但是創建了很多線程來處理,這樣會造成大量線程都處於等待狀態。
- 使用協程。在單線程裏實現多任務的調度,並在單線程裏維持多個任務間的切換。
協作-內存同步
爲了數據的正確性,同步手段往往會使用禁止編譯器優化、使 CPU 內的緩存失效。
我創建了一個免費的知識星球,用於分享知識日記,歡迎加入!