1. 程式人生 > 實用技巧 >10,7學習記錄(多執行緒)

10,7學習記錄(多執行緒)

多執行緒

  1. 建立多執行緒的方式一:繼承Thread類

    1. 自定義執行緒類繼承Thread類

    2. 重寫run()方法,編寫執行緒執行體

    3. 建立執行緒物件,呼叫start()開啟執行緒

  2. 建立多執行緒的方式二:實現Runable介面

    1. 建立一個實現了Runnable介面的類

    2. 實現類去實現Runnable中的抽象方法:run()

    3. 建立實現類的物件

    4. 將此物件作為引數傳遞到Thread類的構造器中,建立Thread類的物件

    5. 通過Thread類的物件呼叫start()

  3. 建立多執行緒的方式三:實現Callable介面

    1. 建立一個實現Callable的實現類

    2. 實現call方法,將此執行緒需要執行的操作宣告在call()方法中

    3. 建立Callable介面實現類的物件

    4. 將此Callable介面實現類的物件作為傳遞到FutureTask構造器中,建立FutureTask的物件

    5. 將FutureTask的物件作為引數傳遞到Thread類的構造器中,建立Thread物件,並呼叫start()

    6. 獲取Callable中call方s法的返回值

  4. 建立多執行緒的方式四:實現Callable介面

    1. 提供指定執行緒數量的執行緒池

    2. 執行指定的執行緒的操作。需要提供實現Runnable介面或Callable介面實現類的物件

    3. 關閉連線池

  • 注意:執行緒開啟不一定立即執行,由CPU排程執行

  • 注意:若要再啟動一個執行緒,不可以還讓已經start()的執行緒去執行。(會報ILLeaglThreadStateException異常) 我們需要重新建立一個執行緒的物件呼叫start()

  • 建立多執行緒的方式一:繼承Thread類

public class ThreadTeat extends Thread{
@Override
public void run() {
//run方法執行緒體
for (int i = 0; i < 20; i++) {
System.out.println("我在看程式碼*****"+i);
}
}
public static void main(String[] args) {
//主執行緒 main執行緒

//建立一個執行緒物件
ThreadTeat threadTeat = new ThreadTeat();
//呼叫start()方法開啟執行緒
threadTeat.start();
for (int i = 0; i < 200; i++) {
System.out.println("我在學習多執行緒*****"+i);
}
}
}
  • 建立多執行緒的方式二:實現Runable介面

public class ThreadTest {
public static void main(String[] args) {
//建立實現類的物件
MyThread myThread = new MyThread();
//將此物件作為引數傳遞到Thread類的構造器中,建立Thread類的物件
Thread t1 = new Thread(myThread);
//通過Thread類的物件呼叫start() 1、啟動執行緒 2、呼叫當前執行緒的run()--> 呼叫了Runnable型別的target的run()
t1.start();
//再啟動一個執行緒,遍歷100以內的偶數
Thread t2=new Thread(myThread);
t2.start();
}

}
//建立一個實現了Runnable介面的類
class MyThread implements Runnable{
//實現類去實現Runnable中的抽象方法:run()
@Override
public void run() {
for (int i = 0; i <=100; i++) {
if(i%2==0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
  • 例項1:建立三個視窗賣票,總票數為100張,使用繼承類方法實現

//建立三個視窗賣票,總票數為100張,使用繼承類方法實現
//目前存線上程的安全問題 (待解決)
public class WindowsTest {

public static void main(String[] args) {
Ticket ticket = new Ticket();
Ticket ticket1 = new Ticket();
Ticket ticket2 = new Ticket();
ticket.setName("視窗1");
ticket1.setName("視窗2");
ticket2.setName("視窗3");
ticket.start();
ticket1.start();
ticket2.start();
}
}

class Ticket extends Thread{
private static int ticket=100;
@Override
public void run() {
while (true){
if(ticket>0)
{
System.out.println(getName()+": 賣票,票號為:"+ticket);
ticket--;
}
else{
break;
}
}
}
}

  • 例項2:建立三個視窗賣票,總票數為100張,使用實現Runnable介面的方式

//建立三個視窗賣票,總票數為100張,使用實現Runnable介面的方式

//目前存線上程的安全問題 (待解決)
public class WindowsTestRunnable {
public static void main(String[] args) {
windows w1 = new windows();
Thread t1 = new Thread(w1);
Thread t2=new Thread(w1);
Thread t3=new Thread(w1);
t1.setName("視窗1");
t2.setName("視窗2");
t3.setName("視窗3");
t1.start();
t2.start();
t3.start();

}
}

class windows implements Runnable{
private int ticket=100;
@Override
public void run() {
while (true){
if(ticket>0)
{
System.out.println(Thread.currentThread().getName()+": 賣票,票號為:"+ticket);
ticket--;
}
else{
break;
}
}
}
}

  • 練習一:建立兩個分執行緒,其中一個執行緒遍歷100以內的偶數,另外一個遍歷100以內的奇數

public class ThreadDemo1 {

public static void main(String[] args) {
Mythread1 mythread1 = new Mythread1();
Mythread2 mythread2 = new Mythread2();
mythread1.start();
mythread2.start();

}
}
class Mythread1 extends Thread{
@Override
public void run() {
for (int i = 0; i <=100; i++) {
if(i%2==0)
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}

class Mythread2 extends Thread{
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if(i%2==1)
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
  • 練習二:建立兩個分執行緒,其中一個執行緒遍歷100以內的偶數,另外一個遍歷100以內的奇數(利用Thread類的匿名子類的方式)

public class ThreadDemo2 {
public static void main(String[] args) {
new Thread(){
@Override
public void run() {
for (int i = 0; i <=100; i++) {
if(i%2==0)
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}.start();

new Thread(){
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if(i%2==1)
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}.start();
}
}

比較建立執行緒的兩種方式

開發中:優先選擇:實現Runnable介面的方式

原因:1.實現的方式沒有類的單繼承性的侷限性

2.實現的方式更適合來處理多個執行緒有共享資料的情況

聯絡:public class Thread implements Runnable

相同點:兩種方式都需要重寫run(),將執行緒要執行的邏輯宣告再run()中。

目前兩種方式,要想啟動執行緒,都是呼叫的Thread類中的start()。

測試Thread中的常用方法:

  1. start():啟動當前執行緒;呼叫當前執行緒的run();

  2. run():通常需要重寫Thread類中的此方法,將建立的執行緒要執行的操作宣告在此方法中

  3. currentThread():靜態方法,返回執行當前程式碼的執行緒

  4. getName():獲取當前執行緒的名字

  5. setName():設定當前執行緒的名字

  6. yield():釋放當前cpu的執行權

  7. join():線上程a中呼叫執行緒b的join(),此時執行緒a就進入阻塞狀態,直到執行緒完全執行完以後,執行緒a才結束阻塞狀態。

  8. stop():已過時。當執行此方法時,強制結束當前執行緒。

  9. sleep(long millitime):讓當前執行緒“睡眠”指定的millitime毫秒。在指定的millitime毫秒時間內,當前執行緒是阻塞狀態。

  10. isAlive():判斷當前執行緒是否存活

執行緒的優先順序

  • MAX_PRIORITY:10

  • MIN_PRIORITY:1

  • NORM_PRIORITY:5

如何獲取和設定當前執行緒的優先順序

  • getPriority():獲取執行緒的優先順序

  • setPriority(int p):設定執行緒的優先順序

執行緒的生命週期

處理實現Runnable的執行緒安全問題(以賣車票的例子舉例)

  1. 問題:賣票的過程中,出現了重票、錯票 -->出現了執行緒的安全問題

  2. 問題出現的原因:當某個執行緒操作車票的過程中,尚未完成操作時,其他執行緒參與進來,也操作車票

  3. 如何解決:當一個執行緒a在操作ticket的時候,其他執行緒不能參與進來。直到執行緒a操作完ticket時,執行緒才可以開始操作ticket。這種情況即使a出現了阻塞,也不能被改變。

  4. 在Java中,我們通過同步機制,來解決執行緒的安全問題。

  5. 同步的方式,解決了執行緒的安全問題。------優點

    操作同步程式碼時,只能有一個執行緒參與,其他執行緒等待。相當於時一個單執行緒的過程,效率低。-------侷限性

方法一:同步程式碼塊

 synchronized(同步監視器){
//需要被同步的程式碼
}
public class WindowsTestRunnable {
public static void main(String[] args) {
windows w1 = new windows();
Thread t1 = new Thread(w1);
Thread t2=new Thread(w1);
Thread t3=new Thread(w1);
t1.setName("視窗1");
t2.setName("視窗2");
t3.setName("視窗3");
t1.start();
t2.start();
t3.start();

}
}

class windows implements Runnable{
private int ticket=100;
Object oj=new Object();
@Override
public void run() {
while (true){
synchronized(oj) {
if (ticket > 0) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": 賣票,票號為:" + ticket);
ticket--;
} else {
break;
}
}
}
}
}

說明:

  1. 操作共享資料的程式碼,即為需要被同步的程式碼 ----->不能包多也不能包少

  2. 共享資料:多個執行緒共同操作的變數。比如ticket就是共享資料。

  3. 同步監視器,俗稱:鎖。任何一個類的物件,都可以充當鎖。

    要求:多個執行緒必須要共用一把鎖

    補充:在實現Runnable介面建立多執行緒的方式中,我們可以考慮使用this充當同步監視器

方法二:同步方法

public class WindowsTestRunnablemethod {
    public static void main(String[] args) {
        Windows2 w2 = new Windows2();

        Thread t1 = new Thread(w2);
        Thread t2 = new Thread(w2);
        Thread t3 = new Thread(w2);

        t1.setName("視窗1");
        t2.setName("視窗2");
        t3.setName("視窗3");

        t1.start();
        t2.start();
        t3.start();
    }

}
class Windows2 implements Runnable{
    private int ticket=100;
    @Override
    public void run() {
        while(true){
            show();
            if (ticket==0)
                break;
        }
    }
    private synchronized void show(){
        if(ticket>0){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+": 賣票,票號為:"+ticket);
            ticket--;
        }
    }
}

關於同步方法的總結:

  1. 同步方法仍然涉及到同步監視器,只要是不需要我們顯示的宣告。

  2. 非靜態的同步方法,同步監視器是:this

    靜態的同步方法,同步監視器是:當前類本身

解決繼承Thread類的方式的執行緒安全問題(以賣車票的例子舉例)

例子:建立三個視窗賣票,總票數為100張,使用繼承Thread類的方式

方法一:同步程式碼塊

public class WindowsTestExtends {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        Ticket ticket1 = new Ticket();
        Ticket ticket2 = new Ticket();
        ticket.setName("視窗1");
        ticket1.setName("視窗2");
        ticket2.setName("視窗3");
        ticket.start();
        ticket1.start();
        ticket2.start();
    }
}

class Ticket extends Thread{
    private static int ticket=100;
    private static Object obj=new Object();
    @Override
    public void run() {
       while (true){
          // synchronized(Ticket.class) {
               synchronized(obj) {
               if (ticket > 0) {
                   System.out.println(getName() + ": 賣票,票號為:" + ticket);
                   ticket--;
               } else {
                   break;
               }
           }
       }
    }
}

說明:

  1. 操作共享資料的程式碼,即為需要被同步的程式碼----->不能包多也不能包少

  2. 共享資料:多個執行緒共同操作的變數。比如ticket就是共享資料。

  3. 同步監視器,俗稱:鎖。任何一個類的物件,都可以充當鎖。

    要求:多個執行緒必須要共用一把鎖

    補充:在繼承Thread類建立多執行緒的方式中,慎用this充當同步監視器,考慮使用當前類充當同步監視器

方法二:同步方法

public class WindowsTestExtendsmethod {
    public static void main(String[] args) {
        Windows1 w1 = new Windows1();
        Windows1 w2 = new Windows1();
        Windows1 w3 = new Windows1();

        w1.setName("視窗1");
        w2.setName("視窗2");
        w3.setName("視窗3");

        w1.start();
        w2.start();
        w3.start();
    }
}


class Windows1 extends Thread{
    private static int ticket=100;
    @Override
    public void run() {
        while(true){
             show();
             if (ticket==0)
                 break;
        }
    }

    private static synchronized void show(){//同步監視器:Windows1.class
        if(ticket>0){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+": 賣票,票號為:"+ticket);
            ticket--;
        }
    }
}

解決執行緒安全問題的方式三:Lock鎖 --- JDK5.0新增

步驟:

  1. 例項化ReentrantLock

  2. 呼叫鎖定方法lock()

  3. 呼叫解鎖方法unlock()

public class WindowsTestLockmethod {
    public static void main(String[] args) {
        window w1 = new window();

        Thread t1 = new Thread(w1);
        Thread t2 = new Thread(w1);
        Thread t3 = new Thread(w1);

        t1.setName("視窗1");
        t2.setName("視窗2");
        t3.setName("視窗3");

        t1.start();
        t2.start();
        t3.start();
    }
}

class window implements Runnable{
    private int ticket=100;
    //1.例項化ReentrantLock
    private ReentrantLock lock=new ReentrantLock(true);
    @Override
    public void run() {
        while(true){
            try{
                //2.呼叫鎖定方法Lock()
                lock.lock();
                if (ticket>0)
                {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+":賣票! 票號為"+ticket);
                    ticket--;
                }
                else {
                    break;
                }
            }finally {
//                3.呼叫解鎖方法:unlock()
                lock.unlock();
            }

        }
    }
}

synchronized與lock的異同

相同:二者都可以解決執行緒的安全問題

不同:synchronized機制在執行完相應的同步程式碼以後,自動的釋放同步監視器。

lock需要手動的啟動同步(lock()),同時結束同步也需要手動的實現(unlock())

優先使用順序:

lock -->同步程式碼塊(已經進入了方法體,分配了相應資源)-->同步方法(方法體之外)

執行緒通訊的三個方法

  1. wait():一旦執行此方法,當前執行緒就進入阻塞狀態,並釋放同步監視器

  2. notify():一旦執行此方法,就會喚醒被wait的一個執行緒。如果有多個執行緒被wait,就喚醒優先順序高的那個

  3. notifyAll():一旦執行此方法,就會喚醒所有被wait的執行緒

說明:

  1. wait(),notify(),notifyAll()三個方法必須使用在同步程式碼塊或同步方法中。

  2. wait(),notify(),notifyAll()三個方法的呼叫者必須是同步程式碼塊或同步方法中的同步監視器。 否則會出現IllegalMonitorStateException異常

  3. wait(),notify(),notifyAll()三個方法是定義在java.long.Object類中。

***sleep()和wait()的異同

  1. 相同的:一旦執行方法,都可以使得當前的執行緒進入阻塞狀態。

  2. 不同點:1).兩個方法宣告的位置不同:Thread類中宣告sleep(),Object類中宣告wait()

    2).呼叫的要求不同:sleep()可以在任何需要的場景下呼叫。wait()必須使用在同步程式碼塊或同步方法中

    3).關於是否釋放同步監視器:如果兩個方法都使用在同步程式碼塊或同步方法中,sleep()不會釋放鎖,wait()會釋放鎖。

執行緒通訊的應用經典例題:生產者/消費者問題

//執行緒通訊的應用:經典例題:生產者/消費者問題
/**
 * 生產者(Productor)將產品交給店員(clerk),而消費者(Customer)從店員處取走產品,
 * 店員一次只能持有固定數量的產品(比如:20),如果生產者試圖生產更多的產品,店員
 * 會叫生產者停一下,如果店中有空位放產品了再通知生產者繼續生產;如果店中沒有產品了,
 * 店員會告訴消費者等一下,如果店中有了產品了再通知消費者來取走產品
 */
public class ProductTest {
    public static void main(String[] args) {
        clerk clerk=new clerk();
        Productor p1 = new Productor(clerk);
        Consumer c1 = new Consumer(clerk);

        p1.setName("生產者1");
        c1.setName("消費者1");

        p1.start();
        c1.start();

    }
}


class clerk{
     private int produceCount=0;
    public synchronized void productormethod() {//生產產品
        if (produceCount<20)
        {
            produceCount++;
            System.out.println(Thread.currentThread().getName()+"開始生產第"+produceCount+"個產品");

            notify();
        }else{
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public synchronized void consumermethod() {//消費產品
        if (produceCount>0)
        {
            System.out.println(Thread.currentThread().getName()+"開始消費第"+produceCount+"個產品");
            produceCount--;
            notify();

        }else{
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class Productor extends Thread{//生產者
    private clerk cle;

    public Productor(clerk cle) {
        this.cle = cle;
    }
    @Override
    public void run() {
        System.out.println(getName()+":開始生產產品。。。。");
        while(true){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            cle.productormethod();
        }
    }
}

class Consumer extends Thread{//消費者
    private clerk cle;

    public Consumer(clerk cle) {
        this.cle = cle;
    }
    @Override
    public void run() {
        System.out.println(getName()+":開始消費產品。。。。");
        while(true){
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            cle.consumermethod();
        }
    }
}

JDK5.0新增執行緒建立方式一:實現Callable介面

  • 與使用Runnable相比,Callable功能更加強大些

    • 相比run()方法,可以有返回值

    • 方法可以丟擲異常

    • 支援泛型的返回值

    • 需要藉助FutureTask類,比如獲取返回結果

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/*
建立執行緒的方式三:實現Callable介面。------JDK  5.0新增

如何理解實現Callable介面的方式建立多執行緒比實現Runnable介面建立多執行緒方式強大?
1、call()方法可以有返回值
2、call()方法可以丟擲異常,被外面的操作捕獲,獲取異常的資訊
3、Callable是支援泛型
 */
//1、建立一個實現Callable的實現類
class NumThread implements Callable{
    //2、實現call方法,將此執行緒需要執行的操作宣告在call()方法中
    @Override
    public Object call() throws Exception {
        int sum=0;
        for (int i = 0; i <=100 ; i+=2) {
            System.out.println(i);
            sum+=i;
        }
        return sum;
    }
}

public class ThreadNew {
    public static void main(String[] args) {
        //3、建立Callable介面實現類的物件
        NumThread numThread = new NumThread();
        //4、將此Callable介面實現類的物件作為傳遞到FutureTask構造器中,建立FutureTask的物件
        FutureTask futureTask = new FutureTask(numThread);
        //5、將FutureTask的物件作為引數傳遞到Thread類的構造器中,建立Thread物件,並呼叫start()
        new Thread(futureTask).start();

        try {
            //6、獲取Callable中call方法的返回值
            //get()返回值即為FutureTask構造器引數Callable實現類重寫的call()的返回值。
            Object sum= futureTask.get();
            System.out.println("總和為:"+sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

JDK5.0新增執行緒建立方式一:使用執行緒池

  • 背景:經常建立和銷燬、使用量特別大的資源,比如併發情況下的執行緒,對效能影響很大。

  • 思路:提前建立好多個執行緒,放入執行緒池中,使用時直接獲取,使用完放回池中。可以避免頻繁建立銷燬、實現重複利用。類似生活中的公共交通工具。

  • 好處:

    • 提高了響應速度(減少了建立新執行緒的實踐)

    • 降低資源消耗(重複利用執行緒池執行緒,不需要每次都建立)

    • 便於執行緒的管理

      • corePoolSize:核心池的大小

      • maximumPoolSize:最大執行緒數

      • KeepAliveTIme:執行緒沒任務時最多保持多長時間會終止

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;


class NumberThread implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i <=100; i++) {
            if(i%2==0)
                System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}

class NumberThread1 implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i <=100; i++) {
            if(i%2!=0)
                System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}
public class ThreadPool {
    public static void main(String[] args) {
        //1.提供指定執行緒數量的執行緒池
        ExecutorService service = Executors.newFixedThreadPool(10);
        //2.執行指定的執行緒的操作。需要提供實現Runnable介面或Callable介面實現類的物件
        service.execute(new NumberThread());   //適用於Runnable
        service.execute(new NumberThread1());   //適用於Runnable
//        service.submit();   //適用於Callable
        // 3.關閉連線池
        service.shutdown();

    }
}