JAVA多執行緒
目錄
基本概念
創建執行緒
Lambda運算式
靜態代理
執行緒的狀態
守護執行緒
執行緒的同步機制
執行緒協作
執行緒池(還沒寫完,后續更新)
基本概念
- 在我們作業系統中運行的程式就是行程,比如播放器,游戲等等......
- 一個行程可以有多個執行緒,如視頻中同時可以聽聲音,看影像,看彈幕等等.......
- 程式是指令和資料的有序集合,其本身沒有任何運行的含義,是一個靜態的概念,
- 但是行程則是執行程式的一次執行程序,他是一個動態的概念,是系統資源分配的單位
- 通常在一個行程中可以包含若干個執行緒,當然一個行程中至少有一個執行緒,不然沒有存在的意義,
- 執行緒是CPU調度和執行的單位
- 注意:很多多執行緒是模擬出來的,真正的多執行緒是指有多個cpu,即多核,如服務器等等......很多模擬出來的多執行緒實際上是一個cpu快速切換出來的錯覺,其原因在于單個cpu只能在一個時間點執行一個代碼,并沒有同時進行兩個代碼及以上,
- 在我們對同一份資源操作是4,會存在資源搶奪的問題,需要我們加入并發控制
- 執行緒會帶來額外的開銷,如cpu調度事件,并發控制開銷
- 每個執行緒在自己的作業記憶體互動,記憶體控制不當會造成資料不一致
創建執行緒
- 執行緒的三種創建方式:
- 繼承Thread類
- 實作Runnable介面
- 實作Callable介面
Thread的使用
-
自定義執行緒類繼承Thread類
-
重寫run()方法,撰寫執行緒的執行體
-
創建執行緒物件,呼叫start()方法啟動執行緒
-
代碼例子:
package Demo01;
//創建執行緒方式1:使用繼承Thread類后重寫run()方法,呼叫start方法開啟執行緒
public class ThreadStudy extends Thread{
@Override
public void run(){
//重寫run方法,以下為執行緒體
for (int i = 0; i < 20; i++) {
System.out.println("HIBIKI---"+i);
}
}
public static void main(String[] args) {
//main執行緒,主執行緒
//創建一個執行緒的物件
ThreadStudy tst=new ThreadStudy();
//呼叫start()方法開啟執行緒
tst.start();
for (int i = 0; i < 200; i++) {
System.out.println("Ayanami---"+i);
}
//運行后在"Ayanami---"后面有時候會出現"HIBIKI---"的字串,說明程式的多執行緒運行實作了,這兩個執行緒是同時運行的
}
}
- 執行緒開啟后不一定立即執行,需要由CPU調度才能執行,因此在順序上可能與我們想象的會有所不同
Runnable的使用
-
自定義類實作Runnable介面
-
實作run()方法,撰寫執行緒執行體
-
創建執行緒物件,呼叫start()方法啟動執行緒
-
代碼例子
package Demo01;
//創建執行緒方式2:使用Runnable介面,并重寫介面里的run方法,執行執行緒需要放入Runnable介面實作類,呼叫start方法
public class MyRunnable implements Runnable {
@Override
public void run() {
//重寫run方法,以下為執行緒體
for (int i = 0; i < 20; i++) {
System.out.println("HIBIKI---" + i);
}
}
public static void main(String[] args) {
//main執行緒,主執行緒
//創建Runnable介面的實作類物件
MyRunnable myRunnable = new MyRunnable();
//創建執行緒物件,通過執行緒物件來開啟我們的執行緒
Thread thread = new Thread(myRunnable); //將Runnable介面的實作類物件放入
thread.start(); //啟動執行緒
//此處也可以直接使用陳述句: new Thread (myRunnable).start();
for (int i = 0; i < 200; i++) {
System.out.println("Ayanami---"+i);
}
//運行后在"Ayanami---"后面有時候會出現"HIBIKI---"的字串,說明程式的多執行緒運行實作了,這兩個執行緒是同時運行的
}
}
- 實作Runnable介面來進行多執行緒比繼承Thread類的好處就是:
- 子類繼承Thread具有單繼承的局限性,會導致無法繼承其他類
- 避免了單繼承的局限性,方便同一個物件被多個執行緒使用
Callable介面的使用
-
使用方法:
- 實作Callable介面,需要回傳值的型別
- 重寫call方法,需要拋出例外
- 創建目標物件
- 創建執行服務:
ExecutorService ser = Executors.newFixedThreadPool(1); - 提交執行:
Future<Boolean>result1=ser.submit(t1); - 獲取結果:
boolean r1=result1.get() - 關閉服務:
ser.shutdownNow();
-
下面為以commons-io工具包為基礎的代碼例子:
package ThreadStudy;
//創建執行緒的方式3:使用Callable介面,并重寫call方法
import java.io.File;
import java.io.IOException;
import org.apache.commons.io.FileUtils;
import java.net.URL;
import java.util.concurrent.*;
//執行緒主體
public class ThreadStudy3 implements Callable<Boolean> {
private String url; //網路圖片地址
private String name; //保存的檔案名
public ThreadStudy3(String url,String name){
this.url=url;
this.name=name;
}
//下載圖片執行緒的執行體
@Override
public Boolean call(){
WebDownloader webDownloader=new WebDownloader();
webDownloader.downloader(url,name);
System.out.println("下載了檔案名為:"+name);
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadStudy3 s1=new ThreadStudy3("https://i0.hdslb.com/bfs/album/[email protected]","初音1.jpg");
ThreadStudy3 s2=new ThreadStudy3("https://i0.hdslb.com/bfs/album/[email protected]","初音2.jpg");
ThreadStudy3 s3=new ThreadStudy3("https://i0.hdslb.com/bfs/album/[email protected]","初音3.jpg");
//創建執行服務:
ExecutorService ser = Executors.newFixedThreadPool(3);
//提交執行
Future<Boolean> r1=ser.submit(s1);
Future<Boolean> r2=ser.submit(s2);
Future<Boolean> r3=ser.submit(s3);
//獲取結果
boolean rs1=r1.get(); //此處拋出了例外
boolean rs2=r2.get(); //此處拋出了例外
boolean rs3=r3.get(); //此處拋出了例外
System.out.println(rs1);
System.out.println(rs2);
System.out.println(rs3);
//關閉服務
ser.shutdownNow();
}
}
//下載器
class WebDownloader{
//下載方法
public void downloader(String url,String name) {
try {
FileUtils.copyURLToFile(new URL(url),new File(name));
} catch (IOException e) {
System.out.println("IO例外,downloader方法出現問題");
}
}
}
/*
輸出結果為:
下載圖片名為:初音1.jpg
下載圖片名為:初音2.jpg
下載圖片名為:初音3.jpg
true
true
true
并且根目錄下會出現三個圖片,有時三個圖片的下載順序會不同,因為執行緒是同時運行的
*/
commons-io工具包地址
運算式
-
Lambda運算式的用處:
- 避免匿名內部類定義過多
- 使代碼變得簡潔
- 可以省去很多無意義代碼,留下核心邏輯
-
理解函式式介面(Functional Interface)對學習Lambda運算式至關重要,
-
函式式介面的定義:
-
任何介面,如果只包含唯一一個抽象方法,那么它被稱為函式式介面,
//example: @FunctionalInterface public interface Runnable{ public abstract void run(); } -
對于函式式介面,我們可以通過Lambda運算式來創建該介面的物件,
-
-
代碼例子
package LambdaStudy;
public class LambdaDemo01 {
//表達一個程式有以下幾種方式:
//2.靜態內部類
static class ME2 implements Study{
@Override
public void lambda() {
System.out.println("I study Lambda2");
}
}
public static void main(String[] args) {
//1.創建物件后參考方法
Study me1= new ME();
me1.lambda();
//以下使靜態內部類使用方法
Study me2 =new ME2();
me2.lambda();
//3.區域內部類
class ME3 implements Study{
@Override
public void lambda() {
System.out.println("I study Lambda3");
}
}
Study me3 =new ME3();
me3.lambda();
//4.匿名內部類
Study me4=new Study() {
@Override
public void lambda() {
System.out.println("I study Lambda4");
}
};
me4.lambda();
//5.Lambda運算式
Study me5 =new ME();
me5=()-> {
System.out.println("I study Lambda5");
};
me5.lambda();
}
}
//定義一個函式式介面
@FunctionalInterface
interface Study{
void lambda();
}
//實作類
class ME implements Study{
@Override
public void lambda() {
System.out.println("I study Lambda1");
}
}
- Lambda運算式簡化代碼例子:
package LambdaStudy;
public class LambdaDemo01 {
public static void main(String[] args) {
//1.標準的Lambda運算式
Study study=(String a)->{
System.out.println("I study Lambda"+"\t"+a);
};
study.lambda("標準");
//2.簡化引數(如果有兩個及以上引數,則需要都簡化,不能只簡化一部分)
study = (a)->{
System.out.println("I study Lambda"+"\t"+a);
};
study.lambda("簡化引數");
//3.簡化括號(如果引數為兩個及以上,則要保留括號)
study = a->{
System.out.println("I study Lambda"+"\t"+a);
};
study.lambda("簡化括號");
//4.簡化花括號:必須保證執行體只有一句指令代碼
study = a-> System.out.println("I study Lambda"+"\t"+a);
study.lambda("簡化花括號");
}
}
//定義一個函式式介面
interface Study{
void lambda(String a);
}
/*其輸出結果為:
*I study Lambda 標準
*I study Lambda 簡化引數
*I study Lambda 簡化括號
*I study Lambda 簡化花括號
*
* */
靜態代理
- 代理物件需要和直接物件實作同一個介面
- 代理物件可以使用直接物件里面的方法
- 其好處就是直接物件可以專注于實作自己的需要的方法,而代理物件可以一同實作許多直接物件的方法
package StaticProxy;
public class staticpro {
public static void main(String[] args) {
new BattleSystem(new Soilder()).Attacking(); //此處需要代理的物件為Soilder,通過battleSystem來間接實作士兵的攻擊效果
/*
* 此處輸出結果為:
* 遇到Boss,開始戰斗
* 使用了物理攻擊
* 傷害不夠,我們失敗了
* */
//如果此處有兩個角色來進行攻擊
new BattleSystem(new Soilder(),new Wizard()).Attacking(); //需要代理的物件變成了兩個,分別是Soilder和Wizard,通過battleSysteman來間接實作兩個角色的攻擊效果
/*
* 此處輸出結果為:
*遇到Boss,開始戰斗
*士兵使用了物理攻擊
*魔法師使用了魔法攻擊
*戰斗結束,我們勝利了
* */
}
}
interface Attack{ //攻擊系統介面
void Attacking();
}
class Soilder implements Attack{ //直接物件士兵實作介面
@Override
public void Attacking(){
System.out.println("士兵使用了物理攻擊");
}
}
class Wizard implements Attack{ //直接物件魔法師實作介面
@Override
public void Attacking() {
System.out.println("魔法師使用了魔法攻擊");
}
}
//代理角色,幫助自己
class BattleSystem implements Attack{ //代理物件戰場系統實作介面
private Attack target1;
private Attack target2;
public BattleSystem(Attack target){ //通過構造器將需要代理的物件傳進去
this.target1=target;
}
public BattleSystem(Attack target1,Attack target2){
this.target1=target1;
this.target2=target2;
}
@Override
public void Attacking() {
before();
this.target1.Attacking();
if(target2!=null) {
this.target2.Attacking();
after();
}
else{
anotherafter();
}
}
private void after() {
System.out.println("戰斗結束,我們勝利了");
}
private void before() {
System.out.println("遇到Boss,開始戰斗");
}
private void anotherafter(){
System.out.println("傷害不夠,我們失敗了");
}
}
- 靜態代理可以和Thread,Runnable介面進行對比,發現其原理
執行緒的狀態
- 創建狀態
- 就緒狀態
- 阻塞狀態
- 運行狀態
- 終止狀態
執行緒具體流程
- 創建執行緒物件:例如
Thread thread=new Thread();,執行緒會進入新建狀態 - 創建完執行緒物件后呼叫
start()方法時,執行緒會進入就緒狀態,并不意味著執行緒被立即調度執行 - 當執行緒被cpu進行調度之后,執行緒就會進入運行狀態,這個時候執行緒才會真正執行執行緒體的代碼塊
- 當執行緒物件呼叫
sleep(),wait()或同步鎖時執行緒會進入阻塞狀態,就是代碼不往下執行,阻塞事件解除后,將會重新進入就緒狀態,等待cpu的調度執行 - 如果執行緒沒有進入阻塞狀態,那么執行緒會在運行狀態之后進入關閉狀態,在執行緒中斷或者結束的時候就會進入關閉狀態,一旦進入則該執行緒就再也不能啟動了
執行緒的方法
setPriority(int newPriority):更改執行緒的優先級static void sleep(long millis):在指定的毫秒數內讓當前正在執行的執行緒休眠void join():使原有執行緒放棄執行,并呼叫該方法的回傳對應執行緒,直至該執行緒執行完畢static void yield():暫停當前正在執行的執行緒物件,使該執行緒處于就緒狀態,重新等待cpu調度void interrupt:中斷執行緒boolean isAlive():測驗執行緒是否處于活動狀態
停止執行緒(stop)
- 不推薦使用JDK顯示的已不推薦使用的方法,如stop(),destroy()等方法
- 推薦令執行緒自己停下來
- 可以通過使用一個標志位進行終止,令執行緒使用該標識的時候,通過對外提供方法來改變標識
- 代碼例子:
package ThreadStoptest;
public class StopTest implements Runnable{
private boolean flag=true;
@Override
public void run() {
int i=0;
while (flag){
System.out.println("get Hibiki "+i+" times");
i++;
}
}
public void stop(){
this.flag=false;
}
public static void main(String[] args) {
StopTest stopTest=new StopTest();
new Thread(stopTest).start();
for (int i = 0; i < 30; i++) {
System.out.println("主執行緒的進行次數為"+i);
if(i==25){
stopTest.stop();
}
}
}
}
執行緒的休眠(sleep)
-
要讓執行緒進行休眠需要用到sleep方法
- sleep方法存在例外InterruptedException;
- sleep時間達到后執行緒會重新進入就緒狀態
- sleep可以模擬網路延遲或者倒計時等
- 每個物件都會有一個鎖,且sleep不會釋放鎖
-
代碼例子(模擬倒計時):
package Demo01;
//模擬倒計時
public class TestSleep {
public static void main(String[] args) {
try {
time();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void time() throws InterruptedException { //拋出InterruptedException例外
int num = 13;
while (true) {
Thread.sleep(1000); //設定延時為1000毫秒,1000毫秒等于1秒
System.out.println(num--);
if (num <= 0) {
break;
}
}
}
}
- 獲取系統時間進行倒計時:
package Demo01;
import java.text.SimpleDateFormat;
import java.util.Date;
//模擬倒計時
public class TestSleep {
public static void main(String[] args) {
Date startTime = new Date(System.currentTimeMillis()); //獲取系統當前時間
while (true) {
try {
Thread.sleep(1000); //延時1000毫秒
System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
startTime = new Date(System.currentTimeMillis()); //更新獲取的系統時間
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
執行緒的讓步(yield)
- 執行緒的讓步,讓當前正在執行的執行緒暫停,但是不會阻塞
- 將執行緒從運行狀態轉為就緒狀態點擊跳轉狀態具體流程
- 需要讓cpu重新調度,因此讓步不一定成功
- 舉個比喻:一個盲人隨機發射幾門炮塔的炮彈,一次發射一個,一個炮彈引爆后才能隨機發射其余的炮彈,yield就是讓一個正在飛行的炮彈重新回到炮臺,然后盲人去重新隨機的發射一門炮塔的炮彈,
- 代碼例子:
package Demo01;
public class TestYield {
public static void main(String[] args) {
MyYield myYield=new MyYield();
new Thread(myYield,"hibiki").start();
new Thread(myYield,"ayanami").start();
new Thread(myYield,"Raffe").start();
}
}
class MyYield implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"執行緒開始執行"+i);
Thread.yield(); //執行緒讓行
System.out.println(Thread.currentThread().getName()+"執行緒停止執行"+i);
}
}
}
執行緒的強制加入(join)
-
join可以讓原本執行的執行緒停止執行(進入阻塞狀態)之后讓呼叫該方法的執行緒優先執行,等待該執行緒執行完畢之后,原本的執行緒和其他執行緒才會繼續執行,
-
代碼例子:
package JoinStudy;
public class TestJoin implements Runnable{
@Override
public void run() {
for (int i = 1; i < 100; i++) {
System.out.println("塞壬插了"+i+"次隊");
}
}
public static void main(String[] args) throws InterruptedException {
//創建執行緒物件
TestJoin testJoin=new TestJoin();
Thread thread =new Thread(testJoin);
//啟動執行緒
thread.start();
//主執行緒
for (int i = 1; i < 1000; i++) {
System.out.println("大家都在排隊"+i);
if(i%10==0) {
thread.join();
}
}
}
}
觀測執行緒的狀態
-
基本流程
- 需要先創建執行緒物件
- 為變數設定型別
Thread.state - 令變數等于執行緒呼叫
getstate方法后獲取的狀態 - 列印輸出
-
大致結構:
Thread thread =new Thread();
@Override
public void run(){
xxxxxxxxxxxx;
xxxxxxxxxxx;
}
Thread.State xxx = thread.getState();
System.out.println(xxx);
- 代碼例子:
package StateStudy;
import com.sun.media.sound.SoftTuning;
public class TestState {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("--------------");
});
//觀測狀態--------->NEW
Thread.State state = thread.getState();
System.out.println(state);
//觀測啟動后的狀態---------->Runnable
thread.start();
state=thread.getState();
System.out.println(state);
while(state!=Thread.State.TERMINATED){ //<-----只要執行緒不終止,就會一直運行下去
thread.sleep(100);
state=thread.getState();
System.out.println(state);
}
}
}
執行緒的優先級
-
Java會提供一個執行緒調度器來監控程式中啟動后進入就緒狀態的所有執行緒,執行緒調度器會按照優先級來調度哪個執行緒來執行,(最高優先級不一定被優先第一個調度,提高優先級的大小只能提高cpu調度其執行緒的概率)
-
執行緒的優先級用數字表示,范圍:1-10
- Thread.MIN_PRIIRITY = 1;
- Thread.MAX_PRIORITY= 10;
- Thread.NORM_PRIORITY= 5;
-
可以使用如下方式改變或獲取優先級
getPriority().setPriority(int xxx);
-
代碼例子:
package PriorityStudy;
public class TestPriority {
public static void main(String[] args) {
//主執行緒默認優先級
System.out.println(Thread.currentThread().getName()+"------->"+Thread.currentThread().getPriority());
//其他執行緒
MyPriority myPriority = new MyPriority();
Thread m1=new Thread(myPriority);
Thread m2=new Thread(myPriority);
Thread m3=new Thread(myPriority);
Thread m4=new Thread(myPriority);
Thread m5=new Thread(myPriority);
Thread m6=new Thread(myPriority);
//先設定優先級,再啟動
m1.start();
m2.setPriority(1); //設定優先級1
m2.start();
m3.setPriority(4);
m3.start();
m4.setPriority(Thread.MAX_PRIORITY);
m4.start();
m5.setPriority(6);
m5.start();
m6.setPriority(5);
m6.start();
}
}
class MyPriority implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"------->"+Thread.currentThread().getPriority());
}
}
守護執行緒
-
執行緒分為用戶執行緒和守護執行緒
-
虛擬機必須保證用戶執行緒執行完畢
-
虛擬機不用等待守護執行緒執行完畢
-
代碼例子:
package DaemonStudy;
public class TestDaemon {
public static void main(String[] args) {
Wizard wizard=new Wizard();
Thread thread=new Thread(wizard);
thread.setDaemon(true); //設定為守護執行緒,如果是false則為用戶執行緒
Warrior warrior=new Warrior();
thread.start();
new Thread(warrior).start();
}
}
//魔法師
class Wizard implements Runnable{
@Override
public void run() {
while(true){ //判定為true,按理來說為無限回圈
System.out.println("魔法師為戰士加了buff");
}
}
}
//戰士
class Warrior implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("戰士的能力加強了");
}
System.out.println("戰士結束了戰斗");
}
}
執行緒的同步機制
基本概念
-
并發:同一個物件被多個執行緒同時操作
-
多個執行緒訪問同一個物件,并且某些執行緒還想修改這個物件,就需要執行緒同步
-
執行緒同步其實是一種等待機制,多個需要同時訪問此物件的執行緒會進入這個物件的等待池形成佇列,等待前面的執行緒結束后再執行
-
鎖機制
- 由于同一個行程的多個執行緒共享同一塊存盤空間,在帶來方便的同時,也帶來了訪問沖突的問題,為了保證資料在方法中被訪問時的正確性,在訪問時加入鎖機制,當一個執行緒獲得物件的排它鎖,獨占資源,其他執行緒必須等待,使用后釋放鎖即可
- 存在以下問題:
- 一個執行緒持有鎖會導致其他所有需要此鎖的執行緒掛起
- 在多個執行緒競爭下,加鎖和釋放鎖會導致比較多的背景關系切換和調度延時,引起性能問題
- 如果一個優先級高的執行緒等待一個優先級低的執行緒釋放鎖,會導致優先級倒置,引起性能問題
同步方法及同步塊
-
private關鍵字可用來保證資料物件只能被方法訪問,而對于方法則提出了另一套機制,就叫做synchronized關鍵字
-
synchronized關鍵字包括兩種用法:synchronized方法和synchronized塊
- 同步方法:
public synchronized void method(int args){}
- 同步方法:
-
synchronized方法控制對"物件"的訪問,每個物件對應一把鎖,每個synchronized方法都必須獲得呼叫該方法的物件的鎖才能執行,否則執行緒會阻塞,方法一旦執行,就會獨占該鎖,直到該方法回傳才釋放鎖,這樣后面被阻塞的執行緒才能獲得這個鎖后再繼續執行
-
synchronized關鍵字的缺點:將一個較大且復雜的方法宣告為synchronized時會影響其效率
-
同步方法代碼例子:
package SynchronizedStudy;
public class TestSynchronized {
public static void main(String[] args) {
MeetCommander station = new MeetCommander();
System.out.println("---------艦隊回港后----------");
new Thread(station,"Hibiki").start();
new Thread(station,"Raffe").start();
new Thread(station,"Z23").start();
}
}
class MeetCommander implements Runnable{
//見面次數
private int meetNums = 10;
boolean flag = true; //外部寫入停止方法
@Override
public void run() {
//見面
while(flag){
try {
meet();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private synchronized void meet() throws InterruptedException { //同步方法鎖的是使用這個方法的物件,因此此處鎖的是MeetCommander這個物件
//判斷還能見多少次
if(meetNums<=0){
flag=false;
return;
}
//模擬延時
Thread.sleep(100);
//見面
System.out.println(Thread.currentThread().getName()+"見了"+meetNums--+"次指揮官");
}
}
倘若該代碼不加上synchronized關鍵字,則該程式會出現負數的情況
- 同步塊:
synchronized(Obj){} //一般鎖的是需要改變其內部屬性的物件,即會發生變化的物件 - Obj稱之為同步監視器
- Obj可以是任何物件,但是推薦使用共享資源作為同步監視器
- 同步方法中無需指定同步監視器,因為同步方法的同步監視器就是其本身的物件,或者是class
- 同步監視器的執行程序
- 第一個執行緒訪問,鎖定同步監視器,執行其中代碼
- 第二個執行緒訪問,發現同步監視器被鎖定,無法訪問
- 第一個執行緒訪問完畢,解鎖同步監視器
- 第二個執行緒訪問,發現同步監視器沒有鎖,然后鎖定并訪問
- 代碼例子:
package SynchronizedStudy;
import java.util.List;
import java.util.ArrayList;
public class TestSynchronized {
public static void main(String[] args){
List<String> list= new ArrayList<String>();
for (int i = 0; i < 10000; i++) {
new Thread(()->{
synchronized (list) {
list.add(Thread.currentThread().getName());
}
}).start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
}
JUC安全型別
- Java它本身會提供一個安全的ArrayList的型別,使用其型別也能達到synchronized關鍵字的效果
- 代碼例子:
package SynchronizedStudy;
import java.util.concurrent.CopyOnWriteArrayList;
public class TestSynchronized {
public static void main(String[] args){
CopyOnWriteArrayList<String> list= new CopyOnWriteArrayList<String>();
for (int i = 0; i < 10000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
}
死鎖
- 多個執行緒各自占有一些共享資源,當一個執行緒需要其他執行緒占有的資源才能運行時,會導致連鎖反應使其他執行緒都在等待對方釋放占有的資源,從而使全部執行緒都停止執行,當一個同步塊同時擁有"兩個以上物件的鎖"時,就有可能發生"死鎖"的問題,
- 代碼例子:
package LockStudy;
public class TestLock {
public static void main(String[] args) {
TakeFood g1=new TakeFood(1,"Hibiki");
TakeFood g2=new TakeFood(0,"Ayanami");
new Thread(g1).start();
new Thread(g2).start();
}
}
class Bread{
}
class Milk{
}
class TakeFood implements Runnable{
//用static來保證資源只有一份
static Bread bread =new Bread();
static Milk milk =new Milk();
int choice ; //選擇
String Name; //拿資源的人
TakeFood(int choice,String Name){
this.choice=choice;
this.Name=Name;
}
@Override
public void run() {
try {
takefood();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void takefood() throws InterruptedException {
if(choice ==0){
synchronized (bread){
System.out.println(this.Name+"拿到了面包");
Thread.sleep(1000);
synchronized (milk){
System.out.println(this.Name+"拿到了牛奶");
}
}
}
else{
synchronized (milk){
System.out.println(this.Name+"拿到了牛奶");
Thread.sleep(1000);
synchronized (bread){
System.out.println(this.Name+"拿到了面包");
}
}
}
}
}
/*最后會發現程式執行到一一半之后就不會執行了,原因是執行緒同時執行的時候都需要對方所有的資源,從而導致雙方都不能拿到自己需要的,因此程式相當于被打了個"死結"*/
鎖
-
從JDK5.0開始,Java提供了更強大的執行緒同步機制——通過顯式定義同步鎖物件來實作同步(同步鎖由Lock物件實作),
-
java.util.concurrent.locks.Lock介面是控制多個執行緒對共享資源進行訪問的工具,鎖提供了對共享資源的獨占訪問,每次只能有一個執行緒對Lock物件加鎖,執行緒開始訪問共享資源之前應先得到Lock物件
-
ReentrantLock類實作了Lock,它擁有與synchronized相同的并發性和記憶體語意,在實作執行緒安全的控制中,兩者比較常用的是ReentrantLock,可以顯式加鎖和釋放鎖,
-
基本用法:
class Name{
private final ReentrantLock lock =new ReenTrantLock();
public void m(){
lock.lock();
try{
//保證執行緒安全的代碼
}
finally{
lock.unlock();
//如果同步代碼有例外,要將unlock()寫入finally陳述句塊
}
}
}
- synchronized與Lock的對比
- Lock是顯式鎖(手動開啟和關閉鎖),synchronized是隱式鎖,出了作用域自動釋放
- Lock只有代碼塊鎖,synchronized有代碼塊鎖和方法鎖
- 使用Lock鎖,JVM將花費較少的時間來調度執行緒,性能更好,并且具有更好的拓展性(提供更多的子類)
- 優先使用順序:
- Lock>同步代碼塊(已經進入了方法體,分配了相應資源)>同步方法(在方法體之外)
執行緒協作
生產消費者模式
-
這是一個執行緒同步問題,生產者和消費者共享同一個資源,并且生產者和消費者之間互相依賴,互為條件,
- 對于生產者,沒有生產產品之前,要通知消費者等待,而生產了產品之后,又需要馬上通知消費者消費
- 對于消費者,在消費之后,要通知生產者已經結束消費,需要生產新的產品以供消費
- 在生產者消費者問題中,僅有synchronized是不夠的
- synchronized可阻止并發更新同一個共享資源,實作了同步
- synchronized不能用來實作不同執行緒之間的訊息傳遞(通信)
-
Java提供了以下方法來解決執行緒之間的通信問題:
wait():表示執行緒一直等待,直到其他執行緒通知,與sleep()不同,會釋放鎖wait(long timeout):指定等待的毫秒數notify():喚醒一個處于等待狀態的執行緒notifyAll():喚醒同一個物件上所有呼叫wait()方法的執行緒,優先級別高的執行緒優先調度- 注意:以上均是Object類的方法,都只能在同步方法或者同步代碼塊中使用,否則會拋出例外IIlegalMonitorStateException
-
解決方式:
-
解決方式1:
- 并發協作模型"生產者/消費者模式"--->管程法
- 生產者:負責生產資料的模塊(可能是方法、物件、執行緒、行程);
- 消費者:負責處理資料的模塊(可能是方法、物件、執行緒、行程);
- 緩沖區:消費者不能直接使用生產者的資料,他們之間有個"緩沖區"
生產者將生產好的資料放入緩沖區,消費者從緩沖區拿出資料
Producer(生產者)--->資料快取區---->Consumer(消費者)
-
代碼例子:
package PCstudy; //測驗生產者消費者模式--->管程法 public class TestPC { public static void main(String[] args) { SynContainer container =new SynContainer(); new Producer(container).start(); new Consumer(container).start(); } } //生產者 class Producer extends Thread { SynContainer container; public Producer(SynContainer container) { this.container = container; } //生產 @Override public void run(){ for (int i = 0; i < 100; i++) { System.out.println("生產了"+i+"個產品"); container.put(new Product(i)); } } } //消費者 class Consumer extends Thread{ SynContainer container; public Consumer(SynContainer container){ this.container=container; } //消費 @Override public void run(){ for (int i = 0; i < 100; i++) { System.out.println("消費了--->"+container.pop().id+"個產品"); } } } //產品 class Product{ int id; //產品編號 public Product(int id) { this.id = id; } } //緩沖區 class SynContainer{ //需要一個容器大小 Product[] products= new Product[10]; //容器計數器 int count=0; //生產者放入產品 public synchronized void put(Product product){ //如果容器滿了,就需要等待消費者消費 if(count==products.length){ try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //如果沒滿,就需要放入產品 products[count]=product; count++; //可以通知消費者消費 this.notifyAll(); } public synchronized Product pop(){ //判斷能否消費 if(count==0){ //等待生產者生產,消費者等待消費 try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //如果可以消費 count--; Product product=products[count]; //產品沒了,通知生產者生產 this.notifyAll(); return product; } } -
解決方法2:
- 并發協作模型"生產者/消費者模式"--->信號法
- 例如給一個標志位進行一次判斷,若為true則等待,false則喚醒其他執行緒
-
代碼例子:
package PCStudy2; public class TestPC2 { public static void main(String[] args) { TVprogram tVprogram=new TVprogram(); new Actor(tVprogram).start(); new Audience(tVprogram).start(); } } //觀眾 class Audience extends Thread{ TVprogram tVprogram; public Audience(TVprogram tVprogram) { this.tVprogram = tVprogram; } @Override public void run(){ for (int i = 0; i < 20; i++) { tVprogram.watch(); } } } //演繹人 class Actor extends Thread{ TVprogram tVprogram; public Actor(TVprogram tVprogram){ this.tVprogram=tVprogram; } @Override public void run(){ for (int i = 0; i < 20; i++) { if(i%2==0){this.tVprogram.play("掃黑風暴");} else{ this.tVprogram.play("bilibili"); } } } } //電視劇 class TVprogram { String program; //表演的節目 boolean flag=true; //默認信標為T //演員表演 觀眾等待 T public synchronized void play(String program){ if(!flag){ try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("演員表演了:"+program); this.notifyAll(); //通知觀眾觀看 this.program=program; this.flag=!this.flag; } //觀眾觀看 演員表演 F public synchronized void watch(){ if(flag){ try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("觀看了:"+program); //通知演員表演 this.notifyAll(); this.flag=!this.flag; } } -
執行緒池
-
什么時候要使用執行緒池:經常創建和銷毀、使用量特別大的資源,比如并發情況下的執行緒,這種情況會對性能影響很大,
-
具體用法思路:提前創建好多個執行緒,放入執行緒池中,使用時直接獲取,使用完放回池中,可以避免頻繁創建銷毀,實作重復利用,
-
好處:
- 提高了回應速度(減少了創建新執行緒的時間)
- 降低資源消耗(重復利用執行緒池中的執行緒,不需要每次都創建)
- 便于執行緒的管理(不是方法):
corePoolSize:執行緒池大小maximumPoolSize:最大執行緒數keepAliveTime:執行緒沒有任務時最多保持多長時間后會終止
-
JDK5.0之后提供了執行緒池相關的API:ExecutorService和Executors
- ExecutorService:真正的執行緒池介面,常見子類ThreadPoolExecutor
void execute(Runnable command):執行任務/命令,沒有回傳值,一般用來執行Runnable<T>Future<T>submit(Callable<T>task):執行任務,有回傳值,一般用來執行Callablevoid shutdown():關閉連接池
- Executors:工具類、執行緒池的工廠類,用于創建并回傳不同型別的執行緒池
- ExecutorService:真正的執行緒池介面,常見子類ThreadPoolExecutor
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/302893.html
標籤:Java
下一篇:學習筆記:juc并發編程總結
