《塞爾達傳說:御天之劍HD》按鍵模式操作演示視訊
java多執行緒
執行緒簡介
普通方法和多執行緒
Process與Thread
-
說起程序,就不得不說一下程式。程式是指令和資料的有序集合,其本身沒有任何執行的含義,是一個靜態的概念。
-
而程序則是執行程式的一次執行過程,它是一個動態的概念。是系統資源分配的單位。
-
通常在一個程序中可以包含若干個執行緒,當然一個程序中至少有一個執行緒,不然沒有存在的意義。執行緒是CPU排程和執行的單位。
-
很多多執行緒是模擬出來的,真正的多執行緒是指由多個cpu,即多核,如伺服器。如果是模擬出來的多執行緒,即在一個cpu的情況下,在同一個時間點,cpu只能執行一個程式碼,因為切換的很快,所以就有同時執行的錯覺。
-
執行緒就是獨立的執行路徑;
-
在程式執行時,即使沒有自己建立執行緒,後臺也會有多個執行緒,如主執行緒、gc執行緒(垃圾回收執行緒);
-
main()稱之為主執行緒,為系統的入口,用於執行整個程式;
-
在一個程序中,如果開闢了多個執行緒,執行緒的執行由排程器安排排程,排程器是與作業系統緊密相關的,先後順序是不能人為的干擾的。
-
對同一份資源操作時,會存在資源搶奪的問題,需要加入併發控制;
-
執行緒會帶來額外的開銷,如cpu排程時間,併發控制開銷。
-
每個執行緒在自己的工作記憶體互動,記憶體控制不當會造成資料不一致
執行緒的實現(重要)
三種建立方式
Thread class
Runnable 介面 → 實現 Runnable 介面(重點)
Callable 介面 → 實現 Callable 介面 (瞭解)
Thread
- 自定義執行緒類繼承 Thread 類
- 重寫 run() 方法,編寫執行緒執行體
- 建立執行緒物件,呼叫 start() 方法啟動執行緒
//繼承 Thread 類
public class TestThread01 extends Thread {
//重寫 run() 方法執行緒體
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println("子執行緒-----"+i);
}
}
public static void main(String[] args) {
//main 主執行緒
//建立一個執行緒物件
TestThread01 testThread01 = new TestThread01();
// testThread01.run(); 按順序執行,列印完run()內方法,再回主執行緒
testThread01.start();
//呼叫start() 方法啟動執行緒時,主執行緒和子執行緒同時交替進行,子執行緒列印會出現主執行緒列印之間。
for (int i = 0; i < 500; i++) {
System.out.println("主執行緒------"+i);
//執行的順序由CPU控制,隨機的每次出來的結構都不一樣
}
}
}
注意:執行緒開啟不一定立即執行,由CPU排程執行
案例:下載圖片
使用Commons IO包,是針對IO流功能的工具類庫。
下載圖片步驟:
- 呼叫下載器方法
- 重寫run方法
- 填入網址、名字並啟動執行緒
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
//練習Thread,實現多執行緒同步下載圖片
public class TestThread2 extends Thread{
private String url; //網路圖片地址
private String name; //儲存的檔名
public TestThread2(String url,String name){
this.url = url;
this.name = name;
}
@Override
public void run() {//(2)使用多執行緒方法下載
WebDownloader WebDownloader = new WebDownloader();
WebDownloader.downloader(url,name);
System.out.println("下載了檔名為:"+name);
}
public static void main(String[] args) {
//(3)主方法內填入網址及名字,並執行
TestThread2 T1= new TestThread2("https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=4015125149,3656732410&fm=26&gp=0.jpg","1.jpg");
TestThread2 T2= new TestThread2("https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=176549861,2908328616&fm=26&gp=0.jpg","2.jpg");
TestThread2 T3= new TestThread2("https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=2654924537,3043598140&fm=26&gp=0.jpg","3.jpg");
T1.start();
T2.start();
T3.start();
}
}
//下載器(1)
class WebDownloader{
//下載方法
public void downloader(String url,String name){
try {
FileUtils.copyURLToFile(new URL(url),new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO異常,downloader方法出現問題");
}
}
}
實現Runnable
- 實現 Runnable 介面
- 實現 run() 方法,編寫執行緒執行體
- 建立執行緒物件,呼叫 start() 方法啟動執行緒
//建立執行緒方式2:實現runnable介面,重寫run方法,執行執行緒需要丟入runnable介面實現類,呼叫start方法
public class TestThread03 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println("子執行緒TTT——"+i);
}
}
public static void main(String[] args) {
//建立runnbale介面的實現類物件
TestThread03 testThread03 = new TestThread03();
/*建立執行緒物件,通過執行緒物件來開啟我們的執行緒,代理
Thread thread = new Thread(testThread03);
thread.start();*/
new Thread(testThread03).start();
for (int i = 0; i < 500; i++) {
System.out.println("主執行緒MMM--"+i);
}
}
}
推薦使用Runnable介面,因為Java單繼承的侷限性
小結
-
繼承Thread類
- 子類繼承Thread類具備多執行緒能力
- 啟動執行緒:子類物件.start()
- 不建議使用:避免OOP單繼承侷限性
-
實現Runnable介面
- 實現介面Runnable具有多執行緒能力
- 啟動執行緒:傳入目標物件+Thread物件.start()
- 推薦使用:避免單繼承侷限性,靈活方便,方便同一物件被多個執行緒使用
初識併發問題
例題
//多個執行緒同時操作同一個物件
//搶票系統
public class TestThread04 extends Thread{
private int ticketNums = 10; //總票數
@Override
public void run() {
while (true){
if (ticketNums<=0){
//當票數為0時,跳出迴圈
break;
}
try {//降低速度,並且丟擲異常
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
//當while迴圈一次,票少一張;匯入執行緒名字
System.out.println(Thread.currentThread().getName()+"-->拿到了第"+ticketNums--+"票");
}
}
public static void main(String[] args) {
TestThread04 t1 = new TestThread04();
new Thread(t1,"小明").start();
new Thread(t1,"老師").start();
new Thread(t1,"黃牛黨").start();
}
}
最後產生的結果即有相同票,又有0票和負數票
/*小明-->拿到了第10票
黃牛黨-->拿到了第9票
老師-->拿到了第10票
老師-->拿到了第8票
黃牛黨-->拿到了第7票
小明-->拿到了第6票
老師-->拿到了第5票
小明-->拿到了第4票
黃牛黨-->拿到了第3票
老師-->拿到了第2票
小明-->拿到了第1票
黃牛黨-->拿到了第0票
老師-->拿到了第-1票*/
發現:多個執行緒操作同一個資源的情況下,執行緒不安全,系統紊亂
例題:龜兔賽跑
/*
1.定義一個贏家
2.設定賽道
3.判斷輸贏
4.確定比賽是否結束
*/
public class Race implements Runnable{
private static String winner; //設定一個贏家
@Override
public void run() {
for (int i = 0; i <=100 ; i++) {//賽道
//模擬兔子每10步睡一次覺,而烏龜本身速度就慢
if (Thread.currentThread().getName().equals("兔子") && i%10==0){
try {
Thread.sleep(65);
} catch (InterruptedException e) {
e.printStackTrace();
}
}if (Thread.currentThread().getName().equals("烏龜")){
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//判斷比賽是否結束
boolean flag = gameOver(i);
//比賽結束就停止程式
if (flag){
break;
}
System.out.println(Thread.currentThread().getName()+"-->跑了"+i+"步"); //列印步數
}
}
//判斷輸贏
//假如winner不為空就說明已經有冠軍
//假如steps>=100,則產生冠軍
//否則比賽還沒結束
private boolean gameOver(int steps){
if (winner!=null){
return true;
}
{
if (steps >= 100) {
winner = Thread.currentThread().getName();
System.out.println("winner is"+winner);
return true;
}
}return false;
}
public static void main(String[] args) {
Race race = new Race();
new Thread(race,"兔子").start();
new Thread(race,"烏龜").start();
}
}
實現Callable介面(瞭解即可)
- 實現Callable介面,需要返回值型別
- 重寫call方法,需要丟擲異常
- 建立目標物件
- 建立執行服務:
ExecutorService ser = Executors.newFixedThreadPool(1)
; - 提交執行:
Future< Boolean > result1 = ser.submit(t1)
; - 獲取結果:
boolean r1 = result1.get()
- 關閉服務:
ser.shutdownNow()
;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.*;
//執行緒建立方式三:實現calladle介面
/*
1. 實現Callable介面
2.實現call方法
3.建立執行服務
4.提交執行
5.獲取結果
6.關閉服務
*/
public class TestCallable implements Callable<Boolean> {
private String url;
private String name;
public TestCallable(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 {
TestCallable T1= new TestCallable("https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=4015125149,3656732410&fm=26&gp=0.jpg","1.jpg");
TestCallable T2= new TestCallable("https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=176549861,2908328616&fm=26&gp=0.jpg","2.jpg");
TestCallable T3= new TestCallable("https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=2654924537,3043598140&fm=26&gp=0.jpg","3.jpg");
//建立執行服務,new一個池子裡面有三個執行緒
ExecutorService ser = Executors.newFixedThreadPool(3);
//提交執行,返回三個值
Future<Boolean> r1 = ser.submit(T1);
Future<Boolean> r2 = ser.submit(T2);
Future<Boolean> r3 = ser.submit(T3);
//獲取結果
boolean rs1 = r1.get();
boolean rs2 = r2.get();
boolean rs3 = r3.get();
//關閉
ser.shutdownNow();
}
}
//下載器
class WebDownloader{
//下載方法
public void downloader(String url,String name){
try {
FileUtils.copyURLToFile(new URL(url),new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO異常,downloader方法出現問題");
}
}
}
Callable的好處:
- 可以定義返回值
- 可以丟擲異常
靜態代理模式
使用婚期公司的案例
- 你You
- 婚慶公司Wedding Company
- 婚禮Marry
//靜態代理模式總結
//真實物件和代理物件都要實現同一個介面
//代理物件要代理真實角色,做了“結婚前”和“結婚後”的事,而真實物件只需要“結婚”
/*
好處:
1. 代理物件可以做很多真實物件做不了的事情
2. 真實物件專注做自己的事情
*/
public class StacticProxy {
public static void main(String[] args) {
You you = new You();
new Thread(()-> System.out.println("我愛你")).start();
new WeddingCompany(you).HappyMarry();
/*
多執行緒原理與靜態代理模式很像
Thread和WeddingCompany都是代理,都代理了真實物件
*/
}
}
interface Marry{ //婚禮
void HappyMarry();
}
//真實角色,你去結婚
class You implements Marry{
@Override
public void HappyMarry() {
System.out.println("我要結婚了!!!");
}
}
//代理角色,幫助你結婚
class WeddingCompany implements Marry{
private Marry target; //建立目標物件
public WeddingCompany(Marry target) {
this.target = target; //匯入值等於目標物件
}
@Override
public void HappyMarry() {
before();
this.target.HappyMarry(); //目標物件進行結婚
after();
}
private void after() {
System.out.println("結婚之後,收尾款");
}
private void before() {
System.out.println("結婚之前,佈置現場");
}
}
真實物件和代理物件都要實現同一個介面
好處:
- 代理物件可以做很多真實物件做不了的事情
- 實物件專注做自己的事情
多執行緒原理與靜態代理模式很像
Thread和WeddingCompany都是代理,都代理了真實物件Runnable和Marry
Lambde表示式
-
其實質屬於函數語言程式設計的概念
-
避免匿名內部類定義過多
-
可以讓你的程式碼看起來很簡潔,去掉了一堆沒有意義的程式碼,只留下核心的邏輯
函式式介面的定義:
-
任何介面,如果只包含唯一一個抽象方法,那麼它就是一個函式式介面
public interface Runnable{ public abstract void run(); }
-
對於函式式介面,我們可以通過lambda表示式來建立該介面的物件。
外部類,靜態內部類,區域性內部類,匿名內部類,Lambda表示式練習
public class TestLambda {
public static void main(String[] args) {
ILove love;
//用lambda簡化
//1.Lambda表示式
love =(int a)->System.out.println("I love you->" + a);
love.love(6);
//2.簡化引數型別
love =(a)->System.out.println("I love you->" + a);
//3.簡化括號
love =a->System.out.println("I love you->" + a);
//4.簡化花括號
love =a->{System.out.println("I love you->" + a);};
/*love =(a,b,c)->System.out.println("I love you->" + a+b+c);
love.love(520,1314,"love");*/
}
}
//定義一個函式式介面
interface ILove{
void love(int a);
}
總結:
- Lambda表示式可以簡化方法、簡化引數型別、簡化括號、簡化花括號。前提是介面為函式式介面
- Lambda表示式只能有一行程式碼(函式式介面)的情況下才能簡化成為一條,如果有多行,那麼就用程式碼塊包裹{}
- 多個引數也可以去掉引數型別,但要去掉就都去掉,必須加上括號
執行緒狀態
執行緒的5種狀態
-
新建(NEW):
新建立了一個執行緒物件。
-
可執行(RUNNABLE)
要執行執行緒,必須呼叫執行緒的start()方法。當執行緒物件呼叫start()方法即啟動了執行緒,start()方法建立執行緒執行的系統資源,並排程執行緒執行run()方法。當start()方法返回後,執行緒就處於就緒狀態。
-
執行狀態(running)
當執行緒獲得CPU時間後,它才進入執行狀態,真正開始執行run()方法。
-
阻塞狀態(blocked)
阻塞狀態是指執行緒因為某種原因放棄了cpu 使用權,也即讓出了cpu timeslice,暫時停止執行。直到執行緒進入可執行(runnable)狀態,才有機會再次獲得cpu timeslice 轉到執行(running)狀態。阻塞的情況分三種:
- 等待阻塞:執行(running)的執行緒執行o.wait()方法,JVM會把該執行緒放入等待佇列(waitting queue)中。
- 同步阻塞:執行(running)的執行緒在獲取物件的同步鎖時,若該同步鎖被別的執行緒佔用,則JVM會把該執行緒放入鎖池(lock pool)中。
- 其他阻塞:執行(running)的執行緒執行Thread.sleep(long ms)或t.join()方法,或者發出了I/O請求時,JVM會把該執行緒置為阻塞狀態。當sleep()狀態超時、join()等待執行緒終止或者超時、或者I/O處理完畢時,執行緒重新轉入可執行(runnable)狀態。
-
死亡狀態(dead)
-
run方法正常退出而自然死亡;
-
一個未捕獲的異常終止了run方法而使執行緒猝死;
為了確定執行緒在當前是否存活著(就是要麼是可執行的,要麼是被阻塞了),需要使用isAlive方法,如果是可執行或被阻塞,這個方法返回true;如果執行緒仍舊是new狀態且不是可執行的,或者執行緒死亡了,則返回false。
-
執行緒狀態圖
執行緒中常用的方法
void intreeupt()
中斷執行緒(建議不要用這個方法)
停止執行緒
- 不推薦使用JDK提供的
stop()、destroy()
方法。【已廢棄】 - 推薦執行緒自己停止下來
- 建議使用一個標誌位進行終止變數當flag = false ,則終止執行緒執行。
/*
測試stop
1. 建議執行緒正常停止-->利用次數,不建議死迴圈(死迴圈也要限制執行速度,防止cpu卡死)
2.建議使用標誌位-->設定一個標誌位
3.不要使用 stop 或者 destroy 等過時或者JDk不建議使用的方法
*/
public class TestStop implements Runnable{
//1.設定一個標誌位
private boolean flag = true;
//多執行緒方法
@Override
public void run() {
int i = 0;
while (flag){
System.out.println("run...Thread"+i++);
}
}
//2.設定一個公開的方法停止執行緒,轉換標誌位
public void stop(){
this.flag = false;
}
public static void main(String[] args) {
//開啟多執行緒
TestStop ts = new TestStop();
new Thread(ts).start();
for (int i = 0; i < 1000; i++) {
System.out.println("main"+i);
if (i == 900){
//3.呼叫stop方法切換標誌位,讓執行緒停止
ts.stop();
System.out.println("執行緒該停止了");
}
}
}
}
執行緒休眠【sleep】
- sleep(時間)指出當前執行緒阻塞的毫秒數
- sleep存在異常
InterruptedException
- sleep時間達到後執行緒進入就緒狀態
- sleep可以模擬網路延遲,倒計時等
- 每一個物件都有一個鎖,sleep不會釋放鎖
//模擬延遲
//特點:放大問題的發生性
try {
Thread.sleep(65);
} catch (InterruptedException e) {
e.printStackTrace();
}
//模擬倒計時
public static void tenDown() throws InterruptedException {
int num = 10;
while (true){
Thread.sleep(1000);
System.out.println(num--);
if (num<=0){
break;
}
}
}
//列印當前系統時間
Date starTime = new Date(System.currentTimeMillis()); //獲取系統當前時間
while (true){
try {
Thread.sleep(1000);
System.out.println(new SimpleDateFormat("HH:mm:ss").format(starTime)); //列印時間格式
starTime = new Date(System.currentTimeMillis()); //更新系統當前時間
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
執行緒禮讓【yield】
- 禮讓執行緒,就是讓當前正在執行的執行緒暫停,但不阻塞
- 將執行緒從執行狀態轉為就緒狀態
- 讓cpu重新排程,禮讓不一定成功!看cpu心情
//測試禮讓執行緒
//禮讓不一定成功
public class TestYield {
public static void main(String[] args) {
MyYield myYield = new MyYield();
new Thread(myYield,"a").start();
new Thread(myYield,"b").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{
@Override
public void run() {
//子執行緒
for (int i = 0; i < 300; i++) {
System.out.println("執行緒VIP來了!!"+i);
}
}
public static void main(String[] args) throws InterruptedException {
//啟動執行緒
TestJoin testJoin = new TestJoin();
Thread thread = new Thread(testJoin);
thread.start();
//主執行緒和子執行緒併發隨機執行,當i==400時,子執行緒強制執行, 待子執行緒執行完畢後,主執行緒才再起執行。
for (int i = 0; i < 1000; i++) {
if (i==400){
thread.join();
}
System.out.println("main"+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);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主執行緒+/");
}
});
//觀察狀態
Thread.State state = thread.getState();
System.out.println("新生+"+state); //new 新生
//觀察啟動後
thread.start(); //啟動執行緒
state = thread.getState();
System.out.println("啟動+"+state); //run
//監控,主執行緒只要不死亡,就一直執行監控
while (state!=Thread.State.TERMINATED) { //多執行緒.狀態.終止狀態
Thread.sleep(100);
state = thread.getState(); //更新執行緒狀態
System.out.println("觀察+"+state); //輸出狀態
}
}
}
可以利用執行緒的狀態進行一些操作,如:如果執行緒秒處於TIMED_WAITING
長達30秒,可以讓它強制崩潰。
死亡之後的執行緒,不能再次執行
執行緒優先順序【Priority】
-
Java提供一個執行緒排程器來監控程式中啟動後進入就緒狀態的所有執行緒,執行緒排程器按照優先順序決定應該排程哪個執行緒來執行。
-
執行緒的優先順序用數字表示,範圍從1~10
Thread.MIN_PRIORITY = 1;
Thread.MAX_PRIORITY = 10;
Thread.NORM_PRIORITY = 5;
-
使用以下方式改變或獲取優先順序
getPriority() setPriority(int xxx)
//測試執行緒優先順序
public class TestPriority {
public static void main(String[] args) {
//主執行緒優先順序列印
System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());
//設定6中優先級別
MyPriority myPriority = new MyPriority();
Thread t1 = new Thread(myPriority);
Thread t2 = new Thread(myPriority);
Thread t3 = new Thread(myPriority);
Thread t4 = new Thread(myPriority);
Thread t5 = new Thread(myPriority);
Thread t6 = new Thread(myPriority);
//先設定優先順序,再設定啟動
t1.start(); //子程式預設優先順序=5
t2.setPriority(Thread.MAX_PRIORITY); //MAX_PRIORITY最高階10
t2.start();
t3.setPriority(1);
t3.start();
t4.setPriority(3);
t4.start();
t5.setPriority(6);
t5.start();
t6.setPriority(8);
t6.start();
}
}
class MyPriority implements Runnable{
//子執行緒優先順序列印
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());
}
}
結果:
main-->5
Thread-1-->10
Thread-5-->8
Thread-0-->5
Thread-4-->6
Thread-3-->3
Thread-2-->1
結果Thread-0和Thread-4證明,優先順序低只是意味著獲得排程的概率低,並不是優先順序低就不會被呼叫,這都是看CPU的排程(效能倒置)
守護執行緒【Daemon】
- 執行緒分為使用者執行緒和守護執行緒
- 虛擬機器必須確保使用者執行緒執行完畢
- 虛擬機器不用等待守護執行緒執行完畢
- 如,後代記錄操作日誌,監控記憶體,垃圾回收等待…
thread.setDaemon(true)
//測試守護執行緒,上帝守護你
public class TestDaemon {
public static void main(String[] args) {
God god = new God();
You you = new You();
Thread thread = new Thread(god);
thread.setDaemon(true); //預設是false表示是使用者執行緒,正常線路都是使用者執行緒
//啟動
thread.start();
new Thread(you).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 <36000; i++) {
System.out.println("快樂生活每一天!"+i);
}
System.out.println("=====goodbye!World!=====");
}
}
執行緒同步(重要)
多個執行緒操作同一個資源
併發:同一個物件被多個執行緒同時操作
- 現實生活中我們常用排隊去解決並發現象
- 而執行緒中,會將多個需要同時訪問的執行緒裝進物件等待池中形成列隊,等待前面執行緒使用完畢後,下個執行緒再使用。
同步形成條件:佇列+鎖
鎖機制【synchronized】
為了保證資料在方法中被訪問時的正確性,在訪問時加入鎖機制synchronized,當一個執行緒獲得物件的排它鎖,獨佔資源,其他執行緒必須等待,使用後釋放鎖即可.
存在以下問題:
-
一個執行緒持有鎖會導致其他所有需要此鎖的執行緒掛起
-
在多執行緒競爭中,加鎖,釋放鎖會導致比較多的上下文切換和排程延時,引起效能問題
-
如果一個優先順序高的執行緒等待一個優先順序低的執行緒釋放鎖,會導致優先順序倒置,引起效能問題
三個不安全情況
多個執行緒同一時刻操作了同一資料,比如一個執行緒執行了i=9
就掛起了,而另一個執行緒拿到i=9
之後掛起,這時候第一個執行緒執行i+1
使i
=10之後掛起,就會導致另一個執行完後也是i
=10.
不安全取票
//不安全買票
//執行緒不安全有負數和重複購票
public class UnsafeBuyTicket {
public static void main(String[] args) {
//取票人
BuyTicket station = new BuyTicket();
new Thread(station,"你").start();
new Thread(station,"我").start();
new Thread(station,"他").start();
}
}
class BuyTicket implements Runnable{
//買票系統
//票
private int ticketNums = 20;
//停止條件
boolean flag = true;
@Override
public void run() {
//根據flag判斷,迴圈買票
while (flag){
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//取票並列印
private void buy() throws InterruptedException {
//當票<=0時,停止迴圈,並修改flag判斷
if (ticketNums<=0){
flag = false;
return;
}
Thread.sleep(100);
//否則列印,名字+第幾張票
System.out.println(Thread.currentThread().getName()+"-->第"+(ticketNums--)+"票");
}
}
不安全的取錢
//不安全的取錢
public class UnsafeBank {
public static void main(String[] args) {
Account account = new Account(100,"結婚基金");
Drawing you = new Drawing(account,50,"你");
Drawing girlFiend = new Drawing(account,100,"girlFiend");
you.start();
girlFiend.start();
}
}
//個人賬號+餘額
class Account{
int Mymoney; //餘額
String Myname; //賬號
public Account(int Mymoney, String Myname) {
this.Mymoney = Mymoney;
this.Myname = Myname;
}
}
//銀行
class Drawing extends Thread{
Account account; //賬號
int drawingMoney; //取了多少錢
int nowMoney; //現在手裡有多少錢
public Drawing(Account account,int drawingMoney,String name){
super(name);
this.account = account;
this.drawingMoney = drawingMoney;
}
@Override
public void run() {
if (account.Mymoney-drawingMoney<0){
System.out.println("餘額不足!");
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.Mymoney=account.Mymoney-drawingMoney;
nowMoney = nowMoney +drawingMoney;
//Thread.currentThread().getName() = this.getName()
System.out.println(this.getName()+"手裡的錢:"+nowMoney);
System.out.println(account.Myname+"餘額:"+account.Mymoney);
}
}
執行緒不安全的集合
import java.util.ArrayList;
import java.util.List;
//執行緒不安全的集合
public class UnsafeList {
public static void main(String[] args) {
List<String> list = new ArrayList<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());
}
}
同步方法
-
由於我們可以通過 private 關鍵字來保證資料物件只能被方法訪問,所以我們只需要針對方法提出一套機制,這套機制就是 synchronized 關鍵字,它包括兩種用法: synchronized 和 synchronized 快
同步方法:
public synchronized void method(int args ){}
-
sychronized 方法控制對“物件”的方法,每個物件對應一把鎖,每個 sychronized 方法都必須獲得呼叫該方法的物件的鎖才能執行,否則執行緒會阻塞,方法一旦執行,就獨佔該鎖,直到該方法返回才能釋放鎖,後面被阻塞的執行緒才能獲得這個鎖,才能繼續執行
缺陷:若將一個大的方法申明為 synchronized 將會影響效率
方法裡面需要修改的內容才需要鎖,鎖的太多,浪費資源
同步塊
同步塊:synchroized(Obj){}
Obj 稱為:同步監視器
- Obj 可以是任何物件,但是推薦使用共享資源作為同步監視器
- 同步方法中無需制定同步監視器,因為同步方法的同步監視器是 this,就是這個物件本身,或是 class
同步監視器的執行過程
- 第一個執行緒訪問,鎖定同步監視器,執行其中程式碼
- 第二個執行緒訪問,發現同步監視器被鎖定,無法訪問
- 第一個執行緒訪問完畢,解鎖同步監視器
- 第二個執行緒訪問,發現同步監視器沒有鎖,然後鎖定並訪問
JUC安全型別的集合
CopyOnWriteArrayList<E>
import java.util.concurrent.CopyOnWriteArrayList;
// 測試JUC安全型別的集合
public class TestJUC {
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());
}
}
死鎖
多個執行緒各自佔有一些共享資源,並且互相等待其他執行緒佔有的資源才能執行,而導致兩個或多個執行緒都在等待對方釋放資源,都停止執行的情況。某一個同步塊同時擁有“兩個以上物件的鎖”時,就可能會發生“死鎖”的問題。
/*步驟:
1. 建立“口紅、鏡子”物件
2. 建立多執行緒,保證資源只有一份,使用static
3. 建立變數“選擇權、使用人”,並生成構造器
4. 建立同步塊,並持有對方的鎖
5. 重寫run方法
6. 啟動使用人
*/
public class DeadLock {
public static void main(String[] args) {
Makeup q1 = new Makeup(0,"灰姑娘");
Makeup q2 = new Makeup(1,"白雪公主");
q1.start();
q2.start();
}
}
//口紅
class Lipstick{
}
//鏡子
class Mirror{
}
class Makeup extends Thread{
//需要的資源只有一份,用 static 來保證只有一份
static Lipstick lipstick = new Lipstick();
static Mirror mirror = new Mirror();
int choice; //選擇
String girlName; //使用化妝品的人
//構造器
Makeup(int choice,String girlName){
this.choice = choice;
this.girlName = girlName;
}
//run方法
@Override
public void run() {
try {
makeup();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//化妝,互相持有物件的鎖,就是需要拿到對方的資源
private void makeup() throws InterruptedException {
if (choice == 0){
synchronized (lipstick){ //獲得口紅
System.out.println(this.girlName+"獲得口紅的鎖");
Thread.sleep(1000);
// synchronized (mirror){
// System.out.println(this.girlName+"獲得鏡子的鎖");}
}synchronized (mirror){
System.out.println(this.girlName+"獲得鏡子的鎖");}
}else {
synchronized (mirror) { //獲得口紅
System.out.println(this.girlName + "獲得鏡子的鎖");
Thread.sleep(2000);
//synchronized (lipstick) {
// System.out.println(this.girlName + "獲得口紅的鎖");}
}synchronized (lipstick) {
System.out.println(this.girlName + "獲得口紅的鎖");}
}
//將對方的鎖移除程式碼塊,防止死鎖
}
}
死鎖避免方法
- 產生死鎖的四個必要條件:
- 互斥條件:一個資源每次只能被一個程序使用
- 請求與保持條件:一個程序因請求資源而阻塞時,對已獲得的資源保持不放
- 不剝奪條件:程序已獲得的資源,在未使用完之前,不能強制剝奪
- 迴圈等待條件:若干程序之間形成一種頭尾相接的迴圈等資源關係
破調其中的任意一個或多個條件就可以避免死鎖發生
Lock(鎖)
-
從 JDK5.0開始,Java 提供了更強大的執行緒同步機制-----通過顯示定義同步鎖物件來實現同步。同步鎖使用 Lock 物件充當
-
java.util.concurrent.locks.Lock
介面是控制多個執行緒對共享資源進行訪問的工具。鎖提供了對共享資源的獨佔訪問,每次只能有一個執行緒和對 Lock 物件加鎖,執行緒開始訪問共享資源之前應先獲得 Lock 物件 -
ReentrantLock 類(可重入鎖)實現了 Lock,它擁有與 synchronized 相同的併發性和記憶體語義,在實現執行緒安全的控制中,比較常用的是 ReentrantLock,可以顯式加鎖、釋放鎖
import java.util.concurrent.locks.ReentrantLock;
//測試Lock
public class TestLock {
public static void main(String[] args) {
TestLock2 testLock2 = new TestLock2();
new Thread(testLock2).start();
new Thread(testLock2).start();
new Thread(testLock2).start();
}
}
class TestLock2 implements Runnable{
//設定票數
int ticketNums = 10;
//定義Lock鎖
private final ReentrantLock lock = new ReentrantLock();
//private 私人的;final 鎖定,不能再做修改與指向
@Override
public void run() {
//票數遞減
while (true){
try { //(try-finally)lock加鎖的方式
lock.lock(); //啟動加鎖
//假如有票就減,沒票就退出迴圈
if (ticketNums>0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticketNums--);
}else {
break;
}
}finally {
//解鎖
lock.unlock();
//如果有異常,unlock 需要寫到 finally 裡;但是一般也可以直接寫入 finally 裡。
}
}
}
}
synchronized 與 Lock 的對比
-
Lock 是顯示鎖(手動開啟和關閉鎖,別忘記關閉鎖);synchronized 是隱式鎖,出了作用域自動釋放
-
Lock 只有程式碼塊鎖;synchronized 有程式碼塊鎖和方法鎖
-
使用 Lock 鎖,JVM 將花費較少的時間來排程執行緒,執行緒更好。並且具有更好的擴充套件性(提供更多的子類)
-
優先使用順序:
Lock > 同步程式碼塊(已經進入了方法體,分配了相應資源)> 同步方法(在方法體外)
執行緒通訊問題
執行緒協作
應用場景:生產者和消費者問題
-
假設倉庫中只能存放一件產品,生產者將生產出來的產品放入倉庫,消費者將倉庫中產品取走消費
-
如果倉庫中沒有產品,則生產者將產品放入倉庫,否者停止生產並等待,直到倉儲中的產品被消費者取走為止
-
如果倉庫中放有產品,則消費者可以將產品取走消費,否則停止消費並等待,直到倉庫中再次放入產品為止
這是一個執行緒同步問題,生產者和消費者共享同一個資源,並且生產者和消費者之間相互依賴,互為條件
-
對於生產者,沒有生產產品之前,要通知消費者等待,而生產了產品之後,又需要馬上通知消費者消費
-
對於消費者,在消費之後,要通知生產者已經結束消費,需要生產新的產品以供消費
-
在生產者消費者問題中,僅有 synchronized 是不夠的
- synchronized 可阻止併發更新同一個共享資源,實現了同步
- synchronized 不能用來實現不同執行緒之間的訊息傳遞(通訊)
解決方法
-
管程法
- 生產者:負責生產資料的模組(可以是方法,物件,執行緒,程序)
- 消費者:負責處理資料的模組(可能是方法,物件,執行緒,程序)
- 緩衝區:消費者不能直接使用生產者的資料,他們之間有個“緩衝區”
生產者將生產好的資料放入緩衝區,消費者從緩衝區拿出資料
-
訊號燈法
設定一個 boolean 值當 true 時生產,false 時取貨。
管程法
package com.sxmz.ThreadClass.gaoji;
/*
1. 建立產品、生產者、消費者、緩衝區
2.緩衝區、容器、生產者、消費者
*/
public class TestPC {
public static void main(String[] args) {
SynContainer container = new SynContainer();
new Productor(container).start();
new Consumer(container).start();
}
}
//生產者
class Productor extends Thread {
//構造器
SynContainer container;
public Productor(SynContainer container) {
this.container = container;
}
//生產
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("生產了" + i + "只雞");
container.push(new Chicken(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 Chicken{
int id;
public Chicken(int id) {
this.id = id;
}
}
//緩衝區
class SynContainer{
//需要一個容器大小
Chicken[] chickens = new Chicken[10];
//容器計數器
int count = 0;
//生產者放入產品
public synchronized void push(Chicken chicken){ //同步方法
//如果容器滿了,就需要等待消費者消費
if (count == chickens.length){
//通知消費者,生產等待
try {
this.wait(); //生產等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果沒有滿,我們就需要丟入產品
chickens[count] = chicken;
count++;
//可以通知消費者消費了
this.notifyAll(); //通知消費者,並解除消費者等待
}
//消費者消費產品
public synchronized Chicken pop(){
//判斷能否消費
if (count ==0){
//等待生產者生產,消費者等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//可以消費
count--;
Chicken chicken = chickens[count];
//吃完了,通知生產者生產
this.notifyAll(); //解除生產者的等待
return chicken;
}
}
訊號燈法
//生產者、消費者問題-->訊號燈法,標誌物解決
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 ; //需要一個共同的物件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("快樂大本營播放中");
}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{
String voice;
boolean flag =true;
//演員表演,觀眾等待 T
//觀眾等待,演員表演 F
//表演
public synchronized void play(String voice){
if (!flag){ //取反
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("演員在表演:"+voice);
//同時觀眾觀看
this.notifyAll();
this.voice = voice; //更新揭幕,讓觀眾可以看到當前節目
this.flag = !flag; //表演完,取反,變為false
}
//觀看
public synchronized void watch(){
if (flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("觀眾在觀看:"+voice); //觀看了更新的節目
//通知演員表演
this.notifyAll();
this.flag = !flag; //觀看完取反,變為true
}
}
高階主題
執行緒池
使用執行緒池
-
背景:經常建立和銷燬,使用量特別大的資源,比如併發情況下的執行緒,對效能影響很大
-
思路:提前建立好多個執行緒,放入執行緒池中,使用時直接獲取,使用完放回池中。
可以避免頻繁建立銷燬、實現重複利用。類似生活中的公共交通工具。
-
好處:
- 提高響應速度(減少額建立執行緒的時間)
- 降低資源消耗(重複利用執行緒池中執行緒,不需要每次都建立)
- 便於執行緒管理(…)
corePoolSize
:核心池的大小maximumPoolSiz
:最大執行緒數keepAliveTime
:執行緒沒有任務時最多保持多長時間後會終止
-
JDK 5.0起提供了執行緒池相關 API:ExecutorService 和 Executors
-
ExecutorService:真正的執行緒池介面,常見子類 ThreadPoolExecutor
void execute(Runnable command)
:執行任務/命令,沒有返回值,一般用來執行 Runnable< T >Future< T >submit(Callable< T > task)
:執行任務,有返回值,一般用來執行 Callablevoid shutdown()
:關閉連線池
-
Executors:工具類、執行緒池的工廠類,用於建立並返回不同型別的執行緒池
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestPool {
public static void main(String[] args) {
//1.建立服務,建立執行緒池
//new FixedThreadPool 引數為:執行緒池大小
ExecutorService service = Executors.newFixedThreadPool(10);
//執行
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
//關閉執行緒池
service.shutdown();
}
}
class MyThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}