對於普通的變量,在涉及多線程操做時,會遇到經典的線程安全問題。考慮以下代碼:java
private static final int TEST_THREAD_COUNT = 100; private static int counter = 0; public static void main(String[] args) { final CountDownLatch latch = new CountDownLatch(TEST_THREAD_COUNT); Thread[] threads = new Thread[TEST_THREAD_COUNT]; for (int i = 0; i < TEST_THREAD_COUNT; i++) { threads[i] = new Thread(new Runnable() { @Override public void run() { ++counter; System.out.println("Thread " + Thread.currentThread().getId() + " / Counter : " + counter); latch.countDown(); } }); threads[i].start(); } try { latch.await(); System.out.println("Main Thread " + " / Counter : " + counter); } catch (InterruptedException e) { e.printStackTrace(); } }
屢次執行這段程序,咱們會發現最後counter
的值會出現98
,99
等值,而不是預想中的100
。程序員
... ... Thread 100 / Counter : 90 Thread 101 / Counter : 91 Thread 102 / Counter : 92 Thread 103 / Counter : 93 Thread 104 / Counter : 95 Thread 105 / Counter : 95 Thread 106 / Counter : 96 Thread 107 / Counter : 97 Thread 108 / Counter : 98 Thread 109 / Counter : 99 Main Thread / Counter : 99
這個問題發生的緣由是++counter
不是一個原子性操做。當要對一個變量進行計算的時候,CPU須要先從內存中將該變量的值讀取到高速緩存中,再去計算,計算完畢後再將變量同步到主內存中。這在多線程環境中就會遇到問題,試想一下,線程A從主內存中複製了一個變量a
=3到工做內存,而且對變量a
進行了加一操做,a
變成了4,此時線程B也從主內存中複製該變量到它本身的工做內存,它獲得的a的值仍是3,a
的值不一致了(這裏工做內存就是高速緩存)。緩存
java有個sychronized
關鍵字,它能後保證同一個時刻只有一條線程可以執行被關鍵字修飾的代碼,其餘線程就會在隊列中進行等待,等待這條線程執行完畢後,下一條線程才能對執行這段代碼。
它的修飾對象有如下幾種:安全
如今咱們開始使用咱們的新知識,調整以上代碼,在run()
上添加sychronized
關鍵字。多線程
private static final int TEST_THREAD_COUNT = 100; private static int counter = 0; public static void main(String[] args) { final CountDownLatch latch = new CountDownLatch(TEST_THREAD_COUNT); Thread[] threads = new Thread[TEST_THREAD_COUNT]; for (int i = 0; i < TEST_THREAD_COUNT; i++) { threads[i] = new Thread(new Runnable() { @Override public synchronized void run() { ++counter; System.out.println("Thread " + Thread.currentThread().getId() + " / Counter : " + counter); latch.countDown(); } }); threads[i].start(); } try { latch.await(); System.out.println("Main Thread " + " / Counter : " + counter); } catch (InterruptedException e) { e.printStackTrace(); } }
屢次執行新代碼,咱們依舊發現結果不正確:ide
... ... Thread 98 / Counter : 87 Thread 97 / Counter : 86 Thread 99 / Counter : 89 Thread 100 / Counter : 89 Thread 101 / Counter : 90 Thread 102 / Counter : 91 Thread 104 / Counter : 95 Thread 108 / Counter : 97 Thread 106 / Counter : 96 Thread 105 / Counter : 95 Thread 103 / Counter : 95 Thread 109 / Counter : 98 Thread 107 / Counter : 97 Main Thread / Counter : 98
這裏的緣由在於synchronized
是鎖定當前實例對象的代碼塊。也就是當多條線程操做同一個實例對象的同步方法是時,只有一條線程能夠訪問,其餘線程都須要等待。這裏Runnable
實例有多個,因此鎖就不起做用。
咱們繼續修改代碼,使得Runnable
實例只有一個:性能
private static final int TEST_THREAD_COUNT = 100; private static int counter = 0; private final static CountDownLatch latch = new CountDownLatch(TEST_THREAD_COUNT); static class MyRunnable implements Runnable { @Override public synchronized void run() { ++counter; System.out.println("Thread " + Thread.currentThread().getId() + " / Counter : " + counter); latch.countDown(); } } public static void main(String[] args) { Thread[] threads = new Thread[TEST_THREAD_COUNT]; MyRunnable myRun = new MyRunnable(); for (int i = 0; i < TEST_THREAD_COUNT; i++) { threads[i] = new Thread(myRun); threads[i].start(); } try { latch.await(); System.out.println("Main Thread " + " / Counter : " + counter); } catch (InterruptedException e) { e.printStackTrace(); } }
如今咱們發現屢次執行代碼後,最後結果都是100
。
咱們能夠給counter
變量添加volatile
關鍵字(這裏它對於結果沒有影響)。
當一個變量被定義爲volatile以後,它對全部的線程就具備了可見性,也就是說當一個線程修改了該變量的值,全部的其它線程均可以當即知道。經過synchronized
和Lock
也可以保證可見性,synchronized
和Lock
能保證同一時刻只有一個線程獲取鎖而後執行同步代碼,而且在釋放鎖以前會將對變量的修改刷新到主存當中。所以能夠保證可見性。優化
synchronized
在發生異常時,會自動釋放線程佔有的鎖,所以不會致使死鎖現象發生。另外在資源競爭不是很激烈的狀況下,偶爾會有同步的情形下,synchronized是很合適的。緣由在於,編譯程序一般會盡量的進行優化synchronized
,另外可讀性很是好,無論用沒用過5.0多線程包的程序員都能理解。可是當同步競爭很是激烈的時候,synchronized
的性能一會兒會降低幾十倍。還有一個最大的問題就是多線程競爭一個鎖時,其他未獲得鎖的線程只能不停的嘗試得到鎖,而不能中斷。這種狀況下就會形成大量的競爭線程性能的降低。atom
針對synchronized的一系列缺點,JDK5提供了Lock
類,目的是爲同步機制進行改善。Lock
和synchronized
有一點很是大的不一樣,採用synchronized不須要用戶手動的去釋放鎖,當synchronized
方法或者代碼塊執行完畢以後,系統會自動的讓線程釋放對鎖的佔有,而Lock
則必需要用戶去手動釋放鎖,若是沒有主動的釋放鎖,就會可能致使出現死鎖的現象。不過這篇文章這裏不討論Lock
類。
在Java 1.5的java.util.concurrent.atomic包下提供了一些原子操做類,即對基本數據類型的 自增(加1操做),自減(減1操做)、以及加法操做(加一個數),減法操做(減一個數)進行了封裝,保證這些操做是原子性操做。
咱們這裏使用AtomicInteger
類spa
private static final int TEST_THREAD_COUNT = 100; private static AtomicInteger at = new AtomicInteger(0); public static void main(String[] args) { final CountDownLatch latch = new CountDownLatch(TEST_THREAD_COUNT); Thread[] threads = new Thread[TEST_THREAD_COUNT]; for (int i = 0; i < TEST_THREAD_COUNT; i++) { threads[i] = new Thread(new Runnable() { @Override public void run() { int value = at.incrementAndGet(); System.out.println("Thread " + Thread.currentThread().getId() + " / Counter : " + value); latch.countDown(); } }); threads[i].start(); } try { latch.await(); System.out.println("Main Thread " + " / Counter : " + at.get()); } catch (InterruptedException e) { e.printStackTrace(); } }