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采用锁操作得到的结果是一样的,都是先锁住对象,遍历完成再进行其他操作