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採用鎖操作得到的結果是一樣的,都是先鎖住對象,遍歷完成再進行其他操作