Java并发BUG提升篇
- 2020 年 2 月 21 日
- 笔记
内存一致性问题
当多个线程访问为相同数据的结果不一致时,将发生内存一致性问题。
根据Java内存模型,除主内存(RAM)
外,每个CPU都有自己的缓存。因此,任何线程都可以缓存变量,因为与主内存相比,它提供了更快的访问速度。
问题
让我们回想一下我们的Counter示例:
class Counter { private int counter = 0; public void increment() { counter++; } public int getValue() { return counter; } }
让我们考虑以下情形:线程1递增计数器,然后线程2读取其值。可能会发生以下事件序列:
- thread1从其自己的缓存中读取计数器值;计数器为0
- thread1递增计数器并将其写回到其自己的缓存中;计数器是1
- thread2从其自己的缓存中读取计数器值;计数器为0
当然也可能不会发生这样的错误,thread2
将读取正确的值(1)
,但不能保证一个线程所做的更改每次都会对其他线程可见。
解决方案
为了避免内存一致性错误,我们需要建立一个事前发生的关系。这种关系只是对一个特定语句的内存更新对另一特定语句可见的保证。
有几种策略可以创建事前发生的关系。其中之一是同步,已经介绍过了。同步可确保互斥和内存一致性。但是,这会带来性能成本。
我们也可以通过使用volatile
关键字来避免内存一致性问题。简而言之,对volatile
变量的每次更改始终对其他线程可见。
让我们使用volatile
重写Counter
示例:
class SyncronizedCounter { private volatile int counter = 0; public synchronized void increment() { counter++; } public int getValue() { return counter; } }
我们应该注意,我们仍然需要同步增量操作,因为volatile
不能确保我们相互排斥。使用简单的原子变量访问比通过同步代码访问这些变量更有效。
滥用同步
同步机制是一个强大的工具来实现线程安全。它依赖于内部和外部锁的使用。我们还记得以下事实:每个对象都有一个不同的锁,一次只能有一个线程获得一个锁。
但是,如果我们不注意并为关键代码仔细选择正确的锁,则可能会发生意外行为。
引用同步
方法级同步是许多并发问题的解决方案。但是,如果使用过多,它也可能导致其他并发问题。这种同步方法依赖于此引用作为锁定,也称为固有锁定。
在以下示例中,我们可以看到如何使用引用作为锁,将方法级同步转换为块级同步。
这些方法是等效的:
public synchronized void foo() { //dosomething() } public void foo() { synchronized(this) { //dosomething() } }
当线程调用这种方法时,其他线程无法同时访问该对象。由于所有操作最终都以单线程运行,因此这可能会降低并发性能。当读取的对象多于更新的对象时,此方法特别糟糕。
此外,我们代码的客户端也可能会获得此锁。在最坏的情况下,此操作可能导致死锁。
死锁
死锁描述了两个或多个线程相互阻塞,每个线程等待获取某个其他线程持有的资源的情况。
让我们考虑示例:
public class DeadlockExample { public static Object lock1 = new Object(); public static Object lock2 = new Object(); public static void main(String args[]) { Thread threadA = new Thread(() -> { synchronized (lock1) { System.out.println("ThreadA: Holding lock 1..."); sleep(); System.out.println("ThreadA: Waiting for lock 2..."); synchronized (lock2) { System.out.println("ThreadA: Holding lock 1 & 2..."); } } }); Thread threadB = new Thread(() -> { synchronized (lock2) { System.out.println("ThreadB: Holding lock 2..."); sleep(); System.out.println("ThreadB: Waiting for lock 1..."); synchronized (lock1) { System.out.println("ThreadB: Holding lock 1 & 2..."); } } }); threadA.start(); threadB.start(); } }
在上面的代码中,我们可以清楚地看到第一个ThreadA
获取lock1
,而ThreadB
获取lock2
。然后,ThreadA
中尝试获取lock2
,其已经被threadB
获取而threadB
尝试获取lock1
,其已经被ThreadA
获取。因此,他们两个都不会继续运行,这意味着他们陷入了死锁。
我们可以通过更改其中一个线程的锁定顺序来轻松解决此问题。
- 郑重声明:文章首发于公众号“FunTester”,禁止第三方(腾讯云除外)转载、发表。