Java线程安全与非线程安全解析
Java的线程安全问题是非常重要的一个主题,尤其是在多线程程序的开发中。本文将从线程安全和非线程安全的概念入手,深入探讨Java线程安全与非线程安全的区别,并以代码示例详细说明。
线程安全与非线程安全
Java中的线程安全问题可以简单理解为多线程同时访问同一块内存时所出现的问题。如果多个线程并发地访问同一块内存时,程序仍然能够正确地达到预期的结果,那么这个程序就是线程安全的;否则,这个程序就是线程不安全的。
我们来看一个例子:
public class Counter {
private int count;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
上面的代码是一个计数器类,其中有一个count属性,还有两个方法increment和getCount。increment方法是用于将count加一的,getCount方法是获取当前计数器的值。
我们在程序中新建了两个线程,每个线程都获取了Counter实例,并调用了increment方法:
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.getCount());
我们预期的结果是20000,但实际上的运行结果却不一定是20000,因为count++的操作并不是一个原子操作,而是由多个步骤组成的。
具体而言,count++操作包含了三个步骤:
- 获取count的值;
- 将count的值加一;
- 将新的count值写回到内存中。
如果在t1线程获取count的值后,还没来得及执行加一的操作,t2线程就已经获取了count的值并执行了加一操作,那么t1线程的加一操作就会被t2线程的加一操作覆盖掉,导致计数器值的不准确。
因此,这个计数器类是线程不安全的。为了保证线程安全,需要对increment方法进行同步操作:
public void increment() {
synchronized (this) {
count++;
}
}
这里使用synchronized关键字对increment方法进行同步,保证同一时间只有一个线程能够修改计数器的值。
线程安全的数据结构
Java中已经有很多线程安全的数据结构了,这些数据结构封装了线程安全的实现细节,对开发者来说非常方便。
以ArrayList和CopyOnWriteArrayList为例,这两个数据结构都是动态数组,但CopyOnWriteArrayList是线程安全的,而ArrayList是线程不安全的。
我们先来看看ArrayList的使用例子:
List<Integer> list = new ArrayList<>();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
list.add(i);
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
list.add(i);
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(list.size());
这个例子和之前的计数器例子类似,我们在两个线程中操作同一个ArrayList实例,向其添加元素。
运行该程序,程序很有可能出现Concurrent Modification Exception异常,因为ArrayList是线程不安全的数据结构,如果在一个线程迭代元素的时候,其他线程修改了这个ArrayList的内容,就会导致迭代器无效,出现并发修改异常。
由于CopyOnWriteArrayList是线程安全的,所以我们只需要将ArrayList改成CopyOnWriteArrayList,就不会出现上述异常:
List<Integer> list = new CopyOnWriteArrayList<>();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
list.add(i);
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
list.add(i);
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(list.size());
CopyOnWriteArrayList内部采用了一种写时复制的策略,当有线程对CopyOnWriteArrayList进行写操作时,会创建一个新的备份,并将新元素添加到备份中,然后将备份的引用指向CopyOnWriteArrayList的引用。这种策略保证了多个线程并发操作CopyOnWriteArrayList时,不会互相干扰,而且读操作也不会阻塞写操作,因为读操作从旧备份中读取元素,不会受到写操作的影响。
总结
Java中的线程安全问题随处可见,开发者必须对线程安全和非线程安全有深入的理解,才能够写出高质量、高性能的代码。在编写多线程程序时,经常要使用一些线程安全的数据结构,比如ConcurrentHashMap、BlockingQueue等,还要注意使用synchronized关键字或者ReentrantLock进行同步,避免多线程并发操作同一块内存时出现问题。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:Java线程安全与非线程安全解析 - Python技术站