上週由於一些事情回了一趟長沙,因此更新晚了幾天。Sorry~html
首先咱們從 Object 類中的三個方法來學習。java
方法名 | 做用 |
---|---|
void notify() | 喚醒在此對象監視器上等待的單個線程 |
void notifyAll() | 喚醒在此對象監視器上等待的全部線程 |
void wait() | 使當前的線程等待,直到其餘線程調用此對象的 notify()方法或 notifyAll()方法 |
關於 等待/通知,要記住的關鍵點是:git
敲黑板!!!👆👆上面這段話是重點。會用 wait、notify 方法的童鞋先理解這段話,不會用 wait、notify 方法的童鞋請看懂下面的例子再結合例子理解。程序員
public static void main(String[] args) {
Thread1 thread1 = new Thread1();
thread1.start();
synchronized (thread1.obj) {
try {
System.out.println("等待 thread1 完成計算。。。");
//線程等待
thread1.obj.wait();
// thread1.sleep(1000);//思考一下,若是把上面這行代碼注掉,執行這行代碼
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("thread1 對象計算的總和是:" + thread1.total);
}
}
public static class Thread1 extends Thread {
int total;
public final Object obj = new Object();
@Override
public void run() {
synchronized (obj) {
for (int i = 0; i < 101; i++) {
total += i;
}
//(完成計算了)喚醒在此對象監視器上等待的單個線程,在本例中線程 thread1 被喚醒
obj.notify();
System.out.println("計算結束:" + total);
}
}
}
}複製代碼
以上代碼的兩個 synchronize 代碼塊的鎖都用 Thread1 的實例對象也是能夠的,這裏爲了方便你們理解必需要用同一個鎖,才 new 了一個 Obj 對象。github
注意:當在對象上調用 wait 方法時,執行該代碼的線程當即放棄它在對象上的鎖。然而調用 notify 時,並不意味着這時線程會放棄其鎖。若是線程仍然在完成同步代碼,則線程在同步代碼結束以前不會放棄鎖。所以,調用了 notify 並不意味着這時該鎖變得可用
。算法
上面的運行結果忘記粘貼出來了,童鞋們自行測試吧~數據庫
在多數狀況下,最好通知等待某個對象的全部線程。若是這也作,能夠在對象使用 notifyAll()讓全部在此對象上等待的線程從新活躍。編程
public class ThreadMutual extends Thread{
int total;
public static void main(String[] args) {
ThreadMutual t = new ThreadMutual();
new Thread1(t).start();
new Thread1(t).start();
new Thread1(t).start();
new Thread1(t).start();
new Thread1(t).start();
new Thread1(t).start();
t.start();
}
@Override
public void run() {
synchronized (this) {
for (int i = 0; i < 11; i++) {
total += i;
}
//(完成計算了)喚醒在此對象監視器上等待的單個線程,在本例中線程A被喚醒
System.out.println("計算結束:" + total);
notifyAll();
}
}
public static class Thread1 extends Thread {
private final ThreadMutual lock;
public Thread1(ThreadMutual lock){
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "獲得結果:"+lock.total);
}
}
}
}
計算結束:55
Thread-5獲得結果:55
Thread-6獲得結果:55
Thread-4獲得結果:55
Thread-3獲得結果:55
Thread-2獲得結果:55
Thread-1獲得結果:55複製代碼
注意:上面的代碼若是線程 t 若是第一個 start,則會發生不少意料以外的狀況,好比說notifyAll 已經執行了,wait 的代碼還沒執行。而後, 就形成了某個線程一直處於等待狀態。
一般,解決上面問題的最佳方式是利用某種循環,該循環檢查某個條件表達式,只有當正在等待的事情尚未發生的狀況下,它才繼續等待。api
Java 線程的調度是 Java 多線程的核心,只有良好的調度,才能充分發揮系統的性能,提升程序的執行效率。安全
這裏要明確一點,無論程序員怎麼編寫調度,只能最大限度的影響線程執行的次序,而不能作到精準控制。
線程休眠的目的是時線程讓出 CPU 的最簡單的作法之一,線程休眠時,會將 CPU資源交給其餘線程,以便能輪換執行,當休眠必定時間後,線程會甦醒,進入準備狀態等待執行。
線程休眠的方法是 Thread.sleep(),是個靜態方法,那個線程調用了這個方法,就睡眠這個線程。
public class Test {
public static void main(String[] args) {
Thread t1 = new MyThread1();
Thread t2 = new Thread(new MyRunnable());
t1.start();
t2.start();
}
}
class MyThread1 extends Thread {
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("線程1第" + i + "次執行!");
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class MyRunnable implements Runnable {
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("線程2第" + i + "次執行!");
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}複製代碼
運行結果:
線程2第0次執行!
線程1第0次執行!
線程1第1次執行!
線程2第1次執行!
線程1第2次執行!
線程2第2次執行!複製代碼
與線程休眠相似,線程的優先級仍然沒法保證線程的執行次序。只不過,優先級高的線程獲取 CPU 資源的機率較大,低優先級的並不是沒有機會執行。
線程的優先級用1-10之間的整數表示,數值越大優先級越高,默認爲5.
public class Test {
public static void main(String[] args) {
Thread t1 = new MyThread1();
Thread t2 = new Thread(new MyRunnable());
t1.setPriority(10);
t2.setPriority(1);
t2.start();
t1.start();
}
}
class MyThread1 extends Thread {
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("線程1第" + i + "次執行!");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class MyRunnable implements Runnable {
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("線程2第" + i + "次執行!");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}複製代碼
運行結果:
線程2第0次執行!
線程1第0次執行!
線程1第1次執行!
線程2第1次執行!
線程1第2次執行!
線程2第2次執行!
線程1第3次執行!
線程2第3次執行!
線程1第4次執行!
線程2第4次執行!
線程1第5次執行!
線程2第5次執行!
線程1第6次執行!
線程2第6次執行!
線程1第7次執行!
線程2第7次執行!
線程1第8次執行!
線程2第8次執行!
線程1第9次執行!
線程2第9次執行!複製代碼
咱們能夠看到,每隔50ms 打印一次,優先級高的線程1大機率先執行。
線程的讓步含義就是使當前運行着的線程讓出 CPU 資源,可是給誰不知道,只是讓出,線程回到可執行狀態。
線程讓步使用的是靜態方法 Thread.yield(),用法和 sleep 同樣,做用的是當前執行線程。
public class Test {
public static void main(String[] args) {
Thread t1 = new MyThread1();
Thread t2 = new Thread(new MyRunnable());
t2.start();
t1.start();
}
}
class MyThread1 extends Thread {
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("線程1第" + i + "次執行!");
}
}
}
class MyRunnable implements Runnable {
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("線程2第" + i + "次執行!");
Thread.yield();
}
}
}複製代碼
運行結果:
線程1第0次執行!
線程2第0次執行!
線程1第1次執行!
線程2第1次執行!
線程1第2次執行!
線程1第3次執行!
線程1第4次執行!
線程1第5次執行!
線程1第6次執行!
線程1第7次執行!
線程1第8次執行!
線程1第9次執行!
線程2第2次執行!
線程2第3次執行!
線程2第4次執行!
線程2第5次執行!
線程2第6次執行!
線程2第7次執行!
線程2第8次執行!
線程2第9次執行!複製代碼
線程的合併的含義就是將幾個並行線程的線程合併爲一個單線程,應用場景是當一個線程必須等待另外一個線程執行完畢才能執行,使用 join 方法。
public class Test {
public static void main(String[] args) {
Thread t1 = new MyThread1();
t1.start();
for (int i = 0; i < 10; i++) {
System.out.println("主線程第" + i + "次執行!");
if (i > 2) try {
//t1線程合併到主線程中,主線程中止執行過程,轉而執行t1線程,直到t1執行完畢後繼續。
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class MyThread1 extends Thread {
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("線程1第" + i + "次執行!");
}
}
}複製代碼
運行結果:
主線程第0次執行!
主線程第1次執行!
主線程第2次執行!
主線程第3次執行!
線程1第0次執行!
線程1第1次執行!
線程1第2次執行!
主線程第4次執行!
主線程第5次執行!
主線程第6次執行!
主線程第7次執行!
主線程第8次執行!
主線程第9次執行!複製代碼
不逼逼了,線程 join 只有第一次有效。這裏我也很懵逼,我覺得線程1第***
這句話的打印次數應該是(10-3)*3 次的。
這裏咱們來回顧一下上篇文章說的線程的基本知識,線程是死亡以後就不能從新啓動了對吧。咱們再來理解一下 join 的概念當一個線程必須等待另外一個線程執行完畢才能執行
,咱們在主線程中join 線程 t1,因此直到 t1執行完畢,才能再次執行主線程。當 i=4 的時候再次執行 t1.join()時,t1 線程已是處於死亡狀態,因此不會再次執行 run 方法。所以 t1線程裏面 run 方法的打印語句只執行了三次。爲了驗證咱們的猜測,我建議去閱讀如下源碼。
如下是 Java8 Thread#join() 方法的源碼。
public final void join() throws InterruptedException {
this.join(0L);
}
public final synchronized void join(long var1) throws InterruptedException {
long var3 = System.currentTimeMillis();
long var5 = 0L;
if(var1 < 0L) {
throw new IllegalArgumentException("timeout value is negative");
} else {
if(var1 == 0L) {
while(this.isAlive()) {
this.wait(0L);
}
} else {
while(this.isAlive()) {
long var7 = var1 - var5;
if(var7 <= 0L) {
break;
}
this.wait(var7);
var5 = System.currentTimeMillis() - var3;
}
}
}
}
public final native boolean isAlive();複製代碼
咱們能夠看到 t1調用 join 方法的時候調用了重載的方法,而且傳了參數0,而後關鍵來了while(this.isAlive())
條件一直知足的狀況下,調用了 this.wait(0)
,這裏的 this 至關於對象 t1。
咱們來思考一下,t1.wait()
究竟是哪一個線程須要 wait?給大家三秒鐘時間。
3...
2...
1...
好了,我直接說了,你們記住,t1只是個對象,這裏不能當成是 t1線程 wait,主線程裏面經過對象 t1做爲鎖,並調用了 wait 方法,實際上是主線程 wait 了。while 的判斷條件是線程 t1.isAlive(),注意,這裏是判斷線程 t1是否存活,若是存活,則主線程一直 wait(0),直到 t1 線程執行結束死亡。這樣能夠了解了吧,再來思考一下若是在 Android 主線程裏面調用 join 方法可能會形成什麼問題?
這個問題很簡單,我就不說答案了。
守護線程與普通線程寫法上基本沒啥區別,調用線程對象的方法 setDaemon(true),則能夠將其設置爲守護線程。
守護線程的使用狀況較少,但並不是無用,舉例來講,JVM 的垃圾回收、內存管理等線程都是守護線程。還有就是在作數據庫應用的時候,使用數據庫鏈接池,鏈接池自己也包含着不少後臺現場,監控鏈接個數、超時時間、狀態等等。
將該線程標記爲守護線程或用戶線程。當正在運行的線程都是守護線程時,Java虛擬機退出。該方法必須在啓動線程前調用。
public class ThreadDaemon {
public static void main(String[] args) {
Thread t1 = new MyCommon();
Thread t2 = new Thread(new MyDaemon());
t2.setDaemon(true); //設置爲守護線程
t2.start();
t1.start();
}
}
class MyCommon extends Thread {
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("線程1第" + i + "次執行!"+"——————活着線程數量:"+Thread.currentThread().getThreadGroup().activeCount());
try {
Thread.sleep(7);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class MyDaemon implements Runnable {
public void run() {
for (long i = 0; i < 9999999L; i++) {
System.out.println("後臺線程第" + i + "次執行!");
try {
Thread.sleep(7);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}複製代碼
啥也別說了,來看結果吧:
後臺線程第0次執行!
線程1第0次執行!——————活着線程數量:4
後臺線程第1次執行!
線程1第1次執行!——————活着線程數量:4
後臺線程第2次執行!
線程1第2次執行!——————活着線程數量:4
後臺線程第3次執行!
線程1第3次執行!——————活着線程數量:4
後臺線程第4次執行!
線程1第4次執行!——————活着線程數量:4
後臺線程第5次執行!複製代碼
從上面的結果咱們能夠看出,前臺線程是包裝執行完畢的,後臺線程尚未執行完畢就退出了。也就是說除了守護線程覺得的其餘線程執行完以後,守護線程也就結束了。
而後,咱們來看看,爲何活着的線程數量會是4,明明只開了兩個子線程呀,加上 main 線程也才三個,那再加一個垃圾回收線程吧哈哈哈哈。
這個問題也是我在學習過程當中困擾了好久的問題。以前糾結的是,main 線程執行完了,若是還有子線程在運行。那麼 main 線程究竟是先結束仍是等待子線程執行結束以後再結束?main 線程結束是否是表明程序退出?
而後我就 Debug 線程池裏面全部的線程,發現裏面有一個叫 DestoryJavaVM 的線程,而後我也不知道這是個什麼東西,遂問了一下度娘,度娘告訴我~
DestroyJavaVM:main執行完後調用JNI中的jni_DestroyJavaVM()方法喚起DestroyJavaVM線程。 JVM在Jboss服務器啓動以後,就會喚起DestroyJavaVM線程,處於等待狀態,等待其它線程(java線程和native線程)退出時通知它卸載JVM。線程退出時,都會判斷本身當前是不是整個JVM中最後一個非deamon線程,若是是,則通知DestroyJavaVM線程卸載JVM
大概就是醬紫吧,4個線程分別是兩個我手動開的子線程,一個DestroyJavaVM ,還有一個大概是垃圾回收線程吧,哈哈哈哈,若是不對,請務必拍磚~
上一篇已經就同步問題作了詳細的講解。
對於多線程來講,無論任何編程語言,生產者消費者模型都是最經典的。這裏咱們拿一個生產者消費者模型來深刻學習吧~
實際上,應該是「生產者-消費者-倉儲」模型,離開了倉儲,生產者消費者模型就顯得沒有說服力。
對於此模型,應該明確如下幾點:
此模型將要的知識點,咱們上面都學過了,直接擼代碼吧~
public class Model {
public static void main(String[] args) {
Godown godown = new Godown(30);
Consumer c1 = new Consumer(50, godown);
Consumer c2 = new Consumer(20, godown);
Consumer c3 = new Consumer(30, godown);
Producer p1 = new Producer(10, godown);
Producer p2 = new Producer(10, godown);
Producer p3 = new Producer(10, godown);
Producer p4 = new Producer(10, godown);
Producer p5 = new Producer(10, godown);
Producer p6 = new Producer(10, godown);
Producer p7 = new Producer(40, godown);
c1.start();
c2.start();
c3.start();
p1.start();
p2.start();
p3.start();
p4.start();
p5.start();
p6.start();
p7.start();
}
}
/**
* 倉庫
*/
class Godown {
public static final int max_size = 100;//最大庫存量
public int curnum; //當前庫存量
Godown() {
}
Godown(int curnum) {
this.curnum = curnum;
}
/**
* 生產指定數量的產品
*
* @param neednum
*/
public synchronized void produce(int neednum) {
//測試是否須要生產
while (neednum + curnum > max_size) {
System.out.println("要生產的產品數量" + neednum + "超過剩餘庫存量" + (max_size - curnum) + ",暫時不能執行生產任務!");
try {
//當前的生產線程等待
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//知足生產條件,則進行生產,這裏簡單的更改當前庫存量
curnum += neednum;
System.out.println("已經生產了" + neednum + "個產品,現倉儲量爲" + curnum);
//喚醒在此對象監視器上等待的全部線程
notifyAll();
}
/**
* 消費指定數量的產品
*
* @param neednum
*/
public synchronized void consume(int neednum) {
//測試是否可消費
while (curnum < neednum) {
try {
//當前的生產線程等待
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//知足消費條件,則進行消費,這裏簡單的更改當前庫存量
curnum -= neednum;
System.out.println("已經消費了" + neednum + "個產品,現倉儲量爲" + curnum);
//喚醒在此對象監視器上等待的全部線程
notifyAll();
}
}
/**
* 生產者
*/
class Producer extends Thread {
private int neednum; //生產產品的數量
private Godown godown; //倉庫
Producer(int neednum, Godown godown) {
this.neednum = neednum;
this.godown = godown;
}
public void run() {
//生產指定數量的產品
godown.produce(neednum);
}
}
/**
* 消費者
*/
class Consumer extends Thread {
private int neednum; //生產產品的數量
private Godown godown; //倉庫
Consumer(int neednum, Godown godown) {
this.neednum = neednum;
this.godown = godown;
}
public void run() {
//消費指定數量的產品
godown.consume(neednum);
}
}
已經消費了20個產品,現倉儲量爲10
已經生產了10個產品,現倉儲量爲20
已經生產了10個產品,現倉儲量爲30
已經生產了10個產品,現倉儲量爲40
已經生產了10個產品,現倉儲量爲50
已經消費了30個產品,現倉儲量爲20
已經生產了40個產品,現倉儲量爲60
已經生產了10個產品,現倉儲量爲70
已經消費了50個產品,現倉儲量爲20
已經生產了10個產品,現倉儲量爲30複製代碼
在本例中,要說明的是當發現不能知足生產者或消費條件的時候,調用對象的 wait 方法,wait 方法的做用是釋放當前線程的所得到的鎖,並調用對象的 notifyAll()方法,通知(喚醒)該對象上其餘等待的線程,使其繼續執行。這樣,整個生產者、消費者線程得以正確的協做執行。
Java 語言包含兩種內在同步機制:同步塊(方法)和 volatile 變量。這兩種機制的提出都是爲了實現代碼線程的安全性。其中 volatile 變量的同步性較差(但有時它更簡單而且開銷更地),而且其使用也容易出錯。
首先考慮一個問題,爲何變量須要volatile來修飾呢?
要搞清楚這個問題,首先應該明白計算機內部都作什麼了。好比作了一個i++操做,計算機內部作了三次處理:讀取-修改-寫入。
一樣,對於一個long型數據,作了個賦值操做,在32系統下須要通過兩步才能完成,先修改低32位,而後修改高32位。
假想一下,當將以上的操做放到一個多線程環境下操做時候,有可能出現的問題,是這些步驟執行了一部分,而另一個線程就已經引用了變量值,這樣就致使了讀取髒數據的問題。
經過這個設想,就不難理解volatile關鍵字了。
更多的內容,請參考《Java理論與實踐:正確使用 Volatile 變量》一文,寫得很好。
這兩天在逛 github 的時候無心發現了這個項目LeetCode 算法與 java 解決方案,天天上班以前刷一個算法題,真的巨爽,強烈推薦想打好基礎去大廠的小夥伴們一塊兒刷題。