文章目錄
- 一、併發編程
- 二、線程
- 三、線程安全性
- 四、鎖
- 五、線程間的通信
- 5.1、wait、notify、notifyAll
- 5.3、等待通知經典模型之生產者消費者
- 5.4、使用管道流進行通信
- 5.5、 Thread.join通信及其源碼淺析
- 5.6、ThreadLocal的使用
- 5.7、Condition的使用
- 六、原子類
- 七、容器
- 八、jdk提供的併發工具類
- 九、線程池及Executor框架
- 9.1、爲什麼要使用線程池
- 9.2、創建線程池及其使用
- 9.3、Future與Callable、FutureTask
- 9.4、線程池的核心組成部分及其運行機制
- 9.5、線程池拒絕策略
- 9.6、Executor線程池框架
- 9.7、線程池的使用建議
- 十、jvm與併發
- 十一、實戰
- 十二、總結(面試)
一、併發編程
1.1、什麼是併發編程
①、併發編程介紹
早期計算機:從頭到尾執行一個程序,,沒有做併發,導致資源浪費;
後期出現操作系統:計算機能運行多個程序,不同的程序在不同的單獨的進程中運行;
一個進程,有多個線程 ,可以提高資源的利用率;
②、串行與並行的區別
串行:
執行完上一步執行下一步,一步一步,按部就班來做;
並行:
同時做多件事情,比如在電腦上下載軟件的同時可以看視頻、可以聽歌;
並行優點:節約時間;
③、併發編程目的
使得程序充分利用計算機資源,加快程序響應速度(耗時任務、web服務器),簡化異步事件的處理;
④、什麼時候適合使用併發編程
任務會阻塞線程,導致之後的代碼不能執行:比如一邊從文件中讀取,一邊進行大量計算的情況 ;任務執行時間過長,可以劃分爲分工明確的子任務:比如分段下載 ;任務間斷性執行:日誌打印 ;任務本身需要協作執行:比如生產者消費者問題;
比如程序中會調用發短信的接口,網絡不好的話,這裏會卡死,但是爲了不影響後續流程,這個發短信的任務就可以使用併發去做;
1.2、併發編程的挑戰之頻繁的上下文切換
①、什麼是上下文切換以及上下文切換所帶來的挑戰
cpu爲線程分配時間片,時間片非常短(毫秒級別),cpu不停的切換線程執行,在切換前會保存上一個任務的狀態,以便下次切換回這個任務時,
可以再加載這個任務的狀態,讓我們感覺是多個程序同時運行的。
上下文的頻繁切換帶來的問題:會帶來一定的性能開銷;
②、如何減少上下文切換的開銷
使用無鎖併發編程可以減少上下文切換的開銷;
無鎖併發編程:
多線程競爭鎖時,會引起上下文切換,所以多線程處理數據時,可以用一些辦法來避免使用鎖,如將數據的ID按照Hash算法取模分段,不同的線程處理不同段的數據;
所以,線程不是開的越多越好,來回切換線線程會耗時的;(如果數據庫運行最大連接數500,那麼開1000個線程也不會加快訪問速度)
③、CAS
Java的Atomic包使用CAS算法來更新數據,而不需要加鎖,可以達到使用最少線程的目的;
使用最少線程:
避免創建不需要的線程,比如任務很少,但是創建了很多線程來處理,這樣會造成大量線程都處於等待狀態;
④、協程
很古老的技術了;在單線程裏實現多任務的調度,並在單線程裏維持多個任務間的切換;jdk源生沒有協程,有些框架有;
1.3、併發編程的挑戰之死鎖
①、什麼是死鎖以及死鎖所帶來的挑戰
package com.maltose.concurrence.demo;
/**
* @Author: sgw
* @Date 2019/4/20 11:26
* @Description: 模擬死鎖
**/
public class DeadLockDemo {
private static final Object HAIR_A = new Object();
private static final Object HAIR_B = new Object();
public static void main(String[] args){
new Thread(()->{
synchronized (HAIR_A){
try {
//爲了模擬死鎖,這裏休眠一下
Thread.sleep(50L);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (HAIR_B) {
System.out.println("A成功的抓住B的頭髮");
}
}
}).start();
new Thread(()->{
synchronized (HAIR_B){
synchronized (HAIR_A) {
System.out.println("B成功抓到A的頭髮");
}
}
}).start();
}
}
可以看到控制檯什麼都沒有,也沒報錯
通過以下兩種方法判斷是否死鎖
②、查看線程是否死鎖方式一
jps命令查看相應的id,找到id後通過jstack命令查看
下邊裏的日誌提示已經死鎖:
③、查看線程是否死鎖方式二
控制檯輸入jconsole命令,會彈出這個框:
點擊"不安全的連接"
1.4、線程安全
模擬線程不安全代碼:下邊的輸出,每運行一次都會有不同的結果
package com.maltose.concurrence.demo;
import java.util.concurrent.CountDownLatch;
/**
* @Author: sgw
* @Date 2019/4/20 12:04
* @Description: 模擬線程不安全
**/
public class UnSafeThread {
private static int num = 0;
/**
* 每次調用對num進行++操作
*/
public static void inCreate() {
num++;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
for (int j = 0; j < 100; j++) {
inCreate();
}
}).start();
}
//每次運行時輸出的結果都不一樣
System.out.println(num);
}
}
上邊的現象出現的原因:
新的線程去拿取num的值時,可能拿到的是num+1之前的值,沒有取到另一個線程num+1後的值,即這裏應該做一下同步;
同步工具類:CountDownLatch
package com.maltose.concurrence.demo;
import java.util.concurrent.CountDownLatch;
/**
* @Author: sgw
* @Date 2019/4/20 12:04
* @Description: 模擬線程不安全
**/
public class UnSafeThread {
private static int num = 0;
/**
*CountDownLatch是一個同步工具類,它允許一個或多個線程一直等待,直到其他線程的操作執行完後再執行。
* 幾個線程,參數就寫幾
*/
private static CountDownLatch countDownLatch = new CountDownLatch(10);
/**
* 每次調用對num進行++操作
*/
public static void inCreate() {
num++;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
for (int j = 0; j < 100; j++) {
inCreate();
}
//每個線程執行完成之後,調用countDownLatch
countDownLatch.countDown();
}).start();
}
while (true) {
if (countDownLatch.getCount() == 0) {
//這裏每次輸出就是正確的1000
System.out.println(num);
break;
}
}
}
}
二、線程
2.1、進程與線程的區別
進程:
是系統進行分配和管理資源的基本單位;
線程:
進程的一個執行單元,是進程內調度的實體、是CPU調度和分派的基本單位,是比進程更小的獨立運行的基本單位。
線程也被稱爲輕量級進程,線程是程序執行的最小單位。
進程與線程關係:
一個程序至少一個進程,一個進程至少一個線程。
進程有自己的獨立地址空間,每啓動一個進程,系統就會爲它分配地址空間,建立數據表來維護代碼段、堆棧段和數據段,這種操作非常昂貴。
而線程是共享進程中的數據的,使用相同的地址空間,因此CPU切換一個線程的花費遠比進程要小很多,同時創建一個線程的開銷也比進程要小很多。 線程之間的通信更方便,同一進程下的線程共享全局變量、靜態變量等數據,而進程之間的通信需要以通信的方式進行。
如何處理好同步與互斥是編寫多線程程序的難點。 多進程程序更健壯,進程有獨立的地址空間,一個進程崩潰後,在保護模式下
會對其它進程產生影響, 而線程只是一個進程中的不同執行路徑。
線程有自己的堆棧和局部變量,但線程之間沒有單獨的地址空間,所以可能一個線程出現問題,進而導致整個程序出現問題;
2.2、線程的狀態及其相互轉換
線程總共有六種狀態
①、初始狀態(NEW)
新創建了一個線程對象,但還沒有調用start()方法。
②、運行(RUNNABLE)
處於可運行狀態的線程(調用了start方法了),正在JVM中執行,但它可能正在等待來自操作系統的其他資源,例如處理器。
③、阻塞(BLOCKED)
線程阻塞於synchronized鎖,等待獲取synchronized鎖的狀態。
④、等待(WAITING)
Object.wait()、join()、 LockSupport.park(),進入該狀態的線程需要等待其他線程做出一些特定動作(通知或中斷)。
⑤、超時等待(TIME_WAITING)
Object.wait(long)、Thread.join()、LockSupport.parkNanos()、LockSupport.parkUntil,該狀態不同於WAITING,它可以在指定的時間內自行返回。
⑥、終止(TERMINATED)
表示該線程已經執行完畢。
各個線程之間的轉換:
2.3、創建線程
①、創建線程方式一
繼承Thread類,並重寫父類的run方法
public class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.setName("線程demo");
myThread.start();
}
}
實現Runnable接口(推薦使用該方法,因爲java只能單繼承,但是可以多實現)
import java.io.IOException;
import java.io.Serializable;
public class MyRunable implements Runnable,Serializable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
try {
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Thread thread = new Thread(new MyRunable());
thread.setName("maltose");
thread.start();
// thread.run(); 調這個方法,就沒有啓動新線程了,而是調普通的方法
}
}
②、創建線程方式二
匿名內部類創建新線程:
public class MyThread {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
thread.start();
}
}
jdk8提供的Lambda 表達式創建新線程:
public class Lambda {
public static void main(String[] args) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName());
}).start();
}
}
線程池創建新線程
package com.maltose.concurrence.thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPool {
public static void main(String[] args) {
//Executors線程池創建新線程
ExecutorService executorService = Executors.newSingleThreadExecutor();
//jdk8的Lambda 表達式
executorService.execute(()->{
System.out.println(Thread.currentThread().getName());
});
}
}
2.4、線程掛起和恢復
①、掛起線程介紹
線程的掛起操作實質上就是使線程進入“非可執行”狀態下,在這個狀態下CPU不會分給線程時間片,進入這個狀態可以用來暫停
一個線程的運行。 在線程掛起後,可以通過重新喚醒線程來使之恢復運行
②、爲什麼要掛起線程
cpu分配的時間片非常短、同時也非常珍貴。有些線程不需要在可執行狀態,爲了避免資源的浪費,就把這個線程掛起。
③、如何掛起線程
掛起方式一:(已經被廢棄了,只做瞭解)
//下邊這兩個方法已經被廢棄掉了,實際開發不要再用了,下邊只做瞭解
thread.suspend() ;掛起線程
thread.resume();喚醒線程
#上邊兩個方法不能用的原因:thread.suspend()該方法不會釋放線程所佔用的資源。如果使用該方法將某個線程掛起,則可能會使其他等待資源的線程死鎖;
#thread.resume() 方法本身並無問題,但是不能獨立於suspend()方法存在 ;
掛起操作:
public class SuspendDemo implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"執行run方法,準備調用suspend方法");
//掛起線程
Thread.currentThread().suspend();
System.out.println(Thread.currentThread().getName()+"執行run方法,調用suspend方法結束");
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new SuspendDemo());
thread.start();
//休眠3秒
Thread.sleep(3000L);
//對線程進行喚醒操作
thread.resume();
}
}
掛起方式二:
wait() 暫停執行、放棄已經獲得的鎖、進入等待(掛起)狀態;
notify() 隨機喚醒一個在等待鎖的線程;
notifyAll() 喚醒所有在等待鎖的線程,自行搶佔cpu資源;
package com.maltose.concurrence.demo;
/**
* @Author: sgw
* @Date 2019/4/20 14:59
* @Description: TODO
**/
public class WaitDemo implements Runnable{
private static Object object = new Object();
private static Object waitObj = new Object();
@Override
public void run() {
//持有資源
synchronized (waitObj) {
System.out.println(Thread.currentThread().getName()+"佔用資源");
try {
waitObj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"釋放資源");
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new WaitDemo(),"對比線程");
thread.start();
Thread thread2 = new Thread(new WaitDemo(),"對比線程2");
thread2.start();
Thread.sleep(3000L);
//注意:鎖對象一定要是同一個
synchronized (waitObj) {
waitObj.notify();
}
}
}
控制檯:
對比線程佔用資源
對比線程2佔用資源
//3秒後打印下邊這句
對比線程釋放資源
④、何時掛起線程
當前線程等待的資源一直不來(資源沒準備好)的話,就把當前線程掛起,直到notify方法被調用後就可以執行了;
2.5、線程的中斷操作
①、stop() 廢棄方法:
開發中不要使用。因爲一調用該方法,線程就立刻停止,此時有可能引發相應的線程安全性問題;
②、使用interrupt()方法來中斷;注意
public class InterruptDemo implements Runnable {
@Override
public void run() {
//這裏一定要進行判斷
while (!Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName());
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new InterruptDemo());
thread.start();
Thread.sleep(1000L);
thread.interrupt();
}
}
③、使用boolean類型的變量來判斷(該變量一定要用volatile修飾)
public class MyInterruptDemo implements Runnable {
//一定要用volatile修飾(後續會講)
private static volatile boolean FLAG = true;
@Override
public void run() {
while (FLAG) {
System.out.println(Thread.currentThread().getName());
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new MyInterruptDemo());
thread.start();
Thread.sleep(1000L);
FLAG = false;
}
}
2.6、線程的優先級
①、優先級介紹
線程的優先級告訴程序該線程的重要程度有多大。如果有大量線程都被堵塞,都在等候運行,程序會儘可能地先運行優先級高的那個線程。
但是,這並不表示優先級較低的線程不會運行。若線程的優先級較低,只不過表示它被准許運行的機會小一些而已。
②、設置線程的優先級:setPriority(),參數是int類型1-10,值越大,越優先
//低優先級——1
thread.setPriority(Thread.MIN_PRIORITY);
//正常優先級——5
thread3.setPriority(Thread.NORM_PRIORITY);
//高優先級——10
thread2.setPriority(Thread.MAX_PRIORITY);
eg:
package com.maltose.concurrence.demo;
/**
* @Author: sgw
* @Date 2019/4/20 20:18
* @Description: TODO
**/
public class PriorityDemo {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (true) {
System.out.println(Thread.currentThread().getName());
}
}, "線程1");
Thread thread2 = new Thread(() -> {
while (true) {
System.out.println(Thread.currentThread().getName());
}
}, "線程2");
//設置線程優先級,參數可以是1-10之間的數字
thread.setPriority(Thread.MIN_PRIORITY);
thread2.setPriority(Thread.MAX_PRIORITY);
thread.start();
thread2.start();
}
}
2.7、守護線程
①、線程分類
用戶線程
只要線程沒有完成就不會退出;
守護線程
任何一個守護線程都是整個程序中所有用戶線程的守護者,只要有活着的用戶線程,守護線程就活着。
當JVM實例中最後一個非守護線程結束時,該守護線程也隨JVM一起退出;
②、守護線程的用處
比如,當所有線程執行完畢之後,"垃圾清理線程"就會隨着jvm一起退出;
注意:
1、儘量少使用守護線程,因其不可控;
2、不要在守護線程裏去進行讀寫操作、執行計算邏輯
③、設置一個守護線程:setDaemon(true);
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new DaemonThreadDemo());
//開啓守護線程(true:開啓,默認false),一定要在start之前執行
thread.setDaemon(true);
thread.start();
Thread.sleep(2000L);
}
三、線程安全性
3.1、什麼是線程安全
當多個線程訪問某個類,不管運行時環境採用何種調度方式或者這些線程如何交替執行,並且在主調代碼中不需要任何額外的同步或協同,
這個類都能表現出正確的行爲,那麼就稱這個類爲線程安全的。
什麼是線程不安全:
多線程併發訪問時,得不到正確的結果。
3.2、從字節碼角度剖析線程不安全操作
不安全的源碼類如下:
import java.util.concurrent.CountDownLatch;
/**
* 線程不安全操作代碼實例
*/
public class UnSafeThread {
private static int num = 0;
private static CountDownLatch countDownLatch = new CountDownLatch(10);
/**
* 每次調用對num進行++操作
*/
public static void inCreate() {
num++;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
for (int j = 0; j < 100; j++) {
inCreate();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//每個線程執行完成之後,調用countDownLatch
countDownLatch.countDown();
}).start();
}
while (true) {
if (countDownLatch.getCount() == 0) {
System.out.println(num);
break;
}
}
}
}
將這個類使用cmd來編譯
# 編譯成.class
javac -encoding UTF-8 UnsafeThread.java
進行反編譯,得到相應的字節碼指令
javap -c UnsafeThread.class
字節碼文件分析
0: getstatic #2 // Field num:I 獲取指定類的靜態域,並將其押入棧頂
3: iconst_1 將int型1押入棧頂
4: iadd 將棧頂兩個int型相加,將結果押入棧頂
5: putstatic #2 // Field num:I 爲指定類靜態域賦值
8: return
多個線程取到的num的值是上一個線程加1之前的值,即代碼裏的num++不是原子操作,被拆分成好幾個步驟,在多線程併發執行的情況下,因爲cpu調度,多線程快速切換,有可能兩個同一時刻都讀取了同一個num值,之後對它進行+1操作,導致線程安全性得不到保障。
3.3、原子性操作
①、什麼是原子性
一個操作或者多個操作 要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。
②、如何把非原子性操作變爲原子性操作
volatile關鍵字僅僅保證可見性,並不保證原子性;
synchronize關機字,使得操作具有原子性;
3.4、深入理解synchronize(鎖)關鍵字
①、內置鎖
每個java對象都可以用做一個實現同步的鎖,這些鎖稱爲內置鎖。線程進入同步代碼塊或方法的時候會自動獲得該鎖,在退出同步代碼塊或方法時會釋放該鎖。
獲得內置鎖的唯一途徑就是進入這個鎖的保護的同步代碼塊或方法。
②、互斥鎖
內置鎖是一個互斥鎖,這就是意味着最多隻有一個線程能夠獲得該鎖,當線程A嘗試去獲得線程B持有的內置鎖時,線程A必須等待或者阻塞,
直到線程B釋放這個鎖,如果B線程不釋放這個鎖,那麼A線程將永遠等待下去。
③、synchronize修飾方法與代碼塊
1、修飾普通方法:鎖住對象的實例
package com.maltose.concurrence.demo;
/**
* @Author: sgw
* @Date 2019/4/21 20:37
* @Description: TODO
**/
public class SynDemo {
public synchronized void out() throws InterruptedException {
System.out.println(Thread.currentThread().getName());
Thread.sleep(5000L);
}
public static void main(String[] args) {
SynDemo synDemo1 = new SynDemo();
SynDemo synDemo2 = new SynDemo();
new Thread(() -> {
try {
synDemo1.out();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
synDemo2.out();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
運行結果(幾乎同時輸出):
Thread-0
Thread-1
當new了一個SynDemo 時,會鎖住這個對象實例,但是new了兩個SynDemo 的話,即使開啓兩個線程去訪問這兩個對象,這兩個對象也互相不干預,其中一個線程不需等待另一個線程釋放鎖再去操作;即synchronize修飾普通方法時只會鎖住對象,不會鎖整個類;
2、修飾靜態方法:鎖住整個類(所以一般不要使用synchronize修飾靜態方法)
package com.maltose.concurrence.demo;
/**
* @Author: sgw
* @Date 2019/4/21 20:37
* @Description: TODO
**/
public class SynDemo {
public static synchronized void staticOut() throws InterruptedException {
System.out.println(Thread.currentThread().getName());
Thread.sleep(5000L);
}
public static void main(String[] args) {
SynDemo synDemo1 = new SynDemo();
SynDemo synDemo2 = new SynDemo();
new Thread(() -> {
try {
synDemo1.staticOut();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
synDemo2.staticOut();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
運行結果(下邊兩個結果的輸出間隔5秒鐘):
Thread-0
Thread-1
3、修飾代碼塊: 鎖住一個對象(即鎖住synchronize後面括號裏的內容),下邊的代碼兩個對象持有同一個鎖
package com.maltose.concurrence.demo;
/**
* @Author: sgw
* @Date 2019/4/21 20:37
* @Description: TODO
**/
public class SynDemo {
private Object lock = new Object();
public void myOut() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(5000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
SynDemo synDemo = new SynDemo();
new Thread(() -> {
synDemo.myOut();
}).start();
new Thread(() -> {
synDemo.myOut();
}).start();
}
}
運行結果(下邊兩個結果的輸出間隔5秒鐘):
Thread-0
Thread-1
④、volatile關鍵字及其場景
- 能且僅能修飾變量
- 保證該變量的可見性,volatile關鍵字僅僅保證可見性,並不保證原子性(即一個線程對該變量做了改變後,volatile關鍵字會通知到其他線程說這個變量的值變了)
- 禁止指令重排序(後續會講)
eg:
A、B兩個線程同時讀取volatile關鍵字修飾的對象;
A讀取之後,修改了變量的值;
修改後的值,對B線程來說,是可見;
volatile關鍵字使用場景
1:作爲線程開關
2:單例,修飾對象實例,禁止指令重排序
作爲線程開關:
/**
* volatile關鍵字Demo
*/
public class VolatileDemo implements Runnable {
private static volatile boolean flag = true;
@Override
public void run() {
while (flag) {
System.out.println(Thread.currentThread().getName());
}
}
public static void main(String[] args) {
}
}
3.5、單例與線程安全
餓漢式–本身線程安全
在類加載的時候,就已經進行實例化,無論之後用不用到。如果該類比較佔內存,之後又沒用到,就白白浪費了資源。
懶漢式 – 最簡單的寫法是非線程安全的
在需要的時候再實例化
IDEA創建單例類:
自動生成如下代碼
package com.maltose.concurrence.demo;
/**
* @Author: sgw
* @Date 2019/4/21 21:12
* @Description: 餓漢式單例
* 在類加載的時候,就已經進行實例化,無論之後用不用的到。
* 如果該類比較佔內存,之後又沒用到,就白白浪費了資源。
**/
public class HungerSingleton {
private static HungerSingleton ourInstance = new HungerSingleton();
public static HungerSingleton getInstance() {
return ourInstance;
}
private HungerSingleton() {
}
}
package com.maltose.concurrence.demo;
/**
* @Author: sgw
* @Date 2019/4/21 21:12
* @Description: 餓漢式單例
* 在類加載的時候,就已經進行實例化,無論之後用不用到。
* 如果該類比較佔內存,之後又沒用到,就白白浪費了資源。
**/
public class HungerSingleton {
private static HungerSingleton ourInstance = new HungerSingleton();
public static HungerSingleton getInstance() {
return ourInstance;
}
private HungerSingleton() {
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
System.out.println(HungerSingleton.getInstance());
}).start();
}
}
}
結果:
com.maltose.concurrence.demo.HungerSingleton@47e3e4b5
com.maltose.concurrence.demo.HungerSingleton@47e3e4b5
com.maltose.concurrence.demo.HungerSingleton@47e3e4b5
com.maltose.concurrence.demo.HungerSingleton@47e3e4b5
com.maltose.concurrence.demo.HungerSingleton@47e3e4b5
com.maltose.concurrence.demo.HungerSingleton@47e3e4b5
com.maltose.concurrence.demo.HungerSingleton@47e3e4b5
com.maltose.concurrence.demo.HungerSingleton@47e3e4b5
com.maltose.concurrence.demo.HungerSingleton@47e3e4b5
com.maltose.concurrence.demo.HungerSingleton@47e3e4b5
可以看到,輸出的結果,都是同一個實例,說明只實例化了一次,是線程安全的;
懶漢式:最簡單的寫法是非線程安全的,想變爲安全的-----synchronized
package com.maltose.concurrence.demo;
/**
* @Author: sgw
* @Date 2019/4/21 21:20
* @Description: 懶漢式單例
* 在需要的時候再實例化
**/
public class LazySingleton {
// volatile :禁止指令重排序(一定要使用這個關鍵字修飾)
private static volatile LazySingleton lazySingleton = null;
private LazySingleton() {
}
public static LazySingleton getInstance() {
//判斷實例是否爲空,爲空則實例化
if (null == lazySingleton) {
//模擬實例化時耗時的操作
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
//爲了解決線程安全,這裏使用synchronized
synchronized (LazySingleton.class) {
if (null == lazySingleton) {
lazySingleton = new LazySingleton();
}
}
}
//否則直接返回
return lazySingleton;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
System.out.println(LazySingleton.getInstance());
}).start();
}
}
}
返回結果(同一個對象,線程安全):
com.maltose.concurrence.demo.LazySingleton@ae526cf
com.maltose.concurrence.demo.LazySingleton@ae526cf
com.maltose.concurrence.demo.LazySingleton@ae526cf
com.maltose.concurrence.demo.LazySingleton@ae526cf
com.maltose.concurrence.demo.LazySingleton@ae526cf
com.maltose.concurrence.demo.LazySingleton@ae526cf
com.maltose.concurrence.demo.LazySingleton@ae526cf
com.maltose.concurrence.demo.LazySingleton@ae526cf
com.maltose.concurrence.demo.LazySingleton@ae526cf
3.6、避免線程安全性問題
①、線程安全性問題成因
1:多線程環境;
2:多個線程操作同一共享資源;
3:對該共享資源進行了非原子性操作;
②、如何避免——打破成因中三點中任意一點即可
1:多線程環境--將多線程改爲單線程(在必要的代碼塊,進行加鎖(synchronized)訪問)
2:多個線程操作同一共享資源--不共享資源(各自操作屬於各自的資源,ThreadLocal(每個線程存自己的資源,大家不共享)、不共享、把一個操作變爲無狀態化、讓共享資源不可變--final修飾變量)
3:對該共享資源進行了非原子性操作-- 將非原子性操作改成原子性操作(加鎖、使用JDK自帶的原子性操作的類、JUC提供的相應的併發工具類等)
四、鎖
4.1、鎖的分類
1、自旋鎖
線程狀態及上下文切換消耗系統資源,當訪問共享資源的時間短,頻繁上下文切換不值得。jvm實現,使線程在沒獲得鎖的時候,
不被掛起,轉而執行空循環,循環幾次之後,如果還沒能獲得鎖,則被掛起
2、阻塞鎖
阻塞鎖改變了線程的運行狀態,讓線程進入阻塞狀態進行等待,當獲得相應的信號(喚醒或者時間)時,
纔可以進入線程的準備就緒狀態,轉爲就緒狀態的所有線程,通過競爭,進入運行狀態
3、重入鎖
支持線程再次進入的鎖,就跟我們有房間鑰匙,可以多次進入房間類似
4、讀寫鎖
兩把鎖,讀鎖跟寫鎖,寫寫互斥、讀寫互斥、讀讀共享
5、互斥鎖
上廁所,進門之後就把門關了,不讓其他人進來,如synchronized
6、悲觀鎖
總是假設最壞的情況,每次去拿數據的時候都認爲別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖
7、樂觀鎖
每次去拿數據的時候都認爲別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號等機制
8、公平鎖
大家都老老實實排隊,對大家而言都很公平
9、 非公平鎖
一部分人排着隊,但是新來的可能插隊
10、偏向鎖
偏向鎖使用了一種等到競爭出現才釋放鎖的機制,所以當其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖
11、獨佔鎖
獨佔鎖模式下,每次只能有一個線程能持有鎖
12、共享鎖
允許多個線程同時獲取鎖,併發訪問共享資源
4.2、Lock接口
1、使用方式
//Lock包:import java.util.concurrent.locks.Lock;
private static Lock lock = new ReentrantLock();
/**
* 每次調用對num進行++操作
*/
public static void inCreate() {
//獲得鎖
lock.lock();
num++;
//釋放鎖
lock.unlock();
}
2、lock與synchronized的區別
lock
獲取鎖與釋放鎖的過程,都需要程序員手動的控制;
Lock用的是樂觀鎖方式。所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操作,
如果因爲衝突失敗就重試,直到成功爲止。樂觀鎖實現的機制就是CAS操作
synchronized
synchronized託管給jvm執行
原始採用的是CPU悲觀鎖機制,即線程獲得的是獨佔鎖。獨佔鎖意味着其他線程只能依靠阻塞來等待線程釋放鎖。
查看Lock接口的實現類:
得到:
Lock接口的實現類如下
查看Lock接口的方法
Lock接口的方法如下
3、實現屬於自己的鎖
3.1自定義鎖,實現Lock接口
package com.maltose.concurrence.mylock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* @Author: sgw
* @Date 2019/5/16 21:54
* @Description: TODO
**/
public class MyLock implements Lock {
/**
* 鎖是否被持有了
*/
private boolean isHoldLock = false;
//持有鎖的線程
private Thread holdLockThread = null;
//重入的次數
private int reentryCount = 0;
/**
* 同一時刻,能且僅能有一個線程獲取到鎖,
* 其他線程,只能等待該線程釋放鎖之後才能獲取到鎖
*/
@Override
public synchronized void lock() {
//如果鎖被獲取了,並且不是當前線程獲取的,則當前線程就進行等待
if (isHoldLock && Thread.currentThread() != holdLockThread) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//線程第一次進來
holdLockThread = Thread.currentThread();
isHoldLock = true;
reentryCount++;
}
/**
* 釋放鎖
* 注意:手動添加synchronized
*/
@Override
public synchronized void unlock() {
//判斷當前線程是否是持有鎖的線程,是:重入次數減去1,不是就不做處理
if (Thread.currentThread() == holdLockThread) {
reentryCount--;
if (reentryCount == 0) {
//喚醒線程
notify();
//釋放鎖
isHoldLock = false;
}
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public Condition newCondition() {
return null;
}
}
測試上邊自定義的鎖
package com.maltose.concurrence.mylock;
import java.util.concurrent.locks.Lock;
/**
* @Author: sgw
* @Date 2019/5/16 22:24
* @Description: TODO
**/
public class ReentryDemo {
public Lock lock = new MyLock();
public void methodA() {
lock.lock();
System.out.println("進入方法A");
methodB();
lock.unlock();
}
public void methodB() {
lock.lock();
System.out.println("進入方法B");
lock.unlock();
}
public static void main(String[] args) {
ReentryDemo reentryDemo = new ReentryDemo();
reentryDemo.methodA();
}
}
輸出結果:
進入方法A
進入方法B
4、AbstractQueuedSynchronizer淺析
AbstractQueuedSynchronizer -- 爲實現依賴於先進先出 (FIFO) 等待隊列的阻塞鎖和相關同步器(信號量、事件,等等)提供一個框架。
此類的設計目標是成爲依靠單個原子 int 值來表示狀態的大多數同步器的一個有用基礎。
子類必須定義更改此狀態的受保護方法,並定義哪種狀態對於此對象意味着被獲取或被釋放。
假定這些條件之後,此類中的其他方法就可以實現所有排隊和阻塞機制。子類可以維護其他狀態字段,但只是爲了獲得同步而只追蹤使用 getState()、setState(int) 和 compareAndSetState(int, int) 方法來操作以原子方式更新的 int 值。
應該將子類定義爲非公共內部幫助器類,可用它們來實現其封閉類的同步屬性。類 AbstractQueuedSynchronizer 沒有實現任何同步接口。而是定義了諸如 acquireInterruptibly(int) 之類的一些方法,在適當的時候可以通過具體的鎖和相關同步器來調用它們,以實現其公共方法。
此類支持默認的獨佔 模式和共享 模式之一,或者二者都支持。處於獨佔模式下時,其他線程試圖獲取該鎖將無法取得成功。在共享模式下,多個線程獲取某個鎖可能(但不是一定)會獲得成功。此類並不“瞭解”這些不同,除了機械地意識到當在共享模式下成功獲取某一鎖時,下一個等待線程(如果存在)也必須確定自己是否可以成功獲取該鎖。處於不同模式下的等待線程可以共享相同的 FIFO 隊列。通常,實現子類只支持其中一種模式,但兩種模式都可以在(例如)ReadWriteLock 中發揮作用。只支持獨佔模式或者只支持共享模式的子類不必定義支持未使用模式的方法。
此類通過支持獨佔模式的子類定義了一個嵌套的 AbstractQueuedSynchronizer.ConditionObject 類,可以將這個類用作 Condition 實現。isHeldExclusively() 方法將報告同步對於當前線程是否是獨佔的;使用當前 getState() 值調用 release(int) 方法則可以完全釋放此對象;如果給定保存的狀態值,那麼 acquire(int) 方法可以將此對象最終恢復爲它以前獲取的狀態。沒有別的 AbstractQueuedSynchronizer 方法創建這樣的條件,因此,如果無法滿足此約束,則不要使用它。AbstractQueuedSynchronizer.ConditionObject 的行爲當然取決於其同步器實現的語義。
此類爲內部隊列提供了檢查、檢測和監視方法,還爲 condition 對象提供了類似方法。可以根據需要使用用於其同步機制的 AbstractQueuedSynchronizer 將這些方法導出到類中。
此類的序列化只存儲維護狀態的基礎原子整數,因此已序列化的對象擁有空的線程隊列。需要可序列化的典型子類將定義一個 readObject 方法,該方法在反序列化時將此對象恢復到某個已知初始狀態。
tryAcquire(int)
tryRelease(int)
tryAcquireShared(int)
tryReleaseShared(int)
isHeldExclusively()
Acquire:
while (!tryAcquire(arg)) {
enqueue thread if it is not already queued;
possibly block current thread;
}
Release:
if ((arg))
unblock the first queued thread;
5、深入剖析ReentrantLock源碼之公平鎖的實現
公平鎖與非公平鎖的區別:
公平鎖:顧名思義--公平,大家老老實實排隊等候;
非公平鎖:只要有機會,就先嚐試搶佔資源;
公平鎖與非公平鎖其實有點像在公廁上廁所。公平鎖遵守排隊的規則,只要前面有人在排隊,那麼剛進來的就老老實實排隊。而非
公平鎖就有點流氓,只要當前茅坑沒人,它就佔了那個茅坑,不管後面的人排了多久。
非公平鎖的弊端:
可能導致後面排隊等待的線程等不到相應的cpu資源,從而引起線程飢餓;
/**
* @Author: sgw
* @Date 2019/5/22 20:59
* @Description: ReentrantLock源碼分析
**/
public class ReentraintLockDemo {
public static void main(String[] args) {
//true:公平鎖
ReentrantLock reentrantLock = new ReentrantLock(true);
reentrantLock.lock();
reentrantLock.unlock();
}
}
6、線程執行順序之多線程debug
package com.maltose.concurrence.demo;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author: sgw
* @Date 2019/5/26 10:27
* @Description: TODO
**/
public class ReentrantLockDebugDemo {
private int i = 0;
private ReentrantLock reentrantLock = new ReentrantLock();
public void inCreate() {
//獲取鎖
reentrantLock.lock();
try {
i++;
System.out.println(i);
} finally {
//釋放鎖,釋放鎖的操作一定要放在finally裏
reentrantLock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockDebugDemo reentrantLockDebugDemo = new ReentrantLockDebugDemo();
//啓動三個線程
for (int i = 0; i < 3; i++) {
new Thread(() -> {
//三個線程同時訪問inCreate方法
reentrantLockDebugDemo.inCreate();
}).start();
}
}
}
debug:
F8下一步:
上邊現象出現的原因:
剛一點擊debug運行的時候,就有線程開始運行了,只有其中一個線程停在了斷點的位置,
所以i的值顯示不是0;所以,多線程的時候這樣debug是不對的;
使用IDEA對多線程進行debug的步驟:
1、首先需要在代碼裏打上斷點
2、進行相關設置
再次debug運行該代碼:
F9:
現在運行出的結果就是我們想要的了;
7、讀寫鎖特性及ReentrantReadWriteLock的使用
特性:
寫寫互斥、讀寫互斥、讀讀共享
鎖降級:寫線程獲取寫入鎖後可以獲取讀取鎖,然後釋放寫入鎖,這樣就從寫入鎖變成了讀取鎖,從而實現鎖降級的特性。
package com.maltose.concurrence.demo;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* @Author: sgw
* @Date 2019/5/26 11:06
* @Description: ReentrantReadWriteLock的使用
**/
public class ReentrantReadWriteLockDemo {
private int i = 0;
private int j = 0;
public void out(){
System.out.println(Thread.currentThread().getName()+"i的值====》"+i + "j的值====》"+j);
}
public void inCreate() {
i++;
try {
Thread.sleep(500L);
} catch (InterruptedException e) {
e.printStackTrace();
}
j++;
}
public static void main(String[] args) {
ReentrantReadWriteLockDemo reentrantReadWriteLockDemo = new ReentrantReadWriteLockDemo();
for (int i = 0; i < 3; i++) {
new Thread(()->{
reentrantReadWriteLockDemo.inCreate();
reentrantReadWriteLockDemo.out();
}).start();
}
}
}
結果:
結果分析:
i已經執行了i++操作,但是由於sleep了(耗時操作),j還沒有執行j++,j的值就被讀的線程給讀取了;
優化上邊的代碼——讀寫鎖:
代碼:
package com.maltose.concurrence.demo;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* @Author: sgw
* @Date 2019/5/26 11:06
* @Description: ReentrantReadWriteLock的使用
**/
public class ReentrantReadWriteLockDemo {
private int i = 0;
private int j = 0;
//創建讀寫鎖
private ReadWriteLock lock = new ReentrantReadWriteLock();
//獲取讀鎖對象
Lock readLock = lock.readLock();
//獲取寫鎖對象
Lock writeLock = lock.writeLock();
public void out(){
//獲取讀鎖
readLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"i的值====》"+i + "j的值====》"+j);
}finally {
//釋放讀鎖
readLock.unlock();
}
}
public void inCreate() {
//獲取寫鎖
writeLock.lock();
try {
i++;
Thread.sleep(500L);
j++;
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//釋放寫鎖
writeLock.unlock();
}
}
public static void main(String[] args) {
ReentrantReadWriteLockDemo reentrantReadWriteLockDemo = new ReentrantReadWriteLockDemo();
for (int i = 0; i < 3; i++) {
new Thread(()->{
reentrantReadWriteLockDemo.inCreate();
reentrantReadWriteLockDemo.out();
}).start();
}
}
}
結果:
8、鎖降級
鎖降級
寫線程獲取寫入鎖後可以獲取讀取鎖,然後釋放寫入鎖,這樣就從寫入鎖變成了讀取鎖,從而實現鎖降級的特性。
寫寫互斥、讀寫互斥、讀讀共享
注意點:
鎖降級之後,寫鎖並不會直接降級成讀鎖,不會隨着讀鎖的釋放而釋放,因此需要顯式地釋放寫鎖
鎖降級的應用場景
用於對數據比較敏感,需要在對數據修改之後,獲取到修改後的值,並進行接下來的其他操作
eg:
package com.maltose.concurrence.demo;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* @Author: sgw
* @Date 2019/6/24 21:50
* @Description: 鎖降級
**/
public class LockDegrade {
public static void main(String[] args) {
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
Lock readLock = reentrantReadWriteLock.readLock();
Lock writeLock = reentrantReadWriteLock.writeLock();
/*
* 寫寫互斥、讀寫互斥、讀讀共享
* */
writeLock.lock();
//寫線程獲取讀取鎖
readLock.lock();
//寫線程釋放寫入鎖,變爲讀取鎖,實現鎖降級
writeLock.unlock();
readLock.unlock();
System.out.println("程序運行結束");
}
}
鎖降級使用場景:
package com.maltose.concurrence.demo;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* @Author: sgw
* @Date 2019/6/24 22:01
* @Description: 鎖降級使用場景
**/
public class LockDegradeDemo {
private int i = 0;
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
Lock readLock = readWriteLock.readLock();
Lock writeLock = readWriteLock.writeLock();
public void doSomething() {
//獲得寫入所
writeLock.lock();
try {
i++;
//在沒有釋放寫鎖之前,先獲取讀鎖,讀取到數據
readLock.lock();
} finally {
//釋放寫入所,做到鎖降級
writeLock.unlock();
}
try {
//模擬其他複雜的操作
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
if (i == 1) {
System.out.println("i的值是======》1");
} else {
System.out.println("i的值是" + i);
}
} finally {
readLock.unlock();
}
}
public static void main(String[] args) {
LockDegradeDemo lockDegradeDemo = new LockDegradeDemo();
for (int i = 0; i < 4; i++) {
new Thread(() -> {
lockDegradeDemo.doSomething();
}).start();
}
}
}
運行結果:(得到的數據是當前線程修改後的值)
i的值是======》1
i的值是2
i的值是3
i的值是4
9、StampedLock原理及使用
StampedLock是jdk1.8新加的;1.8之前,鎖已經那麼多了,爲什麼還要有StampedLock
一般應用,都是讀多寫少,ReentrantReadWriteLock 因讀寫互斥,故讀時阻塞寫,因而性能上上不去。可能會使寫線程飢餓;
StampedLock的特點
所有獲取鎖的方法,都返回一個郵戳(Stamp),Stamp爲0表示獲取失敗,其餘都表示成功;
所有釋放鎖的方法,都需要一個郵戳(Stamp),這個Stamp必須是和成功獲取鎖時得到的Stamp一致;
StampedLock是不可重入的;(如果一個線程已經持有了寫鎖,再去獲取寫鎖的話就會造成死鎖)
支持鎖升級跟鎖降級
可以樂觀讀也可以悲觀讀
使用有限次自旋,增加鎖獲得的機率,避免上下文切換帶來的開銷
樂觀讀不阻塞寫操作,悲觀讀,阻塞寫得操作
StampedLock的優點
相比於ReentrantReadWriteLock,吞吐量大幅提升
StampedLock的缺點
api相對複雜,容易用錯
內部實現相比於ReentrantReadWriteLock複雜得多
StampedLock的原理
每次獲取鎖的時候,都會返回一個郵戳(stamp),相當於mysql裏的version字段
釋放鎖的時候,再根據之前的獲得的郵戳,去進行鎖釋放
使用stampedLock注意點
如果使用樂觀讀,一定要判斷返回的郵戳是否是一開始獲得到的,如果不是,要去獲取悲觀讀鎖,再次去讀取
eg:
import java.util.concurrent.locks.StampedLock;
/**
* StampedLock Demo
*/
public class StampedLockDemo {
// 成員變量
private double x, y;
// 鎖實例
private final StampedLock sl = new StampedLock();
// 排它鎖-寫鎖(writeLock)
void move(double deltaX, double deltaY) {
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
// 樂觀讀鎖
double distanceFromOrigin() {
// 嘗試獲取樂觀讀鎖(1)
long stamp = sl.tryOptimisticRead();
// 將全部變量拷貝到方法體棧內(2)
double currentX = x, currentY = y;
// 檢查在(1)獲取到讀鎖票據後,鎖有沒被其他寫線程排它性搶佔(3)
if (!sl.validate(stamp)) {
// 如果被搶佔則獲取一個共享讀鎖(悲觀獲取)(4)
stamp = sl.readLock();
try {
// 將全部變量拷貝到方法體棧內(5)
currentX = x;
currentY = y;
} finally {
// 釋放共享讀鎖(6)
sl.unlockRead(stamp);
}
}
// 返回計算結果(7)
return Math.sqrt(currentX * currentX + currentY * currentY);
}
// 使用悲觀鎖獲取讀鎖,並嘗試轉換爲寫鎖
void moveIfAtOrigin(double newX, double newY) {
// 這裏可以使用樂觀讀鎖替換(1)
long stamp = sl.readLock();
try {
// 如果當前點在原點則移動(2)
while (x == 0.0 && y == 0.0) {
// 嘗試將獲取的讀鎖升級爲寫鎖(3)
long ws = sl.tryConvertToWriteLock(stamp);
// 升級成功,則更新票據,並設置座標值,然後退出循環(4)
if (ws != 0L) {
stamp = ws;
x = newX;
y = newY;
break;
} else {
// 讀鎖升級寫鎖失敗則釋放讀鎖,顯示獲取獨佔寫鎖,然後循環重試(5)
sl.unlockRead(stamp);
stamp = sl.writeLock();
}
}
} finally {
// 釋放鎖(6)
sl.unlock(stamp);
}
}
}
五、線程間的通信
5.1、wait、notify、notifyAll
何時使用
在多線程環境下,有時候一個線程的執行,依賴於另外一個線程的某種狀態的改變,這個時候,我們就可以使用wait與notify或者notifyAll
wait跟sleep的區別
wait會釋放持有的鎖,而sleep不會,sleep只是讓線程在指定的時間內,不去搶佔cpu的資源
注意點
wait notify必須放在同步代碼塊中, 且必須擁有當前對象的鎖,即不能取得A對象的鎖,而調用B對象的wait
哪個對象wait,就得調哪個對象的notify
notify跟notifyAll的區別
nofity隨機喚醒一個等待的線程
notifyAll喚醒所有在該對象上等待的線程
eg:
package com.maltose.concurrence.demo;
/**
* @Author: sgw
* @Date 2019/6/25 22:14
* @Description: TODO
**/
public class Demo1 {
private static volatile boolean flag = false;
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
new Thread(()->{
while (!flag) {
synchronized (obj) {
try {
System.out.println("flag is false");
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println("flag is true");
}).start();
Thread.sleep(1000L);
new Thread(()->{
flag = true;
synchronized (obj) {
obj.notifyAll();
}
}).start();
}
}
輸出結果:
flag is false
flag is true
5.3、等待通知經典模型之生產者消費者
中間商:
package com.maltose.concurrence.demo2;
/**
* @Author: sgw
* @Date 2019/7/7 10:03
* @Description: 中間商
**/
public class Medium {
/**
* 當前庫存容量(默認是0)
*/
private int num = 0;
/**
* 最大庫存容量、
*/
private static final int TOTAL = 20;
/**
* 接收生產數據
*/
public synchronized void put() {
//判斷當前的庫存,是否已經是最大的庫存容量
if (num < TOTAL) {
//如果不是,則生產完成之後,通知消費者進行消費
System.out.println("新增庫存-------->當前庫存" + ++num);
try {
Thread.sleep(500L);
} catch (InterruptedException e) {
e.printStackTrace();
}
notifyAll();
} else {
//如果是,則通知生產者進行等待
try {
System.out.println("新增庫存-------->庫存已滿" + num);
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 獲取消費數據
*/
public synchronized void take() {
//判斷當前庫存是否不足
if (num > 0) {
//如果充足,在消費完成之後通知生產者進行生產
System.out.println("消費庫存-------->當前庫存容量" + --num);
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
notifyAll();
} else {
//如果不足,通知消費者暫停消費
System.out.println("消費庫存-------->庫存不足" + num);
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
生產者:
package com.maltose.concurrence.demo2;
/**
* @Author: sgw
* @Date 2019/7/7 10:04
* @Description: 生產者
**/
public class Producer implements Runnable {
/**
* 不能new中間商,因爲消費者也要使用中間商,如果new的話,消費者與生產者用的就不是同一個中間商了
*/
private Medium medium;
public Producer(Medium medium) {
this.medium = medium;
}
@Override
public void run() {
//一直不停的生產數據,往中間商裏放數據
while (true) {
medium.put();
}
}
}
消費者:
package com.maltose.concurrence.demo2;
/**
* @Author: sgw
* @Date 2019/7/7 10:05
* @Description: 消費者
**/
public class Consumer implements Runnable {
private Medium medium;
public Consumer(Medium medium) {
this.medium = medium;
}
@Override
public void run() {
//一直不停的從中間商取數據
while (true) {
medium.take();
}
}
}
測試類:
package com.maltose.concurrence.demo2;
/**
* @Author: sgw
* @Date 2019/7/7 10:05
* @Description: 啓動類
**/
public class Main {
public static void main(String[] args) {
Medium medium = new Medium();
//模擬多個消費者去消費
new Thread(new Consumer(medium)).start();
new Thread(new Consumer(medium)).start();
new Thread(new Consumer(medium)).start();
//模擬多個生產者來生產數據
new Thread(new Producer(medium)).start();
new Thread(new Producer(medium)).start();
new Thread(new Producer(medium)).start();
new Thread(new Producer(medium)).start();
new Thread(new Producer(medium)).start();
}
}
啓動測試類的輸出結果:
消費庫存-------->庫存不足0
消費庫存-------->庫存不足0
消費庫存-------->庫存不足0
新增庫存-------->當前庫存1
新增庫存-------->當前庫存2
消費庫存-------->當前庫存容量1
消費庫存-------->當前庫存容量0
消費庫存-------->庫存不足0
消費庫存-------->庫存不足0
新增庫存-------->當前庫存1
新增庫存-------->當前庫存2
新增庫存-------->當前庫存3
新增庫存-------->當前庫存4
新增庫存-------->當前庫存5
新增庫存-------->當前庫存6
新增庫存-------->當前庫存7
新增庫存-------->當前庫存8
新增庫存-------->當前庫存9
新增庫存-------->當前庫存10
新增庫存-------->當前庫存11
新增庫存-------->當前庫存12
新增庫存-------->當前庫存13
新增庫存-------->當前庫存14
新增庫存-------->當前庫存15
新增庫存-------->當前庫存16
新增庫存-------->當前庫存17
新增庫存-------->當前庫存18
新增庫存-------->當前庫存19
新增庫存-------->當前庫存20
新增庫存-------->庫存已滿20
新增庫存-------->庫存已滿20
新增庫存-------->庫存已滿20
消費庫存-------->當前庫存容量19
消費庫存-------->當前庫存容量18
消費庫存-------->當前庫存容量17
消費庫存-------->當前庫存容量16
5.4、使用管道流進行通信
簡介:
以內存爲媒介,用於線程之間的數據傳輸。
主要有面向字節:【PipedOutputStream、PipedInputStream】
面向字符:【PipedReader、PipedWriter】
讀取另一個線程數據的類:
package com.maltose.concurrence.demo3;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PipedInputStream;
import java.util.stream.Collectors;
/**
* @Author: sgw
* @Date 2019/7/7 10:35
* @Description: 讀取其他線程的數據
**/
public class Reader implements Runnable {
private PipedInputStream pipedInputStream;
public Reader(PipedInputStream pipedInputStream) {
this.pipedInputStream = pipedInputStream;
}
@Override
public void run() {
//獲取從另一個管道傳進來的數據
if (pipedInputStream != null) {
String collect;
collect = new BufferedReader(new InputStreamReader(pipedInputStream)).lines().collect(Collectors.joining("\n"));
System.out.println(Thread.currentThread().getName() + collect);
}
try {
pipedInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
主線程來生產數據,讓上邊的線程獲取到本線程的數據:
package com.maltose.concurrence.demo3;
import java.io.*;
/**
* @Author: sgw
* @Date 2019/7/7 10:40
* @Description: 測試管道通信
**/
public class Main {
public static void main(String[] args) throws IOException {
PipedInputStream pipedInputStream = new PipedInputStream();
PipedOutputStream pipedOutputStream = new PipedOutputStream();
pipedOutputStream.connect(pipedInputStream);
new Thread(new Reader(pipedInputStream)).start();
BufferedReader bufferedReader = null;
try {
//獲取控制檯裏的輸入值
bufferedReader = new BufferedReader(new InputStreamReader(System.in));
//向管道里寫入從控制檯獲取到的數據
pipedOutputStream.write(bufferedReader.readLine().getBytes());
} finally {
pipedOutputStream.close();
if (bufferedReader != null) {
bufferedReader.close();
}
}
}
}
結果(在控制檯輸入數據,被另一個線程獲取到了數據):
meltose(控制檯手動輸入的數據)
Thread-0meltose(另一個線程獲取到的數據)
5.5、 Thread.join通信及其源碼淺析
使用場景:
線程A執行到一半,需要一個數據,這個數據需要線程B去執行修改,只有B修改完成之後,A才能繼續操作;
線程A的run方法裏面,調用線程B的join方法,這個時候,線程A會等待線程B運行完成之後,再接着運行;
測試代碼:
package com.maltose.concurrence.demo4;
/**
* @Author: sgw
* @Date 2019/7/7 10:54
* @Description: Thread.join通信
**/
public class Main {
public static void main(String[] args) {
//啓動一個線程,只做打印操作
Thread thread = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "開始運行");
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "結束運行");
}, "線程1");
//啓動另一個線程,
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "開始運行");
//該線程在運行中間,讓線程一運行,必須調線程一的join()方法
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "運行結束");
}, "線程2").start();
}
}
結果:
線程2開始運行
線程1開始運行
線程1結束運行
線程2運行結束
5.6、ThreadLocal的使用
是一個線程變量,是一個以ThreadLocal對象爲鍵、任意對象爲值的存儲結構。爲每個線程單獨存放一份變量副本,也就是說一個線程可以根據一個ThreadLocal對象查詢到綁定在這個線程上的一個值。
只要線程處於活動狀態並且ThreadLocal實例可訪問,那麼每個線程都擁有對其本地線程副本的隱式引用變量一個線程消失後,它的所有副本線程局部實例受垃圾回收(除非其他存在對這些副本的引用)
一般用的比較多的是
1、ThreadLocal.get: 獲取ThreadLocal中當前線程共享變量的值。
2、ThreadLocal.set: 設置ThreadLocal中當前線程共享變量的值。
3、ThreadLocal.remove: 移除ThreadLocal中當前線程共享變量的值。
4、ThreadLocal.initialValue: ThreadLocal沒有被當前線程賦值時或當前線程剛調用remove方法後調用get方法,返回此方法值。
代碼:
package com.maltose.concurrence.demo5;
/**
* @Author: sgw
* @Date 2019/7/7 16:21
* @Description: ThreadLocal學習
**/
public class ThreadLocalDemo {
/**
* 定義變量,ThreadLocal爲每一個線程單獨存放了一個該變量的副本;
*/
ThreadLocal<Integer> num = ThreadLocal.withInitial(() -> 0);
/**
* 自增並輸出num的值
*/
public void inCreate() {
Integer myNum = num.get();
myNum++;
System.out.println(Thread.currentThread().getName() + "----------->" + myNum);
num.set(myNum);
}
/**
* 總共有兩個線程,主線程和新new的線程
* @param args
*/
public static void main(String[] args) {
ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();
for (int i = 1; i < 3; i++) {
int finalI = i;
new Thread(() -> {
while (true) {
threadLocalDemo.inCreate();
try {
Thread.sleep(finalI * 1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
}
結果:
Thread-1----------->1
Thread-0----------->1
Thread-0----------->2
Thread-1----------->2
Thread-0----------->3
Thread-0----------->4
Thread-1----------->3
Thread-0----------->5
Thread-0----------->6
Thread-1----------->4
Thread-0----------->7
Thread-0----------->8
Thread-1----------->5
Thread-0----------->9
Thread-0----------->10
Thread-1----------->6
Thread-0----------->11
Thread-0----------->12
結果說明ThreadLocal爲每一個線程單獨存放了一個變量的副本;
5.7、Condition的使用
可以在一個鎖裏面,存在多種等待條件;
主要的方法:
await
signal
signalAll
對前邊中間商代碼改造如下:
package com.maltose.concurrence.demo6;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author: sgw
* @Date 2019/7/7 16:44
* @Description: Condition
**/
public class Medium {
private int num = 0;
private static final int TOTAL = 20;
private Lock lock = new ReentrantLock();
private Condition consumerCondition = lock.newCondition();
private Condition producerCondition = lock.newCondition();
/**
* 接收生產數據
*/
public void put() {
lock.lock();
try {
//判斷當前庫存,是否已經是最大的庫存容量,
if (num < TOTAL) {
System.out.println("新增庫存---------> 當前庫存:" + ++num);
// 如果不是,生產完成之後,通知消費者進行消費
try {
Thread.sleep(500L);
} catch (InterruptedException e) {
e.printStackTrace();
}
consumerCondition.signalAll();
} else {
// 如果是,則通知生產者進行等待,
try {
System.out.println("新增庫存---------> 庫存已滿:" + num);
producerCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
lock.unlock();
}
}
/**
* 獲取消費數據
*/
public void take() {
lock.lock();
try {
//判斷當前庫存是否不足
if (num > 0) {
//如果充足,在消費完成之後,通知生產者進行生產
System.out.println("消費庫存------> 當前庫存容量" + --num);
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
producerCondition.signalAll();
} else {
//如果不足,通知消費者暫停消費
try {
System.out.println("消費庫存---------> 庫存不足:" + num);
consumerCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
lock.unlock();
}
}
}
六、原子類
6.1、什麼是原子類
一度認爲原子是不可分割的最小單位,故原子類可以認爲其操作都是不可分割
爲什麼要有原子類?
對多線程訪問同一個變量,我們需要加鎖,而鎖是比較消耗性能的,JDk1.5之後,
新增的原子操作類提供了一種用法簡單、性能高效、線程安全地更新一個變量的方式,
這些類同樣位於JUC包下的atomic包下,發展到JDk1.8,該包下共有17個類,
囊括了原子更新基本類型、原子更新數組、原子更新屬性、原子更新引用
jdk1.8新增的原子類:
DoubleAccumulator、DoubleAdder、LongAccumulator、LongAdder、Striped64
6.2、 原子更新基本類型
發展至JDk1.8,基本類型原子類有以下幾個:
AtomicBoolean、AtomicInteger、AtomicLong、DoubleAccumulator、DoubleAdder、
LongAccumulator、LongAdder
大致可以歸爲3類:
AtomicBoolean、AtomicInteger、AtomicLong 元老級的原子更新,方法幾乎一模一樣
DoubleAdder、LongAdder 對Double、Long的原子更新性能進行優化提升
DoubleAccumulator、LongAccumulator 支持自定義運算
原子類AtomicInteger實例:
package com.maltose.concurrence.atomic.demo1;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @Author: sgw
* @Date 2019/7/7 17:06
* @Description: AtomicInteger
**/
public class Demo1 {
private static AtomicInteger sum = new AtomicInteger(0);
public static void inCreate() {
//自增後獲取相應結果
sum.incrementAndGet();
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
for (int j = 0; j < 100; j++) {
inCreate();
System.out.println(sum);
}
}).start();
}
}
}
結果:
1
2
3
4
5
6
.....
997
998
999
1000
原子類LongAccumulator實例
package com.maltose.concurrence.atomic.demo1;
import java.util.concurrent.atomic.LongAccumulator;
/**
* @Author: sgw
* @Date 2019/7/7 17:07
* @Description: LongAccumulator
**/
public class Demo2 {
public static void main(String[] args) {
//輸入一個數字,如果比上一個輸入的大,則直接返回,如果小,則返回上一個
LongAccumulator longAccumulator = new LongAccumulator((left, right) ->
left > right ? left:right, 4L
);
//模擬自己輸入的數字
longAccumulator.accumulate(3L);
System.out.println(longAccumulator.get());
longAccumulator.accumulate(5L);
System.out.println(longAccumulator.get());
}
}
結果:
4
5
代碼:
package com.maltose.concurrence.atomic.demo1;
import java.util.concurrent.atomic.LongAccumulator;
/**
* @Author: sgw
* @Date 2019/7/7 17:07
* @Description: LongAccumulator
**/
public class Demo2 {
public static void main(String[] args) {
//輸入一個數字,返回相乘的結果
LongAccumulator longAccumulator = new LongAccumulator((left, right) ->
left * right, 3L
);
/* LongAccumulator longAccumulator = new LongAccumulator((left, right) ->
left > right ? left:right, 4L
);*/
//模擬自己輸入的數字
longAccumulator.accumulate(3L);
System.out.println(longAccumulator.get());
longAccumulator.accumulate(5L);
System.out.println(longAccumulator.get());
}
}
結果:
9
45
6.3、原子更新數組類型
AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
代碼:
package com.maltose.concurrence.atomic.demo1;
import java.util.concurrent.atomic.AtomicIntegerArray;
/**
* @Author: sgw
* @Date 2019/7/7 17:46
* @Description: AtomicIntegerArray
**/
public class AtomicIntegerArrayDemo {
public static void main(String[] args) {
int[] arr = new int[]{3, 2};
AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(arr);
//先自加,再獲取;下邊的參數意思:給第1個元素(數組裏的第二個值)加8
System.out.println(atomicIntegerArray.addAndGet(1, 8));
}
}
結果:
10
代碼2:
package com.maltose.concurrence.atomic.demo1;
import java.util.concurrent.atomic.AtomicIntegerArray;
/**
* @Author: sgw
* @Date 2019/7/7 17:46
* @Description: AtomicIntegerArray
**/
public class AtomicIntegerArrayDemo {
public static void main(String[] args) {
int[] arr = new int[]{3, 2};
AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(arr);
//先自加,再獲取;下邊的參數意思:給第1個元素(數組裏的第二個值)加8
// System.out.println(atomicIntegerArray.addAndGet(1, 8));
//支持自定義的計算:數組下標從0開始,數組裏每一個值與2比較,
int i = atomicIntegerArray.accumulateAndGet(0, 2, (left, right) ->
left * right / 3
);
System.out.println(i);
}
}
結果:
2
6.4、原子地更新屬性
簡介:
原子地更新某個類裏的某個字段時,就需要使用原子更新字段類,Atomic包提供了以下4個類進行原子字段更新:
AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicStampedReference、AtomicReferenceFieldUpdater
案例1:
package com.maltose.concurrence.atomic.demo1;
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
/**
* @Author: sgw
* @Date 2019/8/3 9:03
* @Description: AtomicLongFieldUpdate的使用
**/
public class AtomicLongFieldUpdaterDemo {
public static void main(String[] args) {
AtomicLongFieldUpdater<Student> longFieldUpdater = AtomicLongFieldUpdater.newUpdater(Student.class, "id");
Student maltose = new Student(1L, "maltose");
//參數一:要更新的對象 參數二:要更新的字段開始值是1 參數三:要更新爲的值(把id是1改爲100)
longFieldUpdater.compareAndSet(maltose, 1L, 100L);
System.out.println("id=" + maltose.getId());
}
}
class Student {
/**
* 注意 要原子更新的字段 必須使用volatile關鍵字修飾,不然會報錯
*/
volatile long id;
volatile String name;
public Student(Long id, String name) {
this.id = id;
this.name = name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
結果:
id=100
案例2:
package com.maltose.concurrence.atomic.demo1;
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
/**
* @Author: sgw
* @Date 2019/8/3 9:03
* @Description: TODO
**/
public class AtomicLongFieldUpdaterDemo {
public static void main(String[] args) {
AtomicLongFieldUpdater<Student> longFieldUpdater = AtomicLongFieldUpdater.newUpdater(Student.class, "id");
Student maltose = new Student(1L, "maltose");
//參數二:要更改的字段的數據類型
AtomicReferenceFieldUpdater<Student, String> referenceFieldUpdater = AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "name");
referenceFieldUpdater.compareAndSet(maltose, "maltose", "DFmaltose");
System.out.println("name=" + maltose.getName());
}
}
class Student {
/**
* 注意 要原子更新的字段 必須使用volatile關鍵字修飾,不然會報錯
*/
volatile long id;
volatile String name;
public Student(Long id, String name) {
this.id = id;
this.name = name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
結果:
name=DFmaltose
使用上述四個類進行原子更新的注意事項:
要更新的字段必須是volatile修飾的,在線程之間共享變量時保證立即可見;
字段的描述類型是與調用者與操作對象字段的關係一致;
也就是說調用者能夠直接操作對象字段,那麼就可以反射進行原子操作。
對於父類的字段,子類是不能直接操作的,儘管子類可以訪問父類的字段。
只能是實例變量,不能是類變量,也就是說要更新的字段不能加static關鍵字。
只能是可修改變量,不能使final變量,因爲final的語義就是不可修改。
對於AtomicIntegerFieldUpdater和AtomicLongFieldUpdater只能修改int/long類型的字段,不能修改其包裝類型(Integer/Long)。
如果要修改包裝類型就需要使用AtomicReferenceFieldUpdater。
6.5、原子地更新引用
簡介:
AtomicReference:用於對引用的原子更新
AtomicMarkableReference:帶版本戳的原子引用類型,版本戳爲boolean類型。
AtomicStampedReference:帶版本戳的原子引用類型,版本戳爲int類型。
代碼:
package com.maltose.concurrence.atomic.demo1;
import java.util.concurrent.atomic.AtomicReference;
/**
* @Author: sgw
* @Date 2019/8/3 9:27
* @Description: 原子更新引用
**/
public class AtomicReferenceDemo {
public static void main(String[] args) {
AtomicReference<Student> studentAtomicReference = new AtomicReference<>();
Student student = new Student(1L, "maltose");
Student student1 = new Student(2L, "DFmaltose");
//必須先設置進去一個引用,之後才能操作
studentAtomicReference.set(student);
//原子更新引用
studentAtomicReference.compareAndSet(student, student1);
//必須使用這個get方法去獲得更新後的數據
Student student2 = studentAtomicReference.get();
System.out.println(student2.getName());
}
}
class Student {
private long id;
private String name;
public Student(long id, String name) {
this.id = id;
this.name = name;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
結果:
DFmaltose
七、容器
7.1、同步容器與併發容器簡介
同步容器:
Vector、HashTable -- JDK提供的同步容器類
Collections.synchronizedXXX 本質是對相應的容器進行包裝
同步容器類的缺點:
在單獨使用裏面的方法的時候,可以保證線程安全,但是,複合操作需要額外加鎖來保證線程安全;
使用Iterator迭代容器或使用使用for-each遍歷容器,在迭代過程中修改容器會拋出ConcurrentModificationException異常。
想要避免出現ConcurrentModificationException,就必須在迭代過程持有容器的鎖。但是若容器較大,則迭代的時間也會較長。
那麼需要訪問該容器的其他線程將會長時間等待。從而會極大降低性能。
若不希望在迭代期間對容器加鎖,可以使用"克隆"容器的方式。使用線程封閉,由於其他線程不會對容器進行修改,可以避免ConcurrentModificationException。但是在創建副本的時候,存在較大性能開銷。
toString,hashCode,equalse,containsAll,removeAll,retainAll等方法都會隱式的Iterate,也即可能拋出ConcurrentModificationException。
併發容器
CopyOnWrite、Concurrent、BlockingQueue三大系列,用來應對高併發的情況;
根據具體場景進行設計,儘量避免使用鎖,提高容器的併發訪問性。
ConcurrentBlockingQueue:基於queue實現的FIFO的隊列。隊列爲空,取操作會被阻塞
ConcurrentLinkedQueue:基於queue實現的FIFO的隊列。隊列爲空,取得時候就直接返回空
7.2、同步容器
案例1(以Vector爲例):
package com.maltose.concurrence.atomic.demo1;
import java.util.Iterator;
import java.util.Vector;
/**
* @Author: sgw
* @Date 2019/8/3 9:48
* @Description: TODO
**/
public class VectorDemo {
public static void main(String[] args) {
Vector<String> stringVector = new Vector<>();
//添加元素
for (int i = 0; i < 1000; i++) {
stringVector.add("demo" + i);
}
//錯誤遍歷
stringVector.forEach(e->{
if (e.equals("demo3")) {
stringVector.remove(e);
}
System.out.println(e);
});
}
}
報錯:
demo0
Exception in thread "main" java.util.ConcurrentModificationException
demo1
at java.util.Vector.forEach(Vector.java:1278)
demo2
demo3
at com.maltose.concurrence.atomic.demo1.VectorDemo.main(VectorDemo.java:20)
單線程下正確遍歷:
package com.maltose.concurrence.atomic.demo1;
import java.util.Iterator;
import java.util.Vector;
/**
* @Author: sgw
* @Date 2019/8/3 9:48
* @Description: TODO
**/
public class VectorDemo {
public static void main(String[] args) {
Vector<String> stringVector = new Vector<>();
//添加元素
for (int i = 0; i < 1000; i++) {
stringVector.add("demo" + i);
}
//單線程下的正確迭代
Iterator<String> stringIterator = stringVector.iterator();
while (stringIterator.hasNext()) {
String next = stringIterator.next();
if (next.equals("demo2")) {
stringIterator.remove();
}
}
}
}
多線程下正確遍歷:
package com.maltose.concurrence.atomic.demo1;
import java.util.Iterator;
import java.util.Vector;
/**
* @Author: sgw
* @Date 2019/8/3 9:48
* @Description: TODO
**/
public class VectorDemo {
public static void main(String[] args) {
Vector<String> stringVector = new Vector<>();
//添加元素
for (int i = 0; i < 1000; i++) {
stringVector.add("demo" + i);
}
Iterator<String> stringIterator = stringVector.iterator();
//多線程下的正確迭代
for (int i = 0; i < 4; i++) {
new Thread(() -> {
synchronized (stringIterator) {
while (stringIterator.hasNext()) {
String next = stringIterator.next();
if (next.equals("demo2")) {
stringIterator.remove();
}
}
}
}).start();
}
}
}
案例2、synchronizedXXX
package com.maltose.concurrence.atomic.demo1;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* @Author: sgw
* @Date 2019/8/3 11:41
* @Description: synchronizedXXX同步容器
**/
public class Demo {
public static void main(String[] args) {
ArrayList<String> strings = new ArrayList<>();
List<String> stringList = Collections.synchronizedList(strings);
}
}
7.3、併發容器
代碼1—— CopyOnWrite:
package com.maltose.concurrence.atomic.demo1;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* @Author: sgw
* @Date 2019/8/3 11:46
* @Description: 併發容器
**/
public class CopyOnWriteDemo {
public static void main(String[] args) {
CopyOnWriteArrayList<String> strings = new CopyOnWriteArrayList<>();
for (int i = 0; i < 1000; i++) {
strings.add("demo" + i);
}
//單線程遍歷
/* strings.forEach(e->{
if (e.equals("demo2")) {
strings.remove(e);
}
});*/
//報錯:CopyOnWrite不支持在迭代器裏移除元素
/* Iterator<String> iterator = strings.iterator();
while (iterator.hasNext()) {
String next = iterator.next();
if (next.equals("demo2")) {
iterator.remove();
}
}*/
//多線程遍歷
for (int i = 0; i < 4; i++) {
new Thread(() -> {
strings.forEach(e -> {
if (e.equals("demo2")) {
strings.remove(e);
}
});
}).start();
}
}
}
代碼2——LinkedBlockingQueue:
package com.maltose.concurrence.atomic.demo1;
import java.util.concurrent.LinkedBlockingQueue;
/**
* @Author: sgw
* @Date 2019/8/3 11:59
* @Description: LinkedBlockingQueue併發容器
**/
public class LinkedBlockingQueueDemo {
public static void main(String[] args) throws InterruptedException {
//可以寫int型的參數,默認是int類型支持的最大數
LinkedBlockingQueue<String> strings = new LinkedBlockingQueue<>();
//往隊列裏存元素的三種方式
//add 實際上調用的是offer,區別是在隊列滿的時候,add會報異常
strings.add("111");
//對列如果滿了,直接入隊失敗
strings.offer("222");
//在隊列滿的時候,會進入阻塞的狀態
strings.put("333");
//從隊列中取元素的三種方式
//直接調用poll,唯一的區別即使remove會拋出異常,而poll在隊列爲空的時候直接返回null(remove:從隊列裏移除出來返回到這裏)
String remove = strings.remove();
//在隊列爲空的時候直接返回null
String poll = strings.poll();
//在隊列爲空的時候,會進入等待的狀態
String take = strings.take();
System.out.println(remove+"-----"+poll+"-----"+take);
}
}
結果:
111-----222-----333
八、jdk提供的併發工具類
8.1、CountDownLatch
簡介:
使用這個類時會傳入一個數值,然後操作這個數;
await(),進入等待的狀態
countDown(),計數器減一
應用場景:啓動三個線程計算,需要對結果進行累加。
代碼:
package com.maltose.concurrence.tool;
import java.util.concurrent.CountDownLatch;
/**
* @Author: sgw
* @Date 2019/8/3 15:55
* @Description: CountDownLatch的使用
**/
public class CountDownLatchDemo {
public static void main(String[] args) {
//總共8名選手在比賽
CountDownLatch countDownLatch = new CountDownLatch(8);
new Thread(() -> {
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
//所有線程執行完後,執行下邊的操作
System.out.println("800米比賽結束,準備清空跑道並繼續跨欄比賽");
}).start();
for (int i = 0; i < 8; i++) {
int finalI = i;
new Thread(() -> {
try {
//模擬每個選手使用的時間不一樣
Thread.sleep(finalI * 1000L);
System.out.println(Thread.currentThread().getName() + "到達終點");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//每個線程執行完之後就減一
countDownLatch.countDown();
}
}).start();
}
}
}
結果:
Thread-1到達終點
Thread-2到達終點
Thread-3到達終點
Thread-4到達終點
Thread-5到達終點
Thread-6到達終點
Thread-7到達終點
Thread-8到達終點
800米比賽結束,準備清空跑道並繼續跨欄比賽
8.2、CyclicBarrier–柵欄
簡介:
允許一組線程相互等待達到一個公共的障礙點,之後再繼續執行
跟countDownLatch的區別
CountDownLatch一般用於某個線程等待若干個其他線程執行完任務之後,它才執行;不可重複使用
CyclicBarrier一般用於一組線程互相等待至某個狀態,然後這一組線程再同時執行;可重用的
代碼:
package com.maltose.concurrence.tool;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
/**
* @Author: sgw
* @Date 2019/8/3 16:16
* @Description: TODO
**/
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(8);
for (int i = 0; i < 8; i++) {
int finalI = i;
new Thread(() -> {
try {
Thread.sleep(finalI * 1000L);
System.out.println(Thread.currentThread().getName() + "準備就緒");
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
//8名選手都就位或開始比賽
System.out.println("開始比賽");
}).start();
}
}
}
結果:
Thread-0準備就緒
Thread-1準備就緒
Thread-2準備就緒
Thread-3準備就緒
Thread-4準備就緒
Thread-5準備就緒
Thread-6準備就緒
Thread-7準備就緒
開始比賽
開始比賽
開始比賽
開始比賽
開始比賽
開始比賽
開始比賽
開始比賽
8.3、Semaphore–信號量
簡介:
控制併發數量;
使用場景:接口限流;
代碼:
package com.maltose.concurrence.tool;
import java.util.concurrent.Semaphore;
/**
* @Author: sgw
* @Date 2019/8/3 16:29
* @Description: Semaphore使用
**/
public class SemaphoreDemo {
public static void main(String[] args) {
//同一時刻只允許2個線程執行
Semaphore semaphore = new Semaphore(2);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
//獲取到信號後才能執行
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "開始執行");
Thread.sleep(5000L);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//用完後需要釋放
semaphore.release();
}
}).start();
}
}
}
結果(每隔5秒執行打印兩個線程,如果不使用Semaphore的話,則幾乎同時打印出來):
Thread-0開始執行
Thread-1開始執行
Thread-2開始執行
Thread-3開始執行
Thread-4開始執行
Thread-5開始執行
Thread-6開始執行
Thread-7開始執行
Thread-9開始執行
Thread-8開始執行
8.4、Exchanger
簡介:
用於兩個線程交換數據
它提供一個同步點,在這個同步點兩個線程可以交換彼此的數據。這兩個線程通過exchange方法交換數據, 如果第一個線程先執行exchange方法,它會一直等待第二個線程也執行exchange,
當兩個線程都到達同步點時,這兩個線程就可以交換數據,將本線程生產出來的數據傳遞給對方。因此使用Exchanger的重點是成對的線程使用exchange()方法,當有一對線程達到了同步點,就會進行交換數據。
因此該工具類的線程對象是【成對】的。
代碼:
package com.maltose.concurrence.tool;
import java.util.concurrent.Exchanger;
/**
* @Author: sgw
* @Date 2019/8/3 16:41
* @Description: Exchanger交換數據
**/
public class ExchangerDemo {
public static void main(String[] args) {
//泛型是需要交換的數據類型
Exchanger<String> stringExchanger = new Exchanger<>();
String str1 = "maltose";
String str2 = "sgw";
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "初始值==========>" + str1);
try {
String exchange = stringExchanger.exchange(str1);
System.out.println(Thread.currentThread().getName() + "交換後的數據==========>" + exchange);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "線程1").start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "初始值==========>" + str2);
try {
String exchange = stringExchanger.exchange(str2);
System.out.println(Thread.currentThread().getName() + "交換後的數據==========>" + exchange);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "線程2").start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "初始值==========>" + str2);
try {
String exchange = stringExchanger.exchange(str2);
System.out.println(Thread.currentThread().getName() + "交換後的數據==========>" + exchange);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "線程3").start();
}
}
結果:
線程1初始值==========>maltose
線程2初始值==========>sgw
線程3初始值==========>sgw
線程1交換後的數據==========>sgw
線程2交換後的數據==========>maltose
九、線程池及Executor框架
9.1、爲什麼要使用線程池
簡介:
諸如 Web 服務器、數據庫服務器、文件服務器或郵件服務器之類的許多服務器應用程序都面向處理來自某些遠程來源的大量短小的任務。
請求以某種方式到達服務器,這種方式可能是通過網絡協議(例如 HTTP、FTP )、通過 JMS隊列或者可能通過輪詢數據庫。
不管請求如何到達,服務器應用程序中經常出現的情況是:單個任務處理的時間很短而請求的數目卻是巨大的。每當一個請求到達就創建一個新線程,
然後在新線程中爲請求服務,但是頻繁的創建線程,銷燬線程所帶來的系統開銷其實是非常大的。
線程池爲線程生命週期開銷問題和資源不足問題提供瞭解決方案。通過對多個任務重用線程,線程創建的開銷被分攤到了多個任務上。
其好處是,因爲在請求到達時線程已經存在,所以無意中也消除了線程創建所帶來的延遲。這樣,就可以立即爲請求服務,使應用程序響應更快。
而且,通過適當地調整線程池中的線程數目,也就是當請求的數目超過某個閾值時,就強制其它任何新到的請求一直等待,
直到獲得一個線程來處理爲止,從而可以防止資源不足。
風險與機遇:
用線程池構建的應用程序容易遭受任何其它多線程應用程序容易遭受的所有併發風險,諸如同步錯誤和死
鎖,它還容易遭受特定於線程池的少數其它風險,諸如與池有關的死鎖、資源不足和線程泄漏。
9.2、創建線程池及其使用
代碼:
package com.maltose.concurrence.pool;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @Author: sgw
* @Date 2019/8/3 16:57
* @Description: 線程池
**/
public class ThreadPoolDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//參數寫20的話,線程池裏就有20個了,但是會報錯,後邊講解
LinkedBlockingQueue<Runnable> objects = new LinkedBlockingQueue<>();
//線程池
/**
* 參數1:線程池大小
* 參數2:線程池最大值
* 其他參數意義:9.4講解
*/
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10,20,3L,TimeUnit.SECONDS,objects);
//創建50個任務
for (int i = 0; i < 100; i++) {
threadPoolExecutor.submit(()->{
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
});
}
}
}
結果:
pool-1-thread-4
pool-1-thread-9
pool-1-thread-10
pool-1-thread-1
pool-1-thread-2
pool-1-thread-5
pool-1-thread-3
pool-1-thread-6
pool-1-thread-7
pool-1-thread-8
pool-1-thread-9
pool-1-thread-4
pool-1-thread-8
pool-1-thread-3
..........
上邊結果可以看到:只有1-10的線程,不會有更多的線程出現
9.3、Future與Callable、FutureTask
簡介:
Callable與Runable功能相似,Callable的call有返回值,可以返回給客戶端,而Runable沒有返回值,一般情況下,Callable與FutureTask一起使用,或者通過線程池的submit方法返回相應的Future;
Future就是對於具體的Runnable或者Callable任務的執行結果進行取消、查詢是否完成、獲取結果、設置結果操作。get方法會阻塞,直到任務返回結果;
FutureTask則是一個RunnableFuture,而RunnableFuture實現了Runnbale又實現了Futrue這兩個接口;
代碼1、
package com.maltose.concurrence.pool;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @Author: sgw
* @Date 2019/8/3 17:15
* @Description: Callable講解,有返回值;Runable是沒有返回值的;可以這樣理解:Callable補充了Runable
**/
public class CallableDemo implements Callable<String> {
@Override
public String call() throws Exception {
Thread.sleep(3000L);
return "1111";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
CallableDemo callableDemo = new CallableDemo();
FutureTask<String> stringFutureTask = new FutureTask<>(callableDemo);
new Thread(stringFutureTask).start();
System.out.println(stringFutureTask.get());
}
}
結果(3秒後打印):
1111
線程池裏使用Callable
package com.maltose.concurrence.pool;
import java.util.concurrent.*;
/**
* @Author: sgw
* @Date 2019/8/3 16:57
* @Description: 線程池
**/
public class ThreadPoolDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
LinkedBlockingQueue<Runnable> objects = new LinkedBlockingQueue<>();
//線程池
/**
* 參數1:線程池大小
* 參數2:線程池最大值
*/
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10,20,3L,TimeUnit.SECONDS,objects);
Future<String> submit = null;
for (int i = 0; i < 100; i++) {
//需要得到返回值時可以使用Callable
submit=threadPoolExecutor.submit(new CallableDemo());
}
for (int i = 0; i < 100; i++) {
System.out.println(submit.get());
}
}
}
結果(隔一段時間後打印出來的):
1111
1111
1111
1111
1111
1111
1111
1111
........
9.4、線程池的核心組成部分及其運行機制
corePoolSize:核心線程池大小 cSize
maximumPoolSize:線程池最大容量 mSize
keepAliveTime:當線程數量大於核心時,多餘的空閒線程在終止之前等待新任務的最大時間。
unit:時間單位
workQueue:工作隊列 nWorks
ThreadFactory:線程工廠
handler:拒絕策略,默認AbortPolicy
運行機制
通過new創建線程池時,除非調用prestartAllCoreThreads方法初始化核心線程,否則此時線程池中只有0個線程,即使工作隊列中存在多個任務,也不會執行;
任務數X(x個任務)
x <= cSize(任務數小於等於核心線程數的話),則只啓動x個線程;
x >= cSize && x < nWorks(工作隊列裏的數) + cSize ,則會啓動 <= cSize 個線程 其他的任務就放到工作隊列裏
x > cSize && x > nWorks + cSize:有以下兩種情況
1、x-(nWorks) <= mSize,則會啓動x-(nWorks)個線程;
2、x-(nWorks) > mSize ,則會啓動mSize個線程來執行任務,其餘的執行相應的拒絕策略;
9.5、線程池拒絕策略
AbortPolicy:該策略直接拋出異常,阻止系統正常工作
CallerRunsPolicy:只要線程池沒有關閉,該策略直接在調用者線程中,執行當前被丟棄的任務(叫老闆幫你幹活)
DiscardPolicy:直接啥事都不幹,直接把任務丟棄
DiscardOldestPolicy:丟棄最老的一個請求(任務隊列裏面的第一個),再嘗試提交任務
9.6、Executor線程池框架
通過相應的方法,能創建出6種線程池
ExecutorService executorService = Executors.newCachedThreadPool();
ExecutorService executorService1 = Executors.newFixedThreadPool(2);
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
ExecutorService executorService2 = Executors.newWorkStealingPool();
ExecutorService executorService3 = Executors.newSingleThreadExecutor();
ScheduledExecutorService scheduledExecutorService1 = Executors.newSingleThreadScheduledExecutor();
上面的方法最終都創建了ThreadPoolExecutor
newCachedThreadPool:創建一個可以根據需要創建新線程的線程池,如果有空閒線程,優先使用空閒的線程
newFixedThreadPool:創建一個固定大小的線程池,在任何時候,最多隻有N個線程在處理任務
newScheduledThreadPool:能延遲執行、定時執行的線程池
newWorkStealingPool:工作竊取,使用多個隊列來減少競爭
newSingleThreadExecutor:單一線程的線程池,只會使用唯一一個線程來執行任務,即使提交再多的任務,也都是會放到等待隊列裏進行等待
newSingleThreadScheduledExecutor:單線程能延遲執行、定時執行的線程池
代碼:
package com.maltose.concurrence.pool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
/**
* @Author: sgw
* @Date 2019/8/3 18:13
* @Description: Executor線程池框架
**/
public class ExecutorDemo {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
//參數:線程數量
ExecutorService executorService1 = Executors.newFixedThreadPool(2);
//參數:線程數量
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
ExecutorService executorService2 = Executors.newWorkStealingPool();
ExecutorService executorService3 = Executors.newSingleThreadExecutor();
ScheduledExecutorService scheduledExecutorService1 = Executors.newSingleThreadScheduledExecutor();
executorService.submit(() -> {
System.out.println(Thread.currentThread().getName());
});
}
}
9.7、線程池的使用建議
儘量避免使用Executor框架創建線程池:
newFixedThreadPool newSingleThreadExecutor
允許的請求隊列長度爲 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM。
newCachedThreadPool newScheduledThreadPool
允許的創建線程數量爲 Integer.MAX_VALUE,可能會創建大量的線程,從而導致 OOM
創建線程池時,核心線程數不要過大;
相應的邏輯,發生異常時要處理;
submit 如果發生異常,不會立即拋出,而是在get的時候,再拋出異常;
execute 直接拋出異常;
在IDEA裏設置最大堆內存:
IDEA配置:
-Xms60m
-Xmx60m
-XX:+HeapDumpOnOutOfHemoryError
-XX:HeapDumpPath=F:\File
代碼1:
package com.maltose.concurrence.pool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @Author: sgw
* @Date 2019/8/3 18:32
* @Description: 模擬OOM
**/
public class OOMDemo {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
while (true) {
executorService.submit(() -> {
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
}
運行一段時間後出現OOM異常,並在指定路徑生成文件,使用Heapanalyzer(IBM的工具,百度搜索即可)工具來分析這個OOM異常文件;
拉倒最下邊點擊下載:
SHIFT+右鍵,打開shell窗口:
運行剛下載下來的jar:
java -jar .\ha456.jar
彈出界面:
點擊"Accept"
點擊open找到剛纔生成的OOM的文件
此時就找到了發生OOM的地方(類文件)了
代碼2(儘量不要運行這個代碼,電腦會卡死,會把整個電腦的內存撐爆)、
package com.maltose.concurrence.pool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @Author: sgw
* @Date 2019/8/3 19:27
* @Description: 模擬OOM
**/
public class OOMDemo2 {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
while (true) {
//不停的提交任務
executorService.submit(() -> {
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
}
爲什麼上邊的例子,在IDEA裏限定了堆的內存之後,還會把整個電腦的內存撐爆:
創建線程時用的內存並不是我們指定jvm堆內存,而是系統的剩餘內存。(電腦內存-系統其它程序佔用的內存-已預留的jvm內存)
十、jvm與併發
10.1、jvm內存模型
1、jvm內存模型
硬件內存模型:
處理器--》高速緩存--》緩存一致性協議--》主存
java內存模型:
線程《--》工作內存《--》save和load 《---》主存
2、java內存間的交互操作
(1)lock(鎖定):作用於主內存的變量,把一個變量標記爲一條線程獨佔狀態
(2)unlock(解鎖):作用於主內存的變量,把一個處於鎖定狀態的變量釋放出來,釋放後的變量纔可以被其他線程鎖定
(3)read(讀取):作用於主內存的變量,把一個變量值從主內存傳輸到線程的工作內存中,以便隨後的load動作使用
(4)load(載入):作用於工作內存的變量,它把read操作從主內存中得到的變量值放入工作內存的變量副本中
(5)use(使用):作用於工作內存的變量,把工作內存中的一個變量值傳遞給執行引擎
(6)assign(賦值):作用於工作內存的變量,它把一個從執行引擎接收到的值賦給工作內存的變量
(7)store(存儲):作用於工作內存的變量,把工作內存中的一個變量的值傳送到主內存中,以便隨後的write的操作
(8)write(寫入):作用於主內存的變量,它把store操作從工作內存中的一個變量的值傳送到主內存的變量中
3、上面8中操作必須滿足以下規則
1、不允許read和load、store和write操作之一單獨出現,即不允許一個變量從主內存讀取了但工作內存不接受,或者從工作內存發起回寫了但主內存不接受的情況出現。
2、不允許一個線程丟棄它的最近的assign操作,即變量在工作內存中改變了之後必須把該變化同步回主內存。
3、不允許一個線程無原因地(沒有發生過任何assign操作)把數據從線程的工作內存同步回主內存。
4、一個新的變量只能在主內存中“誕生”,不允許在工作內存中直接使用一個未被初始化(load或assign)的變量,換句話說,就是對一個變量實施use、store操作之前,必須先執行過了assign和load操作。
5、一個變量在同一時刻只允許一條線程對其進行lock操作,但lock操作可以被同一條線程重複執行多次,多次執行lock後,只有執行相同次數的unlock操作,變量纔會被解鎖。
6、如果對一個變量執行lock操作,那將會清空工作內存中此變量的值,在執行引擎使用這個變量前,需要重新執行load或assign操作初始化變量的值。
7、如果一個變量事先沒有被lock操作鎖定,那就不允許對它執行unlock操作,也不允許去unlock一個被其他線程鎖定住的變量。
8、對一個變量執行unlock操作之前,必須先把此變量同步回主內存中(執行store、write操作)。
10.2、先行發生原則 happens-before
判斷數據是有有競爭、線程是否安全的主要依據
1. 程序次序規則:同一個線程內,按照代碼出現的順序,前面的代碼先行於後面的代碼,準確的說是控制流順序,因爲要考慮到分支和循環結構。
2. 管程鎖定規則:一個unlock操作先行發生於後面(時間上)對同一個鎖的lock操作。
3. volatile變量規則:對一個volatile變量的寫操作先行發生於後面(時間上)對這個變量的讀操作。
4. 線程啓動規則:Thread的start( )方法先行發生於這個線程的每一個操作。
5. 線程終止規則:線程的所有操作都先行於此線程的終止檢測。可以通過Thread.join( )方法結束、Thread.isAlive( )的返回值等手段檢測線程的終止。
6. 線程中斷規則:對線程interrupt( )方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,可以通過Thread.interrupt( )方法檢測線程是否中斷
7. 對象終結規則:一個對象的初始化完成先行於發生它的finalize()方法的開始。
8. 傳遞性:如果操作A先行於操作B,操作B先行於操作C,那麼操作A先行於操作C。
爲什麼要有該原則?
無論jvm或者cpu,都希望程序運行的更快。如果兩個操作不在上面羅列出來的規則裏面,那麼就可以對他們進行任意的重排序。
時間先後順序與先行發生的順序之間基本沒有太大的關係。
10.3、指令重排序
什麼是指令重排序?
重排序是指編譯器和處理器爲了優化程序性能而對指令序列進行重新排序的一種手段。
數據依賴性
編譯器和處理器在重排序時,會遵守數據依賴性,編譯器和處理器不會改變存在數據依賴關係的兩個操作的執行順序。
(僅針對單個處理器中執行的指令序列和單個線程中執行的操作,不同處理器之間和不同線程之間的數據依賴性不被編譯器和處理器考慮。)
兩操作訪問同一個變量,其兩個操作中有至少一個寫操作,此時就存在依賴性,如下:
先寫後讀 a=0 b=a
先讀後寫 a=b b=1
先寫後寫 a=1 a=2
eg:
a=1,b=1
寫後讀 a=0 b=a 正確:b=0
如果不遵守數據依賴性的話,就得到錯誤結果:b=1
指令重排序遵守的原則:as-if-serial原則
不管怎麼重排序(編譯器和處理器爲了提高並行度),(單線程)程序的執行結果不能被改變。
代碼1、:
package com.maltose.concurrence.jvm;
/**
* @Author: sgw
* @Date 2019/8/3 22:47
* @Description: 指令重排序
**/
public class CommandReorder {
static int x = 0, y = 0, a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
a = 1;
x = b;
});
Thread thread1 = new Thread(() -> {
b = 1;
y = a;
});
thread.start();
thread1.start();
thread.join();
thread1.join();
System.out.println( "x=======>" + x + " y=========>" + y);
}
}
上述兩個線程裏的變量不存在數據依賴性,所以可能發生指令重排序,可能的結果有四種:
x=0,y=1
x=1, y=0
x=1, y=1
x=0, y=0
爲了顯示所有可能,代碼改造如下:
package com.maltose.concurrence.jvm;
/**
* @Author: sgw
* @Date 2019/8/3 22:47
* @Description: 指令重排序
**/
public class CommandReorder {
static int x = 0, y = 0, a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
int i = 0;
boolean flag = true;
while (flag) {
i++;
Thread thread = new Thread(() -> {
a = 1;
x = b;
});
Thread thread1 = new Thread(() -> {
b = 1;
y = a;
});
thread.start();
thread1.start();
thread.join();
thread1.join();
System.out.println("第" + i + "次" + "x=======>" + x + " y=========>" + y);
if (x == 0 && y == 0) {
flag = false;
} else {
x = 0;
y = 0;
a = 0;
b = 0;
}
}
}
}
結果:
.....
第32381次x=======>0 y=========>1
第32382次x=======>0 y=========>1
第32383次x=======>0 y=========>1
第32384次x=======>0 y=========>0
十一、實戰
11.1、數據同步接口
需求分析:
一般系統,多數會與第三方系統的數據打交道,而第三方的生產庫,並不允許我們直接操作。在企業裏面,一般都是通過中間表進行同步,
即第三方系統將生產數據放入一張與其生產環境隔離的另一個獨立的庫中的獨立的表,再根據接口協議,增加相應的字段。
而我方需要讀取該中間表中的數據,並對數據進行同步操作。此時就需要編寫相應的程序進行數據同步。
數據同步一般分兩種情況:
全量同步:每天定時將當天的生產數據全部同步過來(優點:實現簡單 缺點:數據同步不及時)
增量同步:每新增一條,便將該數據同步過來(優點:數據近實時同步 缺點:實現相對困難)
我方需要做的事情::
讀取中間表的數據,並同步到業務系統中(可能需要調用我方相應的業務邏輯)
模型抽離
生產者消費者模型
生產者:讀取中間表的數據
消費者:消費生產者生產的數據
接口協議的制定:
1.取我方業務所需要的字段
2.需要有字段去記錄--數據什麼時候進入中間表
3.增加相應的數據標誌位,用於標誌數據的同步狀態(哪些字段讀取成功了,哪些失敗了)
4.有字段去記錄數據的同步時間
技術選型:
mybatis、單一生產者,多消費者、多線程併發操作:
只有一個線程讀取中間表,然後多個線程去處理這個數據;
多生產者,多消費者:處理起來非常複雜;
11.2、中間表設計
第三方系統的數據表:
中間表:
11.3、基礎環境搭建
1、新建maven項目,添加需要的依賴
<!-- mysql驅動 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>6.0.6</version>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
<!-- 日誌記錄 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
<!-- 連接池 -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>3.2.0</version>
</dependency>
<!--junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
配置mybaties:
點擊mybaties-config,輸入xml文件名(mybaties-config-our.xml,代表本地數據路配置文件):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="logImpl" value="STDOUT_LOGGING" />
</settings>
<typeAliases>
<typeAlias type="java.lang.Integer" alias="Integer"/>
<typeAlias alias="Long" type="java.lang.Long" />
<typeAlias alias="HashMap" type="java.util.HashMap" />
<typeAlias alias="LinkedHashMap" type="java.util.LinkedHashMap" />
<typeAlias alias="ArrayList" type="java.util.ArrayList" />
<typeAlias alias="LinkedList" type="java.util.LinkedList" />
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"></transactionManager>
<!--配置連接池-->
<dataSource type="com.maltose.HikaricpDataSource">
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/our?characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true"></property>
<property name="username" value="root"></property>
<property name="password" value="123123"></property>
<property name="readOnly" value="false" />
<!-- 等待連接池分配連接的最大時長(毫秒),超過這個時長還 沒可用的連接則發生SQLException, 缺省:30秒 -->
<property name="connectionTimeout" value="30000" />
<!-- 一個連接idle狀態的最大時長(毫秒),超時則被釋放(retired),缺省:10分鐘 -->
<property name="idleTimeout" value="600000" />
<!-- 一個連接的生命時長(毫秒),超時而且沒被使用則被釋放(retired),缺省:30分鐘,建議設置比數據庫超時時長少30秒,參考MySQL wait_timeout參數(show variables like '%timeout%';) -->
<property name="maxLifetime" value="1800000" />
<!-- 連接池中允許的最大連接數。缺省值:10;推薦的公式:((core_count * 2) + effective_spindle_count) -->
<property name="maximumPoolSize" value="60" />
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="com/xdclass/our/mapping/StudentMapper.xml"></mapper>
</mappers>
</configuration>
上邊配置文件連接池的類:
package com.maltose;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.ibatis.datasource.unpooled.UnpooledDataSourceFactory;
/**
* @Author: sgw
* @Date 2019/8/4 10:13
* @Description: 連接池
**/
public class HikaricpDataSource extends UnpooledDataSourceFactory {
public HikaricpDataSource() {
this.dataSource = new HikariDataSource();
}
}
配置鏈接第三方庫的配置文件mybaties-config-middle.xml:
配置內容與上邊基本一致,將數據庫鏈接信息修改即可
新建SqlSessionUtil類,用來獲取數據庫連接
package com.maltose.util;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.Reader;
/**
* @Author: sgw
* @Date 2019/8/4 10:24
* @Description: 獲取數據庫鏈接
**/
public class SqlSessionUtil {
private static SqlSessionFactory ourSqlSessionFactory;
private static SqlSessionFactory middleSqlSessionFactory;
private static final String OUR = "our";
private static final String MIDDLE = "middle";
private static final String MIDDLE_MIDDLE = "mybatis-config-middle.xml";
private static final String OUR_MIDDLE = "mybatis-config-our.xml";
private static Reader middleResourceAsReader = null;
private static Reader ourResourceAsReader = null;
static {
try {
middleResourceAsReader = Resources.getResourceAsReader(MIDDLE_MIDDLE);
ourResourceAsReader = Resources.getResourceAsReader(OUR_MIDDLE);
middleSqlSessionFactory = new SqlSessionFactoryBuilder().build(middleResourceAsReader);
ourSqlSessionFactory = new SqlSessionFactoryBuilder().build(ourResourceAsReader);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
middleResourceAsReader.close();
ourResourceAsReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 獲取sqlsession
* @param code
* @return
*/
public static SqlSession getSqlSession(String code) {
if (code.equals(MIDDLE)) {
return middleSqlSessionFactory.openSession();
}
return ourSqlSessionFactory.openSession();
}
}
11.4、生產者
1:分批讀取中間表(狀態是10I:還沒被處理過的數據),並將讀取到的數據狀態修改爲10D(處理中)
2:將相應的數據交付給消費者進行消費(有兩種方式)
1:把生產完的數據,直接放到隊列裏,由消費者去進行消費
2:把消費者放到隊列裏面,生產完數據,直接從隊列裏拿出消費者進行消費(這裏採用這種方式)
十二、總結(面試)
工作線程數是不是設置的越大越好?
不是的,線程頻繁上下文切換會帶來性能的開銷;
調用sleep()函數的時候,線程是否一直佔用CPU?
不是,sleep會暫時放棄對CPU的使用;
java中wait和sleep方法的不同
wait會放棄鎖,但是sleep不會;
synchronized關鍵字可用於哪些地方
詳見3.4節
如果CPU是單核,設置多線程有意義麼,能提高併發性能麼?
有意義,
手寫單例 懶漢式 雙重檢查 爲什麼要加volatile關鍵字
不要單鞋餓漢式,太簡單了;寫懶漢式要突出雙重檢查(線程安全的);
分析問題時常用的命令
jps jstack jconsole
看過跟JUC下的那些源碼,簡單說說 ;
線程池的核心組成部分及其運行機制;