(一)同步容器类
1. 同步容器的分类:
a. 早期JDK的同步容器类包括Vector和Hashtable;
b. JDK1.2种添加的一些功能相似的类,这些同步的封装器类是由Collections.synchronizedXxx等工厂方法创建。这些类实现线程安全的方式是:将它们的状态封装起来,并对每个公有方法都进行同步,使得每次只有一个线程能访问容器的状态。
2. 同步容器的问题--单个公有方法都是同步的(具有原子性),但有些复合操作不具有原子性,使用时应小心
在Vector中,线程安全指的是:增删改查方法都是原子性的,在同一时刻,只能由一个线程访问,而复合操作是非原子性的,当多个线程交叉访问不同的同步方法时,会出现线程非安全的情况;
如下代码演示了会出问题的复合操作:向vector放入5个不重复的元素,放入元素前先判断集合中是否已经存在--使用contains方法,如果不存在则加入--使用add方法,其中contains方法和add方法都是线程同步的,并给出了在JDK中的源码:
Vector中的contains源码:
public boolean contains(Object o) {
return indexOf(o, 0) >= 0;
}
public synchronized int indexOf(Object o, int index) {
if (o == null) {
for (int i = index ; i < elementCount ; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = index ; i < elementCount ; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
Vector中的add源码:
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
1. package test;
2. import java.util.Vector;
3. import java.util.concurrent.CyclicBarrier;
4. public class MyVector2 {
5. final static Vector<String> vector = new Vector<String>();
6. final static CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
7. public static void main(String[] args) throws InterruptedException {
8. testThread();//启动两个线程同时往容器中添加元素
9. Thread.sleep(100);//暂停100毫秒,等待两个线程执行完
10. vector.sort(null);
11. for(int j = 0; j < vector.size(); j++){
12. System.out.println("vector.size() = " + vector.size() + " , " + vector.get(j));
13. }
14. System.out.println(Thread.currentThread().getName() + ",執行完毕。");
15. }
16. public static void testThread(){
17. new Thread(new Runnable(){
18. public void run(){
19. addNumber();
20. }
21. },"t1").start();;
22. new Thread(new Runnable(){
23. public void run(){
24. addNumber();
25. }
26. },"t2").start();
27. }
28.
29. public static void addNumber()
30. {
31. System.out.println(Thread.currentThread().getName() + ",到达");
32. try {
33. cyclicBarrier.await();//使用栅栏,当两个线程都到达后,一起往容器中添加元素
34. } catch (Exception e) {
35. e.printStackTrace();
36. }
37. System.out.println(Thread.currentThread().getName() + ",开始执行");
38. for(int i = 0; i < 5; i++){
39. String str = i + "";
40. if(!vector.contains(str))
41. {
42. System.out.println(Thread.currentThread().getName() + ",str = " + str);
43. vector.add(str);
44. }
45. }
46. }
47. }
其中一次地运行结果如下:
t1,到达
t2,到达
t1,开始执行
t2,开始执行
t1,str = 0
t2,str = 0
t1,str = 1
t1,str = 2
t1,str = 3
t1,str = 4
t2,str = 3
vector.size() = 7 , 0
vector.size() = 7 , 0
vector.size() = 7 , 1
vector.size() = 7 , 2
vector.size() = 7 , 3
vector.size() = 7 , 3
vector.size() = 7 , 4
main,執行完毕。
根据代码的输出结果,我做了如下分析:
a. 在代码中使用了栅栏,当两个线程t1和t2都到达第33行时,这两个线程开始执行第38行的for循环;
b. 第一次循环:由于第40行的方法contains是同步方法,在同一时刻只能有一个线程在执行,根据输出得知,t1先获取到的contains方法的执行权,t2被阻塞;当线程t1执行完第40行代码,而没执行第43行代码时,中间会有一个非常短的时间,在这个非常短的时间会发生如下事情:此时容器中是空的,线程t1执行contains方法后返回true,线程t1释放锁,线程t2获取锁,线程t2获取锁之后,接着执行contains方法,由于此时容器中没有元素0,所以也会返回true;线程t2释放锁之后,线程t1要开始执行add方法,线程t1在执行add方法之前,必须先获取锁,然后才能执行add方法,线程t1在执行完add方法后释放锁,此时容器中就有了个元素0;线程t2这时候开始执行add方法,由于在执行contains方法时,元素0还不在容器中,所以线程t2也会把元素0放入到容器中,此时,容器中就有了两个0元素;
c. 第二、三次循环与第五次循环情况一样:由于线程t1执行的较快,当线程t1执行完add方法后(元素1、2、4已经添加到容器中),线程t2才开始执行contains方法,由于元素1、2、4已经在容器中了,所以线程t2不能将元素1和元素2添加到容器中了;
d. 第四次循环与第一次循环的情况一样,所以容器中含有两个元素3:
若向容器中,添加5个不同的元素,则需要将这个复合操作加锁,其改动后的代码如下:
public static void addNumber()
{
System.out.println(Thread.currentThread().getName() + ",到达");
try {
cyclicBarrier.await();//使用栅栏,当两个线程都到达后,一起往容器中添加元素
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ",开始执行");
// synchronized(vector){第一种加锁地点
for(int i = 0; i < 5; i++){
String str = i + "";
// synchronized(vector){第二种加锁地点
if(!vector.contains(str))
{
System.out.println(Thread.currentThread().getName() + ",str = " + str);
vector.add(str);
}
// }
}
// }
}
若是对addNumber整个方法加锁,就会出现死锁,其错误代码如下:
1. package test;
2. import java.util.Vector;
3. import java.util.concurrent.CyclicBarrier;
4. public class MyVector2 {
5. final static Vector<String> vector = new Vector<String>();
6. final static CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
7. public static void main(String[] args) throws InterruptedException {
8. testThread();//启动两个线程同时往容器中添加元素
9. Thread.sleep(100);//暂停100毫秒,等待两个线程执行完
10. vector.sort(null);
11. for(int j = 0; j < vector.size(); j++){
12. System.out.println("vector.size() = " + vector.size() + " , " + vector.get(j));
13. }
14. System.out.println(Thread.currentThread().getName() + ",執行完毕。");
15. }
16. public static void testThread(){
17. new Thread(new Runnable(){
18. public void run(){
19. addNumber();
20. }
21. },"t1").start();;
22. new Thread(new Runnable(){
23. public void run(){
24. addNumber();
25. }
26. },"t2").start();
27. }
28.
29. public static synchronized void addNumber()
30. {
31. System.out.println(Thread.currentThread().getName() + ",到达");
32. try {
33. cyclicBarrier.await();//使用栅栏,当两个线程都到达后,一起往容器中添加元素
34. } catch (Exception e) {
35. e.printStackTrace();
36. }
37. System.out.println(Thread.currentThread().getName() + ",开始执行");
38. for(int i = 0; i < 5; i++){
39. String str = i + "";
40. if(!vector.contains(str))
41. {
42. System.out.println(Thread.currentThread().getName() + ",str = " + str);
43. vector.add(str);
44. }
45. }
46. }
47. }
出现死锁的原因是:当线程t1开始执行addNumber方式时,要先获取锁,当获取到锁之后并执行到第33行时,线程t1会在此等候第二个线程t2的到来,此时由于线程t1无法释放锁,线程t2一直处于阻塞状态,这时,就出现了死锁情况。