Java的三魂七魄 —— 高級多線程

目錄

1、多線程的建立

==多線程的建立方法有:四種!!! #F44336==java

1.建立Thread子類

先上代碼:面試

/**
 * 建立多線程的方法一:
 *      建立繼承Thread的子類
 * 
 * @author 🏹☂࿈秋鶩࿈🏹️
 * @create 2020/3/7 18:41
 */

//線程類
class NumCount extends Thread{

    //run方法裏是要執行的代碼
    @Override
    public void run() {
        //輸出0-99
        for (int i = 0; i < 100; i++) {
           System.out.println(NumCount.currentThread().getName()+":"+i);
        }
    }
}
//主類
public class MyThread {
    public static void main(String[] args) {
        //建立Thread子類的對象
        NumCount nc1 = new NumCount();
        //給線程起個名字
        nc1.setName("計數線程1");
        //開啓線程
        nc1.start();
    }
}

注意事項windows

  • run()方法爲線程的執行內容
  • 建立的對象須要調用start()方法開啓線程(繼承)
  • NumberCount.currentThread().getName()是獲取當前線程的名稱 (繼承)

2.實現Runnable接口

先上代碼:安全

/**
 * 建立多線程的方法二:
 *      實現Runnable接口
 *
 * @author 🏹☂࿈秋鶩࿈🏹️
 * @create 2020/3/7 18:41
 */

//線程類
class NumCount implements Runnable{

    //run方法裏是要執行的代碼
    @Override
    public void run() {
        //輸出0-99
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}
//主類
public class MyThread {
    public static void main(String[] args) {
        //實例化Runnable接口
        NumCount numberCount = new NumCount();
        //實例化一個線程,構造方法的參數爲實例化的接口
        Thread nc1 = new Thread(numberCount);
        //給線程起個名字
        nc1.setName("計數線程1");
        //開啓線程
        nc1.start();
    }
}

注意事項多線程

  • 因爲是實現Runnable接口,獲取線程名稱時不能用NunberCount類
  • 建立線程時須要先實例化接口,而後建立線程對象

3.實現Callable接口

先上代碼:ide

/**
 * 建立多線程的方法三:
 *      實現Callable接口  --- JDK5.0新增
 *
 * @author 🏹☂࿈秋鶩࿈🏹️
 * @create 2020/3/7 18:41
 */

//線程類
class NumCount implements Callable<Integer> {

    //重寫call()方法
    @Override
    public Integer call() throws Exception {
        //計算1-100的數的和
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
            sum += i;
        }
        return sum;
    }
}
//主類
public class MyThread {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //建立Callable接口實現類的對象
        NumCount numCount = new NumCount();
        //將Callable接口實現類的對象傳遞到FutureTask構造器,建立FutureTask對象
        FutureTask<Integer> futureTask = new FutureTask<>(numCount);
        //建立線程,傳遞FutureTask對象到Thread構造器中
        Thread nc1 = new Thread(futureTask);
        nc1.setName("計數線程1");
        nc1.start();

        //獲取Callable中call方法的返回值
        Integer sum = futureTask.get();
        System.out.println("總和爲:" + sum);
    }
}

注意事項this

  • Callable方法更新於jdk 1.5
  • FutureTask實現了Runnab了接口,因此建立線程時能夠傳入FutureTask的實例化對象
  • Callable相較於Runnable更爲靈活,支持泛型,能夠回傳值
  • call()方法的返回值由FutureTask的get()方法獲取

4.建立線程池

先上代碼:線程

/**
 * 建立多線程的方法四:
 *      建立線程池
 *
 * @author 🏹☂࿈秋鶩࿈🏹️
 * @create 2020/3/7 18:41
 */

//線程類
class NumCount implements Runnable {

    //重寫run方法
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}
//主類
public class MyThread {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //建立指定線程數量的線程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        //執行指定的線程的操做,須要提供實現Runnable接口或Callable接口實現類的對象
        service.execute(new NumCount());//用於Runnable
//        service.submit();//用於Callable
        //關閉線程池
        service.shutdown();
    }
}

注意事項設計

  • 線程池支持Runnable和Callable對象
  • 詳細信息看另外一篇線程池詳解

2、線程安全問題

問題引入:

  • 火車票問題: 三個售票窗口同時賣100張票

問題分析:

  1. 三個窗口同時買票須要使用多線程
  2. 買票過程當中會有一個共享數據:票數
  3. 執行過程當中會出現線程安全問題:重複賣同一張票和賣出錯票

Q: 爲何會重票錯票?
A: 當出現極限狀況:當多個線程同時調用共享數據,而且線程還未結束,就會出現重複調取同一個值的狀況(重票),票數不知足線程數時,會出現負數(錯票)。code

#### 解決方案:線程同步機制
線程同步機制:
當某一個線程在使用共用的數據(執行被同步的代碼)時,其餘線程要進行等候,不管這個線程是否處於阻塞狀態。

若是不明白的話,就想象一下旅遊景點女廁所門口排隊的女性朋友們。

#### 具體辦法:

1. 同步代碼塊
這個方法涉及到synchronized修飾詞。
基本格式:

synchronized(同步監視器(鎖)){
 *          //要被同步的代碼
 *      }

說明:

  1. 操做共享數據的代碼即爲要被同步的代碼
  2. 共享數據:多個線程要共同操做的同一個數據
  3. 同步監視器,俗稱鎖,任意一個類的對象均可擔任。
  4. 同步的多個線程必須共用一把鎖

使用示例:

/**
 * 用同步代碼塊解決線程安全問題
 *
 * 問題:三個窗口賣100張票,用線程解決。
 *
 *
 * @author 🏹☂࿈秋鶩࿈🏹️
 * @create 2020/3/5 17:50
 */

class TicketTread extends Thread{

    //保證多個線程共用一份數據,須要設置成靜態的
    public static int ticket = 100;
    //同一把鎖
    public static Object object = new Object();

    @Override
    public void run() {
        while (true){
            //同步代碼塊
            synchronized (object){
                if (ticket>0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(TicketTread.currentThread().getName() + ":正在售賣第" + ticket + "張票");
                    ticket--;
                }else{
                    break;
                }
            }
        }
    }
}


public class TicketDemo {
    public static void main(String[] args) {
        TicketTread t1 = new TicketTread();
        TicketTread t2 = new TicketTread();
        TicketTread t3 = new TicketTread();

        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");

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

2. 同步方法
這個方法涉及到synchronized修飾詞。
基本格式:

synchronized 數據類型 方法名(){
    //須要被同步的代碼
}

注意:

  • 這個時候仍是有同步監視器(鎖),默認爲this

使用示例:

class Windows1 implements Runnable{

    //這裏無需設定靜態變量,由於多個線程調用同一個接口
    public int ticket = 100;
    Object object = new Object();

    @Override
    public void run() {
        while (true){
               show();
        }

    }
    //同步方法
    public synchronized void show(){
        if (ticket>0){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(TicketTread.currentThread().getName() + ":正在售賣第" + ticket + "張票");
            ticket--;
        }
    }
}


public class TicketDemo2 {
    public static void main(String[] args) {
        Windows1 windows = new Windows1();
        Thread t1 = new Thread(windows);
        Thread t2 = new Thread(windows);
        Thread t3 = new Thread(windows);

        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");

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

3. ReentrantLock
jdk1.5新特性
用法和同步代碼塊相似。用調用方法的辦法替代代碼塊。
使用示例:

class Windows implements Runnable{

    public static int ticket = 100;
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true){
            try {

                //調用鎖定的方法
                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 {
                //調用解鎖的方法
                lock.unlock();
            }

        }
    }
}

public class LockTest {
    public static void main(String[] args) {
        Windows windows = new Windows();
        Thread t1 = new Thread(windows);
        Thread t2 = new Thread(windows);
        Thread t3 = new Thread(windows);

        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");

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

面試題:synchronized 和 Lock 的區別?

  • 相同點:都是實現線程同步機制
  • 不一樣點:
    • synchornized機制在執行完相應的同步代碼之後,自動釋放同步監視器;
    • lock須要手動的啓動同步(lock()),同時結束同步也須要手動實現(unlock())

3、線程通訊問題

問題引入:

兩個線程打印1-100,交替打印
問題代碼:

/**
 * 兩個線程打印1-100,交替打印
 *
 * @author 🏹☂࿈秋鶩࿈🏹️
 * @create 2020/3/6 22:41
 */

class Number implements Runnable{

    private int number = 1;

    @Override
    public void run() {
        while (true){
            synchronized (this) {
                if (number<=100){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":數字:" + number);
                    number++;
                }else {
                    break;
                }
            }
        }
    }
}

public class Communicate {
    public static void main(String[] args) {
        Number number = new Number();
        Thread t1 = new Thread(number);
        Thread t2 = new Thread(number);

        t1.setName("線程1");
        t2.setName("線程2");

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

當你運行此段代碼時,會出現僅單一線程運行的狀況

問題分析:

t1線程一致佔用鎖,沒法進行交替打印

解決方案:線程通訊

須要利用三劍客:wait(),notify(),notifyAll()

  • wait():一旦執行此方法,當前線程就進入阻塞狀態,並釋放同步監視器(鎖)
  • notify():一旦執行此方法,就會喚醒被wait的一個線程,若是有多個線程被wait,就喚醒優先級高的那個。
  • notifyAll():一旦執行此方法,就會喚醒全部被wait的線程。

說明:

  1. 三個方法必須使用在同步代碼塊或同步方法中
  2. 三個方法的調用者,必須是同步代碼塊或同步方法中的同步監視器(鎖爲調用者,只有鎖才能調用三個方法)不然,會出現IllegalMonitorStateException異常
  3. 三個方法定義在java.lang.Object類中

具體辦法:

/**
 * 兩個線程打印1-100,交替打印
 *
 * @author 🏹☂࿈秋鶩࿈🏹️
 * @create 2020/3/6 22:41
 */

class Number implements Runnable{

    private int number = 1;

    @Override
    public void run() {
        while (true){
            synchronized (this) {
                //使用notify()方法喚醒線程
//              this.notify();
                notify();
                if (number<=100){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":數字:" + number);
                    number++;

                    try {
                        //使用wait()方法使得線程處於阻塞狀態
//                      this.wait();
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }else {
                    break;
                }
            }
        }
    }
}

public class Communicate {
    public static void main(String[] args) {
        Number number = new Number();
        Thread t1 = new Thread(number);
        Thread t2 = new Thread(number);

        t1.setName("線程1");
        t2.setName("線程2");

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

面試題:sleep() 和 wait() 的異同?

  • 相同點:一旦執行方法,均可以使得當前的線程進入阻塞狀態
  • 不一樣點:
    • 兩個方法聲明的位置不一樣:Thread類中聲明sleep(),Object類中聲明wait()
    • 調用的要求不一樣:sleep()能夠在任何須要的場景下調用。wait()必須在同步代碼塊和同步方法中使用
    • 關因而否釋放同步監視器:若是兩個方法都使用在同步代碼塊或同步方法中,sleep()不會釋放鎖,wait()會釋放鎖

4、更多實例

1.用線程同步的方法解決單例模式的線程安全問題

問題解決:

/**
 * 用線程同步的方法解決單例模式的線程安全問題
 *
 * @author 🏹☂࿈秋鶩࿈🏹️
 * @create 2020/3/6 16:32
 */
public class BankDemo {
}

class Bank{
    //單例模式(private構造方法,確保該類使用對象惟一)
    private Bank(){};

    public static Bank instence = null;

    //此處可能引起線程安全問題
    public static Bank getInstence(){

        //方式一,效率稍差(線程所有同步)
//        synchronized (Bank.class) {
//            if (instence==null){
//                instence = new Bank();
//            }
//        return instence;
//        }
        //方式二:效率較高(一小部分線程同步)
        if (instence==null){
            synchronized (Bank.class) {
                if (instence==null){
                    instence = new Bank();
                }
            }
        }
        return instence;
    }
}

2.銀行存錢問題(線程安全問題)

問題描述:
銀行有一個帳戶,有兩個儲戶分別向同一個帳戶存3000元,每次存1000,存3次,每次存完打印帳餘額。
問題解決:

/**
 * @author 🏹☂࿈秋鶩࿈🏹️
 * @create 2020/3/6 22:19
 */

//帳戶(共享數據)
class Account{

    private double balance;
    public Account(double balance){
        this.balance =balance;
    }

    //存錢
    public synchronized void deposit(double amt){

        if (amt > 0){
            balance += amt;
            System.out.println(Thread.currentThread().getName() + ":存錢成功!當前餘額爲:" + balance);
        }
    }
}

//儲戶(線程)
class Customer implements Runnable{

    private Account acct;
    //初始化數據
    public Customer(Account acct){
        this.acct = acct;
    }

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            acct.deposit(1000);
        }
    }
}

public class AccountTest {
    public static void main(String[] args) {
        Account acct = new Account(0);
        Customer customer = new Customer(acct);
        Thread c1 = new Thread(customer);
        Thread c2 = new Thread(customer);

        c1.setName("甲");
        c2.setName("乙");

        c1.start();
        c2.start();
    }
}

3.生產者消費者問題(線程通訊問題)

問題描述:
生產者(Productor)將產品交給店員(Clerk),而消費者(Customer)從店員處取走產品。店員一次只能持有固定數量的產品(好比:20),若是生產者試圖生產更多的產品,店員會叫生產者停一下,若是店中有空位放產品了再通知生產者繼續生產;若是店中沒有產品了,店員會告訴消費者等一下,若是店中有產品了再通知消費者來取走產品。
問題分析:

1.是不是多線程問題?是,生產者線程,消費者線程
2.是否有共享數據?是,店員(或產品)
3.如何解決現成的安全問題?同步機制,有三種方法
4.是否設計線程的通訊?是

問題解決:

/**
 * @author 🏹☂࿈秋鶩࿈🏹️
 * @create 2020/3/7 11:04
 */
class Clerk{

    private int productCount = 0;

    //生產產品
    public synchronized void produceProduct() {
        if (productCount<20){
            productCount++;
            System.out.println(Thread.currentThread().getName() + ":正在生產第" + productCount + "個產品");
            notify();
        }else {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    //消費產品
    public synchronized void consumeProduct(){
        if (productCount>0){
            System.out.println(Thread.currentThread().getName() + ":正在消費第" + productCount + "個產品");
            productCount--;
            notify();
        }else {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class Producer implements Runnable{//生產者

   private Clerk clerk;

    public Producer(Clerk clerk){
        this.clerk = clerk;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ":開始生產產品....");

        while (true){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.produceProduct();
        }
    }
}

class Consumer implements Runnable{//消費者

    private Clerk clerk;

    public Consumer(Clerk clerk){
        this.clerk = clerk;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ":開始消費產品....");

        while (true){
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.consumeProduct();
        }
    }
}

public class Product {
    public static void main(String[] args) {
        Clerk clerk = new Clerk();

        Producer producer = new Producer(clerk);
        Thread p1 = new Thread(producer);
        p1.setName("生產者1");

        Consumer consumer = new Consumer(clerk);
        Thread c1 = new Thread(consumer);
        c1.setName("消費者1");
        Thread c2 = new Thread(consumer);
        c2.setName("消費者2");

        p1.start();
        c1.start();
        c2.start();
    }
}
相關文章
相關標籤/搜索