文章目錄
1. Thread
1.1 創建線程
繼承Thread,重寫run方法
@Test
public void helloThreadTest() {
Thread thread = new HelloThread();
thread.start();
}
public class HelloThread extends Thread {
@Override
public void run() {
System.out.println("HelloThread");
}
}
實現Runnable接口
@Test
public void helloRunnableTest() {
Thread thread = new Thread(new HelloRunnable());
thread.start();
}
public class HelloRunnable implements Runnable {
@Override
public void run() {
System.out.println("HelloRunnable");
}
}
Lambda語法簡寫
@Test
public void lambdaTest() {
Thread thread = new Thread(() -> {
System.out.println("lambdaTest");
});
thread.start();
}
1.2 join
join()方法使主線程會等待線程完成後繼續執行。
沒有join()方法,只能保證主線程先打印main start,然後main end;線程先打印thread start,然後thread end
main end具體打印什麼位置未知,由操作系統調度
但是添加join,就可以保證main end在線程執行結束後打印
@Test
public void mainThreadTest() throws InterruptedException {
System.out.println("main start");
Thread thread = new Thread(() -> {
System.out.println("thread start");
System.out.println("thread end");
});
thread.start();
//thread.join();
System.out.println("main end");
}
1.3 interrupt
Thread.sleep(1);需要使用main方法,單元測試會不可用。
線程t中斷,主線程等待線程t中斷後,繼續執行
public static void main(String[] args) throws InterruptedException {
Thread t = new InterruptThread();
t.start();
// 暫停1毫秒
Thread.sleep(1);
// 中斷t線程
t.interrupt();
// 等待t線程結束
t.join();
System.out.println("end");
}
public class InterruptThread extends Thread {
public void run() {
int n = 0;
while (!isInterrupted()) {
n ++;
System.out.println(n + " hello!");
}
}
}
線程t啓動後,線程t內部會啓動線程2,並等待線程2結束
當線程t中斷後,主線程等待線程t結束
如果不把線程2中斷,則線程2會繼續執行
public static void main(String[] args) throws InterruptedException {
Thread t = new InterruptThread1();
t.start();
Thread.sleep(1000);
// 中斷t線程
t.interrupt();
// 等待t線程結束
t.join();
System.out.println("end");
}
public class InterruptThread1 extends Thread {
public void run() {
Thread thread2 = new InterruptThread2();
// 啓動hello線程
thread2.start();
try {
// 等待hello線程結束,如果中斷會拋異常
thread2.join();
} catch (InterruptedException e) {
System.out.println("interrupted!" + new Date());
}
// 不將thread2中斷,還會繼續
thread2.interrupt();
}
}
public class InterruptThread2 extends Thread {
public void run() {
int n = 0;
while (!isInterrupted()) {
n++;
System.out.println(n + " InterruptThread2!" + new Date());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
break;
}
}
}
}
中斷也可通過設置標誌位來實現
通過關鍵詞volatile將其標記爲線程間共享的變量
public static void main(String[] args) throws InterruptedException {
InterruptThread3 thread = new InterruptThread3();
thread.start();
Thread.sleep(1);
thread.running = false;
}
public class InterruptThread3 extends Thread {
public volatile boolean running = true;
@Override
public void run() {
int n = 0;
while (running) {
n ++;
System.out.println(n + " hello!");
}
System.out.println("end!");
}
}
2. wait notify
wait和notify用於多線程協調運行。
在synchronized內部可以調用wait()使線程進入等待狀態,調用notify()或notifyAll()喚醒其他等待線程;
必須在已獲得的鎖對象上調用wait()方法,調用notify()或notifyAll()方法;
已喚醒的線程還需要重新獲得鎖後才能繼續執行。
啓動兩個線程,一個輸出 1,3,5,7…99,另一個輸出 2,4,6,8…100,最後按序輸出 1,2,3,4,5…100
private static final Object lock = new Object();
private volatile int i = 1;
@Test
public void test1() {
Thread thread1 = new Thread(() -> {
while (i <= 100) {
synchronized (lock) {
// 偶數則等待
if (i % 2 == 0) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 奇數打印並判斷
} else {
System.out.print(i + " ");
i++;
lock.notifyAll();
}
}
}
});
Thread thread2 = new Thread(() -> {
while (i <= 100) {
synchronized (lock) {
// 奇數則等待
if (i % 2 != 0) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 偶數打印並判斷
} else {
System.out.print(i + " ");
i++;
lock.notifyAll();
}
}
}
});
thread1.start();
thread2.start();
}
也可以這樣:
@Test
public void test2() {
Thread t1 = new Thread(() -> {
while(i <= 100) {
synchronized (lock) {
if (i % 2 == 0) {
System.out.print(i++);
System.out.print(" ");
} else {
// 奇數,就wait,釋放鎖
try{
lock.wait();
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
Thread t2 = new Thread(() -> {
while(i <= 100) {
synchronized (lock) {
if (i % 2 != 0) {
System.out.print(i++);
System.out.print(" ");
} else {
// 偶數,就notify,會喚醒上方鎖等待的線程
lock.notifyAll();
}
}
}
});
t1.start();
t2.start();
}
兩個線程,一個線程輸出奇數,一個線程輸出偶數,保證輸出順序是:2、1、4、3、……、50、49、52、51、54、53、……、100、99
private static final Object lock = new Object();
private volatile int count = 2;
@Test
public void test3() {
Thread t1 = new Thread(() -> {
while(count <= 100) {
synchronized (lock){
// 偶數則打印,並減1
if (count % 2 == 0) {
System.out.print(count + " ");
count--;
// 需要notify,否則兩個線程執行一次後都進行等待
lock.notifyAll();
if (count <= 100) {
try{
lock.wait();
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
});
Thread t2 = new Thread(() -> {
while(count <= 100) {
synchronized (lock){
// 奇數則打印,並加3
if (count % 2 != 0) {
System.out.print(count + " ");
count += 3;
// 需要notify,否則兩個線程執行一次後都進行等待
lock.notifyAll();
if (count <= 100) {
try{
lock.wait();
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
});
t1.start();
t2.start();
}
3. ReentrantLock Condition
ReentrantLock可以替代synchronized進行同步,並且獲取鎖更安全;必須先獲取到鎖,再進入try {…}代碼塊,最後使用finally保證釋放鎖;可以使用tryLock()嘗試獲取鎖。
用ReentrantLock,可以使用Condition對象來實現,等同於synchronized,wait和notify的功能
啓動兩個線程,一個輸出 1,3,5,7…99,另一個輸出 2,4,6,8…100,最後按序輸出 1,2,3,4,5…100
使用ReentrantLock和Condition:
private static final Lock reentrantLock = new ReentrantLock();
private static final Condition condition = reentrantLock.newCondition();
private volatile int i = 1;
@Test
public void reentrantLockTest() {
Thread thread1 = new Thread(() -> {
while (i <= 100) {
reentrantLock.lock();
try {
// 偶數則等待
if (i % 2 == 0) {
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 奇數打印並判斷
} else {
System.out.print(i + " ");
i++;
condition.signalAll();
}
} finally {
reentrantLock.unlock();
}
}
});
Thread thread2 = new Thread(() -> {
while (i <= 100) {
reentrantLock.lock();
try {
// 齊數則等待
if (i % 2 != 0) {
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 偶數打印並判斷
} else {
System.out.print(i + " ");
i++;
condition.signalAll();
}
} finally {
reentrantLock.unlock();
}
}
});
thread1.start();
thread2.start();
}
4. ReadWriteLock
使用ReadWriteLock可以保證:只允許一個線程寫入(其他線程既不能寫入也不能讀取);沒有寫入時,多個線程允許同時讀(提高性能)。
如果有線程正在讀,寫線程需要等待讀線程釋放鎖後才能獲取寫鎖,即讀的過程中不允許寫,這是一種悲觀的讀鎖。
private static final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private static final Lock readLock = readWriteLock.readLock();
private static final Lock writeLock = readWriteLock.writeLock();
/**
* 獲取寫鎖
* @param i
*/
private void setI (Integer i) {
writeLock.lock();
try {
this.i = i;
} finally {
writeLock.unlock();
}
}
/**
* 獲取讀鎖
* @return
*/
private Integer getI () {
readLock.lock();
try {
return i;
} finally {
readLock.unlock();
}
}
5. StampedLock
StampedLock和ReadWriteLock相比,改進之處在於:讀的過程中也允許獲取寫鎖後寫入!這樣一來,我們讀的數據就可能不一致,所以,需要一點額外的代碼來判斷讀的過程中是否有寫入,這種讀鎖是一種樂觀鎖。
樂觀鎖的意思就是樂觀地估計讀的過程中大概率不會有寫入,因此被稱爲樂觀鎖。反過來,悲觀鎖則是讀的過程中拒絕有寫入,也就是寫入必須等待。顯然樂觀鎖的併發效率更高,但一旦有小概率的寫入導致讀取的數據不一致,需要能檢測出來,再讀一遍就行。
StampedLock提供了樂觀讀鎖,可取代ReadWriteLock以進一步提升併發性能;StampedLock是不可重入鎖。
private static final StampedLock stampedLock = new StampedLock();
private Integer x;
private Integer y;
private void set(Integer x , Integer y) {
long stamp = stampedLock.writeLock();
try {
x += x;
y += y;
} finally {
stampedLock.unlockWrite(stamp);
}
}
private Integer get() {
// 先獲取樂觀讀鎖
long stamp = stampedLock.tryOptimisticRead();
Integer newX = x;
Integer newY = y;
// 判斷獲取樂觀讀鎖後是否有獲取其他寫鎖
if (!stampedLock.validate(stamp)) {
// 有獲取其他寫鎖,則獲取悲觀讀鎖。寫鎖需要等悲觀讀鎖釋放後,才能寫
stamp = stampedLock.readLock();
try {
// 重新賦值
newX = x;
newY = y;
} finally {
stampedLock.unlockRead(stamp);
}
}
return newX * newY;
}
4. ExecutorService
ExecutorService接口表示線程池,Java標準庫提供的幾個常用實現類有:
FixedThreadPool:線程數固定的線程池,任務超過則等待;
CachedThreadPool:線程數根據任務動態調整的線程池;
SingleThreadExecutor:僅單線程執行的線程池,定期反覆執行。
@Test
public void executorServiceTest() {
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 1; i <= 3; i++) {
executorService.submit(new Task(i + ""));
}
executorService.shutdown();
}
public class Task implements Runnable {
private final String name;
public Task(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println(new Date() + " start " + name);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(new Date() + " end " + name);
}
}
5. Future
一個Future接口表示一個未來可能會返回的結果,可以用來獲取線程返回值。
當我們提交一個Callable任務後,我們會同時獲得一個Future對象,然後,我們在主線程某個時刻調用Future對象的get()方法,就可以獲得異步執行的結果。
在調用get()時,如果異步任務已經完成,我們就直接獲得結果。如果異步任務還沒有完成,那麼get()會阻塞,直到任務完成後才返回結果。
@Test
public void callableTest() {
ExecutorService executorService = Executors.newFixedThreadPool(3);
Callable<String> callable = new TaskCallable();
Future<String> future = executorService.submit(callable);
String returnStr = "";
try {
returnStr = future.get();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(returnStr);
}
public class TaskCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "TaskCallable";
}
}
6. CompletableFuture
CompletableFuture針對Future做了改進,可以傳入回調對象,當異步任務完成或者發生異常時,自動調用回調對象的回調方法。
CompletableFuture可多個串行或並行執行
@Test
public void CompletableFutureTest() {
while (true) {
CompletableFuture<Double> completableFuture = CompletableFuture.supplyAsync(ThreadTest::fetchPrice);
// 成功
completableFuture.thenAccept((result) -> {
System.out.println(result);
});
// 失敗
completableFuture.exceptionally((e) -> {
e.printStackTrace();
return null;
});
}
}
static Double fetchPrice() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
if (Math.random() < 0.3) {
throw new RuntimeException("fetch price failed!");
}
return 5 + Math.random() * 20;
}
參考:
Java 多線程
編程題:兩個線程,一個線程輸出奇數,一個線程輸出偶數,保證輸出順序是:2、1、4、3、……、50、49、52、51、54、53、……、100、99
你真的懂wait、notify和notifyAll嗎