文章目錄
- Day 22
- 線程簡介
- 線程建立
- 線程範例:龜兔賽跑
- 線程狀态
- 線程方法
- 線程同步
- 線程死鎖
- 線程協作——生産者消費者模式
- 線程池
Day 22
2019年6月2日。
這是我學習Java的第二十二天。
這一天,我學到了以下的知識。
線程簡介
線程,是作業系統能夠進行運算排程的最小機關。它被包含在程序之中,是程序中的實際運作機關。一條線程指的是程序中一個單一順序的控制流,一個程序中可以并發多個線程,每條線程并行執行不同的任務。
在Java中,線程的執行如圖所示:
說起線程,就必須要說到程式。程式是指令和資料的有序集合,其本身沒有任何運作的含義,是一個靜态的概念。
而程序則是執行程式的一次執行過程,它是一個動态的概念,是系統資源配置設定的機關。
通常在一個程序中可以包含若幹個線程,當然一個程序中至少有一個線程,不然沒有存在的意義。線程是CPU排程和執行的機關。
線程的性質如下所述:
- 線程就是獨立的執行路徑;
- 在程式運作時,即使沒有自己建立線程,背景也會有多個線程,如主線程,gc線程;
- main()稱之為主線程,為系統的入口,用于執行整個程式;
- 在一個程序中,如果開辟了多個線程,線程的運作由排程器安排排程,排程器是與作業系統緊密相關的,先後順序是不能認為的幹預的;
- 對同一份資源操作時,會存在資源搶奪的問題,需要加入并發控制;
- 線程會帶來額外的開銷,如cpu排程時間,并發控制開銷。
- 每個線程在自己的工作記憶體互動,記憶體控制不當會造成資料不一緻。
線程建立
線程的建立,在Java中,具有三種方式:
-
繼承Thread類
- 實作步驟:
1.自定義線程類繼承Thread類
2.重寫run()方法,編寫線程執行體
3.建立線程對象,調用start()方法啟動線程
- 範例如下:
public class TestThread extends Thread{
//自定義run方法的線程
@Override
public void run() {
//線程執行體
for (int i = 0; i < 200; i++) {
System.out.println("a:" + i);
}
}
//主線程
public static void main(String[] args) {
//建立線程對象
TestThread testThread1 = new TestThread1();
//調用start方法啟動線程
testThread1.start();
//同時進行
for (int i = 0; i < 3000; i++) {
System.out.println("b:" + i);
}
}
}
-
實作Runnable接口(常用)
- 實作步驟:
1.定義MyRunnable類實作Runnable接口
2.實作run()方法,編寫線程執行體
3.建立線程對象,調用start()方法啟動線程
- 範例如下:
public class TestThread implements Runnable{
@Override
public void run() {
//線程執行體
for (int i = 0; i < 200; i++) {
System.out.println("a:" + i);
}
}
public static void main(String[] args) {
//重點就是将runable接口實作類的對象丢入Thread構造器
TestThread testThread3 = new TestThread3();
Thread thread = new Thread(testThread3);
thread.start();
for (int i = 0; i < 3000; i++) {
System.out.println("b:" + i);
}
}
}
-
實作Callable接口
- 實作步驟:
1.實作Callable接口,需要傳回值類型
2.重寫call方法,需要抛出異常
3.建立目标對象
4.建立執行服務:ExecutorService ser = Executors.newFixedThreadPool(1);
5.送出執行:Future<Boolean.> result = ser.submit(t1);
6.關閉服務:ser.shutdownNow();
- 範例如下:
public class Demo4Thread implements Callable<Boolean> {
@Override
public Boolean call() throws Exception {
return false;
}
public static void main(String[] args) {
Demo4Thread demo4Thread = new Demo4Thread();
ExecutorService ser = Executors.newFixedThreadPool(1);
Future<Boolean> result1 = ser.submit(demo4Thread);
}
}
-
繼承Thread類和實作Runnable接口的差別
- 繼承Thread類
1.子類繼承Thread類具備多線程能力
2.啟動線程:子類對象.start()
3.不建議使用:避免OOP單繼承局限性
- 實作Runnable接口
1.實作接口Runnable具有多線程能力
2.啟動線程:傳入目标對象+Thread對象.start()
3.推薦使用:避免單繼承局限性,靈活友善,友善同一個對象被多個線程使用
線程範例:龜兔賽跑
需求:模仿“龜兔賽跑”的故事,在Java中用兩個線程來實作
分析:
- 龜兔賽跑開始
- 故事中是烏龜赢的,兔子需要睡覺,是以要模拟兔子睡覺
- 首先定義賽道距離,然後烏龜離賽道終點越來越近
- 判斷比賽是否結束
- 最終,烏龜赢得比賽
- 列印出勝利者
代碼如下:
public class Race implements Runnable{
//winner:隻有一個勝利者
private static String winner;
@Override
public void run() {
//賽道
for (int step = 1; step <= 101; step++) {
//模拟兔子休眠
if (Thread.currentThread().getName().equals("兔子") && step % 50 ==0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//判斷比賽是否結束
boolean flag = gameOver(step);
if (flag){
break;
}
System.out.println( Thread.currentThread().getName() + "跑了" + step +"步");
}
}
//判斷比賽是否結束
private boolean gameOver(int step){
if (winner != null){ // 如果存在勝利者
return true;
}
if (step >= 100){ // 如果跑到了終點
winner = Thread.currentThread().getName();
System.out.println("比賽結束");
System.out.println("勝利者--->" + winner);
return true;
}
return false;
}
public static void main(String[] args) {
Race race = new Race();
new Thread(race,"兔子").start();
new Thread(race,"烏龜").start();
}
}
線程狀态
線程的狀态,在Java中,共具有5種,如圖所示:
線程的狀态轉換,如圖所示:
線程方法
在Java中,線程類裡存在一些特定的方法,可以對線程進行管理,如下所示:
-
線程停止
雖然線程類中提供了stop()方法和destroy()方法,但是不推薦使用,而是推薦線程自己停止下來。
若想要線程自行停止,建議使用一個标志位充當終止變量,當flag=false,則終止線程運作。
- 線程休眠(sleep)
- sleep(時間)指定目前線程阻塞的毫秒數;
- sleep存在異常InterruptedException;
- sleep時間達到後線程進入就緒狀态;
- sleep可以模拟網絡延時,倒計時等‘’
- 每一個對象都有一個鎖,sleep不會釋放鎖;
用線程休眠來模拟系統時間并且讓時間流動,示例如下:
public class TestSleep2 {
public static void main(String[] args) throws InterruptedException {
TestSleep2 testSleep2 = new TestSleep2();
//擷取系統時間
Date startTime = new Date(System.currentTimeMillis());
while (true) {
System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
Thread.sleep(1000);
startTime = new Date(System.currentTimeMillis());
}
}
//倒計時方法
private void tenDown() throws InterruptedException {
int num = 10;
for (int i = 10; i > 0; i--) {
Thread.sleep(1000);
System.out.println("倒計時:"+i);
}
}
}
- 線程禮讓(yield)
- 禮讓線程,讓目前正在執行的線程暫停,但不阻塞;
- 将線程從運作狀态轉為就緒狀态;
- 讓cpu重新排程,禮讓不一定會成功!
示例如下:
public class TestYield {
public static void main(String[] args) throws InterruptedException {
MyYield myYield = new MyYield();
new Thread(myYield,"小明").start();
new Thread(myYield,"老師").start();
}
}
class MyYield implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"-->啟動了");
Thread.yield();//禮讓
System.out.println(Thread.currentThread().getName()+"-->停止了");
}
}
- 線程插隊(join)
- Join可以插入線程,當該線程執行完畢後,再執行其他線程(其他線程之前會阻塞);
-
join與現實世界的插隊類似;
示例如下:
public class TestJoin implements Runnable{
public static void main(String[] args) throws InterruptedException {
TestJoin testJoin = new TestJoin();
Thread thread = new Thread(testJoin);
thread.start();
for (int i = 0; i < 100; i++) {
if (i==80){
//強制執行
thread.join();
}
System.out.println("我是主線程:"+i);
}
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("我是要插隊的線程:"+i);
}
}
}
-
線程狀态觀測(State)
通過Thread.State()方法,可以檢視線程目前的狀态。
線程的狀态,有以下幾種:
-
NEW
尚未啟動的線程處于此狀态
-
RUNNABLE
在Java虛拟機中執行的線程處于此狀态
-
BLOCKED
被阻塞等待螢幕鎖定的線程處于此狀态
-
WAITING
正在等待另一個線程執行特定動作的線程處于此狀态
-
TIMED WAITING
正在等待另一個線程執行動作達到指定等待時間的線程處于此狀态
-
TERMINATED
已退出的線程處于此狀态
一個線程可以在給定時間點處于一個狀态,這些狀态是不反映任何作業系統線程狀态的虛拟機狀态
執行個體如下:
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);
System.out.println(Thread.currentThread().getState());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
}
}
- 線程優先級(priority)
- Java提供一個線程排程器來監控程式中啟動後進入就緒狀态的所有線程,線程排程器按照優先級決定應該排程哪個線程來執行
- 線程的優先級用數字表示,範圍從1~10
- Thread.MIN_PRIORITY = 1;
- Thread.MAX_PRIORITY = 10;
- Thread.NORM_PRIORITY = 5;
-
使用這些方式可以改變(setPriority)或者擷取優先級(getPriority)
示例如下:
public class TestPriority {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());
MyPriority myPriority = new MyPriority();
Thread thread1 = new Thread(myPriority);
Thread thread2 = new Thread(myPriority);
Thread thread3 = new Thread(myPriority);
Thread thread4 = new Thread(myPriority);
Thread thread5 = new Thread(myPriority);
thread1.setPriority(1);
thread1.start();
thread2.setPriority(4);
thread2.start();
thread3.setPriority(8);
thread3.start();
thread4.setPriority(9);
thread4.start();
thread5.setPriority(10);
thread5.start();
}
}
class MyPriority implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());
}
}
- 守護線程(daemon)
- 線程分為使用者線程和守護線程;
- 虛拟機必須確定使用者線程執行完畢;
- 虛拟機不用等待守護線程執行完畢
- 守護程序一般用作背景記錄記錄檔、監控記憶體、以及垃圾回收等待
示例如下:
public class TestDaemon {
public static void main(String[] args) {
God god = new God();
Thread thread = new Thread(god);
thread.setDaemon(true); //設定線程為守護線程,預設為false
thread.start();
//使用者線程
new Thread(()->{
for (int i = 0; i < 30; i++) {
System.out.println("你開心的在這個世界上活着"+i);
}
System.out.println("=======Goodbye , World!");
}).start();
}
}
//守護線程
class God implements Runnable{
@Override
public void run() {
for (;true;){
System.out.println("上帝保佑着你");
}
}
}
線程同步
若多個線程操作同一個資源,可能會出現線程不安全的情況。
處理多線程問題時,多個線程通路同一個對象,并且某些線程還想修改這個對象,這時候就需要線程同步。線程同步實質上就是一種等待機制,多個需要同時通路此對象的線程進入這個對象的等待池形成隊列,等待前面線程使用完畢,下一個線程再使用。
由于同一個程序的多個線程共享同一塊存儲空間,為了保證資料在方法中被通路時的正确性,在通路時加入鎖機制synchronized,當一個線程獲得對象的排它鎖,獨占資源,其他線程必須等待,使用後釋放鎖即可。該機制會帶來以下問題:
- 一個線程持有鎖會導緻其他所有需要此鎖的線程挂起;
- 在多線程競争下,加鎖,釋放鎖會導緻比較多的上下文切換和排程延時,引起性能問題;
- 如果一個優先級高的線程等待一個優先級低的線程釋放鎖,會導緻優先級倒置,引起性能問題。
同步方法(并發問題一)
-
由于可以通過private關鍵字來保證資料對象隻能被方法通路,是以隻需要針對方法提出一套機制,這套機制就是synchronized關鍵字,它包括兩種用法:synchronized方法和synchronized塊
同步方法:
public synchronized void method(int args){}
- synchronized方法控制對”對象“的通路,每個對象對應一把鎖,每個synchronized方法都必須獲得調用該方法的對象的鎖才能執行,否則線程會阻塞,方法一旦執行,就獨占該鎖,直到該方法傳回才釋放鎖,後面被阻塞的線程才能獲得這個鎖,并繼續執行(缺陷:若将一個大的方法申明為synchronized,将會影響效率)
假設同時有三個人在買票,票數總共有10張,若不添加同步關鍵字,則會出現線程不安全的問題(即會出現一張票被同時買到的情況)示例如下:
1.不安全情況
public class UnsafeBuyTicket implements Runnable {
//票數
private int ticketNums = 10;
//标志位
private boolean flag = true;
@Override
public void run() {
//買票
while (flag) {
buyTicket();
}
}
public void buyTicket() {
if (ticketNums <= 0) {
flag = false;
return;
}
//模拟網絡延時
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->拿到了第" + ticketNums-- + "張票");
}
public static void main(String[] args) {
UnsafeBuyTicket station = new UnsafeBuyTicket();
new Thread(station,"苦逼的我").start();
new Thread(station,"牛逼的你們").start();
new Thread(station,"可惡的黃牛黨").start();
}
}
2.安全情況(加入同步方法)
public class SafeBuyTicket implements Runnable {
//票數
private int ticketNums = 10;
//标志位
private boolean flag = true;
@Override
public void run() {
//買票
while (flag) {
buyTicket();
}
}
//同步方法,關鍵詞synchroinzed.
//關鍵字是鎖
//實作的機制是隊列
//還能所反射的那個class
public synchronized void buyTicket() {
if (ticketNums <= 0) {
flag = false;
return;
}
//模拟網絡延時
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->拿到了第" + ticketNums-- + "張票");
}
public static void main(String[] args) {
SafeBuyTicket station = new SafeBuyTicket();
new Thread(station,"苦逼的我").start();
new Thread(station,"牛逼的你們").start();
new Thread(station,"可惡的黃牛黨").start();
}
}
同步塊(并發問題二)
- 格式:
synchronized(Obj obj){}
,Obj稱之為同步螢幕
- Obj可以是任何對象,但是推薦使用共享資源作為同步螢幕
- 同步方法中無需指定同步螢幕,因為同步方法的同步螢幕就是this,就是這個對象本身,或者是class
-
同步監視的執行過程
- 第一個線程通路,鎖定同步螢幕,執行其中代碼;
- 第二個線程通路,發現同步螢幕被鎖定,無法通路;
- 第一個線程通路完畢,解鎖同步螢幕;
- 第二個線程通路,發現同步螢幕沒有鎖,然後鎖定并通路
假設同時有兩個人在銀行取錢,若不添加同步關鍵字,則會出現線程不安全的問題(即會出現第一個人把存款取完,在銀行存款還沒有及時重新整理時,第二個人再次取款,則會讓銀行存款變為負數)示例如下:
1.不安全情況
public class UnsafeBank {
public static void main(String[] args) {
Account account = new Account(100,"招商卡");
Bank you = new Bank("痛苦的你",account,50);
Bank wife = new Bank("開心的媳婦",account,100);
you.start();
wife.start();
}
}
//賬戶
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; //取了多少錢
int nowMoney; //手裡有多少錢
public Bank(String name,Account account,int drawingMoney){
super(name);
this.account = account;
this.drawingMoney = drawingMoney;
}
@Override
public void run() {
//判斷能否取錢
if (account.money-drawingMoney<0){
return;
}
//為了放大問題發生性,我們加個延時.
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//餘額 = 餘額 - 你去走的錢
account.money = account.money - drawingMoney;
//你的錢 = 你的錢 + 你取的錢
nowMoney = drawingMoney + nowMoney;
System.out.println(this.account.name+"賬戶餘額:"+account.money);
System.out.println(this.getName()+"手裡的錢:"+nowMoney);
}
}
2.安全情況(同步塊)
public class SafeBank {
public static void main(String[] args) {
Account2 account = new Account2(100,"招商卡");
Bank2 you = new Bank2("痛苦的你",account,50);
Bank2 wife = new Bank2("開心的媳婦",account,100);
you.start();
wife.start();
}
}
//賬戶
//實體類
class Account2{
int money;//餘額
String name; //卡名
public Account2(int money, String name) {
this.money = money;
this.name = name;
}
}
//銀行
class Bank2 extends Thread{
//存錢:存了多少,取錢:取了多少
Account2 account; //賬戶
int drawingMoney; //取了多少錢
int nowMoney; //手裡有多少錢
public Bank2(String name,Account2 account,int drawingMoney){
super(name);
this.account = account;
this.drawingMoney = drawingMoney;
}
@Override
public void run() {
drwaing();
}
//synchronized本身鎖的是this.就是這個對象本身
public void drwaing(){
//提高性能的代碼
if (account.money<=0){
return;
}
//如何判斷鎖的對象
// 誰需要實作增删改就去鎖定他
synchronized (account){
//判斷能否取錢
if (account.money-drawingMoney<0){
System.out.println(Thread.currentThread().getName()+"活該,沒取到錢");
return;
}
//為了放大問題發生性,我們加個延時.
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//餘額 = 餘額 - 你去走的錢
account.money = account.money - drawingMoney;
//你的錢 = 你的錢 + 你取的錢
nowMoney = drawingMoney + nowMoney;
System.out.println(this.account.name+"賬戶餘額:"+account.money);
System.out.println(this.getName()+"手裡的錢:"+nowMoney);
}
}
}
List(并發問題三)
因為List并非是線程安全的,是以同樣需要用synchronized來令List線程的線程安全。
示例如下:
1.不安全情況
public class UnSafeList {
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<String>();
for (int i = 0; i < 20000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
for (int i = 5;i>0;i--){
Thread.sleep(1000);
System.out.println("倒計時"+i);
}
System.out.println(list.size());
}
}
2.安全情況(同步關鍵字)
public class SafeList {
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<String>();
for (int i = 0; i < 10000; i++) {
new Thread(()->{
synchronized (list){
list.add(Thread.currentThread().getName());
}
}).start();
}
for (int i = 5;i>0;i--){
Thread.sleep(1000);
System.out.println("倒計時"+i);
}
System.out.println(list.size());
}
}
線程死鎖
- 概念:多個線程各自占有一些共享資源,并且互相等待其他線程占有的資源才能運作。而導緻兩個或者多個線程都在等待對方釋放資源,都停止執行的情形。某一個同步塊同時擁有“兩個以上對象的鎖”時,就可能發生“死鎖”的問題
-
産生死鎖的必要條件:
- 互斥條件:一個資源每次隻能被一個程序使用
- 請求與保持條件:一個程序因請求資源而阻塞時,對已獲得的資源保持不放
- 不剝奪條件:程序已獲得的資源,在未使用完之前,不能強行剝奪
- 循環等待條件:若幹程序之間形成一種頭尾相連的循環等待資源關系
解決死鎖
上面列出的死鎖的四個必要條件,隻要想辦法破壞其中的任意一個或多個條件,就可以避免死鎖發生
假設有兩個女生,在出門之前同時進行化妝(需要拿鏡子和口紅),若一個女生先拿到鏡子,另一個女生先拿到口紅,之後,為了得到彼此的口紅和鏡子,兩方都需要等待,就會發生死鎖,示例如下:
1.不安全情況
public class DeadLocked {
public static void main(String[] args) {
Makeup g1 = new Makeup(0,"白雪公主");
Makeup g2 = new Makeup(1,"灰姑涼");
new Thread(g1).start();
new Thread(g2).start();
}
}
//化妝
class Makeup implements Runnable{
//選擇
int choice;
//誰進來了
String girlName;
//兩個對象
static LipStick lipStick = new LipStick();
static Mirror mirror = new Mirror();
public Makeup(int choice,String girlName){
this.choice = choice;
this.girlName = girlName;
}
@Override
public void run() {
//化妝
try {
makeup();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//化妝的方法
public void makeup() throws InterruptedException {
if (choice==0){ //先拿口紅,再拿鏡子
synchronized (lipStick){
System.out.println("拿到口紅");
Thread.sleep(1000);
//等待拿鏡子的人釋放鎖
synchronized (mirror){
System.out.println("拿到鏡子");
}
}
}else { //先拿鏡子 , 再拿口紅
synchronized (mirror){
System.out.println("拿到鏡子");
Thread.sleep(2000);
//等待拿口紅的人釋放鎖
synchronized (lipStick){
System.out.println("拿到口紅");
}
}
}
}
}
2.安全情況(避免synchronized塊嵌套)
public class DeadLocked {
public static void main(String[] args) {
Makeup g1 = new Makeup(0,"白雪公主");
Makeup g2 = new Makeup(1,"灰姑涼");
new Thread(g1).start();
new Thread(g2).start();
}
}
//化妝
class Makeup implements Runnable{
//選擇
int choice;
//誰進來了
String girlName;
//兩個對象
static LipStick lipStick = new LipStick();
static Mirror mirror = new Mirror();
public Makeup(int choice,String girlName){
this.choice = choice;
this.girlName = girlName;
}
@Override
public void run() {
//化妝
try {
makeup();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//化妝的方法
public void makeup() throws InterruptedException {
if (choice==0){ //先拿口紅,再拿鏡子
synchronized (lipStick){
System.out.println("拿到口紅");
Thread.sleep(1000);
//等待拿鏡子的人釋放鎖
}
synchronized (mirror){
System.out.println("拿到鏡子");
}
}else { //先拿鏡子 , 再拿口紅
synchronized (mirror){
System.out.println("拿到鏡子");
Thread.sleep(2000);
//等待拿口紅的人釋放鎖
}
synchronized (lipStick){
System.out.println("拿到口紅");
}
}
}
}
//口紅
class LipStick{
}
//鏡子
class Mirror{
}
Lock(鎖)
- 從JDK 5.0開始,Java提供了更強大的線程同步機制——通過顯示定義同步鎖對象來實作同步。同步鎖使用Lock對象充當
- java.util.concurrent.locks.Lock接口是控制多個線程對共享資源進行通路的工具。鎖提供了對共享資源的獨占通路,每次隻能有一個線程對Lock對象加鎖,線程開始通路共享資源之前應先獲得Lock對象
- ReentranLock類實作了Lock,它擁有與synchronized相同的并發性和記憶體語義,在實作線程安全的控制中,比較常用的是ReentrantLock,可以顯式加鎖、釋放鎖
示例如下:
public class TestLock {
public static void main(String[] args) {
HelloWorld helloWorld = new HelloWorld();
new Thread(helloWorld).start();
new Thread(helloWorld).start();
}
}
class HelloWorld implements Runnable{
int ticketNums = 100;
//可重入鎖
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true){
try {
lock.lock(); //加鎖
//判斷是否有票
if (ticketNums>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticketNums--);
}else {
break;
}
} finally {
lock.unlock();//解鎖
}
}
}
}
synchronized 與 Lock 的對比
- Lock是顯式鎖(手動開啟和關閉鎖,别忘記關閉鎖)synchronized是隐式鎖,出了作用域自動釋放
- Lock隻有代碼塊鎖,synchronized有代碼塊鎖和方法鎖
- 使用Lock鎖,JVM将花費較少的時間來排程線程,性能更好。并且具有更好的擴充性(提供更多的子類)
-
優先使用順序:
- Lock > 同步代碼塊(已經進入了方法體,配置設定了相應資源) > 同步方法(在方法體之外)
線程協作——生産者消費者模式
-
應用場景:生産者和消費者問題
- 假設倉庫隻能存放一件産品,生産者将生産出來的産品放入倉庫,消費者将倉庫中産品取走消費
- 如果倉庫中沒有産品,則生産者将産品放入倉庫,否則停止生産并等待,直到倉庫中的産品被消費者取走為止
- 如果倉庫中放有産品,則消費者可以将産品取走消費,否則停止消費并等待,直到倉庫中再次放入産品為止
-
分析:這是一個線程同步問題,生産者和消費者共享同一個資源,并且生産者和消費者之間互相依賴,互為條件
- 對于生産者,沒有生産産品之前,要通知消費者等待,而生産了産品之後,又需要馬上通知消費者消費
- 對于消費者,在消費之後,要通知生産者已經結束消費,需要生産新的産品以供消費
- 在生産者消費者問題中,僅有synchronized是不夠的:
1.synchronized可阻止并發更新同一個共享資源,實作了同步
2.但synchronized不能用來實作不同線程之間的消息傳遞(通信)
-
線程方法:針對線程之間互相通信的問題,Java提供了幾個方法用于解決
-
wait()
:表示線程一直等待,直到其他線程通知,與sleep不同,會釋放鎖
-
wait(long timeout)
:指定等待的毫秒數
-
notify()
:喚醒一個處于等待狀态的線程
-
notifyall()
:喚醒同一個對象上所有調用wait()方法的線程,優先級别高的線程優先排程
注意:以上的均是Object類的方法,都隻能在同步方法或者同步代碼塊中使用,否則會抛出異常IllegaIMonitorStateException
-
管程法
- 生産者:負責生産資料的子產品(可能是方法、對象、線程、程序);
- 消費者:負責處理資料的子產品(可能是方法、對象、線程、程序);
- 緩沖區:消費者不能直接使用生産者的資料,它們之間有一個緩沖區
生産者将生産好的資料放入緩沖區,消費者從緩沖區拿出資料
示例如下:
//思路
//1.思考需要哪些對象?
// 生産 , 消費 , 産品 , 容器
//2.分工
/*
生産者隻管生産
消費者隻管消費
雞: 實體類
容器 :
容器添加資料.
要判斷容器是否滿 , 滿了等待消費者消費
沒有滿,通知生産者生産
容器減少資料
判斷還有沒有資料, 沒有資料的話 . 等待生産者生産
消費完畢 , 通知生産者生産
*/
import java.sql.SQLOutput;
//測試生産者和消費者問題
public class TestPC {
public static void main(String[] args) {
SynContainer synContainer = new SynContainer();
new Productor(synContainer).start();
new Consumer(synContainer).start();
}
}
//生産者
class Productor extends Thread{
//需要向容器中加入産品
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 Consumer extends Thread{
SynContainer container;
public Consumer(SynContainer container){
this.container = container;
}
@Override
public void run() {
for (int i = 1; i < 100; i++) {
//消費者拿走産品
Chicken chicken = container.pop();
System.out.println("消費者消費了"+chicken.id+"雞");
}
}
}
//緩沖區-->容器
class SynContainer{
//容器
Chicken[] chickens = new Chicken[10];
//容器的計數器
int num = 0;
//生産者放入産品
public synchronized void push(Chicken chicken) {
//假如容易已經滿了,就不用放,等待消費者消費
if (num>=chickens.length){
//等待消費者消費
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//假如容器沒有滿 , 通知生産生成
System.out.println("num,,,,,"+num);
chickens[num] = chicken;
System.out.println("數組有多少個元素"+num);
num++;
//通知消費者消費
this.notifyAll();
}
//消費者拿走産品
public synchronized Chicken pop(){
//假如容器空的,等待
if (num<=0){
//等待生産
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num--;
Chicken chicken = chickens[num];
//通知生産者生産
this.notifyAll();
return chicken;
}
}
//産品->雞
class Chicken {
int id;
public Chicken(int id) {
this.id = id;
}
}
-
信号燈法
在産品中設定一個标志位,利用标志位(flag)來選擇讓生産者執行邏輯還是讓消費者執行邏輯(類似于紅燈停,綠燈行)
示例如下:
//生産者消費2
//生産者--->演員
//消費者--->觀衆
//産品:信号燈--->電視----->聲音
public class TestPC2 {
public static void main(String[] args) {
TV tv = new TV();
new Player(tv).start();
new Watcher(tv).start();
}
}
//生産者
class Player extends Thread{
TV tv;
public Player(TV tv){
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i%2==0){
this.tv.play("節目:快樂大學營播放中");
System.out.println();
}else {
this.tv.play("廣告:抖音,記錄美好生活");
}
}
}
}
//消費者
class Watcher extends Thread{
TV tv;
public Watcher(TV tv){
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
tv.watch();
}
}
}
//電視
class TV{
//演員說話 , 觀衆等待
//觀衆觀看 , 演員等待
boolean flag = true;
//說話
String voice;
//表演
public synchronized void play(String voice){
//演員等待
if (!flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("表演了"+voice);
this.voice = voice;
//讓觀衆觀看
this.notifyAll();
this.flag = !this.flag;
}
//觀看
public synchronized void watch(){
//觀衆等待
if (flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("觀衆聽到了: "+voice);
//通知演員說話
this.notifyAll();
this.flag = !this.flag;
}
}
線程池
- 背景:經常建立和銷毀、使用量特别大的資源,比如并發情況下的線程,對性能影響很大
- 思路:提前建立好多個線程,放入線程池中,使用時直接擷取,使用完放回池中。可以避免頻繁建立銷毀、實作重複利用。類似生活中的公共交通工具
-
好處:
- 提高響應速度(減少了建立新線程的時間)
- 降低資源消耗(重複利用線程池中線程,不需要每次都建立)
- 便于線程管理
-
定義:
- corePoolSize:核心池的大小
- maximumPoolSize:最大線程數
- keepAliveTime:線程沒有任務時最多保持多長時間後終止
-
使用線程池:JDK 5.0起提供了線程池相關API:ExecutorService和Executors
- ExecutorService:真正的線程池接口。常見子類ThreadPoolExecutor
- void execute(Runnable command):執行任務/指令,沒有傳回值,一般用來執行Runnable
- <T.>Future<T.> submit(Callable<T.> task):執行任務,有傳回值,一般用來執行Callable
- void shutdown():關閉線程池
- Executors:工具類、線程池的工廠類,用于建立并傳回不同類型的線程池
public class ThreadPool{
public static void main(String[] args) {
//建立一個線程池(池子大小)
ExecutorService pool = Executors.newFixedThreadPool(10);
//執行runnable接口實作類
pool.execute(new MyThread4());
pool.execute(new MyThread4());
pool.execute(new MyThread4());
pool.execute(new MyThread4());
//關閉連接配接池
pool.shutdown();
}
}
class MyThread4 implements Runnable{
@Override
public void run() {
System.out.println("MyThread4");
}
}