多線程基本使用

多線程

引入:多線程在我們網絡生活中及其常見,例如邊打遊戲邊聽歌,在同一時間斷執行多個代碼,在程序中就要使用多線程。

首先我們要先了解幾個概念:什麼是併發,什麼是並行,什麼是進程,什麼是線程。

並行: 指兩個或多個事件在同一時刻發生(同時執行)。

併發: 指兩個或多個事件在同一個時間段內發生(交替執行)。

在這裏插入圖片描述

進程: 是指一個內存中運行的應用程序,每個進程都有一個獨立的內存空間,一個應用程序可以同時運行多個進程;進程也是程序的一次執行過程,是系統運行程序的基本單位;系統運行一個程序即是一個進程從創建、運行到消亡的過程。

線程: 是進程中的一個執行單元或者叫控制單元,負責當前進程中程序的執行,一個進程中至少有一個線程。一個進程中是可以有多個線程的,這個應用程序也可以稱之爲多線程程序。

例如:我們的main方法就是一個線程,它有它特定的名稱叫做(main線程或者主線程)。

那麼JVM虛擬機啓動時有幾個線程呢?
我的回答是兩個:一個是main線程,一個是垃圾回收線程(GC)。

在這裏插入圖片描述

瞭解這幾個概念後,那麼久讓我們看看如何創建一個線程,線程的創建方式有兩種:
1.繼承Thread類,重寫run方法。
2.實現Runnable接口,重寫run方法。

Thread方式:
首先我們要看Thread中有哪些構造方法和那些成員方法供我們使用

構造方法:
public Thread() :空參構造
public Thread(String name) :創建對象時指定線程名稱
public Thread(Runnable target) :創建Thread對象並傳入一個實現Runnable接口的類
public Thread(Runnable target,String name) :創建Thread對象並傳入一個實現Runnable接口的類並指定線程名稱。

成員方法:這些方法在下面的例子中我們會用到
public String getName() :獲取當前線程名稱。
public void start() :開啓當前線程,JVM默認調用線程中的run方法。
public void run() :線程要執行的代碼。
public static void sleep(long millis) :讓線程睡多久,傳入毫秒值。
public static Thread currentThread() :返回當前線程的引用。

繼承Thread方式創建一個線程類:

//自定義線程類
public class MyThread extends Thread {
	//繼承Thread類重寫run方法
    @Override
    public void run() {
    	//run方法裏面是我們自己編寫線程要執行的代碼。
        System.out.print("我是通過繼承Thread創建的線程!");
    }
}

//測試類
public class ThreadTest {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start(); //start()方法是開啓線程,後面會說到
    }
}

測試結果:
在這裏插入圖片描述

實現Runnable方式創建一個線程類:

//自定義一個線程類 實現Runnable的方式
public class MyThread implements Runnable {
    @Override
    public void run() {
        System.out.print("我是通過實現Runnable接口創建的線程!");
    }
}

public class ThreadTest {
    public static void main(String[] args) {
        //創建線程類對象
        MyThread mt = new MyThread();

        //創建Thread類,傳入繼承了Runnable接口的線程類
        Thread t = new Thread(mt);

        t.start(); //start()方法是開啓線程,後面會說到
    }
}

測試結果:
在這裏插入圖片描述

到這裏我相信大家一定想問使用多線程的好處是啥?
(單核情況下)多線程的好處就是可以讓多個線程同時執行,多個線程搶用一個CPU資源,達到多個程序同時運行的效果。
現在我們看一下多線程的運行效果:

//自定義一個線程類 實現Runnable的方式
public class MyThread implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            //Thread.currentThread().getName()獲取當前線程的名稱
            System.out.println(Thread.currentThread().getName() + "..." + i);
        }
    }
}

//測試類
public class ThreadTest {
    public static void main(String[] args) {
        //創建線程類對象
        MyThread mt = new MyThread();

        //創建Thread類,傳入繼承了Runnable接口的線程類
        Thread t = new Thread(mt);

        //啓動線程
        t.start();

        //注意:這個循環是在main方法中執行的,它屬於main線程
        for (int i = 0; i < 100; i++) {
            //Thread.currentThread().getName()獲取當前線程的名稱
            System.out.println(Thread.currentThread().getName() + "---" + i);
        }
    }
}

測試結果:
在這裏插入圖片描述

上述兩種方式我們創建線程還需要另外創建一個線程類,比較麻煩。
下面介紹一下匿名內部類的方式:

public class ThreadTest {
    public static void main(String[] args) {

        //第一種,直接new Thread (常用)
        //往構造方法中傳入一個字符串就是給Thread創建的時候設置一個名稱
        new Thread("***"){ 
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    //Thread.currentThread().getName()獲取當前線程的名稱
                    System.out.println(Thread.currentThread().getName() + "---" + i);
                }
            }
        }.start();
        
        //第二種,new Thread 並往裏面傳入一個Runnable接口 (基本不用傳入Runnable不是多此一舉嗎?)
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    //Thread.currentThread().getName()獲取當前線程的名稱
                    System.out.println(Thread.currentThread().getName() + "---" + i);
                }
            }
        }).start();
    }
}

測試結果:
在這裏插入圖片描述

sleep方法:

public class ThreadTest {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            try {
                //每循環一次暫停一秒
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(i);
        }
    }
}

測試結果:
在這裏插入圖片描述

(面試題)(可能面試會問到哦!)
繼承Thread和實現Runnable接口有什麼好處與壞處呢?
如果你已經繼承Thread類 就不能再繼承其他類了 (因爲一個類只能繼承一個類)
而實現接口可以實現多個 (而一個類可以實現多個接口)
避免了單繼承的侷限性,減少了類域類之間的依賴,降低了耦合度。

所以說在使用多線程的時候還是建議使用實現Runnable接口的方式。

多線程的高併發問題及線程安全

高併發:是指在某個時間點上,有大量的用戶(線程)同時訪問同一資源。例如:天貓的雙11購物節,12306的在線購票在某個時間點上,都會面臨大量用戶同時搶購同一件商品/車票的情況。

線程安全:在某個時間點上,當大量用戶(線程)訪問同一資源時,由於多線程運行機制的原因,可能會導致被訪問的資源出現"數據污染"的問題。

多線程的運行機制:
當一個線程啓動後,JVM會爲其分配一個獨立的"線程棧區",這個線程會在這個獨立的棧區中運行。
看一下簡單的線程的代碼:

在這裏插入圖片描述
多個線程在各自棧區中獨立、無序的運行,當訪問一些代碼,或者同一個變量時,就可能會產生一些問題

多線程的安全性問題-可見性

例如下面的程序,先啓動一個線程,在線程中將一個變量的值更改,而主線程卻一直無法獲得此變量的新值。

public class MyThread extends Thread {
    public static int num = 0;

    @Override
    public void run() {
        System.out.println("線程開啓!");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        num = 1;
        System.out.println("線程完結!");
    }
}

public class Work1 {
    public static void main(String[] args) {
        new MyThread().start();

        while (true){
            if (MyThread.num == 1){
                break;
            }
        }
        System.out.println("main結束!");
    }
}

運行結果:因爲while循環裏面用的副本num無法得到更新,所以他還是0,會一直進入死循環。main方法無法正常結束。
在這裏插入圖片描述

多線程的安全性問題-有序性

有些時候“編譯器”在編譯代碼時,會對代碼進行“重排”,例如:
int a = 10; //1行
int b = 20; //2行
int c = a + b; //3行
第一行和第二行可能會被“重排”:可能先編譯第二行,再編譯第一行,總之在執行第三行之前,會將1,2編譯
完畢。1和2先編譯誰,不影響第三行的結果。
但在“多線程”情況下,代碼重排,可能會對另一個線程訪問的結果產生影響:
在這裏插入圖片描述
多線程的情況下 ,我們是不希望對代碼進行排重的。

多線程的安全性問題-原子性

public class MyThreadWork2 extends Thread {
    public static int num = 0;
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            num++;
        }
    }
}

public class Work2 {
    public static void main(String[] args) {
        new MyThreadWork2().start();

        for (int i = 0; i < 10000; i++) {
            MyThreadWork2.num++;
        }
        System.out.println(MyThreadWork2.num);
    }
}

運行結果:

在這裏插入圖片描述
結論:按照常理來講最終結果應該是20000,但是這裏的結果卻不是,我們連續運行幾次發現也不是20000,那麼到底是爲什麼呢,因爲num作爲public static 所修飾的共享變量,在進行++時,如果說0線程目前+到了4002,當CPU把執行權給了main線程以後那麼它會從4002開始+,當main中的線程執行完畢後,就會輸出num信息 ,然後main線程結束,此時0線程還沒有+完,所以輸出的值就不是20000,雖然後續0線程還會繼續給num++;但是main線程已經結束,不會再輸出了,所以控制檯打印的就是不到20000的數字。
所以說兩個線程訪問同一個變量num的代碼不具有"原子性

解決方法

下面就會有volatile原子類出場。

先說volatile,它可以用於修飾變量,當共享變量被修飾後就不會出現,可見性有序性問題了。
這個比較簡單就不在演示了。

原子類:他們的基本實現是基於CAS(樂觀鎖)的機制實現的,下一篇會說到。
1).java.util.concurrent.atomic.AtomicInteger:對int變量操作的“原子類”;
2).java.util.concurrent.atomic.AtomicLong:對long變量操作的“原子類”;
3).java.util.concurrent.atomic.AtomicBoolean:對boolean變量操作的“原子類”;
它們可以保證對“變量”操作的:原子性、有序性、可見性。

成員方法:
get() 獲取當前值。
getAndIncrement() 相當於i++;
incrementAndGet() 相當於++i;
addAndGet(int參數) 相當於當前值與傳入的值相加;
getAndSet(int參數) 返回的參數是舊值,參數是新值。

getAndSet演示:

AtomicInteger ai = new AtomicInteger(12);
int num = ai.getAndSet(88);
//最終的值是
num = 12;
ai = 88;

基本使用:

public class MyThread extends Thread {
    public static AtomicInteger ai = new AtomicInteger();//空參就是0,傳入的是幾就是幾

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            ai.getAndIncrement();//先獲取再自增;類似於a++;
        }
    }
}


public class Work1 {
    public static void main(String[] args) {

        new MyThread().start();

        for (int i = 0; i < 10000; i++) {
            MyThread.ai.getAndIncrement();//先獲取再自增;類似於a++;
        }

        try {
            Thread.sleep(1000); //睡一秒 防止主線程一下執行完畢
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(MyThread.ai.get());
    }
}

運行結果:可以看到,問題得到了解決。

在這裏插入圖片描述

數組類型:
1).java.util.concurrent.atomic.AtomicIntegetArray:對int數組操作的原子類。
2).java.util.concurrent.atomic.AtomicLongArray:對long數組操作的原子類。
3).java.utio.concurrent.atomic.AtomicReferenceArray:對引用類型數組操作的原子類。

演示:
在這裏插入圖片描述

多線程.start之後會立即啓動線程嗎?
回答:不是,是要看CPU和系統的調度,源碼中真正啓動的不是start而是start0

好了,這裏就介紹那麼多多線程的基本使用,下面還會持續發佈多線程解決高併發問題和鎖,線程狀態等高級部分的文章,請注意查看。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章