1. 简介

简而言之,当涉及并发时,共享可变状态很容易导致问题。如果未正确管理对共享可变对象的访问,应用程序很快就会变得容易出现一些难以检测的并发错误。

在本文中,我们将重新审视使用锁来处理并发访问,探讨与锁相关的一些缺点,最后引入原子变量作为替代方案。

2. 锁

让我们来看看:

public class Counter {

int counter;

public void increment() {

counter++;

}

}Copy

在单线程环境中,这非常有效;但是,一旦我们允许多个线程写入,我们就会开始得到不一致的结果。

这是因为简单的增量操作 (counter++),它可能看起来像一个原子操作,但实际上是三个操作的组合:获取值、递增和写回更新的值。

如果两个线程尝试同时获取和更新值,则可能会导致更新丢失。

管理对象访问的方法之一是使用锁。这可以通过在增量方法签名中使用同步关键字来实现。sync关键字确保一次只有一个线程可以进入该方法(要了解有关锁定和同步的更多信息,请参阅 –Java 中的同步关键字指南):

public class SafeCounterWithLock {

private int counter;

public synchronized void increment() {

counter++;

}

}Copy

使用锁可以解决问题。但是,性能受到打击。

当多个线程尝试获取锁时,其中一个线程获胜,而其余线程被阻止或挂起。

挂起然后恢复线程的过程非常昂贵,并且会影响系统的整体效率。

在小程序中,例如计数器,在上下文切换上花费的时间可能比实际代码执行要多得多,从而大大降低了整体效率。

3. 原子操作

有一个研究分支专注于为并发环境创建非阻塞算法。这些算法利用低级原子机指令(如比较和交换 (CAS))来确保数据完整性。

典型的 CAS 操作适用于三个操作数:

要操作的内存位置 (M)

变量的现有期望值 (A)

需要设置的新值 (B)

CAS 操作以原子方式将 M 中的值更新为 B,但前提是 M 中的现有值与 A 匹配,否则不执行任何操作。

在这两种情况下,都返回 M 中的现有值。这将三个步骤(获取值、比较值和更新值)组合到单个计算机级别的操作中。

当多个线程尝试通过 CAS 更新相同的值时,其中一个线程获胜并更新该值。但是,与锁的情况不同,没有其他线程被挂起;相反,他们只是被告知他们没有设法更新值。然后,线程可以继续执行进一步的工作,并完全避免上下文切换。

另一个后果是核心程序逻辑变得更加复杂。这是因为我们必须处理 CAS 操作不成功的场景。我们可以一次又一次地重试它,直到它成功,或者我们可以什么都不做,根据用例继续前进。

4. Java中的原子变量

Java 中最常用的原子变量类是 AtomicInteger、AtomicLong、AtomicBoolean和AtomicReference。这些类分别表示可以原子更新的整数、长整型、布尔值和对象引用。这些类公开的主要方法是:

Get()——从内存中获取值,这样其他线程所做的更改就可见了;相当于读取一个volatile变量

 

incrementAndGet() -在当前值上原子地加1

 

Set()——将值写入内存,这样更改对其他线程可见;相当于写一个易失性变量

 

lazySet()——最终将值写入内存,可能会用后续相关的内存操作重新排序。一个用例是为了垃圾收集而使引用无效,而这些引用永远不会再被访问。在这种情况下,通过延迟空易失性写入可以获得更好的性能

 

compareAndSet()——与第3节中描述的相同,当成功时返回true,否则返回false

 

weakCompareAndSet()——与第3节中描述的相同,但在某种意义上更弱,即它不创建happens-before顺序。这意味着它不一定会看到对其他变量的更新。从Java 9开始,这个方法在所有原子实现中都已弃用,取而代之的是weakCompareAndSetPlain()。weakCompareAndSet()的内存效果很简单,但是它的名字暗示了volatile内存效果。为了避免这种混淆,他们弃用了这个方法,并添加了四个具有不同内存效果的方法,如weakCompareAndSetPlain()或weakCompareAndSetVolatile()

使用AtomicInteger实现的线程安全计数器如下例所示:

public class SafeCounterWithoutLock {

private final AtomicInteger counter = new AtomicInteger(0);

int getValue() {

return counter.get();

}

void increment() {

counter.incrementAndGet();

}

}Copy

如您所见,我们使用 incrementAndGet() 作为同步代码块,获取当前值,递增 1 并分配计数器变量的新值,最后将其存储在内存中。

相关阅读

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。