1、什麼是JAVA 內存模型
從上圖來看,線程A與線程B之間如要通信的話,必須要經歷下面2個步驟:
1、線程A把本地內存A中更新過的共享變量刷新到主內存中去。
2、線程B到主內存中去讀取線程A之前已更新過的共享變量。
下面通過示意圖來說明這兩個步驟:
如上圖所示,本地內存A和B有主內存中共享變量x的副本。假設初始時,這三個內存中的x值都爲0。線程A在執行時,把更新後的x值(假設值爲1)臨時存放在自己的本地內存A中。當線程A和線程B需要通信時,線程A首先會把自己本地內存中修改後的x值刷新到主內存中,此時主內存中的x值變爲了1。隨後,線程B到主內存中去讀取線程A更新後的x值,此時線程B的本地內存的x值也變爲了1。
從整體來看,這兩個步驟實質上是線程A在向線程B發送消息,而且這個通信過程必須要經過主內存。JMM通過控制主內存與每個線程的本地內存之間的交互,來爲java程序員提供內存可見性保證。
2、什麼是內存可見性?
內存可見性的概念: 線程之間通信,有主存和工作內存的概念,線程執行的一般三個步驟: 從主存讀取數據 修改數據 寫會主存 如果一個線程執行修改以後不能及時同步到共享內存中 則對B線程來說 數據是不可見的 下面是一個簡單例子:
- class MyThread implements Runnable {
- int num = 1000000;
- public void run() {
- if (Thread.currentThread().getName().equals("t1")) {
- increment();
- } else {
- decrement();
- }
- }
- public void increment() {
- for (int i = 0; i < 10000; i++) {
- num++;
- }
- }
- public void decrement() {
- for (int i = 0; i < 10000; i++) {
- num--;
- }
- }
- }
- public class Test {
- public static void main(String[] args) {
- MyThread thread = new MyThread();
- Thread a = new Thread(thread, "t1");
- Thread b = new Thread(thread, "t2");
- a.start();
- b.start();
- try {
- a.join();
- b.join();
- } catch (Exception e) {
- e.printStackTrace();
- }
- System.out.println(thread.num);
- }
- }
從上面代碼可以看出,這裏有兩個線程,其中一個對num執行1000次加1操作,另一個線程執行1000次減1操作,按理說最後num的值是不變的,但是當你運行後,發現num的值可能並不是初始值。那麼爲什麼會有這種問題呢?這是內存不可見引起的。
從上圖中我們可以看到,當線程1對num值加一以後,還未把最新值寫入主內存,CPU就停止了線程1的執行,並且執行線程2,線程2首先從主內存中獲取num的值,然後減一,最後把值更新到主內存中,這個時候,CPU終止了線程2的執行,轉而繼續執行線程1, 這個時候線程1把最新值刷入主內存,所以主內存結果變爲了1000001.
通過上面的分析,我們得知:內存不可見是由於共享變量的值沒有及時在主內存中更新,爲什麼沒有及時更新呢?是因爲加一(或者減一)的操作不具備原子性(例子中最後一步被打斷)。那麼如何保證操作具有原子性呢?這裏我們引入synchronized關鍵字。
3、Synchronized關鍵字
synchronized用來給對象和方法或者代碼塊加鎖,當它鎖定一個方法或者一個代碼塊的時候,同一時刻最多隻有一個線程執行這段代碼。當兩個併發線程訪問同一個對象object中的這個加鎖同步代碼塊時,一個時間內只能有一個線程得到執行。另一個線程必須等待當前線程執行完這個代碼塊以後才能執行該代碼塊。然而,當一個線程訪問object的一個加鎖代碼塊時,另一個線程仍然可以訪問該object中的非加鎖代碼塊。
JMM關於synchronized的兩條規定:
1. 線程解鎖前,必須把共享變量的最新值刷新到主內存中
2. 線程加鎖時,講清空工作內存中共享變量的值,從而使用共享變量時需要從主內存中重新讀取最新的值。
這樣,線程解鎖前對共享變量的修改在下次加鎖時對其他線程可見。
所以,爲了保證num在某個時刻的修改具有原子性,我們可以在下面兩個方法前加synchronized. 如果你zhi'dui
- public synchronized void increment() {
- for (int i = 0; i < 10000; i++) {
- num++;
- }
- }
- public synchronized void decrement() {
- for (int i = 0; i < 10000; i++) {
- num--;
- }
- }
- class MyThread implements Runnable {
- int num = 1000000;
- Lock lock = new ReentrantLock();
- public void run() {
- if (Thread.currentThread().getName().equals("t1")) {
- increment();
- } else {
- decrement();
- }
- }
- public void increment() {
- for (int i = 0; i < 10000; i++) {
- lock.lock();
- num++;
- lock.unlock();
- }
- }
- public void decrement() {
- for (int i = 0; i < 10000; i++) {
- lock.lock();
- num--;
- lock.unlock();
- }
- }
- }
你可能會問:我們可否在num前面加volatile 達到內存可見性呢? 答案是否定的,volatile實現共享變量內存可見性有一個條件,就是對共享變量的操作必須具有原子性。比如 num = 10; 這個操作具有原子性,但是 num++ 或者num--由3步組成,並不具有原子性,所以是不行的。