Java的线程同步和并发问题示例

并发问题安全

多线程是一个很是强大的工具,它使咱们可以更好地利用系统的资源,但咱们须要在读取和写入多个线程共享的数据时特别当心。多线程

当多个线程尝试同时读取和写入共享数据时,会出现两种类型的问题 -并发

线程干扰错误内存一致性错误让咱们逐一理解这些问题。工具

线程干扰错误(竞争条件)性能

考虑如下Counter类,其中包含一个increment()方法,每次调用它时计数增长一次 -优化

如今,让咱们假设几个线程试图经过increment()同时调用方法来增长计数-线程

您认为上述计划的结果如何?最终计数是1000,由于咱们调用增量1000次?对象

但实际上答案是否认的!只需运行上面的程序,本身查看输出。它不是产生最终计数1000,而是每次运行时都会产生不一致的结果。我在计算机上运行了上述程序三次,输出为992,996和993。blog

让咱们深刻研究该程序并理解程序输出不一致的缘由 -排序

当线程执行increment()方法时,执行如下三个步骤:1。检索计数的当前值2.将检索的值增长1 3.将增长的值从新存储到计数中

如今让咱们假设两个线程 - ThreadA和ThreadB按如下顺序执行这些操做 -

ThreadA:检索计数,初始值= 0ThreadB:检索计数,初始值= 0ThreadA:增长检索值,结果= 1ThreadB:增长检索值,结果= 1ThreadA:存储递增的值,count如今为1ThreadB:存储递增的值,count如今为1两个线程都尝试将计数递增1,但最终结果是1而不是2,由于线程执行的操做相互交错。在上述状况下,ThreadA完成的更新将丢失。

上述执行顺序只是一种可能性。可能有许多这样的命令能够执行这些操做,使程序的输出不一致。

当多个线程尝试同时读取和写入共享变量,而且这些读取和写入操做在执行中重叠时,最终结果取决于读取和写入发生的顺序,这是不可预测的。这种现象称为种族情况。

访问共享变量的代码部分称为Critical Section。

经过同步对共享变量的访问能够避免线程干扰错误。

让咱们首先看一下多线程程序中出现的第二种错误 - 内存一致性错误。

内存一致性错误

当不一样的线程具备相同数据的不一致视图时,会发生内存不一致错误。当一个线程更新某些共享数据时会发生这种状况,但此更新不会传播到其余线程,而且最终会使用旧数据。

为何会这样?嗯,这可能有不少缘由。编译器会对您的程序进行屡次优化以提升性能。它还可能从新排序指令以优化性能。处理器也尝试优化事物,例如,处理器可能从临时寄存器(包含变量的最后读取值)读取变量的当前值,而不是主存储器(具备变量的最新值) 。

请考虑如下示例,该示例演示了操做中的内存一致性错误 -

在理想状况下,上述计划应 -

等待一秒钟,而后打印Hello World!后sayHello变为真。等待一秒钟,而后打印Good Bye!后sayHello变为假。

可是在运行上述程序后咱们是否获得了所需的输出?好吧,若是你运行程序,你会看到如下输出 -

此外,该程序甚至没有终止。

线程等待。什么?怎么可能?

是! 这就是内存一致性错误。第一个线程不知道主线程对sayHello变量所作的更改。

您可使用volatile关键字来避免内存一致性错误。咱们很快就会详细了解volatile关键字。

同步

经过确保如下两件事能够避免线程干扰和内存一致性错误 -

一次只有一个线程能够读写共享变量。当一个线程正在访问共享变量时,其余线程应该等到第一个线程完成。这保证了对共享变量的访问是Atomic,而且多个线程不会干扰。每当任何线程修改共享变量时,它都会自动创建与其余线程后续读取和写入共享变量的先发生关系。这能够保证一个线程所作的更改对其余人可见。幸运的是,Java有一个synchronized关键字,您可使用该关键字同步对任何共享资源的访问,从而避免这两种错误。

同步方法

如下是Counter类的同步版本。咱们synchronized在increment()方法上使用Java的关键字来防止多个线程同时访问它 -

若是运行上述程序,它将产生1000的所需输出。不会出现竞争条件,而且最终输出始终保持一致。该synchronized关键字可确保只有一个线程能够进入increment()一次的方法。

请注意,同步的概念始终绑定到对象。在上面的例子中,increment()在同一个实例上屡次调用方法SynchonizedCounter会致使竞争条件。咱们正在使用synchronized关键字防范这种状况。可是线程能够安全地increment()在不一样的实例上SynchronizedCounter同时调用方法,这不会致使竞争条件。

在静态方法的状况下,同步与Class对象相关联。

同步代码块

Java内部使用所谓的内部锁或监视器锁来管理线程同步。每一个对象都有一个与之关联的内在锁。

当一个线程调用一个对象的synchronized方法时,它会自动获取该对象的内部锁,并在该方法退出时释放它。即便方法抛出异常,也会发生锁定释放。

在静态方法的状况下,线程获取Class与类关联的对象的内部锁,这与该类的任何实例的内部锁不一样。

synchronizedkeyword也能够用做块语句,但与synchronized方法不一样,synchronized语句必须指定提供内部锁的对象 -

 

当线程获取对象的内部锁时,其余线程必须等到锁被释放。可是,当前拥有锁的线程能够屡次获取它而没有任何问题。

容许线程屡次获取同一个锁的想法称为“ 重入同步”。

易变的关键字

Volatile关键字用于避免多线程程序中的内存一致性错误。它告诉编译器避免对变量进行任何优化。若是将变量标记为volatile,则编译器不会优化或从新排序该变量的指令。

此外,变量的值将始终从主存储器而不是临时寄存器中读取。

如下是咱们在上一节中看到的相同MemoryConsistencyError示例,不一样之处在于,此次咱们sayHello使用volatile关键字标记了变量。

 

运行上述程序会产生所需的输出 -

 

结论

经过示例咱们了解了多线程程序中可能出现的不一样并发问题以及如何使用synchronized方法和块来避免它们。同步是一个强大的工具,但请注意,没必要要的同步可能会致使其余问题,如死锁和饥饿。

相关文章
相关标签/搜索