什么是线程安全?
在Java中,线程安全(thread-safety)或线程安全代码是指可以在并发或多线程环境中安全地使用或共享的代码,并且它们的行为符合预期。
使用线程在Java中引入了线程安全性风险,我见过许多Java程序员和开发人员都在努力编写线程安全代码,或者仅仅是理解什么是线程安全代码,以及什么不是?
如何编写Java中的线程安全代码
在学习如何编写线程安全代码之前,你需要了解什么是线程安全,最好的方法是查看一个非线程安全代码的示例。因此,让我们看一个潜在的非线程安全代码示例,并了解如何解决它。
Java中非线程安全代码示例
这是一个非线程安全代码示例,请查看代码,并找出_为什么此代码不是线程安全_?
示例1
public class NumberCounter {
private int count;
AtomicInteger atomicCount = new AtomicInteger( 0 ); public synchronized int getCount(){
return count++;
} public int getCountAtomically(){
return atomicCount.incrementAndGet();
}
}
上面的示例不是线程安全的,因为++(递增运算符)不是原子操作,可以分解为读取、更新和写入操作。如果多个线程同时调用getCount(),这三个操作中的每一个可能会重叠或交错。例如,当线程1正在更新值时,线程2读取并仍然获取旧值,这最终使线程2覆盖线程1的递增,并且一个计数会丢失,因为多个线程同时调用它。
示例2
public class BasicObservableClass { public interface Observer {
void onObservableChanged();
} private Set<Observer> mObservers; public void registerObserver(Observer observer) { if (observer == null) {
return;
} if (mObservers == null) {
mObservers = new HashSet<>(1);
} mObservers.add(observer); } public void unregisterObserver(Observer observer) { if (mObservers != null && observer != null) {
mObservers.remove(observer);
}
} private void notifyObservers() {
if (mObservers == null) {
return;
} for (Observer observer : mObservers) {
observer.onObservableChanged();
}
}
}
如果在多线程环境中使用非线程安全实现会发生什么:
在多线程环境的最一般情况下,我们应该假设所有方法都将在随机时间的随机线程上调用。我们用于单线程环境的实现不再安全。不难想象两个不同的线程尝试同时注册新观察者,并最终使我们的系统出现故障。其中一个这样的流程可能是(可能会破坏非线程安全实现的许多其他流程):
- 线程A调用
<em>registerObserver(Observer)</em>
- 线程A执行
<em>mObservers == null</em>
检查并继续实例化新集合 - 在线程A有机会创建新集合之前,操作系统将其挂起并恢复执行线程B
- 线程B执行上述步骤1和2
- 由于线程A尚未实例化该集合,因此线程B实例化一个新集合并将其引用存储在
<em>mObservers</em>
中 - 线程B向新创建的集合添加观察者
- 在某个时刻,操作系统恢复了线程A的执行(在实例化新集合之前挂起)
- 线程A实例化一个新集合并覆盖
mObservers
中的引用 - 线程A向新创建的集合添加观察者
尽管对
<em>registerObserver(Observer)</em>
的两个调用都成功完成,但最终结果是只有一个观察者将在调用<em>notifyObservers()</em>
时得到通知。重要的是要理解,任何观察者都可能被“忽略”,或者两个观察者都可以成功注册-结果取决于操作系统对线程的调度,我们无法控制。这种不确定性是使多线程错误非常难以跟踪和解决的原因。
如何在Java中编写线程安全代码
在Java中,有多种方法可以使此代码成为线程安全:
1)在Java中使用synchronized关键字,并锁定getCount()
方法,以便只有一个线程可以同时执行它,这消除了重叠或交错的可能性。
2)使用Atomic Integer,这使得此++操作原子,由于原子操作是线程安全的,并且节省了外部同步的成本。
这是Java中NumberCounter类的线程安全版本:
public class NumberCounter { private int count;
AtomicInteger atomicCount = new AtomicInteger( 0 ); public synchronized int getCount(){
return count++;
} public int getCountAtomically(){
return atomicCount.incrementAndGet();
}}
Java中线程安全的重要要点
以下是一些值得记住的要点,以编写Java中的线程安全代码,这些知识还可以帮助你避免Java中的一些严重并发问题,例如竞争条件或死锁:
1)不可变对象默认情况下是线程安全的,因为它们的状态在创建后无法修改。由于Java中的字符串是不可变的,因此它天生就是线程安全的。
2)Java中的只读或final变量也是线程安全的。
3)锁定是在Java中实现线程安全的一种方式。
4)静态变量如果未正确同步,将成为线程安全问题的主要原因。
5)Java中线程安全的类的示例:Vector、Hashtable、ConcurrentHashMap、String等。
6)Java中的原子操作是线程安全的,例如从内存中读取32位int,因为它是原子操作,它不能与其他线程交错。
7)本地变量也是线程安全的,因为每个线程都有自己的副本,并且使用本地变量是编写线程安全代码的一种好方法。
8)为了避免线程安全问题,最小化多个线程之间的对象共享。
9)在Java中,volatile关键字也可以用于指示线程不要缓存变量并从主内存中读取,并且还可以指示JVM不要从线程角度重新排序或优化代码。
译自:https://gowthamy.medium.com/concurrent-programming-fundamentals-thread-safety-6b44c026bd2a
评论(0)