線程的基礎(chǔ)
任務(wù)的概念:是實(shí)現(xiàn)Runnable接口的實(shí)例秕铛,也稱為可運(yùn)行對象,任務(wù)必須在線程中執(zhí)行
線程的概念:線程是一個(gè)任務(wù)從頭至尾的執(zhí)行流程的控制機(jī)制【注:現(xiàn)成不等同于任務(wù)】,在java中是Thread類創(chuàng)建的一個(gè)對象
兩者的關(guān)系:線程本質(zhì)上也是一個(gè)任務(wù)瞎颗,Thread類自身也實(shí)現(xiàn)了Runnable接口典徊,所以另外一種思路是:定義一個(gè)Thread的拓展類寄啼,然后實(shí)現(xiàn)run( )方法,再從客戶端創(chuàng)建這個(gè)類的對象钠乏,并調(diào)用start( )方法來啟動線程栖秕。但是這不是一個(gè)好方法,把任務(wù)和任務(wù)機(jī)制(線程晓避,是為了控制流程)混合在一起了簇捍。將任務(wù)混在了控制機(jī)制里面。
Java語言對多線程的支持:有一些列的接口和類俏拱,提供了多線程的創(chuàng)建和運(yùn)行暑塑,鎖定資源和避免沖突
從計(jì)算機(jī)來看:一個(gè)cpu一次運(yùn)行一個(gè)線程,多線程就可以讓程序的反應(yīng)更快锅必,執(zhí)行效率更高
Runnable接口:
只有一個(gè)run( )方法事格,JVM運(yùn)行任務(wù)(可運(yùn)行對象)時(shí)會自動調(diào)用這個(gè)方法。
任務(wù)和線程的應(yīng)用:
1.創(chuàng)建一個(gè)任務(wù)類:
需要有一個(gè)定義好的任務(wù)類(實(shí)現(xiàn)Runnable和覆寫override)况毅,然后用這個(gè)任務(wù)類的構(gòu)造方法創(chuàng)建一個(gè)任務(wù)
2.創(chuàng)建一個(gè)線程:
直接調(diào)用Java提供的Thread類創(chuàng)建
Thread thread = new Thread(Task)
3.開始線程:
調(diào)用線程對象的start( )方法分蓖,告訴JVM準(zhǔn)備開始運(yùn)行,然后JVM會自動加載任務(wù)類的run方法
thread.start( )
簡單的demo
package com.design.TaskAndThread;
public class TaskThreadDemo {
public static void main(String[] args) {
// 創(chuàng)建任務(wù)
Runnable printA = new PrintChar('a', 100);
Runnable printB = new PrintChar('b', 100);
Runnable print100 = new PrintNum(100);
// 創(chuàng)建線程
Thread thread1 = new Thread(printA);
Thread thread2 = new Thread(printB);
Thread thread3 = new Thread(print100);
//
thread1.start();
thread2.start();
thread3.start();
}
}
// 定義一個(gè)任務(wù)類
class PrintChar implements Runnable {
private char charToPrint;
private int times;
// 構(gòu)建方法不只是為了構(gòu)建一個(gè)對象時(shí)候才使用,也可以初始化屬性(私有屬性)
public PrintChar(char a, int t) {
this.charToPrint = a;
this.times = t;
}
// 覆寫run方法尔许,讓JVM調(diào)用時(shí)知道怎么做
@Override
public void run() {
for (int i = 0; i < times; i++) {
System.out.println(charToPrint);
}
}
}
class PrintNum implements Runnable {
private int lastNum;
public PrintNum(int n) {
this.lastNum = n;
}
@Override
public void run() {
for (int i = 0; i <= lastNum; i++) {
System.out.println(i + " ");
}
}
}
demo里有三個(gè)程序,為了能夠同時(shí)運(yùn)行终娃,創(chuàng)建了三個(gè)線程
注:如果在
thread1.start();
thread2.start();
thread3.start();
改變?yōu)橹苯诱{(diào)用run( )
thread1.run();
thread2.run();
thread3.run();
就只是在單獨(dú)一個(gè)線程中執(zhí)行該方法味廊,沒有新的線程啟動,結(jié)果就是按順序執(zhí)行完thread1然后thread2然后thread3棠耕。
Thread類:
包含創(chuàng)建線程的構(gòu)造方法以及控制線程的很多方法余佛,線程的開始和暫定以及狀態(tài)的判斷和結(jié)束
在java中不建議用stop方法來停止線程,通過對Thread變量賦值為null來表明停止
常用的方法:
線程的兩種暫停:
1.Thread.yield( ) 執(zhí)行一次就臨時(shí)暫停窍荧,讓出cpu時(shí)間
在demo中的修改辉巡,在任務(wù)類中的run( )方法中添加
// 覆寫run方法,讓JVM調(diào)用時(shí)知道怎么做
@Override
public void run() {
for (int i = 0; i < times; i++) {
System.out.println(charToPrint);
// 添加暫停線程的方法蕊退,但不是運(yùn)行一次就暫停一次
Thread.yield();
}
}
2.Thread.sleep(num) 將線程設(shè)置為休眠確保其他的線程啟動郊楣,休眠時(shí)間為毫秒數(shù)憔恳, 會拋出一個(gè)必檢異常IntettuptedException。如果一個(gè)循環(huán)中調(diào)用了sleep方法净蚤,就應(yīng)該將循環(huán)放在try-catch中钥组。這樣一出現(xiàn)錯(cuò)誤才會停止線程
體會一下這兩者的區(qū)別
首先是正確的方式:
try {
while(....)
Thread.sleep(1000);
}
}
catch ( IntettuptedException ex) {
}
接下是錯(cuò)誤的方式:
while(....){
try{
Thread.sleep(1000);
}
}
catch ( IntettuptedException ex) {
}
后面這種錯(cuò)誤的方式會導(dǎo)致,即使線程被中斷今瀑,它還繼續(xù)執(zhí)行程梦,特別注意,IDE自動添加try-catch時(shí)橘荠,會造成第二種屿附,一定要記得循環(huán)控制代碼在try-catch里
demo中修改,一樣在任務(wù)類中修改run( )方法
@Override
public void run() {
for (int i = 0; i <= lastNum; i++) {
System.out.println(i + " ");
try {
if (i >= 29) {
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
使一個(gè)線程等待另一個(gè)線程結(jié)束
anotherThread.join:在一個(gè)線程的run方法運(yùn)行中加入另外一個(gè)thread.join()哥童,然后原本運(yùn)行的線程會等待這個(gè)后加入的anotherThread執(zhí)行完(可以指定執(zhí)行的時(shí)間)
@Override
public void run() {
// 這是直接聲明一個(gè)線程并在里面裝載了任務(wù)
Thread thread4 = new Thread(new PrintChar('Z', 100));
thread4.start();
for (int i = 0; i <= lastNum; i++) {
System.out.println(i + " ");
try {
// 這是通過休眠暫停一個(gè)線程挺份,使用thread.join要注視掉,不然暫停的時(shí)候自動就運(yùn)行不用join了
// if (i >= 29) {
// Thread.sleep(10);
// }
if (i == 50) {
thread4.join( );
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
線程的優(yōu)先級
默認(rèn)情況下如蚜,線程會繼承生成它的線程的優(yōu)先級(java中優(yōu)先級范圍為1~10)压恒,使用setPriority方法更改優(yōu)先級,默認(rèn)三個(gè)優(yōu)先級為(1 小错邦,5 正常探赫, 10 大)JVM會選擇優(yōu)先級大的可運(yùn)行線程,就是數(shù)字越大越早運(yùn)行撬呢,如果線程優(yōu)先級相同伦吠,則會用循環(huán)隊(duì)列給它們分配相同的cpu份額,稱為循環(huán)調(diào)度
注意:在編寫程序的時(shí)候魂拦,最好使用常量來賦值毛仪,數(shù)字可能后期會更改,
因?yàn)榈蛢?yōu)先級的線程必須等待高優(yōu)先級的線程運(yùn)行完芯勘,所以為了避免高優(yōu)先級的線程出現(xiàn)問題一直卡著導(dǎo)致低優(yōu)先級沒機(jī)會運(yùn)行箱靴,所以高優(yōu)先級會加入sleep或者yield方法來暫停。
線程池
線程池的作用:每個(gè)任務(wù)都會開始一個(gè)新的線程荷愕,這會限制吞吐并且造成性能降低衡怀。線程池來管理并發(fā)執(zhí)行任務(wù)。簡單點(diǎn)就是限制線程的數(shù)量
重要的接口和類:Executor接口來執(zhí)行線程池中的任務(wù)安疗,ExecutorService接口繼承Executor接口抛杨,然后管理和控制任務(wù),ExecutorService接口繼承Executor接口荐类,同樣是把管理和創(chuàng)建的功能解耦的設(shè)計(jì)(復(fù)習(xí)前面的任務(wù)和線程的設(shè)計(jì))怖现。Executor類實(shí)現(xiàn)了Exexutor接口和ExecutorService的接口,實(shí)現(xiàn)了接口的execute方法玉罐,并且提供了創(chuàng)建Executor對象的靜態(tài)方法
Executor類的兩個(gè)重要方法:1.newFixedThreadPool(num)這是創(chuàng)建一個(gè)限制最大線程的線程池2.newCachedThreadPool()這是在當(dāng)之前創(chuàng)建的線程可用時(shí)就不會創(chuàng)建新的屈嗤,如果沒有再創(chuàng)造新的線程潘拨,會為每一個(gè)任務(wù)都創(chuàng)建一個(gè)新的線程,所有的任務(wù)都會并發(fā)的進(jìn)行
線程池的工作原理:如果線程池中的完成了一個(gè)任務(wù)恢共,就能夠重新執(zhí)行另外一個(gè)任務(wù)战秋。如果線程池中的所有線程r都不是處于空閑狀態(tài),在(executor.shutdown)關(guān)閉前出現(xiàn)一個(gè)錯(cuò)誤關(guān)閉了一個(gè)線程讨韭,這時(shí)來了一個(gè)新的任務(wù)等待執(zhí)行脂信,就會創(chuàng)建一個(gè)新線程來代替。線程池中的線程在60s內(nèi)都沒有被使用就該終止它透硝。
demo
// ====創(chuàng)建一個(gè)最大線程數(shù)的executor對象====
// Executor定義了一個(gè)限制最大線程的線程池狰闪,然后用執(zhí)行器 管理接口聲明
ExecutorService executor = Executors.newFixedThreadPool(3);
// ====提交任務(wù)到執(zhí)行器====
// 線程執(zhí)行器給線程裝載任務(wù)
executor.execute(new PrintChar('E', 100));
executor.execute(new PrintChar('T', 50));
executor.execute(new PrintNum(100));
// ====關(guān)閉線程池(執(zhí)行器)====
executor.shutdown();
線程同步
作用:協(xié)調(diào)相互依賴的線程的執(zhí)行,如果同一個(gè)資源被多個(gè)線程同時(shí)訪問濒生,可能會出現(xiàn)破壞埋泵,數(shù)據(jù)與真實(shí)的不符合
step | b | task1 | task2 |
---|---|---|---|
1 | 0 | n = b + 1 | |
2 | 0 | n = b + 1 | |
3 | 1 | b = n | |
4 | 1 | b =n |
原因:是因?yàn)槿蝿?wù)2覆蓋了任務(wù)1的結(jié)果,因?yàn)閮蓚€(gè)同時(shí)訪問了一個(gè)公共資源罪治,稱為競爭狀態(tài)丽声,如果一個(gè)類的對象在多線程程序中沒有導(dǎo)致競爭狀態(tài),那才是線程安全的
demo這個(gè)demo里面運(yùn)用了很好的封裝觉义,注意理解雁社,然戶在判斷線程池任務(wù)時(shí)的while要好好琢磨一下
package com.design.TaskAndThread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class AccountWithoutSynchronized {
private static Account account = new Account();
public static void main(String[] args) {
// ====在線程里運(yùn)行,這里的線程池
ExecutorService executor = Executors.newCachedThreadPool();
// 這里通過循環(huán)創(chuàng)建了100個(gè)線程
for(int i = 0; i < 100; i ++) {
executor.execute(new AddPenyTask());
}
executor.shutdown();
// isTerminated 如果線程池中所有任務(wù)都終止晒骇,返回true
while (!executor.isTerminated()) {
}
System.out.println("賬戶是多少錢霉撵? " + account.getBalance());
}
// ====任務(wù)類,增加賬戶金錢洪囤,將具體的操作分剝開徒坡,純粹調(diào)用方法
private static class AddPenyTask implements Runnable{
@Override
public void run() {
account.deposit(1);
}
}
// ====賬戶類,跟賬戶有關(guān)系的操作方法都在這里瘤缩,提供方法讓外面的調(diào)用====
private static class Account {
private int balance = 0;
private int getBalance() {
return balance;
}
// ====每次增加1====
public void deposit(int amount) {
int newBalance = amount + balance;
try {
// 增加效果喇完,線程休眠一會
Thread.sleep(5);
} catch (InterruptedException e) {
}
balance = newBalance;
}
}
}
線程安全的方法:
達(dá)到線程安全就必須讓線程沒有競爭公共資源,就得把程序中的這部分限制起來剥啤,這部分叫做臨界區(qū)何暮。
方法一: 將臨界區(qū)的方法加上限定關(guān)鍵字 synchronized,可以加在方法或是在語句上
方法二: 在執(zhí)行之前加上一把鎖铐殃,對于靜態(tài)方法要在類上加鎖,實(shí)例方法要給對象加鎖跨新。如果一個(gè)線程調(diào)用了一個(gè)對象的同步方法富腊,首先要給對象(類)加鎖,然后執(zhí)行這個(gè)方法域帐。最后解鎖赘被。在解鎖之前是整,另外一個(gè)調(diào)用那個(gè)對象(類)的線程會被堵塞,知道解鎖
synchronized關(guān)鍵字
給整個(gè)方法加上synchronized
public synchronized void deposit(int amount)
給語句加上synchronized同步語句
優(yōu)點(diǎn)就是允許設(shè)置同步方法中的部分代碼民假,而不必是整個(gè)方法浮入,這大大增強(qiáng)了程序的并發(fā)能力
private static class AddPenyTask implements Runnable{
@Override
public void run() {
/* 這是不可行的方法,這里的this的參數(shù)必須是一個(gè)對象的引用
* synchronized (this) {
account.deposit(1);
}*/
synchronized (account) {
account.deposit(1);
}
}
}
這是在調(diào)用的時(shí)候
利用加鎖同步
java中可以采用鎖和狀態(tài)來同步線程羊异,
其實(shí)synchronized在執(zhí)行前都是隱式的加上一把鎖事秀,也有顯示的鎖可以使用。
Lock接口:一個(gè)鎖是Lock接口的實(shí)例野舶,定義了加鎖和釋放鎖的方法易迹。鎖也有newCondition()方法來創(chuàng)建任意個(gè)數(shù)的Condition對象,來進(jìn)行線程間的通信平道。
ReentrantLock類:是Lock的一個(gè)實(shí)現(xiàn)睹欲,創(chuàng)建兩種相互排斥的公平策略的鎖。策略為真時(shí)一屋,等待時(shí)間最長的線程首先得到鎖窘疮,公平策略為假的時(shí)候,鎖將隨機(jī)給線程冀墨。一個(gè)公平鎖的程序被多個(gè)線程訪問時(shí)闸衫,整體性能可能比默認(rèn)設(shè)置的差,但是在獲取鎖并且資源缺乏時(shí)可以更小的時(shí)間變化轧苫。
demo:
public static class Account {
// 創(chuàng)建一個(gè)鎖
private static Lock lock = new ReentrantLock();
private int balance = 0;
public int getBalance() {
return balance;
}
public void deposit(int amount) {
// ====獲取lock后加上try-catch最后finally釋放鎖====
// 獲取鎖
lock.lock();
try {
int newBalance = balance + amount;
Thread.sleep(5);
balance = newBalance;
} catch (InterruptedException e) {
} finally {
// 釋放鎖
lock.unlock();
}
}
}
在任務(wù)的執(zhí)行方法中的公共資源前加鎖楚堤,使用synchronized限定方法和語句的使用比顯示鎖簡單,但是顯示鎖對同步劇透狀態(tài)的線程更加直觀和靈活含懊。