一、多線程
形象理解====多線程相當進程的支流,各走各的,假設在進程上跑的代碼是主程序,當其中的第三行代碼是開啓線程的,那麼開啓線程之後 ,線程運行的代碼就和主程序並行(即它們之間不相干了)
二、多線的創建和啓動
- Java語言的JVM允許程序運行多個線程,它通過java.lang.Thread類和java.util.concurrent併發包下的Callable接口來實現。
創建線程有三種方式:
- 繼承Thread類
- 實現Runnable接口
- 創建一個類實現Callable接口(本次不重點說這一個)
-
以上三種方式的前兩種都是通過Thread類實現的,它們的啓動方式如下:
創建對象Thread對象->調用start()->相當於調用run(),啓動線程。
要實現線程代碼的邏輯,只需將代碼在run()方法中即可。 -
兩者區別:
- 繼承Thread: 線程代碼存放Thread子類run方法中。重寫run方法
- 實現Runnable:線程代碼存在接口的子類的run方法。實現run方法
-
實現接口的好處:
- 避免了單繼承的侷限性
- 多個線程可以共享同一個接口實現類的對象,非常適合多個相同線程來處理同一份資源。
所以一般使用實現接口方式來實現多線程
-
兩種線程實現方法代碼:
-
繼承Thread方式
package ThreadDemo;
/**
* 繼承Thread實現多線程
*/
public class TestThrad extends Thread {
@Override
public void run(){
System.out.println("多線程運行的代碼");
for(int i= 0 ; i<6 ; i++){
System.out.println("這時多線程的邏輯代碼");
}
}
}
- 實現Runnable接口
package ThreadDemo;
public class TestRunable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"Runable接口多線程運行的代碼");
for(int i= 0 ; i<6 ; i++){
System.out.println(Thread.currentThread().getName()+"這是Runable接口多線程的邏輯代碼");
}
}
}
- 測試類
package ThreadDemo;
public class Test {
public static void main(String[] args) {
// Thread to = new TestThrad();
// to.start();
Thread rt = new Thread(new TestRunable());
Thread rt1 = new Thread(new TestRunable(),"t-1");
rt.start();
rt1.start();
for(int i= 0 ; i<5 ; i++){
System.out.println("傘兵一號準備就緒");
System.out.println("我宣佈");
System.out.println("現在這裏叫做盧本偉廣場");
System.out.println("傘兵一號準備就緒");
System.out.println("我宣佈");
System.out.println("現在這裏叫做盧本偉廣場");
}
}
}
三、Thread類的有關方法
- void start(): 啓動線程,並執行對象的run()方法
- run(): 線程在被調度時執行的操作
- String getName(): 返回線程的名稱
- void setName(String name):設置該線程名稱
- static currentThread(): 返回當前線程
- static void yield():線程讓步
暫停當前正在執行的線程,把執行機會讓給優先級相同或更高的線程
若隊列中沒有同優先級的線程,忽略此方法 - join() :當某個程序執行流中調用其他線程的 join() 方法時,調用線程將被阻塞,直到 join() 方法加入的 join 線程執行完爲止
低優先級的線程也可以獲得執行 - static void sleep(long millis):(指定時間:毫秒)
令當前活動線程在指定時間段內放棄對CPU控制,使其他線程有機會被執行,時間到後重排隊。
拋出InterruptedException異常 - stop(): 強制線程生命期結束
- boolean isAlive():返回boolean,判斷線程是否還活着
- 線程的優先級控制
MAX_PRIORITY(10);
MIN _PRIORITY (1);
NORM_PRIORITY (5);
涉及的方法:
getPriority() :返回線程優先值
setPriority(int newPriority) :改變線程的優先級
線程創建時繼承父線程的優先級,默認都是5,數值越高優先級越高。
但不是優先級越高就會先執行,這東西就是概率性的東西,優先級越高被執行的概率越高。
以下代碼對上述方法都有實現。
public class Test1 {
public static void main(String[] args) {
TestRun run0 = new TestRun();
TestRun run1 = new TestRun();
Thread t0 = new Thread(run0);
Thread t1 = new Thread(run1);
t0.setName("線程t-0");//設置線程的名稱
t1.setName("線程t-1");//設置線程的名稱
// t0.setPriority(1);//設置線程的優先級
// t1.setPriority(10);//設置線程的優先級
t0.start();
t1.start();
// System.out.println(t0.getName());//如果在創建線程的時候沒有指定名稱,系統會給出默認名稱,通過getName()獲取線程名稱
// System.out.println(t1.getName());
/**
* 線程的優先級,就是哪個線程有較大個概率被執行
* 優先級是用數組1-10表示,數字越大優先級越高,如果沒有設置默認優先級是5
*/
// System.out.println("t0的優先級:" + t0.getPriority());//獲取線程的優先級
System.out.println("---------------1");
System.out.println("---------------2");
System.out.println(t1.isAlive());//判斷當前的線程是否存活
t1.stop();//強制線程生命期結束,強制停止此線程
try {
t0.join();//相當於在這塊把t0的run的代碼插入到這個位置執行
/**
* 專業的說法
* 就是阻塞當前的main方法,先不執行System.out.println("---------------3");代碼
* 先執行join進來的線程的代碼
* join的線程執行完畢之後繼續執行之前main方法阻塞的代碼System.out.println("---------------3");
*/
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("---------------3");
System.out.println(t1.isAlive());
}
}
class TestRun implements Runnable{
int count = 0;
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":Runnable多線程運行的代碼");
for(int i = 0; i < 5; i++){
// try {
// Thread.sleep(1000);//當前線程睡眠1000毫秒
// //相當於當前的這個循環每隔1000毫秒執行一次循環
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// if(i % 2 == 0){
// Thread.yield();//線程讓步
// }
count++;
System.out.println(Thread.currentThread().getName() + ":這是Runnable多線程的邏輯代碼:" + count);
}
}
}
四、線程的生命週期
JDK中用Thread.State枚舉表示了線程的幾種狀態
要想實現多線程,必須在主線程中創建新的線程對象。Java語言使用Thread類及其子類的對象來表示線程,在它的一個完整的生命週期中通常要經歷如下的五種狀態:
- 新建: 當一個Thread類或其子類的對象被聲明並創建時,新生的線程對象處於新建狀態
- 就緒:處於新建狀態的線程被start()後,將進入線程隊列等待CPU時間片,此時它已具備了運行的條件
- 運行:當就緒的線程被調度並獲得處理器資源時,便進入運行狀態, run()方法定義了線程的操作和功能
- 阻塞:在某種特殊情況下,被人爲掛起或執行輸入輸出操作時,讓出 CPU 並臨時中止自己的執行,進入阻塞狀態
- 死亡:線程完成了它的全部工作或線程被提前強制性地中止
四、線程的同步
-
問題的提出
多個線程執行的不確定性引起執行結果的不穩定
多個線程對賬本的共享,會造成操作的不完整性,會破壞數據。 -
問題的原因:
當多條語句在操作同一個線程共享數據時,一個線程對多條語句只執行了一部分,還沒有執行完,另一個線程參與進來執行。導致共享數據的錯誤。 -
解決辦法:
對多條操作共享數據的語句,只能讓一個線程都執行完,在執行過程中,其他線程不可以參與執行。 -
關於synchronized關鍵字,鎖住的東西是什麼?個人認爲它鎖住的是一個對象。
- 普通方法加Synchronized(同步鎖),鎖的當前方法對應的對象,當前的對象的所有加了同步鎖的方法是共用一個同步鎖。
- 靜態的方法加synchronized,對於所有的對象都是使用同一個一個鎖。
- 代碼塊synchronized(this),所有當前的對象的synchronized(this)同步的的代碼都是使用同一個鎖,在不同方法有synchronized(this)代碼塊,只要它們對象是同一個,那麼就是同一個鎖。
- synchronized(a),這個小括號中傳入不同的對象就是不同的鎖,若是一個對象就是一個同一個鎖。
-
下面例子使用synchronized關鍵字實現線程線程同步
/**
*此代碼邏輯是兩個用戶同時向同一個賬戶取錢,本來餘額爲3000,
*兩個用戶都要取2000,若是不加鎖直接多線程取錢,必定會出現餘額爲-1000的情況
*所需需要加鎖,使線程同步。
*/
public class Test2 {
public static void main(String[] args) {
//定義賬戶對象
Acount a = new Acount();
Acount a1 = new Acount();
//多線程對象
User u_weixin = new User(a, 2000);
User u_zhifubao = new User(a, 2000);
Thread weixin = new Thread(u_weixin,"微信");
Thread zhifubao = new Thread(u_zhifubao,"支付寶");
weixin.start();
zhifubao.start();
}
}
class Acount{
public static int money = 3000;//全局變量,所有的操作共享這個變量
/**
* 提款,判斷賬戶錢夠不夠
* 多線程調用這個方法,就有問題,線程共享資源時,一個線程在執行這個方法沒有完畢時,另一個線程又開始執行這個方法
* 解決思路:顯然一個線程整體執行完這個方法,另一個線程再執行
* 通過synchronized同步鎖來完成
* 可以直接在方法上加上synchronized關鍵字
* 在普通方法上加同步鎖synchronized,鎖的是整個對象,不是某一個方法
* 不同的對象就是不同的鎖,普通方法加synchronized,線程使用不同的此方法的對象,還有共享資源的問題
*
* 普通方法加同步鎖,鎖的當前方法對應的對象,當前的對象的所有加了同步鎖的方法是共用一個同步鎖
* @param m
*/
public synchronized void drawing(int m){
String name = Thread.currentThread().getName();
if(money < m){
System.out.println(name + "操作,賬戶金額不足:" + money);
}else{
System.out.println(name + "操作,賬戶原有金額:" + money);
System.out.println(name + "操作,取款金額:" + m);
System.out.println(name + "操作,取款操作:原金額" + money + " - " + "取款金額" + m);
money = money - m;
System.out.println(name + "操作,取款後的餘額:" + money);
}
}
public synchronized void drawing1(int m){
String name = Thread.currentThread().getName();
if(money < m){
System.out.println(name + "操作,賬戶金額不足:" + money);
}else{
System.out.println(name + "操作,賬戶原有金額:" + money);
System.out.println(name + "操作,取款金額:" + m);
System.out.println(name + "操作,取款操作:原金額" + money + " - " + "取款金額" + m);
money = money - m;
System.out.println(name + "操作,取款後的餘額:" + money);
}
}
/**
* 靜態的方法加synchronized,對於所有的對象都是使用同一個一個鎖
* @param m
*/
public static synchronized void drawing2(int m){
String name = Thread.currentThread().getName();
if(money < m){
System.out.println(name + "操作,賬戶金額不足:" + money);
}else{
System.out.println(name + "操作,賬戶原有金額:" + money);
System.out.println(name + "操作,取款金額:" + m);
System.out.println(name + "操作,取款操作:原金額" + money + " - " + "取款金額" + m);
money = money - m;
System.out.println(name + "操作,取款後的餘額:" + money);
}
}
/**
* 對代碼塊加入同步鎖
* 代碼塊synchronized(this),所有當前的對象的synchronized(this)同步的的代碼都是使用同一個鎖
* @param m
*/
public void drawing3(int m){
synchronized(this){//表示當前的對象的代碼塊被加了synchronized同步鎖
//用this鎖代碼塊是代表當前的對象,如果在其他方法中也有synchronized(this)的代碼塊使用的都是同一個同步鎖
String name = Thread.currentThread().getName();
if(money < m){
System.out.println(name + "操作,賬戶金額不足:" + money);
}else{
System.out.println(name + "操作,賬戶原有金額:" + money);
System.out.println(name + "操作,取款金額:" + m);
System.out.println(name + "操作,取款操作:原金額" + money + " - " + "取款金額" + m);
money = money - m;
System.out.println(name + "操作,取款後的餘額:" + money);
}
}
}
public void drawing4(int m){
synchronized(this){//表示當前的對象的代碼塊被加了synchronized同步鎖
//用this鎖代碼塊是代表當前的對象,如果在其他方法中也有synchronized(this)的代碼塊使用的都是同一個同步鎖
String name = Thread.currentThread().getName();
if(money < m){
System.out.println(name + "操作,賬戶金額不足:" + money);
}else{
System.out.println(name + "操作,賬戶原有金額:" + money);
System.out.println(name + "操作,取款金額:" + m);
System.out.println(name + "操作,取款操作:原金額" + money + " - " + "取款金額" + m);
money = money - m;
System.out.println(name + "操作,取款後的餘額:" + money);
}
}
}
/**
* synchronized修飾代碼塊,想要根據不同的對象有不同的鎖
* synchronized(a),這個小括號中傳入不同的對象就是不同的鎖
* @param m
*/
public void drawing5(int m,Acount a){
synchronized(a){//表示通過方法的參數傳遞進來的對象的代碼塊被加了synchronized同步鎖
//不同的對象就有不同的同步鎖
String name = Thread.currentThread().getName();
//如果是微信操作的,先不執行,等支付寶操作,支付寶操作完,微信再繼續操作
if(name.equals("微信")){
try {
a.wait();//當前的線程進入等待的阻塞狀態
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(money < m){
System.out.println(name + "操作,賬戶金額不足:" + money);
}else{
System.out.println(name + "操作,賬戶原有金額:" + money);
System.out.println(name + "操作,取款金額:" + m);
System.out.println(name + "操作,取款操作:原金額" + money + " - " + "取款金額" + m);
money = money - m;
System.out.println(name + "操作,取款後的餘額:" + money);
}
if(name.equals("支付寶")){
try {
a.notify();//喚醒當前優先級最高的線程,進入就緒狀態
// a.notifyAll();//喚醒當前所有的線程,進入就緒狀態
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
class User implements Runnable{
Acount acount;
int money;
public User(Acount acount,int money){
this.acount = acount;
this.money = money;
}
@Override
public void run() {
// acount.drawing(money);
// if(Thread.currentThread().getName().equals("微信")){
//// acount.drawing(money);
// acount.drawing3(money);
// }else{
//// acount.drawing1(money);
// acount.drawing4(money);
// }
// acount.drawing2(money);//調用類的靜態方法
// acount.drawing3(money);
acount.drawing5(money, acount);
}
}
五、線程的死鎖問題
- 死鎖
不同的線程分別佔用對方需要的同步資源不放棄,都在等待對方放棄自己需要的同步資源,就形成了線程的死鎖。 - 解決方法
專門的算法、原則,比如加鎖順序一致
儘量減少同步資源的定義,儘量避免鎖未釋放的場景
六、線程間通信
線程間通信不是說線程進行收發信息交流的意思,線程間通信是線程間有相應的邏輯,能讓各個線程能按照一定邏輯有序的執行,比如自己暫停讓別人先執行,等等。
線程間的通信運用的3個方法wait() 與 notify() 和 notifyAll()。
- wait():令當前線程掛起並放棄CPU、同步資源,使別的線程可訪問並修改共享資源,而當前線程排隊等候再次對資源的訪問
- notify():喚醒正在排隊等待同步資源的線程中優先級最高者結束等待
- notifyAll ():喚醒正在排隊等待資源的所有線程結束等待.
Java.lang.Object提供的這三個方法只有在synchronized方法或synchronized代碼塊中才能使用,否則會報java.lang.IllegalMonitorStateException異常,當然也可以不放在synchronized,不過要捕獲異常。以上代碼drawing5(int m,Acount a)中有體現。
七、經典線程同步—生產者/消費者問題
/**
* 生產者與消費者
* @author lby
*
*/
public class Test3 {
public static void main(String[] args) {
Clerk c = new Clerk();
//消費時不生產,生產時不消費
//生產者
new Thread(new Runnable() {
@Override
public void run() {
synchronized (c) {
while(true){//無限循環代表無限的生產次數
if(c.productNum == 0){//產品數爲0,開始生產
System.out.println("產品數爲0,開始生產");
while(c.productNum < 4){
c.productNum++;//增加產品
System.out.println("庫存:" + c.productNum);
}
System.out.println("產品數爲" + c.productNum + ",結束生產");
c.notify();//喚醒消費者線程
}else{
try {
c.wait();//生產者線程等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}, "生產者").start();
//消費者
new Thread(new Runnable() {
@Override
public void run() {
synchronized (c) {
while(true){//無限循環代表無限的消費次數
if(c.productNum == 4){//產品數爲4,開始消費
System.out.println("產品數爲4,開始消費");
while(c.productNum > 0){
c.productNum--;//消費產品
System.out.println("庫存:" + c.productNum);
}
System.out.println("產品數爲" + c.productNum + ",結束消費");
c.notify();//喚醒生產者線程
}else{
try {
c.wait();//消費者線程等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}, "消費者").start();
}
}
class Clerk{
public static int productNum = 0;
}