day17【Lock、併發包、線程池】

今日內容

  • Lock
  • 併發包
  • 線程池

教學目標

  • 能夠使用Lock解決線程安全問題
  • 能夠描述ConcurrentHashMap類的作用
  • 能夠描述CountDownLatch類的作用
  • 能夠描述CyclicBarrier類的作用
  • 能夠表述Semaphore類的作用
  • 能夠描述Exchanger類的作用
  • 能夠使用線程池

第一章 Lock鎖(掌握)

從jdk5後java.util.concurrent.locks.Lock機制提供了比synchronized代碼塊和synchronized方法更廣泛的鎖定操作,同步代碼塊/同步方法具有的功能Lock都有,除此之外更強大.Lock鎖是Java中更加符合編程習慣的解決方式。

Lock鎖也稱同步鎖,加鎖與釋放鎖方法化了,如下:

  • public void lock():加同步鎖。
  • public void unlock():釋放同步鎖。

由於Lock屬於接口,不能創建對象,所以我們可以使用它的子類ReentrantLock來創建對象並使用Lock接口中的函數。

需求:使用Lock實現線程安全的賣票。

分析和步驟:

1)定義一個賣票的任務類SellTicketTask 類並實現Runnable接口;

2)在任務類中定義一個成員變量tickets保存票數100;

3)定義一把鎖Lock的對象l;

4)在run函數中模擬賣票,if語句的上面使用鎖對象l調用lock()函數獲取鎖,等待if語句結束之後,使用鎖對象l調用unlock()函數釋放鎖;

5)定義測試類SellTicketDemo ,在這個類中分別創建任務類的對象和線程類的對象,並使用線程類的對象調用start()函數來啓動線程;

/*
 * 需求:使用Lock實現線程安全的賣票。
 * Lock是接口,只能通過他的子類ReentrantLock創建對象
 * 構造函數 ReentrantLock()  創建一個 ReentrantLock 的實例。
 *  void lock() 獲取鎖。 
 *  void unlock() 試圖釋放此鎖。 
 */
//定義一個任務類用來賣票
class SellTicketTask implements Runnable
{
	//定義100張票
	private static int tickets=100;
	//創建對象作爲任意一把鎖
//	private Object obj=new Object();
	//定義一把鎖
	Lock l=new ReentrantLock();
	//模擬賣票
	public void run() {
		/*while(true)
		{
			synchronized (obj) {
				if(tickets>0)
				{
					System.out.println(Thread.currentThread().getName()+"出票:"+tickets--);
				}
			}
		}*/
		//使用Lock鎖替換synchronized
		while(true)
		{
				//獲取鎖
				l.lock();
				if(tickets>0)
				{
					try {Thread.sleep(1);} catch (InterruptedException e) {}
					System.out.println(Thread.currentThread().getName()+"出票:"+tickets--);
				}
				//釋放鎖
				l.unlock();
		}
	}
}
public class SellTicketDemo {
	public static void main(String[] args) {
		// 創建任務類對象
		SellTicketTask stt = new SellTicketTask();
		//創建線程對象
		Thread t1 = new Thread(stt,"窗口1");
		Thread t2 = new Thread(stt,"窗口2");
		Thread t3 = new Thread(stt,"窗口3");
		Thread t4 = new Thread(stt,"窗口4");
		//啓動線程
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}

第二章 併發包

在JDK的併發包裏提供了幾個非常有用的併發容器和併發工具類。供我們在多線程開發中進行使用。

2.1 CopyOnWriteArrayList(理解)

  • ArrayList和CopyOnWriteArrayList效果演示

    • ArrayList在多線程的情況下線程不安全

    • CopyOnWriteArrayList是一個線程安全的集合,解決ArrayList安全問題。 數據不唯一。

      • 這個類的內部其實也就是使用了同步鎖解決線程安全問題,和我們自己寫同步鎖是一樣的意思。
  • 需求:給list集合添加1000個元素,然後打印集合的長度。

    public class MyRun implements Runnable{
        //定義一個集合
        //普通的ArrayList是線程不安全的類
        //static ArrayList<Integer> list = new ArrayList<>();
        
        //只要換成CopyOnWriteArrayList就變成了線程安全的
        static CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
    
        @Override
        public void run() {
            //給集合添加了1000個元素
            for (int i = 0; i < 1000; i++) {
                list.add(i);
            }
        }
    }
    
    public class Demo_ArrayList {
        public static void main(String[] args) throws InterruptedException {
            MyRun mr = new MyRun();
            //開啓線程
            Thread t1 = new Thread(mr);
            t1.start();
            Thread t2 = new Thread(mr);
            t2.start();
    
            //爲了讓for循環先執行結束再打印
            //睡兩秒鐘
            Thread.sleep(2000);
    
            //在這裏打印集合的長度,裏面應該存放了多少個元素?
            System.out.println(MyRun.list.size());
    
    
        }
    }
    
    執行效果:
    	ArrayList:
            一個線程給集合添加1000個元素,兩個線程應該添加2000個元素
            但是打印的結果是小於2000或者可能會拋異常
            因爲ArrayList集合本身就是線程不安全的
    	
    	CopyOnWriteArrayList:
    		結果是2000
    

2.2 CopyOnWriteArraySet(理解)

  • HashSet是線程不安全的
  • CopyOnWriteArraySet是線程安全的
  • 需求:向HashSet中存儲1000個數字。

代碼演示如下:

package com.itheima.sh.demo_14;
public class MyRun implements Runnable {
    //定義集合
    //HashSet集合本身是有線程安全問題的
//   static HashSet<Integer> set = new HashSet<>();

    //CopyOnWriteArraySet解決了安全問題
    static CopyOnWriteArraySet<Integer> set = new CopyOnWriteArraySet<>();

    @Override
    public void run() {
        //添加1000個數字   0-999
        for (int i = 0; i < 1000; i++) {
            set.add(i);
        }
    }
}

public class Test01 {
    public static void main(String[] args) throws InterruptedException {
        MyRun mr = new MyRun();
        //開啓線程
        Thread t1 = new Thread(mr);
        t1.start();
        //開啓線程
        Thread t2 = new Thread(mr);
        t2.start();
        //讓主程序睡兩秒鐘
        Thread.sleep(2000);
        //打印集合的長度 大於1000
        System.out.println(MyRun.set.size());

    }
}

執行效果:
	HashSet:
		本來集合中保存的是1000個元素,但是打印的長度size大於1000.
    CopyOnWriteArraySet:
    	打印的集合長度就是1000

說明:上述打印集合的長度size大於1000的原因如下圖所示:

假設線程一和線程二都向set集合中添加數據0,線程一正在添加的過程中先判斷集合中是否含有0,如果沒有則添加數字0,此時線程二也開始判斷set集合中是否有0,此時沒有則線程二也準備添加,添加一次,那麼底層size就會+1,但實際上set集合只存儲一個數字0.size加錯了。

在這裏插入圖片描述

注意:

CopyOnWriteArraySet 底層就是CopyOnWriteArrayList 。CopyOnWriteArraySet 如何保證唯一的?使用CopyOnWriteArrayList中的方法:

 boolean addIfAbsent(E e) 添加元素(如果不存在)。 
     如果添加的元素存在,則不存儲,如果添加元素不存在則添加。
     舉例:
     aaa 第一次添加可以添加
     bbb 第一次添加可以添加
     aaa 已經存在aaa 不添加

2.3 ConcurrentHashMap(理解)

  • HashMap和Hashtable和ConcurrentHashMap效果演示

  • 需求:向HashMap集合中存儲1000個鍵值對數據

    import java.util.HashMap;
    import java.util.Hashtable;
    import java.util.concurrent.ConcurrentHashMap;
    
    public class MyRun implements Runnable {
        //HashMap線程不安全
        //HashMap<Integer,Integer> map = new HashMap<>();
        
        //Hashtable線程安全的
        //Hashtable<Integer,Integer> map = new Hashtable<>();
    
        //ConcurrentHashMap線程安全的
        ConcurrentHashMap<Integer,Integer> map = new ConcurrentHashMap<>();
    
        @Override
        public void run() {
            //添加1000個元素   0-999
            for (int i = 0; i < 1000; i++) {
                map.put(i,i);
            }
        }
    }
    
    public class DemoHashMap {
        public static void main(String[] args) throws InterruptedException {
    
            MyRun mr = new MyRun();
    
            //開啓線程
            Thread t1 = new Thread(mr);
            t1.start();
    
            //開啓線程
            Thread t2 = new Thread(mr);
            t2.start();
    
            //先睡2秒鐘讓循環執行結束
            Thread.sleep(2000);
    
            //打印集合的長度
            System.out.println(mr.map.size());
    
        }
    }
    
    執行效果:
    	HashMap本來應該打印的長度是1000,但是執行的結果大於1000
    	
    	Hashtable  			 結果是1000
    	ConcurrentHashMap    結果是1000
    

    Hashtable和ConcurrentHashMap的速度區別

    • Hashtable解決線程安全的方式,但是效率很低。

      public synchronized V get(Object key) {}
      public synchronized V put(K key, V value) {}
      
      每個方法全都是同步的,當任意一個方法在執行時,別的方法都不能執行。
      

      HashTable容器使用synchronized來保證線程安全,但在線程競爭激烈的情況下HashTable的效率非常低下。因爲當一個線程訪問HashTable的同步方法,其他線程也訪問HashTable的同步方法時,會進入阻塞狀態。如線程1使用put進行元素添加,線程2不但不能使用put方法添加元素,也不能使用get方法來獲取元素,所以競爭越激烈效率越低。
      在這裏插入圖片描述

    • ConcurrentHashMap

      ​ CAS + 局部(synchronized)鎖定

在這裏插入圖片描述

  • Hashtable和ConcurrentHashMap效率高低的代碼演示

    public class MyRun implements Runnable {
        //HashMap線程不安全
        //HashMap<Integer,Integer> map = new HashMap<>();
    
        //Hashtable線程安全的
        //Hashtable<Integer,Integer> map = new Hashtable<>();
    
        //ConcurrentHashMap線程安全的
        ConcurrentHashMap<Integer,Integer> map = new ConcurrentHashMap<>();
    
        @Override
        public void run() {
            //獲取系統當前時間
            long time1 = System.currentTimeMillis();
    
            //添加1000個元素   0-1999
            for (int i = 0; i < 2000; i++) {
                map.put(i,i);
            }
    
            long time2 = System.currentTimeMillis();
            System.out.println((time2-time1) + "毫秒");
        }
    }
    
    public class DemoHashMap {
        public static void main(String[] args) throws InterruptedException {
    
            MyRun mr = new MyRun();
    
            //開啓線程
            //開啓了很多個線程能夠看到更明顯的速度的區別
            for (int i = 0; i < 1000; i++) {
                Thread t1 = new Thread(mr);
                t1.start();
            }
        }
    }
    

2.4 CountDownLatch(瞭解)

  • 作用

    CountDownLatch允許一個或多個線程等待其他線程完成操作。

  • 方法介紹

    public CountDownLatch(int count)// 初始化一個指定計數器的CountDownLatch對象
        
    void await()     // 讓當前線程等待 
    void countDown() //每調用一次countDown()方法,計數器count就會進行減1,如果減到0等待的線程就會執行
    
    
  • 代碼演示

    • 要求: 必須在先打印C 再打印B
    import java.util.concurrent.CountDownLatch;
    
    public class MyRun1 implements Runnable {
        CountDownLatch latch;
        //使用構造方法傳入參數
        public MyRun1(CountDownLatch latch){
            this.latch = latch;
        }
    
        @Override
        public void run() {
    
            System.out.println("A");
            /*try {
                Thread.sleep(1000);//效率太低
            } catch (InterruptedException e) {
                e.printStackTrace();
            }*/
            //等待
            try {
                latch.await();
            } catch (InterruptedException e) {
            }
    
            System.out.println("B");
        }
    }
    
    
    import java.util.concurrent.CountDownLatch;
    
    public class MyRun2 implements Runnable{
        CountDownLatch latch;
        //使用構造方法傳入參數
        public MyRun2(CountDownLatch latch){
            this.latch = latch;
        }
    
        @Override
        public void run() {
            System.out.println("C");
            //計數器減一
            //latch在計數器等於0的時候就會讓等待的線程執行
            latch.countDown();
        }
    }
    
    
    import java.util.concurrent.CountDownLatch;
    //有一個要求:必須在先打印C 再打印B
    public class Test01 {
        public static void main(String[] args) throws InterruptedException {
    
            //創建對象
            //這裏的參數1表示計數器,只要執行latch.countDown();那麼計數器就會減1
            CountDownLatch latch = new CountDownLatch(1);
            //開啓線程
            MyRun1 myRun1 = new MyRun1(latch);
            Thread t = new Thread(myRun1);
            t.start();
    
            //開啓線程
            MyRun2 myRun2 = new MyRun2(latch);
            Thread t2 = new Thread(myRun2);
            t2.start();
        }
    }
    
    

如果沒有使用CountDownLatch的情況下,會出現如下所示結果:

在這裏插入圖片描述

小結:

CountDownLatch中count down是倒數的意思,latch則是門閂的含義。整體含義可以理解爲倒數的門栓,似乎有一點“三二一,芝麻開門”的感覺。

CountDownLatch是通過一個計數器來實現的,每當一個線程完成了自己的任務後,可以調用countDown()方法讓計數器-1,當計數器到達0時,調用CountDownLatch的await()方法的線程阻塞狀態解除,繼續執行。

2.5 CyclicBarrier(瞭解)

概述

CyclicBarrier的字面意思是可循環使用(Cyclic)的屏障(Barrier)。

  • 作用

    ​ 讓一組線程到達一個屏障(也可以叫同步點)時被阻塞,直到最後一個線程到達屏障時,屏障纔會開門,所有被屏障攔截的線程纔會繼續運行。

  • 方法介紹

    public CyclicBarrier(int parties, Runnable barrierAction) // 創建對象
                                                //parties代表要達到屏障的線程數量
        										//barrierAction達到屏障之後要執行的線程
        
     int await()// 每個線程調用await方法告訴CyclicBarrier我已經到達了屏障,然後當前線程被阻塞
    
    
  • 示例代碼

    需求:有5個學生一起考試,考試有100道題,每個人答題速度不一樣,但是要求,所有人做完之後才能交卷。

    import java.util.concurrent.BrokenBarrierException;
    import java.util.concurrent.CyclicBarrier;
    
    public class MyRun implements Runnable {
    
        //創建屏障對象
        CyclicBarrier cb = new CyclicBarrier(5,new JiaoJuan());
    
        @Override
        public void run() {
    
            //考試一共100道題
            for (int i = 1; i <= 100; i++) {
                System.out.println(Thread.currentThread().getName() + "做完了第" + i + "道題");
            }
    
            System.out.println(Thread.currentThread().getName() + "做完了所有題!");
    
            //等待別人寫題
            try {
                cb.await();
            } catch (InterruptedException e) {
    
            } catch (BrokenBarrierException e) {
            }
        }
    }
    
    public class JiaoJuan implements Runnable {
        @Override
        public void run() {
            System.out.println("大家一起交卷!!!!");
        }
    }
    
    public class Demo01 {
        public static void main(String[] args) {
    
            //創建線程
            MyRun mr = new MyRun();
    
            //開啓5個線程
            new Thread(mr,"張三").start();
            new Thread(mr,"李四").start();
            new Thread(mr,"王五").start();
            new Thread(mr,"趙六").start();
            new Thread(mr,"田七").start();
        }
    }
    

使用場景

使用場景:CyclicBarrier可以用於多線程計算數據,最後合併計算結果的場景。

例如:使用兩個線程讀取2個文件中的數據,當兩個文件中的數據都讀取完畢以後,進行數據的彙總操作。

2.6 Semaphore(瞭解)

  • 作用

    1)Semaphore的主要作用是控制線程的併發數量。

    2)之前學習的synchronized可以起到"鎖"的作用,但某個時間段內,只能有一個線程允許執行。Semaphore 可以設置同時允許幾個線程執行。

    3)Semaphore字面意思是信號量的意思,它的作用是控制訪問特定資源的線程數目。

  • 方法介紹

    public Semaphore(int permits)               			 //permits 表示許可線程的數量 
    
     void acquire()       //表示獲取許可 
     void release()       //表示釋放許可
    
  • 代碼演示

    需求:開了一個飯店,飯店裏面只有3張桌子,有5桌人來喫飯。

    //飯店
    public class FanDian {
        //定義Semaphore控制最大接客數量
        //public Semaphore(int permits)permits 表示許可線程的數量
        Semaphore s = new Semaphore(3);
    
        //飯店的服務喫飯方法(方法名無所謂)
        public void service() throws InterruptedException {
            //獲取方法
            //public void acquire() throws InterruptedException	表示獲取許可
            s.acquire(); //調用了這個方法就表示進來了
            //顯示進來的時間
            Calendar c = Calendar.getInstance();
            int minute = c.get(Calendar.MINUTE);
            int second = c.get(Calendar.SECOND);
            System.out.println(Thread.currentThread().getName() + minute + "分鐘" + second + "秒進來喫飯");
    
            //喫飯
            //等待3秒鐘 模擬喫飯的過程,表示線程正在喫飯
            Thread.sleep(3000);
    
            //顯示出去的時間
            Calendar c2 = Calendar.getInstance();
            int minute2 = c2.get(Calendar.MINUTE);
            int second2 = c2.get(Calendar.SECOND);
            System.out.println(Thread.currentThread().getName() + minute2 + "分鐘" + second2 + "秒喫完飯出去了");
            //釋放
            //public void release()	表示釋放許可
            s.release();
        }
    }
    
    
    public class MyRun implements Runnable {
        //創建對象
        FanDian fd = new FanDian();
        @Override
        public void run() {
            //調用喫飯的方法
            try {
                fd.service();
            } catch (InterruptedException e) {
    
            }
        }
    }
    
    public class Test01 {
        public static void main(String[] args) throws InterruptedException {
            //創建線程
            MyRun mr = new MyRun();
            //開啓5個線程
            new Thread(mr,"張三").start();
            new Thread(mr,"李四").start();
            new Thread(mr,"王五").start();
            new Thread(mr,"鎖哥").start();
            new Thread(mr,"趙六").start();
        }
    }
    

2.7 Exchanger(瞭解)

概述

  • 作用

    Exchanger(交換者)是一個用於線程間協作的工具類。Exchanger用於進行2個線程間的數據交換。

    說明:

    兩個線程通過exchange()方法交換數據,如果第一個線程先執行exchange()方法,它會一直等待第二個線程也執行exchange()方法,當兩個線程都到達同步點時,這兩個線程就可以交換數據,將本線程生產出來的數據傳遞給對方。

  • 方法介紹

    public Exchanger()    		//構造方法,創建對象
    public V exchange(V x)      //參數是給對方的數據,返回值是對方發來的數據
        //說明:如果一個線程執行了exchange(),此時就會阻塞等待另一個線程執行exchange()方法,才結束阻塞
    
  • 代碼演示

    需求:在一個銀行,要統計今年收入一共是多少,讓兩個人都去計算,算完之後兩個人互相交換數據。

    import java.util.concurrent.Exchanger;
    
    public class MyRun1 implements Runnable {
        Exchanger<String> e;
        //構造方法
        public MyRun1(Exchanger<String> e) {
            this.e = e;
        }
    
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "開始計算今年的收入。。");
    
            String s = null;
            try {
                s = e.exchange("收入表A");
            } catch (InterruptedException e1) {
            }
    
            System.out.println(Thread.currentThread().getName() + "收到了 " + s);
        }
    }
    
    public class MyRun2 implements Runnable {
    
        Exchanger<String> e;
        //構造方法
        public MyRun2(Exchanger<String> e) {
            this.e = e;
        }
    
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "開始計算今年的收入。。。");
    
            String s = null;
            try {
                s = e.exchange("收入表B");
            } catch (InterruptedException e1) {
            }
    
            System.out.println(Thread.currentThread().getName() + "收到了 " +s);
        }
    }
    
    import java.util.concurrent.Exchanger;
    
    public class Demo {
        public static void main(String[] args) {
            //在主方法中創建對象
            //泛型用來代表交換的數據的類型
            Exchanger<String> e = new Exchanger<>();
    
            MyRun1 myRun1 = new MyRun1(e);
            new Thread(myRun1,"張三").start();
    
            MyRun2 myRun2 = new MyRun2(e);
            new Thread(myRun2,"李四").start();
        }
    }
    

使用場景

使用場景:可以做數據校對工作

需求:比如我們需要將紙製銀行流水通過人工的方式錄入成電子銀行流水。爲了避免錯誤,採用AB崗兩人進行錄入,錄入到兩個文件中,系統需要加載這兩個文件,

並對兩個文件數據進行校對,看看是否錄入一致,

【自學,瞭解,有興趣的同學可以自學下】

  • exchange方法的超時

1).製作線程A:

public class ThreadA extends Thread {
	private Exchanger<String> exchanger;
	public ThreadA(Exchanger<String> exchanger) {
		super();
		this.exchanger = exchanger;
	}
	@Override
	public void run() {
		try {
			System.out.println("線程A欲傳遞值'禮物A'給線程B,並等待線程B的值,只等5秒...");
			System.out.println("在線程A中得到線程B的值 =" + exchanger.exchange("禮物A",5, TimeUnit.SECONDS));
			System.out.println("線程A結束!");
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (TimeoutException e) {
			System.out.println("5秒鐘沒等到線程B的值,線程A結束!");
		}
	}
}

2).製作測試類:

public class Run {
	public static void main(String[] args) {
		Exchanger<String> exchanger = new Exchanger<String>();
		ThreadA a = new ThreadA(exchanger);
		a.start();
	}
}

3).測試結果:

在這裏插入圖片描述

第三章 線程池方式

3.1 線程池的思想

在這裏插入圖片描述

我們使用線程的時候就去創建一個線程,這樣實現起來非常簡便,但是就會有一個問題:

如果併發的線程數量很多,並且每個線程都是執行一個時間很短的任務就結束了,這樣頻繁創建線程就會大大降低系統的效率,因爲頻繁創建線程和銷燬線程需要時間。

那麼有沒有一種辦法使得線程可以複用,就是執行完一個任務,並不被銷燬,而是可以繼續執行其他的任務?

在Java中可以通過線程池來達到這樣的效果。今天我們就來講解一下Java的線程池。

3.2 線程池概念

程序啓動一個新線程成本是比較高的,因爲它涉及到要與操作系統進行交互。因爲啓動線程的時候會在內存中開闢一塊空間,消耗系統資源,同時銷燬線程的時候首先要把和線程相關東西進行銷燬,還要把系統的資源還給系統。這些操作都會降低操作性能。尤其針對一個線程用完就銷燬的更加降低效率。

而使用線程池可以很好的提高性能,尤其是當程序中要創建大量生存期很短的線程時,生存期較短的線程指的是用完一次線程就丟掉。更應該考慮使用線程池。

線程池裏的每一個線程代碼結束後,並不會死亡,而是再次回到線程池中成爲空閒狀態,等待下一個對象來使用。

線程池工作原理如下圖所示:

需求:我有一段任務,需要執行100次。

在這裏插入圖片描述

在這裏插入圖片描述

說明:

每次線程執行完任務以後,線程不會銷燬,會放回線程池中,每次在執行任務的時候又會到線程池中去取線程。這樣會提高效率。

合理利用線程池能夠帶來三個好處:

  1. 降低資源消耗。減少了創建和銷燬線程的次數,每個工作線程都可以被重複利用,可執行多個任務。
  2. 提高響應速度。當任務到達時,任務可以不需要的等到線程創建就能立即執行。
  3. 提高線程的可管理性。可以根據系統的承受能力,調整線程池中工作線程的數目,防止因爲消耗過多的內存,而把服務器累趴下(每個線程需要大約1MB內存,線程開的越多,消耗的內存也就越大,最後死機或者宕機)。

3.3 線程池的使用

Java裏面線程池的頂級接口是java.util.concurrent.Executor,以及他的子接口java.util.concurrent.ExecutorService

要配置一個線程池是比較複雜的,尤其是對於線程池的原理不是很清楚的情況下,很有可能配置的線程池不是較優的,因此在java.util.concurrent.Executors線程工廠類裏面提供了一些靜態工廠,生成一些常用的線程池。官方建議使用Executors工程類來創建線程池對象。

Executors類中有個創建線程池的方法如下:

  • public static ExecutorService newFixedThreadPool(int nThreads):返回線程池對象。(創建的是有界線程池,也就是池中的線程個數可以指定最大數量)

獲取到了一個線程池ExecutorService 對象,那麼怎麼使用呢,在這裏定義了一個使用線程池對象的方法如下:

  • public Future<?> submit(Runnable task):獲取線程池中的某一個線程對象,並執行

    Future接口:用來記錄線程任務執行完畢後產生的結果。

使用線程池中線程對象的步驟:

​ A:自定義一個類,作爲任務類並實現Runnable接口;

​ B:實現Runnable接口中的run方法;

​ C:創建任務類的對象;

​ D:獲取線程池對象;

​ E:直接執行任務;​

需求:使用線程池來完成賣票任務。

Runnable實現類代碼:

//A:自定義一個類,作爲任務類並實現Runnable接口;
class SellTicketTask implements Runnable
{
	//定義成員變量存儲100張票
	private static int tickets=100;
	//創建鎖對象
	private Lock l=new ReentrantLock();
	//B:實現Runnable接口中的run方法;
	public void run() {
		// 模擬賣票
		while(true)
		{
			//獲取鎖
			l.lock();
			if(tickets>0)
			{
				//休眠
				try {
					Thread.sleep(1);
				} catch (InterruptedException e) {
				}
				System.out.println(Thread.currentThread().getName()+"出票:"+tickets);
                  tickets--;
			}
			//釋放鎖
			l.unlock();
		}
	}
}

線程池測試類:

public class SellTicketDemo {
	public static void main(String[] args) {
		//C:創建任務類的對象;
		SellTicketTask stt = new SellTicketTask();
		//D:獲取線程池對象; 獲取2個線程
		ExecutorService es = Executors.newFixedThreadPool(2);
		//E:直接執行任務; 
      	  //自己創建線程對象的方式
        // Thread t = new Thread(stt);
        // t.start(); ---> 調用MyRunnable中的run()

        // 從線程池中獲取線程對象,然後調用SellTicketTask中的run()
        es.submit(stt);
        // 再獲取個線程對象,調用SellTicketTask中的run()
        es.submit(stt);
	}
}

3.4 Callable開啓多線程

  • <T> Future<T> submit(Callable<T> task) : 獲取線程池中的某一個線程對象,並執行.

    問題1:Callable是什麼?

在這裏插入圖片描述

/*
 * 演示:演示Callable
 * 我們忽略返回值,這個接口就與Runnable接口一樣了
 */
class MyTask implements Callable<Object>{

	@Override
	public Object call() throws Exception {
		for( int i = 0; i < 10; i++){
			System.out.println(Thread.currentThread().getName() + " ... " + i);
		}
		return null;
	}
}
public class CallableDemo {
	public static void main(String[] args) {
		// 創建任務對象
		MyTask mt = new MyTask();
		// 獲取線程池
		ExecutorService es = Executors.newFixedThreadPool(2);
		// 執行任務
		es.submit(mt);
		es.submit(mt);
	}
}

問題2:Future是什麼?

在這裏插入圖片描述

  • 方法:V get() : 獲取計算完成的結果。
  • 需求:通過Callable計算從1到5的和

A:我們自定義類,實現Callable接口

B:實現Call方法,Call方法有返回值

C:然後吧任務類對象交給線程池執行

D:執行完成的結果保存Future中

E:最後我們調用Future的get方法拿到真正的結果。

/*
 * 演示:帶返回值的線程任務
 * 需求:通過Callable計算從1到任意數字的和
 */
class SumTask implements Callable<Integer>{
	
	@Override
	public Integer call() throws Exception {
		int sum = 0;
		for(int i = 1; i <= 5; i++){
			sum += i;
		}
		return sum ;
	}
}
public class CallableDemo02 {
	public static void main(String[] args) throws InterruptedException, ExecutionException {
		// 創建任務對象
		SumTask st = new SumTask();
		// 獲取線程池
		ExecutorService es = Executors.newFixedThreadPool(1);
		// 執行任務
		Future<Integer> future = es.submit(st);
		// 等待運算結束,獲取結果
		Integer i = future.get();
		System.out.println(i);
	}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章