多線程指的是在一個程式中同時運行多個線程,這些線程可以獨立運行或者相互協作,從而完成更加複雜的任務。Java中的多線程可以使用synchronized關鍵字來實現線程同步,避免多個線程同時訪問共用資源而導致的數據錯誤。此外,Java中還提供了Lock、Condition、Semaphore等類和介面... ...
多線程
思維導圖看天下:
1. 概述
並行與併發
並行 :指兩個或多個事件在同一時刻發生(同時發生)
併發 :指兩個或多個事件在同一個時間段內發生。(交替執行)
線程與進程
進程:是指一個記憶體中運行的程式,每個進程都有一個獨立的記憶體空間,一個應用程式可以同時運行多個進程
記憶:進程的英文為Process,Process也為過程,所以進程可以大概理解為程式執行的過程。
(進程也是程式的一次執行過程,是系統運行程式的基本單位; 系統運行一個程式即是一個進程從創建、運行到消亡的過程)
線程:進程中的一個執行單元,負責當前進程中程式的執行,一個進程中至少有一個線程。一個進程中是可以有多個線程的,這個應用程式也可以稱之為多線程程式。【java預設有兩個線程:main、GC】
進程與線程的區別:
- 進程:有獨立的記憶體空間,進程中的數據存放空間(堆空間和棧空間)是獨立的,至少有一個線程。
- 線程:堆空間是共用的,棧空間是獨立的,線程消耗的資源比進程小的多
2. 線程創建的五種方式
推薦使用Runnable介面的方式,因為Java是單繼承的,所以使用Thread有OPP單繼承局限性
2.1 背景介紹
線程類
Java使用 java.lang.Thread 類代表線程,所有的線程對象都必須是Thread類或其子類的實例
每個線程的作用是完成一定的任務,實際上就是執行一段程式流即一段順序執行的代碼
Java使用線程執行體來代表這段程式流。
2.2 ① 繼承Thread類
2.2.1 線程實現
1)實現步驟
- 繼承Thread類的子類,並重寫該類的run()方法(該run()方法的方法體就代表了線程需要完成的任務,因此run()方法稱為線程執行體)
- 創建Thread子類的實例,即創建了線程對象
- 調用線程對象的start()方法來啟動該線程
2)實現案例
自定義線程類:
主函數:
public static void main(String[] args) {
MyThread myThread = new MyThread("MyThread");
myThread.start();
for (int i = 0;i<1000;i++){
System.out.println("main"+i);
}
}
執行結果:
3)執行過程分析
過程:程式啟動運行main時候,java虛擬機啟動一個進程,主線程main在main()調用時候被創建
隨著調用Mt類的對象的start方法,另外一個新的線程也啟動了 ,這樣,整個應用就在多線程下運行。
運行時序圖:
記憶體結構:
4)調用start和run方法的區別
2.2.2 構造方法
- public Thread()
分配一個新的線程對象。 - public Thread(String name)
分配一個指定名字的新的線程對象 - public Thread(Runnable target)
分配一個帶有指定目標新的線程對象 - public Thread(Runnable target,String name)
分配一個帶有指定目標新的線程對象並指定名字
2.2.3 常用方法
- public String getName() :獲取當前線程名稱。
- public void start() :導致此線程開始執行; Java虛擬機調用此線程的run方法
- public void run() :此線程要執行的任務在此處定義代碼。
- public static void sleep(long millis) :使當前正在執行的線程以指定的毫秒數暫停(暫時停止執行)。
- public static Thread currentThread() :返回對當前正在執行的線程對象的引用。
1)獲取線程名稱
-
可以使用Thread類中的方法getName,
String getName() 返回該線程的名稱。 -
可以先獲取當前正在執行的線程,再調用getName方法獲取線程名稱
static Thread currentThread() 返回對當前正在執行的線程對象的引用
//1.可以使用Thread類中的方法getName String name = getName(); System.out.println(name);//創建時, 指定了名稱,獲取的就是指定的名稱 //如果沒有指定名稱,獲取的就是Thread-0 //2.可以先獲取當前正在執行的線程 Thread currentThread = Thread.currentThread(); System.out.println(currentThread);//Thread[Thread-0,5,main] String name2 = currentThread.getName(); System.out.println(name2);//Thread-0
2)設置線程名稱
- 方法一:可以使用Thread類中的方法setName
void setName(String name) 改變線程名稱,使之與參數 name 相同。
MyThread myThread = new MyThread();
myThread.setName("myThreadName");
myThread.start();
- 方法二:添加一個帶參構造方法,參數傳遞線程的名稱;調用父類的帶參構造方法,把名字傳遞給父類,讓父親給兒子起名字
Thread(String name) 分配新的 Thread 對象。
public class MyThread extends Thread{
//定義指定線程名稱的構造方法
public MyThread(String name) {
super(name);
}
3)線程休眠
public static void sleep(long millis)
使當前正在執行的線程以指定的毫秒數暫停(暫時停止執行)睡醒了,繼續執行
/*程式在執行第二秒時, 會暫停2秒,2秒後,繼續執行後面程式*/
for (int i = 1; i <=60; i++) {
System.out.println(i);
/*讓程式睡眠1秒鐘 1秒=1000毫秒*/
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
2.2.4 Thread構造方法底層原理——靜態代理
只針對有參且參是Runnable類的構造方法:public Thread(Runnable target)
由於Thread和target頂層都是Runnable介面,所以Thread是使用了靜態代理的方式代理參數target。
2.3 ② 實現Runnable介面
優勢:
- 避免單繼承的局限性
一個類繼承了Thread類就不能繼承其他的類
一個類實現了Runnable介面,還可以繼續繼承別的類,實現其他的介面 - 增強了程式的擴展性,降低程式的耦合度
使用Runnable介面把設置線程任務和開啟線程相分離
實現類當中,重寫run方法,設置線程任務
創建Thread類對象,調用 start方法,開啟新線程
如果一個類繼承Thread,則不適合資源共用。但是如果實現了Runable介面的話,則很容易的實現資源共用
2.3.1 實現步驟
1.創建一個RunnableImpl類實現Runnable介面
2.重寫Runnable介面中的run方法,設置線程任務
3.創建Runnable介面的實現類RunnableImpl的對象t
4.創建Thread類對象,構造方法中傳遞Runnable介面的實現類RunnableImpl的對象t
5.調用Thread類中的start方法,開啟新的線程,執行run方法
示例:
//實現Runnable介面
public class RunnableImpl implements Runnable{
//2.重寫Runnable介面中的run方法,設置線程任務
@Override
public void run() {
//新線程執行的代碼
for (int i = 0; i <20; i++) {
System.out.println(Thread.currentThread().getName()+"===>"+i);
}
}
}
public static void main(String[] args) {
//3.創建Runnable介面的實現類對象
RunnableImpl r = new RunnableImpl();
//4.創建Thread類對象,構造方法中傳遞Runnable介面的實現類對象
Thread t = new Thread(r);//列印20次i
//5.調用Thread類中的start方法,開啟新的線程,執行run方法
t.start(); //【一般16-18行簡寫為:new Thread(r,"線程名").start();】
//主線程開啟新線程之後繼續執行的代碼
for (int i = 0; i <20; i++) {
System.out.println(Thread.currentThread().getName()+"===>"+i);
}
}
2.3.2 構造方法
- Thread(Runnable target) 分配新的 Thread對象
- Thread(Runnable target, String name) 分配新的 Thread對象【推薦該方法,因為可以自定義線程名】
2.4 ③ 實現Callable介面
十分重要,但本篇只簡單介紹了一下,請去看下一篇JUC
2.4.1 實現步驟
- 實現Callable介面,需要返回值類型
- 重寫call方法,需要拋出異常
- 創建目標對象
- 創建執行服務:ExecutorService ser = Executors.newFixedThreadPool(1); //1為開闢的線程池中線程的數量
- 提交執行Future
result1 = ser.submit(t1); //線程 - 獲取結果:boolean r1 = resut1.get() //指定線程的返回結果
- 關閉服務:ser.shutdownNow()
代碼:
public class MyCallableImpl implements Callable<Boolean> {
@Override
public Boolean call() throws Exception {
PictureCatch t = new PictureCatch();
t.test(url,name);
System.out.println("下載了文件名:"+name);
return true;
}
String url; //網址
String name; //保存的文件名
MyCallableImpl(String url,String name){
this.url=url;
this.name=name;
}
public static void main(String[] args) {
MyCallableImpl t1 = new MyCallableImpl("https://img0.baidu.com/it/u=1151663768,725447312&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500","t4");
MyCallableImpl t2 = new MyCallableImpl("https://img0.baidu.com/it/u=1648512719,1593015989&fm=253&fmt=auto&app=120&f=JPEG?w=891&h=500","t5");
MyCallableImpl t3 = new MyCallableImpl("https://img2.baidu.com/it/u=863703859,746061395&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500","t6");
ExecutorService ser = Executors.newFixedThreadPool(3);
Future<Boolean> result1 = ser.submit(t1);
Future<Boolean> result2 = ser.submit(t2);
Future<Boolean> result3 = ser.submit(t3);
try {
boolean r1 = result1.get();
boolean r2 = result2.get();
boolean r3 = result3.get();
System.out.println(r1);
System.out.println(r2);
System.out.println(r3);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
ser.shutdownNow();
}
class PictureCatch{
void test(String url, String name){
try {
FileUtils.copyURLToFile(new URL(url), new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("獲取文件出錯!");
}
}
}
}
2.5 ④ 線程池Executor
這裡就不講了,放在了JUC併發編程那篇里詳細講解了
2.6 ⑤ Timer
使用 Timer 的方式如下:
public class MyTimer {
public static void main(String[] args) {
timer();
}
/**
* 指定時間 time 執行 schedule(TimerTask task, Date time)
*/
public static void timer() {
Timer timer = new Timer();
// 設定指定的時間time,此處為2000毫秒
timer.schedule(new TimerTask() {
public void run() {
System.out.println("執行定時任務");
}
}, 2000);
}
}
2.7 拓展:匿名內部類,實現Thread/Runnable多現程
2.7.1 匿名內部類
作用
把子類繼承父類,重寫父類的方法,創建子類對象,合成一步完成
把實現類實現介面,重寫介面庫的方法,創建實現類對象,合成一步完成
最終得要子類對象或實現類對象
格式
new 父類/介面(){
重寫父類/介面中的方法
};
2.7.2 Thread
public static void main(String[] args) {
new Thread(){ //new 沒有名稱的類 繼承Thread
//重寫run方法,設置線程任務
@Override
public void run() {
for (int i = 0; i <20 ; i++) {
System.out.println(Thread.currentThread().getName()+"==>"+i);
}
}
}.start();
}
2.7.3 Runnable
new Thread(new Runnable() { //new沒有名稱的類實現了Runnable介面
//重寫run方法,設置線程任務
@Override
public void run() { //實現介面當中run方法
for (int i = 0; i <20 ; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}).start();
3. 線程使用
3.1 六種線程狀態
- NEW(新建)
線程剛被創建,但是並未啟動。還沒調用start方法 - Runnable(可運行)
線程可以在java虛擬機中運行的狀態,可能正在運行自己代碼,也可能沒有,這取決於操 作系統處理器 - Blocked(鎖阻塞)
當一個線程試圖獲取一個對象鎖,而該對象鎖被其他的線程持有,則該線程進入Blocked狀 態;當該線程持有鎖時,該線程將變成Runnable狀態。 - Waiting(無限等待)
一個線程在等待另一個線程執行一個(喚醒)動作時,該線程進入Waiting狀態。
進入這個 狀態後是不能自動喚醒的,必須等待另一個線程調用notify或者notifyAll方法才能夠喚醒。 - Timed Waiting(計時等待)
同waiting狀態,有幾個方法有超時參數,調用他們將進入Timed Waiting狀態。
這一狀態 將一直保持到超時期滿或者接收到喚醒通知。帶有超時參數的常用方法有Thread.sleep 、 Object.wait - Teminated(被終止)
因為run方法正常退出而死亡,或者因為沒有捕獲的異常終止了run方法而死亡。
3.2 線程的常用操作
線程方法
方法 | 說明 |
---|---|
setPriority(int newPriority) | 更改線程的優先順序 |
static void sleep(long millis) | 在指定的毫秒數內讓當前正在執行的線程休眠 |
void join() | 等待該線程終止 |
static void yield() | 暫停當前正在執行的線程對象,並執行其他線程 |
void interrupt() | 中斷線程,別用這個方式 |
boolean isAlive() | 測試線程是否處於活動狀態 |
3.2.1 線程停止
- 不推薦使用JDK提供的stop()、destory()方法。【已廢棄】
- 推薦線程自己停止下來
- 建議使用一個標誌位進行終止變數,當flag=false(flag做while的條件),則終止線程運行
以下舉例是使用一個標誌位falg來終止變數
public class ThreadStopDemo implements Runnable {
private boolean flag = true;
@Override
public void run() {
int i = 1;
while (flag) {
System.out.println("run..."+(i++));
}
}
//設置一個專門修改標誌位的方法來停止線程
public void stop(){
flag = false;
}
public static void main(String[] args) {
ThreadStopDemo demo = new ThreadStopDemo();
new Thread(demo).start();
for (int i = 1; i <= 500; i++) {
System.out.println("main..."+i);
if (i == 300) {
demo.stop();
System.out.println("線程該停止了");
}
}
}
}
3.2.2 線程休眠_sleep
- sleep(long millis) 指定當前線程阻塞的毫秒數
- sleep存在異常InterruptException,所以需要拋出異常
- sleep時間達到後線程進入就緒狀態
- sleep可以模擬網路延時,倒計時等
- 每一個對象都有一個鎖,sleep不會釋放鎖
模擬倒計時+列印當前系統時間
public static void main(String[] args) {
//模擬倒計時
System.out.println("開始倒計時");
int num = 10;
while (true) {
System.out.println(num--);
Thread.sleep(1000);
if (num <= 0) {
break;
}
}
System.out.println("開始報時");
//列印當前系統時間
int count = 10;
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
while (true) {
System.out.println(LocalDateTime.now().format(dateTimeFormatter));
Thread.sleep(1000);
count--;
if (count <= 0) {
break;
}
}
}
3.2.3 線程禮讓_yield
禮讓不一定成功,因為cpu重新調度,可能會再次選到之前的線程
- Thread.yield();禮讓線程,讓當前正在執行的線程暫停,但不阻塞
- 將線程從記憶體中的運行狀態轉為就緒狀態並拿出記憶體
- 讓cup重新調度選擇線程進入記憶體,禮讓不一定成功,看cup調度
代碼演示:結果可能有三種:aabb,abab,abba
public class ThreadYieldDemo implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"開始!");
Thread.yield();
System.out.println(Thread.currentThread().getName()+"結束!");
}
public static void main(String[] args) {
ThreadYieldDemo demo = new ThreadYieldDemo();
new Thread(demo, "a").start();
new Thread(demo, "b").start();
}
}
3.2.4 線程強制執行_join
- Join合併線程,待此線程執行完成後,再執行其他線程,其他線程阻塞(可以想象成插隊)
代碼演示:結果是正常排隊執行到200後,得等強制執行走完200次後,才會繼續執行正常排隊201...
//插隊線程
public class 線程強制執行 {
public static void main(String[] args) {
forceThread forceThread = new forceThread();
Thread thread = new Thread(forceThread, "強制線程");
for (int i = 0; i < 500; i++) {
System.out.println("正常排隊:"+i);
if (i==200){
thread.start();
thread.join();
}
}
}
}
class forceThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println("強制執行——"+i);
}
}
}
3.2.5 線程狀態查看_getState
六種線程狀態看前面寫的
代碼演示:
public static void main(String[] args) {
Thread thread = new Thread(()->{
for (int i = 0; i < 20; i++) {
Thread.sleep(500);
}
},"線程");
System.out.println("線程start前的狀態:"+thread.getState());
thread.start();
System.out.println("線程start後的狀態:"+thread.getState());
while (!Thread.State.TERMINATED.equals(thread.getState())){
System.out.println("線程terminated之前的狀態:"+thread.getState());
Thread.sleep(500);
}
System.out.println("線程的狀態:"+thread.getState());
}
結果:
線程start前的狀態:NEW
線程start後的狀態:RUNNABLE
線程terminated之前的狀態:RUNNABLE
線程terminated之前的狀態:TIMED_WAITING
線程terminated之前的狀態:TIMED_WAITING
線程terminated之前的狀態:TIMED_WAITING
線程terminated之前的狀態:TIMED_WAITING
線程terminated之前的狀態:TIMED_WAITING
線程terminated之前的狀態:TIMED_WAITING
線程terminated之前的狀態:TIMED_WAITING
線程terminated之前的狀態:TIMED_WAITING
線程terminated之前的狀態:RUNNABLE
線程terminated之前的狀態:TIMED_WAITING
線程terminated之前的狀態:TIMED_WAITING
線程terminated之前的狀態:TIMED_WAITING
線程terminated之前的狀態:TIMED_WAITING
線程terminated之前的狀態:RUNNABLE
線程terminated之前的狀態:TIMED_WAITING
線程terminated之前的狀態:TIMED_WAITING
線程terminated之前的狀態:TIMED_WAITING
線程terminated之前的狀態:TIMED_WAITING
線程terminated之前的狀態:RUNNABLE
線程terminated之前的狀態:RUNNABLE
線程的狀態:TERMINATED
3.2.6 線程優先順序_Priority
源碼中所有線程的優先順序預設為5
優先順序的設置建議在start()調度前
優先順序低只是意味著獲取調度的概率低,並不是優先順序低就不會被調用了,這都是看cup的調度
- java提供一個線程調度器來監控程式中啟動後進入就緒狀態的所有線程,線程調度器按照優先順序決定應該調度哪個線程
- 線程的優先順序用數字表示,範圍從1~10【越大優先順序最高】
- Thread.MIN_PRIORITY =1
- Thread.MAX_PRIORITY =10
- Thread.NORM_PRIORITY =5
- 使用以下方式改變或獲取優先順序
- getPriority() setPriority(int x)
代碼演示:
public class 線程優先順序 {
public static void main(String[] args) {
//列印主線程的優先順序(也是所有線程預設的優先順序)
System.out.println(Thread.currentThread().getName() + "--->" + Thread.currentThread().getPriority());
MyPriority myPriority = new MyPriority();
Thread t1 = new Thread(myPriority,"線程1");
Thread t2 = new Thread(myPriority,"線程2");
Thread t3 = new Thread(myPriority,"線程3");
Thread t4 = new Thread(myPriority,"線程4");
Thread t5 = new Thread(myPriority,"線程5");
Thread t6 = new Thread(myPriority,"線程6");
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(7);
t3.setPriority(Thread.MAX_PRIORITY);
t4.setPriority(4);
t5.setPriority(9);
t6.setPriority(2);
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
t6.start();
}
}
class MyPriority implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"--->"+Thread.currentThread().getPriority());
}
}
3.2.7 守護線程_daemon
-
線程分為用戶線程和守護線程(daemon)
-
虛擬機需要確保用戶線程執行完畢
-
虛擬機不用等待守護線程執行完畢(如,後臺記錄操作日誌,監控記憶體,垃圾回收等)
也就是說可以做到主線程結束了,但守護線程還沒結束
代碼演示:
public class 守護線程 {
public static void main(String[] args) {
God god = new God();
You you = new You();
Thread godThread = new Thread(god, "守護線程");
Thread youThread = new Thread(you, "普通線程");
godThread.setDaemon(true); //設置為守護線程
godThread.start();
youThread.start();
}
}
class God implements Runnable{
@Override
public void run() {
while (true){
System.out.println("我是上帝,是你的守護線程");
}
}
}
class You implements Runnable{
@Override
public void run() {
for (int i = 0; i < 30; i++) {
System.out.println("我是普通人,只有三萬多天的日子");
}
System.out.println("======一個月完美的結束了======");
}
}
3.2.8 線程存儲ThreadLocal
用於存儲一個線程專有的值【對象方法】
ThreadLocal類,來創建工作記憶體中的變數,它將我們的變數值存儲在內部(只能存儲一個變數),不同的變數訪問到ThreadLocal對象時,都只能獲取到自己線程所屬的變數。【每個線程的工作記憶體空間不同,所以線程之間相互獨立,互不相關】
public static void main(String[] args) throws InterruptedException {
ThreadLocal<String> local = new ThreadLocal<>(); //註意這是一個泛型類,存儲類型為我們要存放的變數類型
Thread t1 = new Thread(() -> {
local.set("lbwnb"); //將變數的值給予ThreadLocal
System.out.println("線程1變數值已設定!");
try {
Thread.sleep(2000); //間隔2秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("線程1讀取變數值:");
System.out.println(local.get()); //嘗試獲取ThreadLocal中存放的變數
});
Thread t2 = new Thread(() -> {
local.set("yyds"); //將變數的值給予ThreadLocal
System.out.println("線程2變數值已設定!");
});
t1.start();
Thread.sleep(1000); //間隔1秒
t2.start();
}
//結果:lbwnb。就算t2也設置了值,但不影響t1的值
拓展:子類線程也獲得不了父類線程設置的值,但可以通過用InheritableThreadLocal
方法來解決這個問題。(在InheritableThreadLocal存放的內容,會自動向子線程傳遞)
public static void main(String[] args) {
ThreadLocal<String> local = new InheritableThreadLocal<>();
Thread t = new Thread(() -> {
local.set("lbwnb");
new Thread(() -> {
System.out.println(local.get());
}).start();
});
t.start();
}
3.2.9 等待與喚醒
- 等待wait和喚醒notify、notifyall都需要在同步代碼內(鎖方法 or 鎖代碼塊)
- 等待和喚醒只能由鎖對象調用。(鎖代碼塊的鎖對象容易看出,鎖方法的鎖對象一般是this或方法所在的類)
public void wait() : 讓當前線程進入到等待狀態 此方法必須鎖對象調用.
public void notify() : 喚醒當前鎖對象上等待狀態的線程 此方法必須鎖對象調用.會繼續執行wait()方法之後的代碼
方法名 | 作用 |
---|---|
wait() | 表示線程一直等待,直到其他線程通知,與sleep不同,會釋放鎖 |
wait(long timeout) | 指定等待的毫秒數 |
notify() | 喚醒一個處於等待狀態的線程 |
notifyAll() | 喚醒同一個對象上所有調用wait()方法的線程,優先順序別高的線程優先調度 |
註意:均是Object類的方法,都只能在同步方法或者同步代碼塊中使用,否則會拋出異常IllegalMonitorStateException
示例:
顧客與老闆線程:
創建一個顧客線程(消息者):告訴老闆要吃什麼 調用wait方法,放棄cpu的執行,進入wating狀態(無限等待)
創建一個老闆線程(生產者):花5秒做好 做好後 調用notify方法 喚醒顧客 開吃
註意
- 顧客與老闆線程必須使用同步代碼塊包裹起來,保證等待和喚醒只能有一個在執行同步使用的鎖必須要保證唯一,
- 只有鎖對象才能調用wait和notify方法
顧客線程
老闆線程
Object obj = new Object();
new Thread(){
@Override
public void run() {
synchronized (obj){
System.out.println("告訴老闆要吃餃子");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("做好===開始吃餃子");
}
}
}.start();
new Thread(){
@Override
public void run() {
synchronized (obj){
try {
Thread.sleep(3000);
System.out.println("老闆餃子已經做好");
obj.notify();//喚醒當前鎖對象上的等待線程
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
3.2.10 小結
-
進入計時等待狀態的兩種方式
-
使用sleep(long m)方法,在毫秒值結束後,線程睡醒,進入Runnable/Blocked狀態(抱著鎖睡覺,不放鎖)
-
使用wait(long m)方法wait方法如果在毫秒值結束之後,還沒有被喚醒,就會自動醒來,進入Runnable/Blocked狀態(等待的時候會釋放鎖)
-
-
兩種喚醒的方法
-
public void notify()
隨機喚醒1個 -
public void notifyall()
喚醒鎖對象上所有等待的線程.
-
4. 線程安全
4.0 線程同步機制
多個線程操作同一個資源
併發:同一個對象被多個線程同時操作
處理多線程問題時,多個線程訪問同一個對象,並且某些線程還想修改這個對象,這時候我們就需要線程同步。線程同步其實就是一個等待機制,多個需要同時訪問此對象的線程進入這個對象的等待池形成隊列,等待前面的線程時候完畢,下一個線程再使用。
線程同步
- 由於同一進程的多個線程共用同一塊存儲空間,在帶來方便的同時,也帶來了訪問衝突問題,為了保證數據在方法中被訪問時的正確性,在訪問時加入鎖機制synchronized,當一個線程獲得對象的排它鎖,獨占資源,其他線程必須等待,使用後釋放鎖即可。存在以下問題
- 一個線程持有鎖會導致其他所有需要此鎖的線程掛起
- 在多線程競爭下,加鎖,釋放鎖會導致比較多的上下文切換和調度延時,引起性能問題
- 如果一個優先順序高的線程等待一個優先順序低的線程釋放鎖會導致優先順序倒置,引起性能問題
4.1 什麼是線程安全
多線程訪問了共用的數據,就會產生線程的安全
舉例:
- 多個視窗,同時賣一種票. 如果不進行控制, 可以會出現賣重覆的現象
- 多個視窗,同時在銀行同一賬戶取錢,銀行不進行控制就會虧錢
- ArrayList線程不安全
4.1.1 買票問題
解決措施:可鎖代碼塊可鎖方法,後面的解決方案是以買票問題為例
代碼演示:
//買票問題
public class UnsafeTicket implements Runnable{
private static Boolean falg = true;
private int ticket =10;//票數
public static void main(String[] args) {
UnsafeTicket demo1 = new UnsafeTicket();
new Thread(demo1,"小紅").start();
new Thread(demo1,"小明").start();
new Thread(demo1,"黃牛").start();
}
@Override
public void run() {
while (falg){
buy();
}
}
//買票
public void buy(){
//沒票了就停止線程
if (ticket<=0) {
falg = false;
return;
}
//還有票就繼續買
System.out.println(Thread.currentThread().getName()+"買到了第"+ticket+"張票");
ticket--;
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
結果:會出現三個人同時去了第5張票、也可能會出現有人取0 -1張票
4.1.2 銀行取錢問題
解決措施:使用代碼塊鎖account
代碼演示:
//銀行取錢問題
public class UnsafeBank {
public static void main(String[] args) {
Account account = new Account(100, "結婚基金");
Bank drawMoney1 = new Bank(account, 50, "新一");
Bank drawMoney2 = new Bank(account, 100, "小蘭");
drawMoney2.start();
drawMoney1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("卡內餘額:" + account.money);
}
}
//賬戶
class Account{
int money;//賬戶內的錢
String name;//卡名
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
//銀行
class Bank extends Thread{
Account account;//操縱的賬戶
int drawingMoney;//取了多少錢
public Bank(Account account, int drawingMoney,String who) {
super(who);
this.account = account;
this.drawingMoney = drawingMoney;
}
@Override
public void run() {
drawing();
}
//取錢
private void drawing() {
if (this.account.money - drawingMoney < 0) {
System.out.println("餘額不足," + Thread.currentThread().getName() + "取錢失敗");
return;
}
//sleep可以提高問題的發生的概率!
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.account.money = this.account.money - drawingMoney;
System.out.println(Thread.currentThread().getName() + "取了" + drawingMoney);
}
}
結果:
4.1.3 ArrayList問題
解決措施:鎖代碼塊
代碼演示:
public class UnsafeArrayList {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(()->{
int x = 10;
list.add(x++);
},"list線程不安全").start();
}
System.out.println(list.size());
}
}
結果:9997,少了三個,是因為前面插入數據的時候有三個下標被重覆賦值,導致有三次賦值被覆蓋了。
4.2 解決線程安全
鎖類模板 和 鎖用該類模板創建出來的對象 兩者之間互不影響!
4.2.1 synchronized鎖代碼塊
同步代碼塊synchronized的格式:
synchronized(鎖對象obj){
出現安全問題的代碼(訪問了共用數據的代碼)
}
註意
1.鎖對象可以是任意對象 new Person new Student ...(一般是鎖變化的對象,需要增刪改的對象)
2.必須保證多個線程使用的是同一個鎖對象
3.鎖對象的作用:把{}中代碼鎖住,只讓一個線程進去執行
1)鎖實例對象
適用於使用同一個Runnable對象創建多個線程的情況,不適用於多個Runnable對象分別創建多個線程的情況
作用範圍是對象實例,不可跨對象,所以多個線程不同對象實例訪問此方法,互不影響,無法產生互斥。由於本題搶票中是多個線程使用同一個Runnable對象,所以得到的鎖是同一個對象產生的obj,可以實現線程隔離。但銀行例子中是多個線程分別使用不同的Runnable對象,所以使用鎖實例對象是沒用的。
示例
public class TicketRunnableImpl implements Runnable {
//定義共用的票源
private int ticket = 100;
private Object obj = new Object(); //鎖對象
//線程任務:賣票
@Override
public void run() {
synchronized (obj){
while (ticket > 0) {
/*為了提高線程安全問題出現的幾率
讓線程睡眠10毫秒,放棄cpu的執行權*/
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//賣票操作,ticket--
System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket + "張票");
ticket--;
}
}
}
public static void main(String[] args) {
UnsafeTicket demo1 = new UnsafeTicket();
new Thread(demo1,"小紅").start();
new Thread(demo1,"小明").start();
new Thread(demo1,"黃牛").start();
}
}
總結:
同步監視器的執行過程(同步方法中無需指定同步監視器,因為同步方法的同步監視器就是this,就是這個對象本身,或者是class)
- 第一個線程訪問,鎖定同步監視器,執行其中的代碼
- 第二個線程訪問,發現同步監視器被鎖定,無法訪問,處於阻塞狀態,一直等待
- 第一個線程訪問完畢,解鎖同步監視器
- 第二個線程訪問,發現同步監視器沒有鎖,然後鎖定並訪問
2)鎖類
適用於使用同一個Runnable對象創建多個線程的情況,也適用於多個Runnable對象分別創建多個線程的情況
雖然是通過對象訪問的此方法,但是加鎖的代碼塊是類級別的跨對象的,所以鎖的範圍是針對類,多個線程訪問互斥。
public class SynchronizedDemo {
// 代碼塊鎖(類):鎖的應用對象是User類,可以稱之為類鎖
public void method2() {
synchronized (User.class) {
// TODO 業務邏輯
}
}
public static void main(String[] args) {
SynchronizedDemo obj1 = new SynchronizedDemo();
SynchronizedDemo obj2 = new SynchronizedDemo();
new Thread(() ->{
obj1.method2(); //代碼塊鎖,後面是類,多線程訪問互斥
}).start();
new Thread(() ->{
obj2.method2();
}).start();
}
}
4.2.2 synchronized鎖方法
鎖的是this,也就是主方法里調用該方法的對象
同步方法解決線程安全的格式:
修飾符 synchronized 返回值類型 方法名(參數列表){
出現安全問題的代碼(訪問了共用數據的代碼)
}
使用步驟
1.創建一個方法,方法的修飾符添加上synchronized
2.把訪問了共用數據的代碼放入到方法中
3.調用同步方法
1)鎖普通方法(對象鎖)
適用於使用同一個Runnable對象創建多個線程的情況,不適用於多個Runnable對象分別創建多個線程的情況
普通方法作用範圍是對象實例,不可跨對象,所以多個線程不同對象實例訪問此方法,互不影響,無法產生互斥。由於本題搶票中是多個線程使用同一個Runnable對象,所以得到的鎖是同一個類對象this,可以實現線程隔離。但銀行例子中是多個線程分別使用不同的Runnable對象最後鎖的this也是不同類對象的this,所以使用鎖普通方法是沒用的。
示例
@Override
public void run() {
ticketMethods();
}
public synchronized void ticketMethods(){
while (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//賣票操作,ticket--
System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket + "張票");
ticket--;
}
}
鎖對象是誰???
鎖對象為this
public void ticketMethods(){
synchronized(this){
while (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//賣票操作,ticket--
System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket + "張票");
ticket--;
}
}
}
2)鎖靜態方法(類鎖)
適用於使用同一個Runnable對象創建多個線程的情況,也適用於多個Runnable對象分別創建多個線程的情況
靜態方法是通過類訪問,是類級別的跨對象的,所以鎖的範圍是針對類,多個線程訪問互斥。
示例:變化的量記得也要static
public class TicketRunnableImpl implements Runnable {
//定義共用的票源
private static int ticket = 100;
private Object obj = new Object(); //鎖對象
//線程任務:賣票
@Override
public void run() {
ticketMethods();
}
public static synchronized void ticketMethods(){
while (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//賣票操作,ticket--
System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket + "張票");
ticket--;
}
}
}
鎖對象是誰???
對於static方法,我們使用當前方法所在類的位元組碼對象(類名.class)
4.2.3 Lock鎖
概述
- 從jdk5.0開始,java提供了更強大的線程同步機制——通過顯示定義同步鎖對象來實現同步。同步鎖使用Lock對象充當
- java.util.concurrent.locks.Lock介面是控制多個線程對共用資源進行訪問的工具。鎖提供了對共用資源的獨占訪問,每次只能有一個線程對Lock對象加鎖,線程開始訪問共用資源之前先獲得Lock對象
- 。ReetrantLock(可重入鎖)類實現了Lock,它擁有與synchronized相同的併發性和記憶體語義,在實現線程安全的控制中,比較常見的是ReetrantLock,可以顯示加鎖、釋放鎖
Lock介面中的方法
void lock() 獲取鎖。
void unlock() 釋放鎖。//如果有try/catch的話,一般unlock是放在finally里
使用步驟
1.在成員位置創建一個Lock介面的實現類對象ReentrantLock
2.在可能會出現安全問題的代碼前,調用lock方法獲取鎖對象
3.在可能會出現安全問題的代碼後,調用unlock方法釋放鎖對象
示例
public class TicketRunnableImpl implements Runnable {
//定義共用的票源
private int ticket = 100;
//1.在成員位置創建一個Lock介面的實現類對象ReentrantLock
Lock l = new ReentrantLock();
//線程任務:賣票
@Override
public void run() {
while (true) {
l.lock();
if (ticket > 0){
try {
Thread.sleep(10);
//賣票操作,ticket--
System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket + "張票");
ticket--;
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//3.在可能會出現安全問題的代碼後,調用unlock方法釋放鎖對象
l.unlock(); //無論程式是否異常,都會把鎖對象釋放,節約記憶體提高程式的效率
}
}
}
}
}
4.2.4 synchronized與Lock對比
- Lock是顯式鎖(手動開啟和關閉鎖,別忘記關閉鎖)synchronized是隱式鎖,除了作用域自動釋放
- Lock只有代碼塊鎖,synchronized有代碼塊鎖和方法鎖
- 使用Lock鎖,JVM將花費較少的時間來調度線程,性能更好。並且具有更好的擴展性(提供更多的子類)
- 優先使用順序:Lock > 同步代碼塊(已經進入了方法體,分配了響應資源)> 同步方法(在方法體之外)
4.2.5 判斷鎖的對象是誰
8鎖現象:
1)標準情況下,一個對象 兩個同步方法 第一個線程先拿到鎖 誰先執行
2)一個對象 兩個同步方法 第一個線程先拿到鎖 第一個方法延遲4S 誰先執行
3)一個對象 一個同步方法一個普通方法 第一個線程先拿到鎖 誰先執行
4)兩個對象 兩個同步方法 第一個線程先拿到鎖 第一個方法延遲4S 誰先執行
5)一個對象 兩個靜態同步方法 第一個線程先拿到鎖 第一個方法延遲4S 誰先執行
6)兩個對象 兩個靜態同步方法 第一個線程先拿到鎖 第一個方法延遲4S 誰先執行
7)一個對象 一個靜態同步方法一個普通同步方法 第一個線程先拿到鎖 第一個方法延遲4S 誰先執行
8)兩個個對象 一個靜態同步方法一個普通同步方法 第一個線程先拿到鎖 第一個方法延遲4S 誰先執行
package com.ambition;
import java.util.concurrent.TimeUnit;
/**
* 同一個對象 兩個線程 兩個同步方法 誰先執行?
**/
public class Question1 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(() -> phone.sendMsg()).start();
// 延遲的目的是控制哪個線程先拿到鎖
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> phone.call()).start();
}
}
class Phone {
// synchronized 鎖的對象是方法的調用者
// 兩個方法用的是同一個鎖 誰先拿到誰先執行
public synchronized void sendMsg() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("發簡訊");
}
public synchronized void call() {
System.out.println("打電話");
}
}
小結:
- 對於普通同步方法,鎖是當前new實例對象。
- 對於static 靜態同步方法,鎖是當前類模板的Class對象。
5. 生產者與消費者
5.1 問題介紹與分析
1.線程通信
- 應用場景:生產者和消費者問題
- 假設倉庫中只能存放一件產品,生產者將生產出來的產品放入倉庫,消費者將倉庫中的產品取走消費
- 如果倉庫中沒有產品,則生產者將產品放入倉庫,否則停止生產並等待,直到倉庫中的產品被消費者取走為止
- 如果倉庫中放有產品,則消費者可以將產品取走消費,否則停止消費並等待,直到倉庫中再次放入產品為止
2.線程通訊-分析
- 這個一個線程同步問題,生產者和消費者共用同一個資源,並且生產者和消費者之間相互依賴,互為條件。
- 對於生產者,沒有生產產品之前,要通知消費著等待,而生產了產品之後,有需要馬上通知消費者消費
- 對於消費者,在消費之後,要通知生產者已經結束消費,需要生產新的產品以供消費
- 在生產者消費者問題中,僅有synchronized是不夠的,就需要用到之前講的等待與喚醒。
- synchronized可以阻止併發更新同一個共用資源,實現了同步
- synchronized不能用來實現不同線程之前的消息傳遞(通信)
5.2 解決方法
5.2.1 管程法
生產者——緩存區——消費者
併發協作模型”生產者/消費者模式“-->管程法
- 生產者:負責生產數據的模塊(可能是方法,對象,線程,進程)
- 消費者:負責處理數據的模塊(可能是方法,對象,線程,進程)
- 緩衝區:消費者不能直接使用生產者的數據,他們之間有個“緩衝區”
生產者將生產好的數據放入緩衝區,消費者從緩衝區拿出數據
代碼:
//餐廳模式:生產者————廚師、消費者————顧客
public class 管程法 {
public static void main(String[] args) {
SynContainer container = new SynContainer();
new Productor(container).start();
new Cousumer(container).start();
}
}
/**
* 生產者
*/
class Productor extends Thread {
/**
* 緩衝區
*/
private SynContainer container;
public Productor(SynContainer container) {
this.container = container;
}
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
container.push(new Chicken(i));
// System.out.println("生產了" + i + "只雞");
}
}
}
/**
* 消費者
*/
class Cousumer extends Thread {
/**
* 緩衝區
*/
private SynContainer container;
public Cousumer(SynContainer container) {
this.container = container;
}
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
Chicken pop = container.pop();
// System.out.println("消費了" + pop.getId() + "只雞");
}
}
}
/**
* 雞(食物)
*/
class Chicken {
/**
* 雞的編號
*/
private int id;
public Chicken(int id) {
this.id = id;
}
public int getId() {
return id;
}
}
/**
* 緩衝區
*/
class SynContainer {
/**
* 緩衝區的容器大小(十隻雞)
*/
private Chicken[] chickens = new Chicken[10];
/**
* 計數器
*/
private int count = 0;
/**
* 生產者往容器中放入產品
*/
public synchronized void push(Chicken chicken) {
//如果容器滿了,生產者就需要等待消費者消費
if (count == 10) {
//生產者開始等待消費者消費
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果沒有滿,生產者往容器中繼續放入產品
chickens[count] = chicken;
count++;
System.out.println("生產了" + chicken.getId() + "只雞");
//生產者通知消費者消費
this.notifyAll();
//模擬生產者要休息一下下
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 消費者消費容器中的產品
*/
public synchronized Chicken pop() {
//消費者判斷容器中是否有產品
if (count == 0) {
//消費者等待生產者生產
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//消費者開始消費容器中的產品
count--;
Chicken chicken = chickens[count];
System.out.println("消費了" + chicken.getId() + "只雞");
//消費者通知生產者繼續生產
this.notifyAll();
return chicken;
}
}
5.2.2 信號燈法(常用)
flag標誌位來告訴消費者繼續/停止消費,告訴生產者繼續/停止生產
併發協作模型”生產者/消費者模式“-->信號燈法
public class 信號燈法 {
public static void main(String[] args) {
TV tv = new TV();
new Actor(tv).start();
new Audience(tv).start();
}
}
//演員
class Actor extends Thread{
TV tv = new TV();
public Actor(TV tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 1; i <= 20; i++) {
if (i % 2 == 0) {
tv.push("快樂大本營");
} else {
tv.push("抖音");
}
}
}
}
//聽眾
class Audience extends Thread{
TV tv = new TV();
public Audience(TV tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 1; i <= 20; i++) {
tv.pop();
}
}
}
//電視節目
class TV {
String name;//節目名稱
boolean flag=true; //標誌位 T生產 F觀看
//生產節目
public synchronized void push(String name){
//判斷要不要生產
//不生產就等待
if (!flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//生產
this.name=name;
flag=!flag;
System.out.println("我生產了"+name);
//生產完就喚醒觀眾
this.notifyAll();
}
//消費節目
public synchronized void pop(){
//判斷有沒有節目看
//沒有節目看就等待
if (flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//有節目就消費
flag=!flag;
System.out.println("我看完了"+name);
//看完就讓演員再演
this.notifyAll();
}
}
本文來自博客園,作者:不吃紫菜,遵循CC 4.0 BY-SA版權協議,
轉載請附上原文出處鏈接:https://www.cnblogs.com/buchizicai/p/17277623.html及本聲明;
本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。