多線程
引入:多線程在我們網絡生活中及其常見,例如邊打遊戲邊聽歌,在同一時間斷執行多個代碼,在程序中就要使用多線程。
首先我們要先了解幾個概念:什麼是併發,什麼是並行,什麼是進程,什麼是線程。
並行: 指兩個或多個事件在同一時刻發生(同時執行)。
併發: 指兩個或多個事件在同一個時間段內發生(交替執行)。
進程: 是指一個內存中運行的應用程序,每個進程都有一個獨立的內存空間,一個應用程序可以同時運行多個進程;進程也是程序的一次執行過程,是系統運行程序的基本單位;系統運行一個程序即是一個進程從創建、運行到消亡的過程。
線程: 是進程中的一個執行單元或者叫控制單元,負責當前進程中程序的執行,一個進程中至少有一個線程。一個進程中是可以有多個線程的,這個應用程序也可以稱之爲多線程程序。
例如:我們的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
好了,這裏就介紹那麼多多線程的基本使用,下面還會持續發佈多線程解決高併發問題和鎖,線程狀態等高級部分的文章,請注意查看。