1. 程式人生 > 實用技巧 >java面試必問:多執行緒的實現和同步機制,一文幫你搞定多執行緒程式設計

java面試必問:多執行緒的實現和同步機制,一文幫你搞定多執行緒程式設計

前言

程序:一個計算機程式的執行例項,包含了需要執行的指令;有自己的獨立地址空間,包含程式內容和資料;不同程序的地址空間是互相隔離的;程序擁有各種資源和狀態資訊,包括開啟的檔案、子程序和訊號處理。
執行緒:表示程式的執行流程,是CPU排程執行的基本單位;執行緒有自己的程式計數器、暫存器、堆疊和幀。同一程序中的執行緒共用相同的地址空間,同時共享進程序鎖擁有的記憶體和其他資源。

多執行緒的實現

繼承Thread類

  1. 建立一個類,這個類需要繼承Thread類
  2. 重寫Thread類的run方法(run方法中是業務程式碼)
  3. 例項化此執行緒類
  4. 呼叫例項化物件的start方法啟動執行緒
package com.test;

public class Demo1 {
    public static void main(String[] args){
        ThreadDemo threadDemo = new ThreadDemo();
        threadDemo.start();
    }
}

class ThreadDemo extends Thread{
    @Override
    public void run() {
        System.out.println("運行了run方法");
    }
}

在多執行緒程式設計中,程式碼的執行結果與程式碼的執行順序或者呼叫順序是無關的執行緒是一個子任務,CPU以不確定的方式或者是以隨機的時間來呼叫執行緒中的run方法這體現了執行緒執行的隨機性

package com.test;

public class Demo2 {
    public static void main(String[] args) {
        Demo2Thread demo2Thread = new Demo2Thread();
        /*
    *demo2Thread.start方法才是啟動執行緒
    *demo2Thread.run方法只是由main主執行緒來呼叫run方法
    */
        demo2Thread.start();
        try {
            for (int i = 0; i < 3; i++) {
                System.out.println("運行了main方法");
                Thread.sleep(100);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

class Demo2Thread extends Thread{
    @Override
    public void run() {
        try {
            for (int i = 0; i < 3; i++) {
                System.out.println("運行了run方法");
                Thread.sleep(100);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

start的執行順序和執行緒的啟動順序是不一致的
1,2,3,4,5的輸出順序是隨機的

package com.test;

public class Demo3 {
    public static void main(String[] args) {
     	Demo3Thread demo3Thread1 = new Demo3Thread(1);
        Demo3Thread demo3Thread2 = new Demo3Thread(2);
        Demo3Thread demo3Thread3 = new Demo3Thread(3);
        Demo3Thread demo3Thread4 = new Demo3Thread(4);
        Demo3Thread demo3Thread5 = new Demo3Thread(5);

        demo3Thread1.start();
        demo3Thread2.start();
        demo3Thread3.start();
        demo3Thread4.start();
        demo3Thread5.start();
    }
}

class Demo3Thread extends Thread{
    private int i;

    public Demo3Thread(int i){
        this.i = i;
    }

    @Override
    public void run() {
        System.out.println("i=" + i);
    }
}

實現Runnable介面

1)建立一個類,整個類需要實現Runnable介面
2)重寫Runnable介面的run方法
3)例項化建立的這個類
4)例項化一個Thread類,把第3步例項化建立的物件通過Thread類的構造方法傳遞給Thread類
5)呼叫Thread類的run方法

package com.test;

public class Demo4 {
    public static void main(String[] args) {
        Demo4Thread thread = new Demo4Thread();
        Thread t = new Thread(thread);
        t.start();
        System.out.println("運行了main方法");
    }
}

class Demo4Thread implements Runnable{
    @Override
    public void run() {        
    System.out.println("運行了run方法");
    }
}

使用繼承Thread類的方式開發多執行緒應用程式是有侷限的,因為Java是單繼承,繼承了Thread類就無法繼承其他類,所以為了改變這種侷限,用實現Runnable介面的方式來實現多執行緒

成員變數與執行緒安全

自定義執行緒類中的成員變數對於其他執行緒可以是共享或者不共享的,這對於多執行緒的互動很重要

  1. 不共享資料時
package com.test;

public class Demo5 {
    public static void main(String[] args) {
        Thread t1 = new Demo5Thread();
        Thread t2 = new Demo5Thread();
        Thread t3 = new Demo5Thread();
        t1.start();
        t2.start();
        t3.start();    
    }
}

class Demo5Thread extends Thread{
    private int i = 5;
    @Override
    public void run() {
        while(i > 0){
            i--;
            System.out.println(Thread.currentThread().getName() + " i = " + i);
        }
    }
}                    

每個執行緒都有各自的i變數,i變數的值相互之間不影響

  1. 共享資料時
package com.test;

public class Demo6 {
    public static void main(String[] args) {
        Thread t = new Demo6Thread();
            /*
   	 為什麼能將Thread類的物件傳遞給Thread類?
    	 因為Thread類本身就實現了Runnable介面
     	    */
     	Thread thread1 = new Thread(t);
        Thread thread2 = new Thread(t);
        Thread thread3 = new Thread(t);
        Thread thread4 = new Thread(t);
        Thread thread5 = new Thread(t);

	thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
        thread5.start();
   }
}

class Demo6Thread extends Thread{
    private int i = 5;
    @Override
    public void run() {
            i--;
            System.out.println(Thread.currentThread().getName() + " i = " + i);        
    }
}            

共享資料時,將資料所在類的物件傳遞給多個Thread類即可
共享資料有概率出現不同執行緒產生相同的i的值,這就是非執行緒安全

執行緒常用API

  1. currentThread方法
    返回程式碼被哪個執行緒呼叫的詳細資訊
package com.test;

public class Demo7 {
    public static void main(String[] args) {
        //main執行緒呼叫Demo7Thread的構造方法
        Thread thread = new Demo7Thread();
        //Thread-0執行緒呼叫run方法
        thread.start();
        System.out.println("main方法" + Thread.currentThread().getName());
    }
}

class Demo7Thread extends Thread{
    public Demo7Thread(){
        System.out.println(Thread.currentThread().getName() + "的構造方法");
    }
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "的run方法");
    }
}

輸出結果為:
main的構造方法
main方法main
Thread-0的run方法

main方法會被名稱為main的執行緒呼叫,在新建執行緒類的物件時,執行緒類的構造方法會被main執行緒呼叫;
執行緒類物件的start方法會呼叫run方法,此時執行緒類預設的名稱為Thread-0

  1. isAlive方法
    判斷當前的執行緒是否處於活動的狀態,活動狀態就是執行緒已經啟動並且沒有結束執行的狀態
package com.test;

public class Demo8 {
    public static void main(String[] args) {
        Thread t = new Demo8Thread();
        System.out.println("執行緒啟動前:" + t.isAlive());
        t.start();
        System.out.println("執行緒啟動後:" + t.isAlive());
    }
}

class Demo8Thread extends Thread{
    @Override
    public void run() {
        System.out.println("run方法的執行狀態" + this.isAlive());
    }
}

輸出結果為:
執行緒啟動前:false
執行緒啟動後:true
run方法的執行狀態true

true表示執行緒正處於活動狀態,false則表示執行緒正處於非活動狀態

  1. sleep方法
    使當前正在執行的執行緒在指定的毫秒數內暫停執行
package com.test;

public class Demo8 {
    public static void main(String[] args) {
        Thread t = new Demo8Thread();
        System.out.println("執行緒啟動前時間:" + System.currentTimeMillis());
        t.start();
        System.out.println("執行緒啟動後時間:" + System.currentTimeMillis());
    }
}

class Demo8Thread extends Thread{
    @Override
    public void run() {
        System.out.println("執行緒sleep前的時間:" + System.currentTimeMillis());
        try {
	    Thread.sleep(300);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println("執行緒sleep後的時間:" + System.currentTimeMillis());
    }
}
  1. getId方法
    獲取當前執行緒的唯一標識
package com.test;

public class Demo9 {
    public static void main(String[] args) {
        Thread t = Thread.currentThread();
        System.out.println(t.getName() + ", " + t.getId());
        Thread thread = new Thread();
        System.out.println(thread.getName() + ", " + thread.getId());
    }
}
  1. 停止執行緒
    停止一個執行緒,即執行緒在完成任務之前,就結束當前正在執行的操作
    1)使用退出標誌,使執行緒正常停止,即run方法執行完後執行緒終止
package com.test;

public class Demo10 {
    public static void main(String[] args) {
        Demo10Thread thread = new Demo10Thread();
        thread.start();
        try {
            Thread.sleep(2000);
        }catch (Exception e){
            e.printStackTrace();
        }
        /*
        stopThread方法,將flag變為false,為什麼能夠傳遞到當前執行緒中?
        我覺得是因為當前執行緒是一直在執行的,while()中的條件一直成立
        所以當呼叫了stopThread方法,將flag變為false,while迴圈就結束了,run方法中的程式碼也結束了
        所以執行緒停止了
         */
        thread.stopThread();
    }
}

class Demo10Thread extends Thread{
    private Boolean flag = true;
    @Override
    public void run() {
        try {
            while (flag){
                System.out.println("執行緒正在執行");
                Thread.sleep(1000);
            }
            System.out.println("執行緒結束執行");
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    public void stopThread(){
        flag = false;
    }
}

2)stop方法強制結束執行緒

package com.test;

public class Demo11 {
    public static void main(String[] args) {
        Demo11Thread thread = new Demo11Thread();
        thread.start();
        try {
            Thread.sleep(2000);
        }catch (Exception e){
            e.printStackTrace();
        }
        //stop方法中的斜槓表示方法已經被作廢,不建議使用此方法
        thread.stop();
    }
}

class Demo11Thread extends Thread{
    private Boolean flag = true;
    @Override
    public void run() {
        try {
            while (flag){
                System.out.println("執行緒正在執行~~~");
                Thread.sleep(1000);
            }
            System.out.println("執行緒結束執行~~~");
        }catch (Exception e){
            e.printStackTrace();
        }catch (ThreadDeath e){//捕獲執行緒終止的異常
            System.out.println("進入catch塊");
            e.printStackTrace();
        }
    }
}

stop強制停止執行緒可能使一些清理性的工作得不到完成;還會對鎖定的物件進行解鎖,使資料得不到同步的處理,導致資料不一致

3)interrupt方法中斷執行緒

package com.test;
public class Demo12 {
    public static void main(String[] args) {
        Demo12Thread thread = new Demo12Thread();
        thread.start();
        thread.interrupt();
        System.out.println("thread執行緒是否已經停止?" + thread.isInterrupted() + ", " + thread.getName());
        System.out.println("當前執行緒是否已經停止?" + Thread.interrupted() + ", " + Thread.currentThread().getName());
    }
}

class Demo12Thread extends Thread{  
    @Override
    public void run() {
   	for(int i = 0; i < 5; i++){
            System.out.println(i);
        }
    }
}

呼叫interrupt方法不會真正的結束執行緒,而是給當前執行緒打上一個停止的標記
Thread類提供了interrupt方法測試當前執行緒是否已經中斷,isInterrupted方法測試執行緒是否已經中斷

執行結果為:

thread執行緒是否已經停止?true, Thread-0
0
1
2
3
4
當前執行緒是否已經停止?false, main

thread.isInterrupted方法檢查執行緒類是否被打上停止的標記,Thread.interrupted方法檢查主執行緒是否被打上停止的標記

暫停執行緒

暫停執行緒使用suspend方法,重啟暫停執行緒使用resume方法
suspend方法暫停執行緒後,i的值就不會繼續增加。兩次"第一次suspend"輸出的結果一致
resume方法重啟暫停執行緒後,i的值會繼續增加,再使用suspend方法暫停執行緒,兩次"resume後第二次suspend:"輸出的結果一致

package com.test;

public class Demo13 {
	public static void main(String[] args) throws InterruptedException {
	Demo13Thread thread = new Demo13Thread();
	thread.start();
	Thread.sleep(100);
	thread.suspend();
	System.out.println("第一次suspend:" + thread.getI());
        Thread.sleep(100);
        System.out.println("第一次suspend:" + thread.getI());
        thread.resume();
        Thread.sleep(100);
        thread.suspend();
        System.out.println("resume後第二次suspend:" + thread.getI());
        Thread.sleep(100);
        System.out.println("resume後第二次suspend:" + thread.getI());
    }
}

class Demo13Thread extends Thread{
    private long i;
    public long getI() {
        return i;
    }        
    public void setI(long i) {
        this.i = i;
    }
    @Override
    public void run() {
        while (true){
            i++;
        }
    }
}

suspend方法會使執行緒獨佔公共的同步物件,使其他執行緒無法訪問公共的同步物件
suspend方法還可能會造成共享物件的資料不同步

yield方法

yield方法是使當前執行緒放棄CPU資源,將資源讓給其他的執行緒,但是放棄的時間不確定,可能剛剛放棄,馬上又獲取CPU時間片

package com.test;

public class Demo15 {
    public static void main(String[] args) {
    	Demo15Thread thread = new Demo15Thread();
    	thread.start(); 
    }
}

class Demo15Thread extends Thread{
    @Override
    public void run() {
    	long start = System.currentTimeMillis();
    	int count = 0;
    	for(int i = 0; i < 50000; i++){
            Thread.yield();//使當前執行緒放棄CPU資源,但是放棄的時間不確定
            count = count + i;
        }    
        long end = System.currentTimeMillis();
        System.out.println("花費時間:" + (end - start));
    }
}

執行緒的優先順序

在作業系統中,執行緒是可以劃分優先順序的,優先順序較高的執行緒能夠得到更多的CPU資源,即CPU會優先執行優先順序較高的執行緒物件中的任務。設定執行緒優先順序有助於幫助"執行緒排程器"確定下一次選擇哪個執行緒優先執行
設定執行緒的優先順序使用setPriority方法,優先順序分為1~10級,如果設定的優先順序小於1或者大於10,JVM會丟擲IllegalArgumentException異常,JDK預設設定了3個優先順序常量,MIN_PRIORITY=1(最小值),NORM_PRIORITY=5(中間值,也是預設值),MAX_PRIORITY=10(最大值)
獲取執行緒的優先順序使用getPriority方法

package com.test;

public class Demo16 {
    public static void main(String[] args) {
    	System.out.println("主執行緒的執行優先順序是:" + Thread.currentThread().getPriority());
        System.out.println("設定主執行緒的執行優先順序");
        Thread.currentThread().setPriority(8);
        System.out.println("主執行緒的執行優先順序是:" + Thread.currentThread().getPriority());
	/*
        執行緒的優先順序具有繼承性,本來預設的執行緒的優先順序為5
        但是將主執行緒的優先順序設定為8,此子執行緒也會繼承主執行緒的優先順序8
         */
        Thread t = new Demo16Thread();
        t.start();
    }
}

class Demo16Thread extends Thread{
    @Override
    public void run() {
   	 System.out.println("執行緒的優先順序是:" + this.getPriority());
    }
}

優先順序較高的執行緒,先執行的概率較大

執行緒的同步機制

Java多執行緒中的同步,指的是如何在Java語言中開發出執行緒安全的程式,或者如何在Java語言中解決執行緒不安全時所帶來的問題
"執行緒安全"與"非執行緒安全"是多執行緒技術中的經典問題。"非執行緒安全"就是當多個執行緒訪問同一個物件的成員變數時,讀取到的資料可能是被其他執行緒修改過的(髒讀)。"執行緒安全"就是獲取的成員變數的值是經過同步處理的,不會有髒讀的現象

synchronized同步方法

區域性變數是執行緒安全的
區域性變數不存線上程安全的問題,永遠都是執行緒安全的,這是由區域性變數是私有的特性造成的

package com.test.chap2;

public class Demo1 {
    public static void main(String[] args) {
        Service service = new Service();
        ThreadDemo1 t1 = new ThreadDemo1(service);
        t1.start();
        ThreadDemo2 t2 = new ThreadDemo2(service);
        t2.start();
    }
}

class Service {
    public void add(String name){
        int number = 0;//number是方法內的區域性變數 
        if("a".equals(name)){
            number = 100;
            System.out.println("傳入的引數為a,修改number的值為:" + number);
            try {
            //這裡使執行緒休眠是為了等待其他執行緒修改number的值
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else {
            number = 200;
            System.out.println("傳入的引數不為a,修改number的值為:" + number);
        }   
   }
}

class ThreadDemo1 extends Thread{
    private Service service;
    public ThreadDemo1(Service service){
        this.service = service;
    }
    @Override
    public void run() {
        service.add("a");
    }
}

class ThreadDemo2 extends Thread{
    private Service service;
    public ThreadDemo2(Service service){
        this.service = service;
    }               
    @Override
    public void run() {
        service.add("b");
    }
}

成員變數不是執行緒安全的
如果有兩個執行緒,都要操作業務物件中的成員變數,可能會產生"非執行緒安全"的問題,此時需要在方法前使用synchronized關鍵字進行修飾
number是Demo2Service類的成員變數,Demo2Service類的add方法中,當傳入的引數為a時,會進入if條件,休眠1s,並將number的值改為100,當傳入的引數不為a時,不會休眠,將number的值改為200
t3執行緒,傳入的引數為a;t4執行緒,傳入的引數為b,所以線上程start之後,t3執行緒會休眠1s,t4執行緒不會休眠,所以t4執行緒會先將number的值改為200並輸出,但是當t3執行緒結束休眠後,輸出的number的值也是200,這就產生了執行緒安全的問題
為了解決此執行緒不安全的問題,可以在方法前,加上synchronized關鍵字進行修飾,此時呼叫此方法的執行緒需要執行完,方法才會被另一個執行緒所呼叫

package com.test.chap2;

public class Demo2 {
    public static void main(String[] args) {
        Demo2Service service = new Demo2Service();
        ThreadDemo3 t3 = new ThreadDemo3(service);
        t3.start();
        ThreadDemo4 t4 = new ThreadDemo4(service);
        t4.start();
    }
}

class Demo2Service{
    private int number = 0;
    public void add(String name){
        if("a".equals(name)){
            number = 100;
            try {
                //這裡使執行緒休眠是為了等待其他執行緒修改number的值
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("傳入的引數為a,修改number的值為:" + number);
        }else {
            number = 200;
            System.out.println("傳入的引數不為a,修改number的值為:" + number);
        }
   }
}

class ThreadDemo3 extends Thread{
    private Demo2Service service = new Demo2Service();
    public ThreadDemo3(Demo2Service service){
        this.service = service;
    }
    @Override
    public void run() {
        service.add("a");
    }
}

class ThreadDemo4 extends Thread{
    private Demo2Service service;
    public ThreadDemo4(Demo2Service service){
        this.service = service;
    }
    @Override
    public void run() {
        service.add("b");
    }
}                        

多個物件使用多個物件鎖
synchronized設定的鎖都是物件鎖,而不是將程式碼或者方法作為鎖
當多個執行緒訪問同一個物件時,哪個執行緒先執行此物件帶有synchronized關鍵字修飾的方法,其他執行緒就只能處於等待狀態,直到此執行緒執行完畢,釋放了物件鎖,其他執行緒才能繼續執行
如果多個執行緒分別訪問多個物件,JVM會創建出多個物件鎖,此時每個執行緒之間都不會互相干擾

鎖的自動釋放
當一個執行緒執行的程式碼出現了異常,其持有的鎖會自動釋放

synchronized同步語句塊

synchronized關鍵字修飾的方法的不足之處
假如執行緒A和執行緒B都訪問被synchronized關鍵字修飾的get方法,執行緒B就必須等執行緒A執行完後,才能執行,這樣執行的效率低

synchronized同步程式碼塊的使用
同步程式碼塊的作用與在方法上新增synchronized關鍵字修飾的作用是一樣的

t1和t2兩個執行緒同時訪問Demo10Service的synTest方法,synTest方法中部分程式碼加上了同步程式碼塊,從輸出結果可以發現,t1和t2執行緒會同時訪問synTest方法並同時執行非同步程式碼塊的邏輯,但是同步程式碼塊的部分,t1執行緒先訪問的話,t2執行緒就必須等到t1執行緒執行完畢後,才能繼續執行
假如在synTest方法上加上synchronized關鍵字修飾,t1執行緒先訪問synTest方法的話,t2執行緒就必須等到t1執行緒執行完畢後,才會訪問synTest方法並執行,總的來說,同步程式碼塊可以鎖住部分需要同步執行的程式碼,而方法中沒有鎖住的其他程式碼可以非同步執行

package com.test.chap2;

public class Demo10 {
    public static void main(String[] args) throws InterruptedException {
        Demo10Service service = new Demo10Service();
        Thread t1 = new Demo10Thread(service);
        t1.setName("A");
        Thread t2 = new Demo10Thread(service);
        t2.setName("B");
        t1.start();
        t2.start();
    }
}

class Demo10Service{
    public void synTest(){
        System.out.println(Thread.currentThread().getName() + "執行緒訪問synTest方法");        
        try {
            synchronized (this) {
                System.out.println(Thread.currentThread().getName() + "執行緒開始~~~");
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName() + "執行緒結束~~~");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class Demo10Thread extends Thread{
    Demo10Service service;
    public Demo10Thread(Demo10Service service){
        this.service = service;
    }
    @Override
    public void run() {
        service.synTest();
    }
}    

volatile關鍵字

volatile關鍵字的主要作用是使變數在多個執行緒之間可見
當執行緒啟動後,如果flag變數前沒有volatile關鍵字修飾,執行緒會一直卡在run方法中的while迴圈中,修改flag的值不會生效,而加了volatile關鍵字修飾後,修改flag的值會生效,執行緒會退出while迴圈

在啟動執行緒時,flag變數存在於公共堆疊及執行緒的私有堆疊中。JVM為了執行緒的執行效率,一直從私有堆疊中取flag的值,當執行service.flag = false語句時,雖然修改了flag的值,但是修改的卻是公共堆疊的flag值,執行緒還是從私有堆疊中取flag的值,所以並不會退出while迴圈。使用volatile關鍵字修飾成員變數後,會強制JVM從公共堆疊中獲取變數的值,所以能夠退出while迴圈

package com.test.chap2;

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        DemoService service = new DemoService();
        Thread t1 = new Thread(service);
        t1.start();
        Thread.sleep(100);
        System.out.println("準備修改flag的值");
        service.flag = false;
        System.out.println(service.flag);
    }
}

class DemoService extends Thread{
    //沒有volatile關鍵字的話,執行緒會一致處於while迴圈中
    volatile public boolean flag = true;        
    @Override
    public void run() {
        System.out.println("開始執行run方法");
        while (flag){

        }
        System.out.println("結束執行run方法");
    }
}

synchronized和volatile的區別:
1、volatile是執行緒同步的輕量級實現,所以volatile的效能要比synchronized要好,但是volatile只能修飾變數。而synchronized可以修飾方法以及程式碼塊。隨著JDK的版本更新,synchronized在執行效率上也有很大的提升,使用率還是較高
2、多執行緒訪問volatile不會阻塞,而訪問synchronized會出現阻塞
3、volatile能保證資料的可見性,但是不能保證原子性,可能會出現髒讀;而synchronized能夠保證原子性,也能間接保證可見性,因為其能將私有記憶體和公共記憶體中的資料做同步
4、volatile解決的是變數在多個執行緒之間的可見性,而synchronized解決的是多個執行緒之間訪問資源的同步性

最後

大家看完有什麼不懂的可以在下方留言討論,也可以關注我私信問我,我看到後都會回答的。也歡迎大家關注我的公眾號:前程有光,馬上金九銀十跳槽面試季,整理了1000多道將近500多頁pdf文件的Java面試題資料放在裡面,助你圓夢BAT!文章都會在裡面更新,整理的資料也會放在裡面。謝謝你的觀看,覺得文章對你有幫助的話記得關注我點個贊支援一下!