多執行緒筆記 一

CaptainZ發表於2018-05-16

多執行緒筆記一

多執行緒筆記二

多執行緒筆記三

多執行緒相關問題

Callable and Future

  • 一般來說 Runnable 使用來包裝一段程式碼,使之在另外一個執行緒工作。這種方式的侷限性便在於不在執行過程中返回一個結果。想要獲得返回結果的唯一方法便是在外部定義一個變數。
  • Callable 是在java5 被引入的,用來執行一段非同步程式碼的。Callable有一個基礎方法叫做call。call方法比之runnable 方法,有一個額外的功能,返回結果並且允許丟擲檢查異常。
callable 返回的結果通常會封裝成一個Future物件;
Callable Interface
public interface Callable<V> {
V call() throws Exception;
}
複製程式碼
Future Interface
interface Future<V> {
V get();
V get(long timeout, TimeUnit unit);
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
}
複製程式碼
示例
public class ComplexCalculator implements Callable<String> {
@Override
public String call() throws Exception {
// just sleep for 10 secs to simulate a lengthy computation
Thread.sleep(10000);
System.out.println("Result after a lengthy 10sec calculation");
return "Complex Result"; // the result
}
}
複製程式碼
public static void main(String[] args) throws Exception {

ExecutorService es = Executors.newSingleThreadExecutor();

System.out.println("Time At Task Submission : " + new Date());

Future<String> result = es.submit(new ComplexCalculator());
// the call to Future.get() blocks until the result is available.So //we are in for about a 10 sec wait now
System.out.println("Result of Complex Calculation is : " +result.get());
System.out.println("Time At the Point of Printing the Result : " + new Date());
}
複製程式碼

Future 方法介紹

  • get() 獲取Future 執行完之後的實際結果
  • get(long timeout, TimeUnit unit)定義最大超時時間
  • cancel(mayInterruptIfRunning) mayInterruptIfRunning 變數是用來標誌,如果這個task 已經啟動或者正在執行,是否可以停止
  • isDone() 用來檢查這個任務是否執行完畢
  • isCancelled() 用來檢查 我們取消的task 是否已經被取消。

CountDownLatch


  • CountDownLatch 是一種同步工具,讓你一個或者多個執行緒能夠等待另一組執行緒執行完操作。
  1. CountDownLatch 初始化需要有固定的數量
  2. await方法將會一直阻塞,如果countDown() 呼叫次數依然沒有超過初始化的givennumber,在countdown 次數執行完畢之後,所有等待的執行緒都會被釋放,在此之後的任何呼叫都會迅速返回結果。
  3. 這是一個一次性策略,如果你需要重新設定數量沒那麼請使用CyclicBarrier。
關鍵方法
  • public void countDown() 會導致當前執行緒await 只到latch 數到零。除非阻塞執行緒(非當前執行緒)被阻塞。
  • public void countDown() 對當前的latch 的數量進行--1,當數量達到0 時候釋放所有等待的執行緒·
示例程式碼
import java.util.concurrent.*;

class DoSomethingInAThread implements Runnable {
    CountDownLatch latch;

    public DoSomethingInAThread(CountDownLatch latch) {
        this.latch = latch;
    }

    public void run() {
        try {
            System.out.println("Do some thing");
            latch.countDown();
        } catch (Exception err) {
            err.printStackTrace();
        }
    }
}

public class CountDownLatchDemo {
    public static void main(String[] args) {
        try {
            int numberOfThreads = 5;
            if (args.length < 1) {
                System.out.println("Usage: java CountDownLatchDemo numberOfThreads");
                return;
            }
            try {
                numberOfThreads = Integer.parseInt(args[0]);
            } catch (NumberFormatException ne) {
            }
            CountDownLatch latch = new CountDownLatch(numberOfThreads);
            for (int n = 0; n < numberOfThreads; n++) {
                Thread t = new Thread(new DoSomethingInAThread(latch));
                t.start();
            }
            latch.await();
            System.out.println("In Main thread after completion of " + numberOfThreads + "
                    threads");
        } catch (Exception err) {
            err.printStackTrace();
        }
    }
}
複製程式碼

備註:

  1. CountDownLatch 在主執行緒初始化,count為5
  2. 通過呼叫await 方法阻塞主執行緒.
  3. 總共建立了 五個DoSomethingInAThread物件,每個物件都通過執行countDown 減少 count。
  4. 一旦couter 變成0 那麼主執行緒狀態就會被重新恢復。

多執行緒基礎


  • 使用條件:如果你有很多工要進行處理,而且這些任務不依賴當前的資料,那麼你就可以使用多執行緒。
class CountAndPrint implements Runnable {
    private final String name;
    CountAndPrint(String name) {
        this.name = name;
    }
    /** This is what a CountAndPrint will do */
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            System.out.println(this.name + ": " + i);
        }
    }
    public static void main(String[] args) {
// Launching 4 parallel threads
        for (int i = 1; i <= 4; i++) {
// `start` method will call the `run` method
// of CountAndPrint in another thread
            new Thread(new CountAndPrint("Instance " + i)).start();
        }
// Doing some others tasks in the main Thread
        for (int i = 0; i < 10000; i++) {
            System.out.println("Main: " + i);
        }
    }
}

複製程式碼

加鎖同步措施

鎖是同步機制的基礎,一般用來同步程式碼塊或者關鍵字

固有鎖
int count = 0; // shared among multiple threads
public void doSomething() {
synchronized(this) {
        ++count; // a non-atomic operation
        }
        }

複製程式碼
重入鎖

    int count = 0; // shared among multiple threads
    Lock lockObj = new ReentrantLock();
    public void doSomething() {
        try {
            lockObj.lock();
            ++count; // a non-atomic operation
        } finally {
            lockObj.unlock(); // sure to release the lock without fail
        }
    }

複製程式碼
  • 鎖提供了一些固有鎖沒有的的功能。例如在加鎖的同時允許被打斷,或者在在一定條件下才允許加鎖。
加鎖的同時允許被打斷
    class Locky {
        int count = 0; // shared among multiple threads
        Lock lockObj = new ReentrantLock();
        public void doSomething() {
            try {
                try {
                    lockObj.lockInterruptibly();
                    ++count; // a non-atomic operation
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt(); // stopping
                }
            } finally {
                if (!Thread.currentThread().isInterrupted()) {
                    lockObj.unlock(); // sure to release the lock without fail
                }
            }
        }
    }
複製程式碼
只有在符合某些條件下才會被加鎖
    public class Locky2 {
        int count = 0; // shared among multiple threads
        Lock lockObj = new ReentrantLock();
        public void doSomething() {
            boolean locked = lockObj.tryLock(); // returns true upon successful lock
            if (locked) {
                try {
                    ++count; // a non-atomic operation
                } finally {
                    lockObj.unlock(); // sure to release the lock without fail
                }
            }
        }
    }
複製程式碼

Semaphore(訊號燈)

Semaphore是一個高度同步的類,其維護一組許可,這些許可可以被請求以及釋放。Semaphore 可以想象成一個一組許可的計數器。當有執行緒請求的時候計數器-1,有執行緒釋放的時候計數器+1. 如果計數器 為0那麼當有執行緒嘗試發起請求的時候,那麼這個執行緒將會一直阻塞,只到許可條件產生。
Semaphore初始化方式
  • Semaphore semaphore = new Semaphore(1);

  • Semaphore 構造器接受一個boolean 值 ,用來作為公平鎖。如果設定成false,那麼將不保證公平性,即不保證執行緒請求順序。當 設定成true 時,將會保證執行緒呼叫順序與請求順序一直。

  • Semaphore semaphore = new Semaphore(1, true);

  • 現在我們看一下用Semaphore 控制一組物件,Semaphore被用來提供阻塞功能,以便保證當getItem() 被呼叫的時候一直能夠取到資料。

   class Pool {
        /*
        * Note that this DOES NOT bound the amount that may be released!
        * This is only a starting value for the Semaphore and has no other
        * significant meaning UNLESS you enforce this inside of the
        * getNextAvailableItem() and markAsUnused() methods
        */
        private static final int MAX_AVAILABLE = 100;
        private final Semaphore available = new Semaphore(MAX_AVAILABLE, true);
        /**
         * Obtains the next available item and reduces the permit count by 1.
         * If there are no items available, block.
         */
        public Object getItem() throws InterruptedException {
            available.acquire();
            return getNextAvailableItem();
        }
        /**
         * Puts the item into the pool and add 1 permit.
         */
        public void putItem(Object x) {
            if (markAsUnused(x))
                available.release();
        }
        private Object getNextAvailableItem() {
// Implementation
        }
        private boolean markAsUnused(Object o) {
// Implementation
        }
    }
複製程式碼

Runnable Object

Runnable interface 定義一個run()方法,在其中的程式碼都在新的執行緒執行。 Runnable物件可以傳遞給Thread 構造器。Thread 的 start 方法被呼叫便意味著 runnable 物件被執行了。

示例程式碼
  public class HelloRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("Hello from a thread");
        }
        public static void main(String[] args) {
            new Thread(new HelloRunnable()).start();
        }
    }

   public static void main(String[] args) {
        Runnable r = () -> System.out.println("Hello world");
        new Thread(r).start();
    }

複製程式碼

Runnable vs Thread subclass

  • Runnable物件管理較為容易,Runnable比Thread更容易建立子類物件
  • Thread 子類在簡單應用比較容易,由於其必須繼承Thread 限制了其的使用
  • Runnable物件適合較為複雜的執行緒管理

建立一個死鎖

死鎖通常發生在兩個互相等待結束的競爭操作,但是並沒有。在java中一個lock 與每個物件都有關係。為了避免多執行緒同步修改一個物件這種情況,我們可以使用一個叫做synchronized 的關鍵字,不過還是要付出一些代價的。錯誤的使用synchronized可能會產生死鎖。
考慮到有兩個執行緒在操作一個例項,我們給執行緒定義為1,2,然後假設我們有兩個資原始檔R1,R2。執行緒11去請求R1而且需要R2,但是R2 被執行緒2 佔用了 執行緒2 需要執行緒1,這樣下去就比較刺激了 執行緒1 得到了R1 ,執行緒2 得到R2 。雙方都在等待互相釋放物件,那麼神奇的死鎖便產生了。
示例程式碼
 public class Example2 {
        public static void main(String[] args) throws InterruptedException {
            final DeadLock dl = new DeadLock();
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
// TODO Auto-generated method stub
                    dl.methodA();Java® Notes for Professionals 676
                }
            });
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
// TODO Auto-generated method stub
                    try {
                        dl.method2();
                    } catch (InterruptedException e) {
// TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            });
            t1.setName("First");
            t2.setName("Second");
            t1.start();
            t2.start();
        }
    }
    class DeadLock {
        Object mLock1 = new Object();
        Object mLock2 = new Object();
        public void methodA() {
            System.out.println("methodA wait for mLock1 " + Thread.currentThread().getName());
            synchronized (mLock1) {
                System.out.println("methodA mLock1 acquired " + Thread.currentThread().getName());
                try {
                    Thread.sleep(100);
                    method2();
                } catch (InterruptedException e) {
// TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
        public void method2() throws InterruptedException {
            System.out.println("method2 wait for mLock2 " + Thread.currentThread().getName());
            synchronized (mLock2) {
                System.out.println("method2 mLock2 acquired " + Thread.currentThread().getName());
                Thread.sleep(100);
                method3();
            }
        }
        public void method3() throws InterruptedException {
            System.out.println("method3 mLock1 "+ Thread.currentThread().getName());
            synchronized (mLock1) {
                System.out.println("method3 mLock1 acquired " + Thread.currentThread().getName());
            }
        }
    }
複製程式碼

建立一個Thread執行緒例項

在java 中主要有兩種方式建立執行緒,一般來說之間新建一個執行緒然後執行比較容易。兩種方式的主要區別就是在哪兒建立執行程式碼。 在java中一個執行緒就是一個物件,Thread的例項或者子類物件。所以第一種建立執行緒的方式就是建立其子類,然後復現run 方法。
示例程式碼
class MyThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println("Thread running!");
            }
        }
    }
    
    MyThread t = new MyThread();
複製程式碼
Thread 類還可以接受一個String 值作為構造方法,這在多執行緒程式設計除錯的時候會特別好用

    class MyThread extends Thread {
        public MyThread(String name) {
            super(name);
        }
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println("Thread running! ");
            }
        }
    }
    
    MyThread t = new MyThread("Greeting Producer");
複製程式碼
第二種建立執行緒的方式是用一個runnable 物件。然後就會在一個單獨的執行緒中執行runnable中run 的操作。
Thread t = new Thread(aRunnable);
當然你還可以這樣定義
Thread t = new Thread(operator::hardWork, "Pi operator");
一般來說,這兩種建立方式你都可以放心使用,但是明智的做法應該採用後者。
下面我們來說一下本文要說的第四種
ThreadGroup tg = new ThreadGroup("Operators");
Thread t = new Thread(tg, operator::hardWork, "PI operator");
複製程式碼
所以我們來總結一下,執行緒可以通過如下構造
Thread()
Thread(String name)
Thread(Runnable target)
Thread(Runnable target, String name)
Thread(ThreadGroup group, String name)
Thread(ThreadGroup group, Runnable target)
Thread(ThreadGroup group, Runnable target, String name)
Thread(ThreadGroup group, Runnable target, String name, long stackSize)

複製程式碼
最後一個允許我們定義個一個期望的size 來建立新的執行緒
通常來說建立很多相同屬性的執行緒會讓你十分痛苦。這時候就該 java.util.concurrent.ThreadFactory 登場了。這個介面可以通過工廠模式極大的減少我們建立執行緒的過程,該介面使用十分簡單newThread(Runnable)。
 class WorkerFactory implements ThreadFactory {
        private int id = 0;
        @Override
        public Thread newThread(Runnable r) {
            return new Thread(r, "Worker " + id++);
        }
    }
    
複製程式碼

原子操作

原子操作指的是一次操作期間這個物件只能被執行一次,在執行操作期間其他執行緒是沒有機會觀察或者改變狀態。
我們來看一下負面案例
   private static int t = 0;
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(400); // The high thread count
        is for demonstration purposes.
        for (int i = 0; i < 100; i++) {
            executorService.execute(() -> {
                t++;
                System.out.println(MessageFormat.format("t: {0}", t));
            });
        }
        executorService.shutdown();
    }
複製程式碼
這個案例存在兩個問題。第一個就是++1 這個行為不是原子的。他包含多個操作:獲取值,+1,賦值。這就是為什麼當我們允許這個樣例的時候,我們基本上看不到t:100.第二個問題兩個執行緒有可能同時獲取值然後+1賦值。當我們假設當前t=10,然後兩個執行緒同時操作t。兩個執行緒都對t 設定成11,既然這樣後面執行的程式可以獲取t的值了,即便執行緒1還沒有關閉。
為了避免這種情況,我們一般使用java.util.concurrent.atomic.AtomicInteger,該類可以為我們提供很多原子操作。

    private static AtomicInteger t = new AtomicInteger(0);
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(400); // The high thread count
        is for demonstration purposes.
        for (int i = 0; i < 100; i++) {
            executorService.execute(() -> {
                int currentT = t.incrementAndGet();
                System.out.println(MessageFormat.format("t: {0}", currentT));
            });
        }
        executorService.shutdown();
    }

複製程式碼
incrementAndGet 方法會自動遞增然後返回最新值。這樣就消除了之前的那種競爭條件 race condition。但是要注意在這個案例中輸出的依舊是無序的,這是因為我們對於列印輸出沒有做任何處理,這個案例僅僅展示如何使用AtomicInteger來避免競爭條件。

相關文章