一魁巩、首先什么是線程和進程?
進程好比我們電腦里開的程序姐浮,比如dota谷遂、英雄聯(lián)盟、網(wǎng)易云音樂卖鲤,每個程序都是一個獨立的進程肾扰;然后每個程序中畴嘶,會有多個線程,比如英雄聯(lián)盟游戲里有畫面集晚、有音樂窗悯,這就是不同的線程。
二、java中實現(xiàn)多線程的三種方法
1、繼承Thread類买决,重寫run方法
2、實現(xiàn)Runnable接口欺旧,重寫run方法
3、實現(xiàn)Callbale接口蛤签,重寫call方法
手撕代碼:
package com.xusujun.thread_finish;
import static java.util.concurrent.Executors.newFixedThreadPool;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException, ExecutionException {
DemoClass demoClass = new DemoClass();
demoClass.setName("通過繼承thread類實現(xiàn)");
demoClass.start();// 繼承實現(xiàn)的可以直接調用start方法啟動線程
DemoRunnable demoRunnable = new DemoRunnable();
// 通過runnable接口實現(xiàn)的辞友,需要通過thread類進行代理實現(xiàn)
new Thread(demoRunnable, "通過runnable接口實現(xiàn)").start();
DemoCallable demoCallable = new DemoCallable();
// 創(chuàng)建服務
ExecutorService ser = newFixedThreadPool(1);
// 提交服務
Future<Boolean> r = ser.submit(demoCallable);
// 獲取結果
boolean result = r.get();
// 關閉服務
ser.shutdown();
}
}
// 通過繼承thread類實現(xiàn),重寫run方法
class DemoClass extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "執(zhí)行了");
}
}
// 通過實現(xiàn)Runnable接口實現(xiàn)震肮,重寫run方法
class DemoRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "執(zhí)行了");
}
}
// 通過callable接口實現(xiàn),重寫call方法
class DemoCallable implements Callable<Boolean> {
@Override
public Boolean call() throws Exception {
Thread.currentThread().setName("通過callbale接口實現(xiàn)");
System.out.println(Thread.currentThread().getName() + "執(zhí)行了");
return true;
}
}
以上代碼執(zhí)行結果如下:
三種方法個人粗略總結:
1称龙、通過繼承Thread類實現(xiàn),對象可以直接調用Thread類靜態(tài)方法戳晌,入start茵瀑、sleep等。由于單繼承的原因躬厌,靈活性不夠,Thread類其本質也是實現(xiàn)runnable接口竞帽。
2扛施、通過實現(xiàn)runnable接口方法更靈活,但是需要Thread類來進行代理實現(xiàn)
3屹篓、callable重寫的call方法有返回值疙渣,run方法沒有返回值。call可以拋出異常堆巧,run方法無法拋出異常
三妄荔、線程的生命周期
通過上面的例子,大概能感覺出來線程的生命周期包括:
- 創(chuàng)建 對象實例化
- 就緒 .start() 啟動不代表立即執(zhí)行谍肤,由cpu自行調度
- 運行 .run() 獲取系統(tǒng)資源開始運行
- 等待 .sleep() 休眠狀態(tài) .wait() 等待狀態(tài)
- 結束 運行結束啦租,線程一旦運行結束無法重新啟動。推薦使用外部標志位進行停止荒揣,不推薦使用stop等方法篷角。
關于sleep,一條重點系任,執(zhí)行sleep不會導致解鎖
四恳蹲、龜兔賽跑案例
假設龜兔分別代表兩個線程虐块,我們讓他們進行50米比賽,假設我們不人工干預讓他們兩個線程自己跑嘉蕾,你們想下會有什么結果贺奠?
看代碼:
//龜兔賽跑
public class ThreadDemo {
static class Mythread implements Runnable {
int raceway = 50; //賽道設置50米
Boolean flag = false; //比賽是否結束標志
String winner; //獲勝者
@Override
public void run() {
while (raceway >= 0) {
if (flag == true) {
break;
}
raceway--;
System.out.println(Thread.currentThread().getName() + "距離終點" + raceway + "米 !");
gameOver(raceway);
}
}
//是否獲勝方法
public void gameOver(int distance) {
if (winner != null) {
this.flag = true;
}
if (distance == 0) {
this.flag = true;
winner = Thread.currentThread().getName();
System.out.println(winner + "獲得了勝利");
}
}
}
public static void main(String[] args) {
Mythread mythread = new Mythread();
new Thread(mythread, "兔子").start(); //兔子線程
new Thread(mythread, "烏龜").start(); //烏龜線程
}
}
如果我們按照上面的程序跑,結果是不是可能烏龜獲勝错忱,也可能兔子獲勝啊儡率,因為之前說過線程的啟動是cpu自行調度的。那么龜兔賽跑的故事是不是有兔子睡覺啊航背,那么我們加入兔子睡覺再來看喉悴,在run()
方法中加入如下代碼:
if (Thread.currentThread().getName().equals("兔子")) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
以上代碼先找到兔子的線程,然后通過sleep()
方法讓他睡覺200毫秒玖媚,這樣一來是不是每次都是烏龜獲勝了呀箕肃!
五、sleep今魔、yield勺像、join 一起
sleep ---線程休眠
yield ---線程禮讓
join ---線程插隊
直接上例子更好理解
1、sleep例子:
class ThreadDemoread {
static class Mythread implements Runnable {
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("當前時間:" + printDate());
}
}
// 每隔一秒打印當前時間方法
public String printDate() {
// 獲取系統(tǒng)當前時間
Date now = new Date(System.currentTimeMillis());
String formatDate = new SimpleDateFormat("HH:mm:ss").format(now);
return formatDate;
}
}
public static void main(String[] args) {
Mythread mythread = new Mythread();
new Thread(mythread).start();
}
}
執(zhí)行結果如下:
2错森、yeild例子:
public class ThreadDemo {
static class Mythread implements Runnable {
@Override
public void run() {
if (Thread.currentThread().getName().equals("A")) {
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "執(zhí)行了");
System.out.println(Thread.currentThread().getName() + "結束了");
}
}
public static void main(String[] args) {
new Thread(new Mythread(), "A").start();
new Thread(new Mythread(), "B").start();
}
}
在run方法中添加yield后吟宦,線程A會進行線程禮讓,但是注意涩维,禮讓不一定成功殃姓,依舊是看cpu心情。也就是說可能禮讓成功瓦阐,也可能禮讓失敗蜗侈。
3、join
join是強制執(zhí)行睡蟋,他就比較霸道了踏幻,類似于vip,vip來了戳杀,其他人都要讓位子
public class ThreadDemo {
static class Mythread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println(Thread.currentThread().getName() + "執(zhí)行了" + i + "次");
}
}
}
public static void main(String[] args) throws InterruptedException {
Mythread mythread = new Mythread();
Thread thB = new Thread(mythread, "VIP");
thB.start();
for (int i = 0; i < 600; i++) {
if(i==10){
thB.join();//main線程阻塞
}
System.out.println("main線程執(zhí)行了" + i + "次");
}
}
}
以上代碼當main線程打印到第10次的時候该面,vip線程就會插入強制優(yōu)先執(zhí)行,main線程會被阻塞信卡,等待vip執(zhí)行完才能繼續(xù)執(zhí)行隔缀。結果如下:
六、線程狀態(tài)和優(yōu)先級
1傍菇、線程狀態(tài)
既然線程有生命周期蚕泽,那就可以觀測線程的狀態(tài) Thread.State
1、new
2、runnable
3须妻、blocked
4仔蝌、waiting
5、timed_waiting
6荒吏、terminated
上代碼:
class ThreadDemo {
static class Mythread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("******");
}
}
public static void main(String[] args) throws InterruptedException {
Mythread mythread = new Mythread();
Thread thread = new Thread(mythread, "線程A");
Thread.State state = thread.getState();
//觀察狀態(tài)
System.out.println(state);// new
thread.start();
state = thread.getState();
System.out.println(state); // runnable
while (state != Thread.State.TERMINATED) {
Thread.sleep(100);
state = thread.getState();
System.out.println(state);
}
}
}
運行結果如下:
2敛惊、線程優(yōu)先級
每個線程都有一個優(yōu)先級,從1到10上升绰更,數(shù)字越大優(yōu)先級越高瞧挤,如果我們在創(chuàng)建一個線程的手不設置優(yōu)先級,那么默認的優(yōu)先級是5儡湾。
設置優(yōu)先級使用
thread.setPriority(value)
方法進行設置特恬。通過thread.getPriority()
可以獲得線程優(yōu)先級數(shù)值。
七徐钠、Lamada表達式
lamada表達式是函數(shù)式編程的思想癌刽,看過js的我看到這個第一反應就是臥槽 這不就是es6里的箭頭函數(shù)嗎?尝丐?
先看這么一句話:
任何一個接口如果只包含一個抽象方法显拜,那么這個接口就可以稱作函數(shù)式接口,對于函數(shù)式接口爹袁,我們就可以使用lamada表達式來創(chuàng)建對象远荠。
如:
new Thread(()->System.out.println(”lamada表達式創(chuàng)建的線程"));
現(xiàn)在可能有的朋友看起來會比較懵逼,下面我們手撕代碼失息,順便復習下javase中的各種不同名稱的類譬淳,大家腦子里趕緊想下有哪些類的寫法?
我們先把頭寫好盹兢,主函數(shù)以及自定義一個函數(shù)是接口瘦赫,也就是只有一個抽象方法的接口say。
public class ThreadDemo{
public static void main(String[] args) {
}
}
//定義一個函數(shù)式接口
interface Say{
void say(); //只有一個說話的方法
}
1蛤迎、常規(guī)寫法:
//接口的實現(xiàn)類
class Person implements Say{
@Override
public void say() {
System.out.println("我在學lamada表達式-常規(guī)寫法");
}
}
補全主函數(shù)內(nèi)容:
public class ThreadDemo{
public static void main(String[] args) {
Person person = new Person();
person.say();
}
}
運行后應該會打印我在學lamada表達式-常規(guī)寫法
2、實現(xiàn)類寫在外面是不是不好看啊含友,我們可以把它放到主類里面替裆,就構成靜態(tài)內(nèi)部類,改造:
public class ThreadDemo {
// 接口的實現(xiàn)類-靜態(tài)內(nèi)部類
static class Person implements Say {
@Override
public void say() {
System.out.println("我在學lamada表達式-靜態(tài)內(nèi)部類");
}
}
public static void main(String[] args) {
Person person = new Person();
person.say();
}
}
3窘问、這時候有的同學可能要說了我能不能把實現(xiàn)類寫到主函數(shù)方法里辆童?這就是局部內(nèi)部類
public class ThreadDemo {
public static void main(String[] args) {
// 接口的實現(xiàn)類-局部內(nèi)部類
class Person implements Say {
@Override
public void say() {
System.out.println("我在學lamada表達式-局部內(nèi)部類");
}
}
Person person = new Person();
person.say();
}
}
4、匿名內(nèi)部類
以上這些寫法是不是實現(xiàn)類都有名字啊惠赫,也就是Person把鉴,我們能不能不要名字,再作簡化?直接通過接口來創(chuàng)建對象
public class ThreadDemo {
public static void main(String[] args) {
Say person = new Say() {
@Override
public void say() {
System.out.println("我在學lamada表達式-匿名內(nèi)部類");
}
};
person.say();
}
}
5庭砍、還想要簡化场晶?那就要請出lamada表達式了
之前說過,只有一個抽象方法的接口可以采用lamada表達式怠缸,我們這個say接口是不是只有一個say方法啊诗轻,如何寫?趕緊自己想下
public class ThreadDemo {
public static void main(String[] args) {
Say person = ()->System.out.println("我在學lamada表達式-lamada寫法");
person.say();
}
}
和上面那些寫法相比是不是減少了大量代碼揭北,更簡潔了呀扳炬。那么有的同學要問了,我只能寫一句代碼嗎搔体?我想寫多條內(nèi)容可以嗎恨樟?當然可以,加上大括號就可以了:
Say person = () -> {
System.out.println("我在學lamada表達式-lamada寫法");
System.out.println("多輸出一句話也OK拉");
};
person.say();
而且lamada表達式也支持參數(shù)疚俱,具體寫法這里就不多說了劝术,感興趣的自己去了解吧!
八计螺、暴露問題-重要
前面學了很多相關的基礎內(nèi)容夯尽,感覺寫的還挺順利,多線程也挺厲害登馒,可以同時執(zhí)行多個任務匙握,那么多線程會出問題嗎?我們先來看一個例子陈轿,搶購火車票:
class ThreadDemo {
static class BuyTicket implements Runnable {
private int tickets = 10; // 總共有10張票
boolean flag = true; // 搶購是否結束標志
@Override
public void run() {
while (flag) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
buy();
}
}
// 購票
synchronized void buy() {
if (tickets == 0) {
this.flag = false;
} else {
System.out.println(Thread.currentThread().getName() + "搶到了第" + tickets-- + "票圈纺!");
if (tickets == 0) {
this.flag = false;
}
}
}
}
public static void main(String[] args) {
BuyTicket buy = new BuyTicket();
new Thread(buy, "小明").start();
new Thread(buy, "小紅").start();
new Thread(buy, "可惡的黃牛").start();
}
}
大家看下上面代碼,先想想以上代碼會輸出什么麦射,會有問題嗎蛾娶?
是不是發(fā)現(xiàn)很多問題啊,搶到同一張票潜秋,出現(xiàn)負數(shù)的票等蛔琅,為什么會出現(xiàn)這個問題?因為多線程他們是同時進入開始操作是吧峻呛,這個問題出現(xiàn)于當多個對象使用同一份資源的時候罗售,因為多線程對資源的操作是如何操作的?是把資源拷貝到自己的內(nèi)存空間然后進行操作吧钩述,也就是說小紅寨躁、小明、黃牛這3個線程可能會同時分別把同一個資源拷貝到自己的內(nèi)存空間進行操作牙勘,也就出現(xiàn)同時搶到了职恳,但是實際服務器里并沒有,當他們都搶完后,服務器會顯示負數(shù)張票對吧放钦。
如何解決這個問題呢色徘?就要引入線程同步機制了,通過同步方法和同步塊來解決最筒,關鍵詞:synchronized
本質就是加鎖讓他們排隊贺氓。
舉個栗子:大家去熱門景點游玩,都肚子疼床蜘,但是廁所只有一個辙培,大家想象一下,大家同時進去會出現(xiàn)怎樣的場景邢锯,是不是會打架啊扬蕊,排隊是不是就能解決了?一個個去嘛
我們上代碼看如何解決上面這個問題丹擎?
1尾抑、同步方法:
// 購票
synchronized void buy() {
if (tickets == 0) {
this.flag = false;
} else {
System.out.println(Thread.currentThread().getName() + "搶到了第" + tickets-- + "票!");
if (tickets == 0) {
this.flag = false;
}
}
}
在run方法前加上synchronized蒂培,就可以了再愈,再次運行,結果如下:
加入同步機制护戳,讓他們進行排隊購票翎冲,就不會再出現(xiàn)上面那種問題了吧。
2媳荒、同步塊
我們再來看另一個例子抗悍,你有一張銀行卡,卡里有10萬钳枕,今天同一時間你和你媳婦同時用錢缴渊,你用網(wǎng)銀消費8W,你老婆呢刷卡消費8W鱼炒,代碼如下:
class ThreadDemo {
public static void main(String[] args) {
Back back = new Back(new Account("8888", 10), 8);
new Thread(back, "你自己").start();
new Thread(back, "你媳婦").start();
}
}
// 賬戶
class Account {
String id; // 卡號
int money; // 卡里的錢
public Account(String id, int money) {
this.id = id;
this.money = money;
}
}
//銀行
class Back implements Runnable {
Account account;
int takeMoney; // 取多少錢
public Back(Account account, int takeMoney) {
this.account = account;
this.takeMoney = takeMoney;
}
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
takeoutMoney();
}
// 取錢
void takeoutMoney() {
System.out.println(Thread.currentThread().getName() + " 你好衔沼,當前賬戶剩余:" + account.money + "萬");
if (account.money < takeMoney) {
System.out.println(Thread.currentThread().getName() + ": 當前賬戶余額不足");
} else {
int leftMoney = account.money - takeMoney;
System.out.println(Thread.currentThread().getName() + "消費成功!當前余額:" + leftMoney);
account.money = leftMoney;
}
}
}
大家看下代碼后先想想會輸出什么內(nèi)容昔瞧?
是不是你自己和媳婦能分別消費8萬啊
這種情況是我們想看到的嗎指蚁?當然是了呀。但是我估計很快銀行就都倒閉了硬爆。
雖然你跟你媳婦是同時操作,但是是不是應該讓他們排隊啊擎鸠,肯定有個先后順序缀磕,我們看用同步塊來解決這個問題:
//添加同步塊,將賬號信息鎖定
synchronized(account){
takeoutMoney();
}
以上什么意思呢,同步塊可以鎖定一個對象袜蚕,讓多個對象操作這個對象的時候進行排隊糟把,也就是我用的時候給他鎖住,其他人用不了牲剃,我用完了解鎖遣疯,我媳婦才能再用。
現(xiàn)在你和媳婦再去操作結果就會這樣了:
記住同步方法的默認鎖定對象是this凿傅,而同步塊是具體你指定的對象
3缠犀、Lock鎖
前面說的同步方法和同步塊都是隱式的進行加鎖和解鎖吧,我們是看不到鎖的聪舒,但是lock鎖是可以自己定義和自己釋放的辨液,想鎖哪里就鎖哪里,我們看代碼
還是那個買火車票的例子:
class ThreadDemo {
static class BuyTicket implements Runnable {
int num = 10;
@Override
public void run() {
while (num > 0) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "買到了第" + num-- + "張票");
}
}
}
public static void main(String[] args) {
BuyTicket buy = new BuyTicket();
new Thread(buy, "小紅").start();
new Thread(buy, "張三").start();
new Thread(buy, "黃牛").start();
}
}
輸出結果現(xiàn)在大家應該都清晰了吧箱残,會有問題是吧滔迈,現(xiàn)在我們通過顯示的添加lock鎖來解決同步問題,修改部分代碼:
// 創(chuàng)建一把鎖
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
try {
lock.lock(); // 在需要鎖定的區(qū)域添加鎖
while (num > 0) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "買到了第" + num-- + "張票");
}
} finally {
lock.unlock(); // 釋放鎖
}
}
記住這么一句話:鎖lock.lock 必須緊跟在try代碼塊被辑,且unlock必須寫在finally代碼塊的第一行燎悍。
關于死鎖
知道了同步方法和同步塊后,我們來想這么一個問題盼理,比如有兩個小孩分別有汽車和飛機谈山,同時呢他們又想去拿對方的玩具,自己的玩具又不肯松手榜揖,這會導致什么問題勾哩?是不是兩邊就僵持住了呀,在多線程中举哟,互相抱著對方的資源思劳,又不肯釋放自己資源的現(xiàn)象叫死鎖,我們應該避免死鎖的出現(xiàn)妨猩。具體這里就不展開了潜叛,同學們可以自行查閱相關資料了解學習。
九壶硅、生產(chǎn)者-消費者問題-重點
什么是生產(chǎn)者-消費者模式威兜?還是舉個栗子:你去肯德基買吃的,你跟前臺小姐姐說你要一個漢堡庐椒,她是不是會看櫥窗里還有沒有貨啊椒舵,有的話她就會賣給你,沒有她是不是會喊后面的阿姨做啊约谈,然后會喊你等一會是吧笔宿。如果櫥窗的容量只有10個漢堡犁钟,而里面已經(jīng)有10個了,她是不是會喊阿姨先別做了呀泼橘,這個就是一個簡單的生產(chǎn)者-消費者模式涝动,你和阿姨之間存在一個小姐姐作為你們的緩沖。這樣做有什么好處熬婷稹醋粟?你和阿姨是不是都是獨立的呀,由前臺小姐姐負責你們之間的業(yè)務呀重归,而且不會出現(xiàn)漢堡放不下了阿姨還在一直做米愿,或者櫥窗已經(jīng)沒東西了,還在不斷讓消費者購買提前。
廢話不多說吗货,擼個代碼:
public class ThreadDemo {
public static void main(String[] args) {
Container container = new Container();
new Thread(new Product(container)).start();
new Thread(new Cunsumer(container)).start();
}
}
// 緩沖區(qū)容器類
class Container {
int goods = 0;
//生產(chǎn)商品方法
synchronized void put() {
while (goods >= 5) {
System.out.println("產(chǎn)品已滿");
try {
this.wait(); //通知生產(chǎn)者等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("生產(chǎn)了第" + goods++ + "個產(chǎn)品");
notifyAll(); //通知消費者消費
};
//消費東西方法
synchronized void pop() {
while (goods <= 0) {
System.out.println("已經(jīng)沒有產(chǎn)品了");
try {
this.wait(); //通知消費者等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消費了第" + goods-- + "個產(chǎn)品");
notify(); //通知生產(chǎn)者生產(chǎn)
}
}
// 生產(chǎn)者
class Product implements Runnable {
Container container;
public Product(Container container) {
this.container = container;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
container.put();
}
}
}
// 消費者
class Cunsumer implements Runnable {
Container container;
public Cunsumer(Container container) {
this.container = container;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
container.pop();
}
}
}
我們運行后看下結果:
現(xiàn)在可以看到生產(chǎn)多少才會消費多少,不會過度生產(chǎn)也不會過度消費狈网。
十宙搬、總結
多線程中還有守護線程、線程池等其他內(nèi)容和概念拓哺,這里就不多展開說了勇垛,有興趣的同學可以自行查閱相關資料。而且關于上面講的內(nèi)容我這里都只是粗淺的個人見解和認識士鸥,尤其是多線程的進階還有juc等高階內(nèi)容闲孤,我還沒學到所以這篇文章更多的是對多線程基礎知識的梳理,正所謂溫故而知新烤礁,希望這篇文章可以幫助到你讼积。
感謝您的閱讀,有出錯的地方還望見諒和指出脚仔。
我們下一篇文章見勤众!