Java并发BUG提高篇篇

2022年01月15日 阅读数:1
这篇文章主要向大家介绍Java并发BUG提高篇篇,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

书接上文:Java并发BUG基础篇缓存

内存一致性问题

当多个线程访问为相同数据的结果不一致时,将发生内存一致性问题。安全

根据Java内存模型,除​​主内存(RAM)​​外,每一个CPU都有本身的缓存。所以,任何线程均可以缓存变量,由于与主内存相比,它提供了更快的访问速度。并发

问题

让咱们回想一下咱们的Counter示例:ide

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)​​,但不能保证一个线程所作的更改每次都会对其余线程可见。性能

解决方案

为了不内存一致性错误,咱们须要创建一个事前发生的关系。这种关系只是对一个特定语句的内存更新对另外一特定语句可见的保证。this

有几种策略能够建立事前发生的关系。其中之一是同步,已经介绍过了。同步可确保互斥和内存一致性。可是,这会带来性能成本。spa

咱们也能够经过使用​​volatile​​​关键字来避免内存一致性问题。简而言之,对​​volatile​​变量的每次更改始终对其余线程可见。线程

让咱们使用​​volatile​​​重写​​Counter​​示例:code

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”,禁止第三方(腾讯云除外)转载、发表。