Java併發(一)線程安全

1. 什麼是線程安全和非安全

什麼是線程安全和非安全
如上圖所示,所謂線程安全就是指在多個線程同時訪問一個公共對象,不會因爲多個線程併發讀寫,造成數據錯誤的情況。

比如:同時啓動100個線程,對一個list進行add 100個數據操作,對於非安全對象list在執行過程中,會有併發寫的情況,造成數據丟失。

public class Test {
    public static void main(String [] args){
        // 用來測試的List  
        List<String> data = new ArrayList<>();
        // 用來讓主線程等待100個子線程執行完畢  
        CountDownLatch countDownLatch = new CountDownLatch(100);
        // 啓動100個子線程  
        for(int i=0;i<100;i++){
            SampleTask task = new SampleTask(data,countDownLatch);
            Thread thread = new Thread(task);
            thread.start();
        }
        try{
            // 主線程等待所有子線程執行完成,再向下執行  
            countDownLatch.await();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        // List的size  
        System.out.println(data.size());
    }
}
class SampleTask implements Runnable {
    CountDownLatch countDownLatch;
    List<String> data;
    public SampleTask(List<String> data,CountDownLatch countDownLatch){
        this.data = data;
        this.countDownLatch = countDownLatch;
    }
    @Override
    public void run() {
        // 每個線程向List中添加100個元素  
        for(int i = 0; i < 100; i++)
        {
            data.add("1");
        }
        // 完成一個子線程  
        countDownLatch.countDown();
    }
}
7次測試輸出:
9998
10000
10000
ArrayIndexOutOfBoundsException
10000
9967
9936

2. 哪些對象是線程安全

# 線程安全 線程非安全
List Vector,Stack,CopyOnWriteArrayList,SynchronizedList(類似Vector) ArrayList
Map HashTable(摒棄),ConcurrentHashMap,SynchronizedMap HashMap,TreeMap
Set SynchronizedSet HashSet,TreeSet
Queue BlockingQueue,ConcurrentLinkedQueue
String StringBuffer StringBuilder

3. 如何靈活使用線程安全和非安全對象

Vector性能比ArrayList低,因此在單線程或者多線程內部使用時,儘量用ArrayList,Vector主要用於多線程操作共享變量,當然,對ArrayList增加鎖關鍵字synchronized,也可以自己實現線程安全。

4. 線程安全的遍歷

無論是線程安全還是非安全,在併發操作時,進行遍歷操作,都會出現ConcurrentModificationException異常。

public static void main(String[] args) {

    // 初始化一個list,放入5個元素
    final List<Integer> list = new ArrayList<>();
    for(int i = 0; i < 5; i++) {
        list.add(i);
    }

    // 線程一:通過Iterator遍歷List
    new Thread(new Runnable() {
        @Override
        public void run() {
            for(int item : list) {
                System.out.println("遍歷元素:" + item);
                // 由於程序跑的太快,這裏sleep了1秒來調慢程序的運行速度
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }).start();

    // 線程二:remove一個元素
    new Thread(new Runnable() {
        @Override
        public void run() {
            // 由於程序跑的太快,這裏sleep了1秒來調慢程序的運行速度
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            list.remove(4);
            System.out.println("list.remove(4)");
        }
    }).start();
}
運行結果: 
遍歷元素:0 
遍歷元素:1 
list.remove(4) 
Exception in thread “Thread-0” java.util.ConcurrentModificationException

4.1 解決方法1-鎖操作

如何解決併發遍歷的情況,在遍歷過程中,對對象進行鎖操作。

synchronized (list) {
    for(int item : list) {
        System.out.println("遍歷元素:" + item);
        // 由於程序跑的太快,這裏sleep了1秒來調慢程序的運行速度
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

4.2 解決方法2-CopyOnWriteArrayList

CopyOnWriteArrayList是java.util.concurrent包中的一個List的實現類

使用CopyOnWriteArrayList可以線程安全地遍歷,因爲如果另外一個線程在遍歷的時候修改List的話,實際上會拷貝出一個新的List上修改,而不影響當前正在被遍歷的List。

public static void main(String[] args) {

    // 初始化一個list,放入5個元素
    final List<Integer> list = new CopyOnWriteArrayList<>();
    for(int i = 0; i < 5; i++) {
        list.add(i);
    }

    // 線程一:通過Iterator遍歷List
    new Thread(new Runnable() {
        @Override
        public void run() {
            for(int item : list) {
                System.out.println("遍歷元素:" + item);
                // 由於程序跑的太快,這裏sleep了1秒來調慢程序的運行速度
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }).start();

    // 線程二:remove一個元素
    new Thread(new Runnable() {
        @Override
        public void run() {
            // 由於程序跑的太快,這裏sleep了1秒來調慢程序的運行速度
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            list.remove(4);
            System.out.println("list.remove(4)");
        }
    }).start();
}
運行結果:

遍歷元素:0 
遍歷元素:1 
list.remove(4) 
遍歷元素:2 
遍歷元素:3 
遍歷元素:4

從上面的運行結果可以看出,雖然list.remove(4)已經移除了一個元素,但是遍歷的結果還是存在這個元素。由此可以看出被遍歷的和remove的是兩個不同的List。

4.3 解決方法3-Java8的List.forEach

List.forEach方法是Java 8新增的一個方法,主要目的還是用於讓List來支持Java 8的新特性:Lambda表達式。

由於forEach方法是List的一個方法,所以不同於在List外遍歷List,forEach方法相當於List自身遍歷的方法,所以它可以自由控制是否線程安全。

我們看線程安全的Vector的forEach方法源碼:

public synchronized void forEach(Consumer<? super E> action) {
    ...
}

可以看到Vector的forEach方法上加了synchronized來控制線程安全的遍歷,也就是Vector的forEach方法可以線程安全地遍歷。

public static void main(String[] args) {

    // 初始化一個list,放入5個元素
    final List<Integer> list = new Vector<>();
    for(int i = 0; i < 5; i++) {
        list.add(i);
    }

    // 線程一:通過Iterator遍歷List
    new Thread(new Runnable() {
        @Override
        public void run() {
            list.forEach(item -> {
                System.out.println("遍歷元素:" + item);
                // 由於程序跑的太快,這裏sleep了1秒來調慢程序的運行速度
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    }).start();

    // 線程二:remove一個元素
    new Thread(new Runnable() {
        @Override
        public void run() {
            // 由於程序跑的太快,這裏sleep了1秒來調慢程序的運行速度
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            list.remove(4);
            System.out.println("list.remove(4)");
        }
    }).start();
}
運行結果: 

遍歷元素:0 
遍歷元素:1 
遍歷元素:2 
遍歷元素:3 
遍歷元素:4 
list.remove(4)

這個和方法1採用鎖操作得到的結果是一樣的,都是先鎖住對象,遍歷完成再進行其他操作

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章