Java基礎之多執行緒
多執行緒
1、簡介
多執行緒,簡而言之就是一段時間內同時幹多件事(多條執行緒執行),而執行緒就是一條工作線,比如做飯,一個人幹一系列有先後順序的事情就是一條執行緒(比如洗米、洗鍋、煮飯),多執行緒可以是多個人在同一個時間做多件可以同時進行的事情,比如一個人在煮飯的同時另一個人在炒菜。同時多執行緒也可以表示一個人在一段時間內幹多件事情,比如這個人同時起兩個鍋炒兩個菜,這個菜炒一會,那個菜炒一會,讓兩個菜都不會糊(正常工作)。
而在電腦中,多執行緒可以理解成同一時間執行多個程式,每個程式可以看成一個執行緒,當然有些程式也可以看成多執行緒,比如瀏覽器同時開啟兩個視訊。
2、一個執行緒的生命週期
-
新建狀態:
使用 new 關鍵字和 Thread 類或其子類建立一個執行緒物件後,該執行緒物件就處於新建狀態。它保持這個狀態直到程式 start() 這個執行緒。
-
就緒狀態:
當執行緒物件呼叫了start()方法之後,該執行緒就進入就緒狀態。就緒狀態的執行緒處於就緒佇列中,要等待JVM裡執行緒排程器的排程。
-
執行狀態:
如果就緒狀態的執行緒獲取 CPU 資源,就可以執行 run(),此時執行緒便處於執行狀態。處於執行狀態的執行緒最為複雜,它可以變為阻塞狀態、就緒狀態和死亡狀態。
-
阻塞狀態:
如果一個執行緒執行了sleep(睡眠)、suspend(掛起)等方法,失去所佔用資源之後,該執行緒就從執行狀態進入阻塞狀態。在睡眠時間已到或獲得裝置資源後可以重新進入就緒狀態。可以分為三種:
- 等待阻塞:執行狀態中的執行緒執行 wait() 方法,使執行緒進入到等待阻塞狀態。
- 同步阻塞:執行緒在獲取 synchronized 同步鎖失敗(因為同步鎖被其他執行緒佔用)。
- 其他阻塞:通過呼叫執行緒的 sleep() 或 join() 發出了 I/O 請求時,執行緒就會進入到阻塞狀態。當sleep() 狀態超時,join() 等待執行緒終止或超時,或者 I/O 處理完畢,執行緒重新轉入就緒狀態。
-
死亡狀態:
一個執行狀態的執行緒完成任務或者其他終止條件發生時,該執行緒就切換到終止狀態。
3、建立一個執行緒
方式一:繼承Thread類
// 1.建立一個繼承於Thread類的子類 class MyThread extends Thread{ // 2.重寫Thread類的run()方法 @Override public void run() { // 重寫方法,遍歷1000以內的偶數,遍歷1000效果明顯點 for (int i = 0; i < 1000; i+=2) { System.out.println(i); } } } public class Multithreading { public static void main(String[] args) { // 3.建立Thread類的子類例項物件 MyThread thread1 = new MyThread(); // 4.呼叫物件的start()方法(啟動當前執行緒、呼叫執行緒的run()方法,) thread1.start(); // 如果只是執行run()方法,並不會啟動一個新的執行緒 thread1.run(); Thread.sleep(10); System.out.println("============main"); } }
多執行緒解析
程式先開啟了第一條執行緒----用於執行Multithreading的main方法,當執行到MyThread thread1 = new MyThread();
時會建立第二條執行緒,但是不會開啟這條執行緒,而當第一條執行緒執行到thread1.start();
時才會開啟第二條執行緒,並呼叫前面編寫好的run()
方法,注意:thread1.start();
這裡有兩個操作,第一個是啟動一個新的執行緒,第二個是執行這個執行緒的run()
方法,同時第一條執行緒並不會暫停,而是和第二條執行緒同時執行的。
案例:建立兩個分執行緒,一個遍歷偶數,一個遍歷奇數
class Thread1 extends Thread{
@Override
public void run() {
for (int i = 0; i < 1000; i+=2) {
System.out.println(i);
}
}
}
class Thread2 extends Thread{
@Override
public void run() {
for (int i = 1; i < 1000; i+=2) {
System.out.println(i);
}
}
}
public class Multithreading {
public static void main(String[] args) throws InterruptedException {
//=================================1.普通寫法=====================
Thread1 thread1 = new Thread1();
Thread2 thread2 = new Thread2();
thread1.start();
thread2.start();
//=================================2.匿名寫法=====================
new Thread(){
@Override
public void run() {
for (int i = 0; i < 1000; i+=2) {
System.out.println(i);
}
}
}.start();
new Thread(){
@Override
public void run() {
for (int i = 1; i < 1000; i+=2) {
System.out.println(i);
}
}
}.start();
}
}
共享資料
class TicketThread1 extends Thread{
public static int ticket = 100;
@Override
public void run() {
while (true){
if (ticket>0){
System.out.println(ticket);
ticket--;
}
else{
break;
}
}
}
}
public class Multithreading {
public static void main(String[] args) throws InterruptedException {
TicketThread1 thread3 = new TicketThread1();
TicketThread1 thread4 = new TicketThread1();
TicketThread1 thread5 = new TicketThread1();
thread3.start();
thread4.start();
thread5.start();
}
}
方式二:實現Runnable介面(更常用,因為方式一是通過繼承來實現的,而一個類只能繼承一個類,同時在共享資料上面也有優勢)
// 1. 建立一個實現了了Runnable介面的類
class RunnableThread implements Runnable{
// 2. 實現Runnable的抽象方法:run()
public void run() {
for (int i = 0; i < 100; i+=2) {
System.out.println(i);
}
}
}
public class Multithreading2 {
public static void main(String[] args) {
// 3. 建立實現類的物件
RunnableThread runnableThread = new RunnableThread();
// 4. 將此物件作為引數傳遞到Thread類的構造器中,建立Thread類的物件
Thread thread1 = new Thread(runnableThread);
// 5.通過Thread類的物件呼叫start()
thread1.start();
}
}
原始碼解析
public class Thread implements Runnable {
// 方式二在Thread類中呼叫的建構函式
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
// 這是init方法中進行的操作
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals){
//......
this.target = target;
//......
}
// 這是Thread類中的start()方法
public synchronized void start() {
//......
// 這個方法會啟動新執行緒並呼叫run()方法
start0();
//......
}
// 這是Thread類中的run()方法
@Override
public void run() {
if (target != null) {
target.run();
}
}
}
// 在方式1中,我們通過繼承Thread類並重寫run()方法,實現了當我們啟動新執行緒並執行時並呼叫時(thread1.start();)
// 將會呼叫我們重寫的run()方法
// 而在方式二中,我們使用了`public Thread(Runnable target)`這個建構函式,傳入Runnable target引數
// 而在啟動新執行緒時,將會呼叫Thread類中的run()方法,即target.run();也就是我們自己寫的實現了Runnable介面的類的run方法。
共享資料
class TicketThread implements Runnable{
private int ticket = 100;
public void run() {
while (ticket>0){
System.out.println(ticket);
ticket--;
}
}
}
public class Multithreading2 {
public static void main(String[] args) {
TicketThread ticketThread = new TicketThread();
Thread thread2 = new Thread(ticketThread);
Thread thread3 = new Thread(ticketThread);
Thread thread4 = new Thread(ticketThread);
thread2.start();
thread3.start();
thread4.start();
}
}
// 在方式二中,我們不需要在實現了Runnable介面的類中將多個執行緒的共享資料(ticket)定義為static實現資料共享
// 我們只需要在多個執行緒中使用同一個物件進行執行緒初始化即可
// 而在方式一中必須要將共享資料定義為static才行
方式三:實現Callable介面---JDK 5.0新增方式
- 與Runnable類似但功能更加強大
- 需要實現call()方法而不是run()方法
- 相比於run()方法,可以有返回值
- call()可以丟擲異常
- 支援泛型的返回值
- 需要藉助FutureTask類,比如獲取返回結果
// 1. 建立一個實現了Callable介面的實現類
class CallableThread implements Callable<Object>{
// 2. 實現Callable的call()方法,將執行緒需要執行的操作宣告在這裡,相當於原來Runnable的run方法,實際上就是run方法會呼叫call方法
// 這裡的返回值與Callable<Object>中指定的型別對應
public Object call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100; i+=2) {
System.out.println(i);
sum+=i;
}
// 自動裝箱,實際上這裡返回的是Integer
return sum;
}
}
public class Multithreading5 {
public static void main(String[] args) {
// 在這裡寫了一個call方法
// 3. 建立Callable介面實現類的物件
CallableThread callableThread = new CallableThread();
// 這個類實現了Runnable介面,並實現了run()方法,在呼叫run()方法時,實際上去呼叫了callableThread.call()方法
// 同時將呼叫結果存放到outcome屬性中,以便第六步中獲取返回值
// 4. 將Callable介面實現類的物件作為引數傳遞到FutureTask的構造器中,建立FutureTask物件
FutureTask<Object> futureTask = new FutureTask<Object>(callableThread);
// 新建執行緒並啟動
// 5. 將FutureTask物件作為引數傳遞到Thread類的構造器中,建立Thread類的物件,並呼叫start()方法
Thread thread = new Thread(futureTask);
thread.start();
try {
// 6. 獲取Callable介面實現類物件的call()方法的返回值
Object sum = futureTask.get();
System.out.println(sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
方式四:使用執行緒池---JDK 5.0新增方式
背景:經常建立和銷燬、使用量特別大的資源,比如併發情況下的執行緒,對效能影響很大。
思路:提前建立好多個執行緒,放入執行緒池中,使用時直接獲取,使用完放回執行緒池中。可以避免頻繁建立銷燬、實現重複利用。類似生活中的公共交通工具。
好處:
- 提高響應速度(減少了建立新執行緒的時間)
- 降低資源消耗(重複利用執行緒池中執行緒,不需要每次都建立)
- 便於執行緒管理
- corePoolSize:核心池的大小
- maximumPoolSize:最大執行緒數
- keepAliveTime:執行緒沒有任務時最多保持長時間後終止
class ThreadPool1 implements Runnable{
public void run() {
for (int i = 0; i < 100; i+=2) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
class ThreadPool2 implements Runnable{
public void run() {
for (int i = 1; i < 100; i+=2) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
public class Multithreading6 {
public static void main(String[] args) {
// 1. 建立一個可重用固定執行緒池數的執行緒池
ExecutorService service = Executors.newFixedThreadPool(10);
// 2. 執行指定的執行緒池操作。需要提供實現了Runnable介面實現類的物件,這裡使用了設定模式的命令模式
// 適合使用Runnable
service.execute(new ThreadPool1());
service.execute(new ThreadPool2());
// 2. 執行指定的執行緒池操作。需要提供實現了Callable介面實現類的物件
// 適合適用於Callable
// service.submit(Callable callable);
// 關閉連線池
service.shutdown();
}
}
4、Thread類的一些重要方法
序號 | 方法 | 描述 |
---|---|---|
1 | public void start() | 使該執行緒開始執行;Java 虛擬機器呼叫該執行緒的 run 方法。 |
2 | public void run() | 如果該執行緒是使用獨立的 Runnable 執行物件構造的,則呼叫該 Runnable 物件的 run 方法;否則,該方法不執行任何操作並返回。 |
3 | public final void setName(String name) | 改變執行緒名稱,使之與引數 name 相同。 |
4 | public final String getName() | 獲取執行緒名稱。 |
5 | public final void setPriority(int newPriority) | 更改執行緒的優先順序。 |
6 | public final int getPriority() | 獲取執行緒的優先順序。 |
7 | public final void setDaemon(boolean on) | 將該執行緒標記為守護執行緒或使用者執行緒。 |
8 | public final void join(long millisec) | 等待該執行緒終止的時間最長為 millis 毫秒。 |
9 | public void interrupt() | 中斷執行緒。 |
10 | public final boolean isAlive() | 測試執行緒是否處於活動狀態。 |
11 | public static void yield() | 暫停當前正在執行的執行緒物件,並執行其他執行緒。 |
12 | public static void sleep(long millisec) | 在指定的毫秒數內讓當前正在執行的執行緒休眠(暫停執行),此操作受到系統計時器和排程程式精度和準確性的影響。 |
13 | public static boolean holdsLock(Object x) | 當且僅當當前執行緒在指定的物件上保持監視器鎖時,才返回 true。 |
14 | public static Thread currentThread() | 返回對當前正在執行的執行緒物件的引用。 |
15 | public static void dumpStack() | 將當前執行緒的堆疊跟蹤列印至標準錯誤流。 |
5、執行緒優先順序
每一個 Java 執行緒都有一個優先順序,這樣有助於作業系統確定執行緒的排程順序。
Java 執行緒的優先順序是一個整數,其取值範圍是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。
預設情況下,每一個執行緒都會分配一個優先順序 NORM_PRIORITY(5)。
具有較高優先順序的執行緒對程式更重要,並且應該在低優先順序的執行緒之前分配處理器資源。但是,執行緒優先順序不能保證執行緒執行的順序,而且非常依賴於平臺。簡而言之就是,如果有兩個執行緒,一個執行緒的優先順序是1,一個執行緒的優先順序是9,系統執行10s,可能會分給第一個執行緒1s的時間用於執行,分給第二個執行緒9s時間,這個具體分配是看平臺的。
6、執行緒的安全性(同步)
買票問題
class TicketThread implements Runnable{
private int ticket = 100;
public void run() {
while (ticket>0){
System.out.println(ticket);
ticket--;
}
}
}
public class Multithreading2 {
public static void main(String[] args) {
TicketThread ticketThread = new TicketThread();
Thread thread2 = new Thread(ticketThread);
Thread thread3 = new Thread(ticketThread);
Thread thread4 = new Thread(ticketThread);
thread2.start();
thread3.start();
thread4.start();
}
}
在上面的買票問題中,我們會遇到同一張票賣了多次,或者票超買(餘票到-1的情況),這是因為我們建立的三個執行緒是同時執行的。
試想一下,如果其中的多個執行緒同時執行到關鍵的語句
-
即第一個執行緒執行了語句
System.out.println(ticket);
-
但是還沒來得及執行
ticket--;
時 -
而其他執行緒也執行了語句
System.out.println(ticket);
-
這就會導致同一個ticket輸出兩次
-
結果同一張票賣了多次
-
或者當ticket=1時,第一個執行緒準備執行語句
ticket--;
-
在還沒來得及將ticket減到0,其他執行緒執行了語句
ticket>0
進入到了輸出語句 -
與此同時第一個執行緒恰好執行了
ticket--;
, -
然後第二個執行緒執行了
System.out.println(ticket);
-
結果執行緒二會列印0,而這個列印是違法的(超賣)。
鎖
這裡先想象一個場景,在進行多人排隊上廁所廁所,多個要上廁所的使用者需要看著門口的同一個標誌位,廁所中有人一種標識(紅燈亮),廁所中沒人一種標識(綠燈亮),當廁所沒人時,隊伍中的使用者才能去上廁所,否則只能在門口排隊,甚至時不時的要判斷一下標誌位是否改變。
鎖就相當於標誌位,而在java的物件中,每一個物件都有一個鎖標誌位資訊,用於標誌這個物件是否被鎖住,顯然我們需要所有的執行緒關注同一個鎖(不管這個鎖是否是本身的鎖),當鎖釋放時,就可以有執行緒去搶奪資源,反之執行緒只能在外面等待,等待佔用的執行緒釋放資源,因此鎖有時會和物件劃上等號。
// 鎖的模擬
class TicketThread2 implements Runnable{
private int ticket = 100;
// 鎖標誌位,true表示鎖開啟,可以被執行緒佔用
private boolean flag = true;
public void run() {
while (ticket>0) {
if (flag){
if (ticket>0){
flag=false;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+ticket);
ticket--;
flag=true;
}
else {
System.out.println(Thread.currentThread().getName()+":搶不到票");
}
}
else {
break;
}
}
}
}
public class Multithreading2 {
public static void main(String[] args) {
TicketThread ticketThread = new TicketThread2();
Thread thread2 = new Thread(ticketThread);
Thread thread3 = new Thread(ticketThread);
Thread thread4 = new Thread(ticketThread);
thread2.start();
thread3.start();
thread4.start();
}
}
執行緒同步方式一:同步程式碼塊
實現Runnable介面的多執行緒同步
synchronized(同步監視器){
// 需要被同步的程式碼
}
// 說明:1. 操作共享資料的程式碼,就是需要被同步的程式碼
// 2. 共享資料:多個執行緒共同操作的變數,比如ticket就是共享資料
// 3. 同步監視器,俗稱:鎖。任何一個類的物件,都可以充當鎖。
// 要求:多個執行緒必須要共用同一把鎖(同一個物件)
// 注意:下面這種方法只能避免重複賣,但不能解決超賣的問題,
// 因為第一個執行緒在sleep的時候,其他執行緒通過了while判斷,在第一個執行緒釋放時,二三個執行緒會先後執行ticket--操作
// 想要改善就在 synchronized內部加入一個票量判斷即可(將synchronized內部全部包圍)
class TicketThread2 implements Runnable{
private int ticket = 100;
public void run() {
while (ticket>0){
synchronized(this) {
if (ticket>0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 這裡也相當於操作了共享資料,與下面的程式碼實際上是一體的
System.out.println(Thread.currentThread().getName()+":"+ticket);
// 操作了共享資料
ticket--;
}
else {
break;
}
}
}
}
}
繼承Thread類的多執行緒同步
class TicketThread3 extends Thread{
private static int ticket = 100;
private static Object object = new Object();
@Override
public void run() {
while (ticket>0){
// 與實現Runnable介面不同,由於繼承Thread類後建立多執行緒是需要new多個TicketThread3類物件的
// 而不過將object設定為非靜態的屬性,將會導致每個執行緒使用自己的object,無法使用同一個鎖
// 因此需要將object設定為靜態的,或者這裡也可以將object替換成TicketThread3.class
// 繼承Thread類需要額外注意同步監視器的選擇
synchronized(object) {
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + ticket);
ticket--;
}
else {
break;
}
}
}
}
}
public class Multithreading2 {
public static void main(String[] args) {
TicketThread3 thread5 = new TicketThread3();
TicketThread3 thread6 = new TicketThread3();
TicketThread3 thread7 = new TicketThread3();
thread5.start();
thread6.start();
thread7.start();
}
}
執行緒同步方法二:同步方法
如果操作共享資料的程式碼完整的宣告在一個方法中,我們就可以將這個方法宣告為同步的。
實現Runnable介面的多執行緒同步
class TicketThread4 implements Runnable{
private int ticket = 100;
public void run() {
while (ticket > 0) {
show();
}
}
// 同步監視器(鎖)就是this
private synchronized void show(){
if(ticket>0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+ticket);
ticket--;
}
}
// 相當於
private void show(){
synchronized(this){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+ticket);
ticket--;
}
}
}
public class Multithreading2 {
public static void main(String[] args) {
TicketThread ticketThread = new TicketThread4();
Thread thread5 = new Thread(ticketThread);
Thread thread6 = new Thread(ticketThread);
Thread thread7 = new Thread(ticketThread);
thread5.start();
thread6.start();
thread7.start();
}
}
繼承Thread類的多執行緒同步
class TicketThread5 extends Thread{
private static int ticket = 100;
@Override
public void run() {
while (ticket>0){
show();
}
}
// 如果方法為非靜態的,那麼同步監視器會是this,那麼三個執行緒物件將使用三個鎖
// 如果設定為static,同步監視器將會是TicketThread5.class(唯一)
private static synchronized void show(){
if (ticket>0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+ticket);
ticket--;
}
}
}
public class Multithreading2 {
public static void main(String[] args) {
TicketThread5 thread11 = new TicketThread5();
TicketThread5 thread12 = new TicketThread5();
TicketThread5 thread13 = new TicketThread5();
thread11.start();
thread12.start();
thread13.start();
}
}
方式三:Lock鎖------JDK 5.0新增的執行緒同步機制
class LockThread implements Runnable{
private int ticket = 100;
// 1. 例項化ReentrantLock
private ReentrantLock lock = new ReentrantLock();
public void run() {
while (ticket>0){
try {
// 2. 呼叫鎖定方法lock()---加鎖
lock.lock();
if (ticket>0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"售賣第:"+ticket+"張票");
ticket--;
}
} finally {
// 3. 呼叫解鎖方法:unlock()----如果上面程式碼出現異常,也會呼叫解鎖
lock.unlock();
}
}
}
}
public class Multithreading3 {
public static void main(String[] args) {
LockThread lockThread = new LockThread();
Thread thread1 = new Thread(lockThread);
Thread thread2 = new Thread(lockThread);
Thread thread3 = new Thread(lockThread);
thread1.start();
thread2.start();
thread3.start();
}
}
synchronized和lock的區別:
synchronized在執行完相應的同步程式碼之後會自動釋放同步監視器(鎖),而Lock則需要手動地啟動同步(加鎖lock())以及結束同步(解鎖unlock())。
Lock只有程式碼塊鎖,而synchronized還有方法鎖。
使用Lock鎖,JVM將花費較少時間來排程執行緒,效能更好,並且具有更好的擴充套件性(提供更多的子類)。
建議的優先順序
Lock>同步程式碼塊>同步方法
7、執行緒間通訊(參考第2部分---執行緒的生命週期)
class CommunicationThread implements Runnable{
private int ticket = 100;
public void run() {
while (ticket>0){
synchronized (this){
// 隨機通知一個等待阻塞的執行緒去等待鎖的釋放,其他的執行緒依舊處於等待阻塞狀態(wait)
// notify();
// 通知其他所有的執行緒解除等待阻塞狀態,並在執行接下去的程式碼之前,去競爭物件鎖,決定哪個執行緒真正地執行程式碼
notifyAll();
if (ticket>0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"售賣第:"+ticket+"張票");
ticket--;
try {
// 讓本執行緒進入登臺阻塞狀態,等待其他執行緒喚醒本執行緒
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
else {
break;
}
}
}
}
}
public class Multithreading4 {
public static void main(String[] args) {
CommunicationThread communicationThread = new CommunicationThread();
Thread thread1 = new Thread(communicationThread);
Thread thread2 = new Thread(communicationThread);
Thread thread3 = new Thread(communicationThread);
thread1.start();
thread2.start();
thread3.start();
}
}
注意事項
-
notify()、notifyAll()、wait()必須寫在同步程式碼塊或者同步方法中
-
同時這三個方法的呼叫者必須是同步程式碼塊或者同步方法中的同步監視器(即都是this.notify()....../object.notify()......)
-
否則將會報java.lang.IllegalMonitorStateException錯
-
三個方法是定義在Object類中的