我們經常用的okhttp和rxjava等,都是基於線程進行封裝,我們從java最基礎上了解線程對於以後是有幫助的,那麼直接進入主題
相關概念
在說線程之前,我們先了解一下進程。
什麼是進程
我們平日裏打開的微信、簡書App,都是一個進程。
什麼是線程
線程是比進程更小的執行單位。一個程序只可以有一個進程,但這個進程可以包含多個線程
什麼是多線程
這些線程可以同時存在,同時運行,一個進程可能包含多個同時執行的線程
併發和並行
併發:併發是指一個處理器同時處理多個任務
並行:並行是指多個處理器或者是多核的處理器同時處理多個不同的任務
打比方:併發是一個人同時喫三個包子,而並行是三個人同時喫三個包子
什麼是線程池
創建並銷燬線程的過程勢必會消耗內存,如果創建多個線程對於Java來說是不合適的,Java的內存資源是極其寶貴的,所有就有了這個線程池重複利用線程
1. 線程
自定義Thread
/**
* 自定義線程類
* @author zhongjh
* @date 2021/5/7
*/
public class MyThread extends Thread {
private static final int COUNT = 10;
/**
* 線程名稱
*/
private final String threadName;
public MyThread(String name) {
this.threadName = name;
}
@Override
public void run() {
for (int i = 0; i < COUNT; i++) {
System.out.println(threadName + ": " + i);
}
}
}
// 並行執行多個線程
MyThread myThread1 = new MyThread("線程1");
MyThread myThread2 = new MyThread("線程2");
myThread1.start();
myThread2.start();
打印日誌
實現Runnable接口
/**
* @author zhongjh
* @date 2021/5/7
*/
public class MyThreadImpl implements Runnable {
private static final int COUNT = 10;
/**
* 線程名稱
*/
private final String threadName;
public MyThreadImpl(String name) {
this.threadName = name;
}
@Override
public void run() {
for (int i = 0; i < COUNT; i++) {
System.out.println(threadName + ": " + i);
}
}
}
// 並行執行多個線程
MyThreadImpl myThreadImpl1 = new MyThreadImpl("線程1");
MyThreadImpl myThreadImpl2 = new MyThreadImpl("線程2");
Thread myThreadOne = new Thread(myThreadImpl1);
Thread myThreadTwo = new Thread(myThreadImpl2);
myThreadOne.start();
myThreadTwo.start();
打印日誌的內容跟繼承Thread是一樣的,Thread的源碼也是實現Runnable接口,兩者區別就是接口跟繼承的區別,在很多場景中接口比繼承靈活多了,當然,這是接口繼承的另一篇文章了。
線程流程
創建
new Thread()創建線程後,此時已經有了相應的內存空間和其他資源。
準備
調用線程的start()方法後,線程將進入線程隊列排隊,等待 CPU 服務,此時的線程已經具備了運行條件。
運行
當就緒狀態被調用並獲得處理器資源時,線程就進入了運行狀態。此時該線程自動它的 run() 方法。
阻塞
線程在運行過程中,如果人爲調用sleep(),suspend(),wait() 等方法或者別的因素,線程將進入阻塞狀態,發生阻塞時線程不能進入排隊隊列,只有當引起阻塞的原因被消除後,線程纔可以轉入就緒狀態。
運行
線程調用 stop() 方法時或 run() 方法執行結束後,即處於死亡狀態。處於死亡狀態的線程將不會有繼續運行的能力。
定義線程名稱
Thread.currentThread().getName(); // 取得當前線程的名稱
也可以通過new Thread(Runnable, "線程1");這種方式自定義賦值名稱
join
join方法的功能就是使異步執行的線程變成同步執行。
平常的調用線程實例的start方法後,這個方法會立即返回,如果後面的代碼想得到這個線程返回的值才能計算,那麼就必須使用join方法。
public class MyThreadJoin extends Thread {
int m = (int) (Math.random() * 10000);
@Override
public void run() {
try {
System.out.println("我在子線程中會隨機睡上0-9秒,時間爲="+m);
Thread.sleep(m);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* join方法示例
*/
private void testJoin() {
MyThreadJoin myThread =new MyThreadJoin();
myThread.start();
try {
myThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("正常情況下肯定是我先執行完,但是加入join後,main主線程會等待子線程執行完畢後才執行");
}
打印日誌,可以看到6秒後才顯示
sleep
線程常用方法之一,sleep(xx毫秒),線程的休眠。顧名思義,暫停線程xx毫秒之後繼續執行,直接看示例代碼
public class MyThreadSleep extends Thread {
private static final int COUNT = 3;
@Override
public void run() {
for (int i = 0; i < COUNT; i++) {
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
/**
* Sleep示例
*/
private void testSleep() {
MyThreadSleep myThread1 = new MyThreadSleep();
myThread1.start();
}
打印日誌,可以看到相隔1秒才顯示一句日誌
sleep還有一種寫法,Thread.sleep(long millis),這是針對所有線程的睡眠
yield
yield()會禮讓給相同優先級的或者是優先級更高的線程執行,yield()這個方法只是把線程的狀態打回準備狀態,他會繼續跑起來,可以看到代碼例子有個停住1秒的,可以嘗試把1秒暫停看看打印出來的文字
/**
* Yield示例
*/
private void testYield() {
MyThreadYield myThread1 = new MyThreadYield("線程一");
MyThreadYield myThread2 = new MyThreadYield("線程二");
myThread1.start();
myThread2.start();
}
/**
* 禮讓線程
* @author zhongjh
* @date 2021/5/14
*/
public class MyThreadYield extends Thread {
public MyThreadYield(String name) {
super(name);
}
@Override
public synchronized void run() {
for (int i = 0; i < 5; i++) {
System.out.println(getName() + "在運行,i的值爲:" + i + " 優先級爲:" + getPriority());
if (i == 2) {
System.out.println(getName() + "禮讓");
Thread.yield();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
打印文字
synchronized
在更深入的講解線程其他機制前,我們先講另一個關鍵字,synchronized。
這是一個同步關鍵字,不管多少個線程調用該關鍵字修飾的方法,都是一個一個的按照順序執行完。假設我們多個線程(人)買火車票
private int ticket = 10;
/**
* Synchronized購買火車票的示例
*/
private void testSynchronized() {
for (int i = 0; i < 10; i++) {
new Thread() {
@Override
public void run() {
// 買票
sellTicket();
}
}.start();
}
}
/**
* 減少票,同步synchronized
*/
public synchronized void sellTicket() {
ticket--;
System.out.println("剩餘的票數:" + ticket);
if (ticket == 0) {
// 重新填充票數用於測試
ticket = 10;
}
}
打印日誌,可以看到票數順序減少,如果去掉synchronized,可以發現亂序的
synchronized 對象鎖和類鎖
不同對象之間的對象鎖是互不影響的,而類鎖只有一個。但是同時對象鎖和類鎖又不互不影響,接着會通過代碼分別加深鎖的印象
首先創建一個實體類,包含了對象鎖和類鎖的方法
public class SynchronizedEntity {
private int ticket = 10;
/**
* 同步方法,對象鎖
*/
public synchronized void syncMethod() {
for (int i = 0; i < 1000; i++) {
if (ticket > 0) {
ticket--;
System.out.println(Thread.currentThread().getName() + "剩餘的票數:" + ticket);
}
}
}
/**
* 同步塊,對象鎖
*/
public void syncThis() {
synchronized (this) {
for (int i = 0; i < 1000; i++) {
if (ticket > 0) {
ticket--;
System.out.println(Thread.currentThread().getName() + "剩餘的票數:" + ticket);
}
}
}
}
/**
* 同步class對象,類鎖
*/
public void syncClassMethod() {
synchronized (SynchronizedEntity.class) {
for (int i = 0; i < 50; i++) {
if (ticket > 0) {
ticket--;
System.out.println(Thread.currentThread().getName() + "剩餘的票數:" + ticket);
}
}
}
}
/**
* 同步靜態方法,類鎖
*/
public static synchronized void syncStaticMethod(){
// 暫不演示該方法
}
}
多個線程調用同一個對象鎖
/**
* 多個線程調用同一個對象鎖
*/
private void testSynchronized2() {
final SynchronizedEntity synchronizedEntity = new SynchronizedEntity();
// 線程一
new Thread() {
@Override
public void run() {
synchronizedEntity.syncMethod();
}
}.start();
// 線程二
new Thread() {
@Override
public void run() {
synchronizedEntity.syncThis();
}
}.start();
}
打印日誌可以看到有效的順序執行
兩個線程分別調用不同對象鎖
/**
* 兩個線程分別調用不同對象鎖
*/
private void testSynchronized3() {
final SynchronizedEntity synchronizedEntity1 = new SynchronizedEntity();
final SynchronizedEntity synchronizedEntity2 = new SynchronizedEntity();
// 線程一
new Thread() {
@Override
public void run() {
synchronizedEntity1.syncMethod();
}
}.start();
// 線程二
new Thread() {
@Override
public void run() {
synchronizedEntity2.syncMethod();
}
}.start();
}
打印日誌可以看到票數順序亂了
兩個線程分別調用對象鎖、類鎖
/**
* 兩個線程分別調用對象鎖、類鎖
*/
private void testSynchronized4() {
final SynchronizedEntity synchronizedDemo = new SynchronizedEntity();
// 線程一
new Thread() {
@Override
public void run() {
synchronizedDemo.syncMethod();
}
}.start();
// 線程二
new Thread() {
@Override
public void run() {
synchronizedDemo.syncClassMethod();
}
}.start();
}
打印日誌如圖,他們互不影響,所以也是線程不安全的
總結:不同對象之間的對象鎖是互不影響的,而類鎖只有一個。但是同時對象鎖和類鎖又不互不影響
wait()、notify()、notifyAll()
wait、notify、notifyAll都必須在synchronized中執行,否則會拋出異常。所以爲什麼會先講synchronized
notify跟notifyAll區別是notify會喚醒等待喚醒隊列中
的第一個線程,而notifyAll()方法則是喚醒整個喚醒隊列中
的所有線程
直接上代碼,先創建一個線程,該線程是sleep自身2秒後再喚醒所有線程
public class MyThreadWait extends Thread {
private final Object lockObject;
public MyThreadWait(Object lockObject) {
this.lockObject = lockObject;
}
@Override
public void run() {
synchronized (lockObject) {
try {
// 子線程等待了2秒鐘後喚醒lockObject鎖
sleep(2000);
System.out.println("lockObject喚醒");
lockObject.notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* wait示例
*/
private void testWait() {
// 創建子線程
Thread thread = new MyThreadWait(lockObject);
thread.start();
long start = System.currentTimeMillis();
synchronized (lockObject) {
try {
System.out.println("lockObject等待");
lockObject.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("lockObject繼續 --> 等待的時間:" + (System.currentTimeMillis() - start));
}
}
wait()、notify()、notifyAll() 進階
接着是java經典題目,子線程循環2次,接着主線程循環3次,接着又回到子線程循環2次,接着再回到主線程又循環3次,如此循環10次
/**
* 鎖對象
*/
private final Object lock = new Object();
/**
* 是否執行子線程標誌位
*/
boolean beShouldSub = true;
/**
* wait和notify示例
* 子線程循環2次,接着主線程循環3次,接着又回到子線程循環2次,接着再回到主線程又循環3次,如此循環10次
*/
private void testWaitNotify() {
// 子線程
new Thread() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
testWaitNotifyThread();
}
}
}.start();
// 主線程
for (int i = 0; i < 10; i++) {
testWaitNotifyMain();
}
}
/**
* 子線程循環兩次
*/
private void testWaitNotifyThread() {
synchronized (lock) {
if (!beShouldSub) {
// 等待
try {
Log.d("testWaitNotify","子線程等待lock");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for (int j = 0; j < 2; j++) {
Log.d("testWaitNotify","子循環第" + (j + 1) + "次");
}
// 子線程執行完畢,子線程標誌位設爲false
beShouldSub = false;
// 喚醒
Log.d("testWaitNotify","子線程喚醒lock");
lock.notify();
}
}
/**
* 主線程循環3次
*/
private void testWaitNotifyMain() {
synchronized (lock) {
if (beShouldSub) {
// 等待
try {
Log.d("testWaitNotify","主線程等待lock");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for (int j = 0; j < 3; j++) {
Log.d("testWaitNotify","主循環第" + (j + 1) + "次");
}
// 主線程執行完畢,子線程標誌位設爲true
beShouldSub = true;
// 喚醒
Log.d("testWaitNotify","主線程喚醒lock");
lock.notify();
}
}
volatile進階
一旦一個共享變量(類的成員變量、類的靜態成員變量)被volatile修飾之後,那麼就具備了兩層語義:
1)保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。
2)禁止進行指令重排序。
該鏈接超級詳細的講解了volatile
Java併發編程:volatile關鍵字解析 - Matrix海子 - 博客園 (cnblogs.com)
在DEMO中我也詳細的寫了一個錯誤示範例子
/**
* 這是Volatile的一個錯誤示範
* 事實上運行它會發現每次運行結果都不一致,都是一個小於10000的數字。
* 可見性只能保證每次讀取的是最新的值,但是volatile沒辦法保證對變量的操作的原子性
* 自增操作是不具備原子性的,它包括讀取變量的原始值、進行加1操作、寫入工作內存
* 那麼就是說自增操作的三個子操作可能會分割開執行,就有可能導致下面這種情況出現:
* 假如某個時刻變量inc的值爲10
* 線程1對變量進行自增操作,線程1先讀取了變量inc的原始值,然後線程1被阻塞了
* 然後線程2對變量進行自增操作,線程2也去讀取變量inc的原始值,增加1變成11,並把11寫入工作內存,最後寫入主存
* 然後線程1接着進行加1操作,由於已經讀取了inc的值,注意此時在線程1的工作內存中inc的值仍然爲10,所以線程1對inc進行加1操作後inc的值爲11,然後將11寫入工作內存,最後寫入主存。
* 那麼兩個線程分別進行了一次自增操作後,inc只增加了1。
*/
private void testVolatileNo() {
final Test test = new Test();
for (int i = 0; i < 10; i++) {
int finalI = i;
new Thread() {
@Override
public void run() {
for (int j = 0; j < 1000; j++) {
test.increase();
}
if (finalI ==9) {
System.out.println("test.inc: " + test.inc);
}
}
}.start();
}
}
參考學習文章:
媽媽再也不用擔心你不會使用線程池了(ThreadUtils) - 簡書 (jianshu.com)
Android-多線程 - 簡書 (jianshu.com)
深入理解線程和線程池(圖文詳解)weixin_40271838的博客-CSDN博客線程池
安卓Thread的運用 Thread.join()_ruiruiddd的博客-CSDN博客
Android進階——多線程系列之wait、notify、sleep、join、yield、synchronized關鍵字、ReentrantLock鎖_點擊置頂文章查看博客目錄(全站式導航)-CSDN博客