Java併發編程之線程安全性

目錄

  1. 引言
  2. 線程安全性
    1.什麼是線程安全性
    2.一些相關概念
    3.如何保證對象的線程安全性
    4.哪些對象是須要使用同步機制的
  3. Java基本的同步方式
    1.synchronized
    2.顯示鎖(lock)
    3.volatile
    4.原子變量

一.引言

現代計算機程序,基本都運行在多核多線程的環境之中,若是不懂併發編程,那麼咱們編寫出來的程序在多線程併發環境下極可能出現不可預知的錯誤,所以掌握並理解併發編程是成爲一名優秀程序員的必備技能。本人將分享Java併發編程的一些知識及學習心得,主要基於對《Java併發編程實戰》這本書的學習和本人平時的一些工做經驗。

二.線程安全性

1.什麼是線程安全性

當多個線程訪問某個類時,這個類始終都能表現出正確的行爲,那麼就稱這個類是線程安全的。

2.一些相關概念

共享:意味着變量能夠被多個線程同時訪問。
可變:意味着變量的值能夠在其生命週期內能夠發生變化。
對象的狀態:能夠理解爲對象的屬性或一組屬性。
原子操做:一組操做,要麼所有執行完,要麼不執行。
複合操做:一組操做,必須保證執行的原子性,才能確保線程安全。
正確性:某個類的行爲與其規範徹底一致,所見即所知。
競態條件:因爲不恰當的執行時序而出現不正確的結果。

正是因爲許多看似原子操做的操做,其實是複合操做,而又沒有加上正確的同步,纔會出現競態條件,也就是多線程併發訪問產生的不正確性。例如,i++自增操做,其實是由「讀取-修改-寫入」這三個操做組成的複合操做,因此在多線程併發的條件下會產生不正確的結果。java

3.如何保證對象的線程安全性

(1)無狀態的對象必定是線程安全的程序員

例如沒有任何屬性的對象。
public class Test {
    public int add(int count) {
        return count++;
    }
}

(2)不在線程之間共享狀態變量編程

例如該對象中屬性只有單線程能夠訪問。

(3)對象狀態變量均爲不可變的安全

例如咱們常常寫的一些常量類,裏面的屬性均爲final修飾的基本類型。
public class Test {
    public static final int count = 10;
}

(4)訪問對象的狀態變量時使用正確的同步多線程

經過使用正確的同步機制訪問對象的狀態變量,java提供了synchronized、顯示鎖、volatile、原子變量等同步機制。一個類的狀態可能由多個變量組成,那麼訪問這些變量的時候要使用同一個鎖。例以下面的test類,該類的狀態由count和hit兩個變量決定,若是在修改兩個變量的時候不使用同一個鎖,以下所示,就會出現錯誤。
package com;
//錯誤示例
public class Test {
    
    private int count;
    private int hit;
    
    public int addHit() {
        //使用Test類對象做爲鎖
        synchronized (Test.class) {
            hit++;
            return hit;
        }
    }
    
    public int addCount() {
        //使用當前對象做爲鎖
        synchronized (this) {
            if(hit % 2 == 0) {
                count++;
                return count;
            } else {
                return -1;
            }
            
        }
    }
    
    public static void main(String[] args) {
        
        Test test = new Test();
        new Thread() {
            @Override
            public void run() {
                // TODO Auto-generated method stub
                for(int i = 0; i <= 100000; i++) {
                    int hit = test.addHit();
                    int count = test.addCount();
                    System.out.println("線程A, hit:" + hit +" count:" + count);
                }                
            }
        }.start();
        new Thread() {
            @Override
            public void run() {
                // TODO Auto-generated method stub
                for(int i = 01; i < 100000; i++) {
                    int hit = test.addHit();
                    int count = test.addCount();
                    System.out.println("線程B,hit:" + hit +" count:" + count);
                }                
            }
        }.start();
    }
}
運行截圖

併發編程錯誤截圖.png

正確寫法:
public class Test {
    
    private int count;
    private int hit;
    
    public int addHit() {
        //線程安全,使用當前對象做爲鎖
        synchronized (this) {
            hit++;
            return hit;
        }
    }
    
    public int addCount() {
        //線程安全,使用當前對象做爲鎖
        synchronized (this) {
            if(hit % 2 == 0) {
                count++;
                return count;
            } else {
                return -1;
            }
            
        }
    }
}
正確結果截圖:

concurrency_right.png

4.哪些對象是須要使用同步機制的

對象的狀態變量是共享和可變的時候,須要使用同步機制對其進行訪問。

三.Java基本同步方式

1.synchronized

Java提供隱式鎖、可重入鎖,其用法:
(1)修飾一段代碼塊
任意java對象皆可做爲鎖,叫作內置鎖,又叫作監視器鎖,內置鎖是可重入的。
public class Test {
    public int count;
    
    public int add(int count) {
        //線程安全,使用當前對象做爲鎖
        synchronized (this) {
            return count++;
        }
    }
}
(2)修飾實例方法
就是使用當前對象做爲鎖。
public class Test {
    public int count;
    
    public synchronized int add(int count) {
        //線程安全,使用當前對象做爲鎖
        return count++;
    }
}
(3)修飾靜態方法
就是使用當前對象的類對象~~~~
public class Test {
public class Test1 {
    private static int count;
    
    /**
     * 使用Test.class對象做爲鎖
     */
    public synchronized static void add() {
        count++;
    }
}

2.volatile

(1)保證了不一樣線程對這個變量進行操做時的可見性,即一個線程修改了某個變量的值,這新值對其餘線程來講是當即可見的。(可見性)
(2)禁止進行指令重排序。(有序性)
(3)volatile 只能保證對單次讀/寫的原子性。

上面這幾條是我在網上找到的對volatile特性的概述,具體詳情本文就不詳述了。可是什麼時候使用volatile,只要記住一句話:當你能確保只有一個線程寫變量的時候,這個變量就能夠用volatile修飾,多少個線程讀沒有關係。volatile比直接加鎖更加輕量級,因此根據場景來使用加鎖仍是volatile。併發

其實在平時工做中,咱們只要能理解併發的概念和場景,而後能正確使用synchronized和volatile,就能應付大部分的併發場景了。

3.顯示鎖

就是使用Java的Lock與ReentrantLock,具體本文暫不詳述。

4.原子變量

使用Java的AtomicInteger等變量,顧名思義這些變量的方法都是原子性的、線程安全的,具體本文暫不詳述。
相關文章
相關標籤/搜索