如何在Java中同步两个线程

时间:2021-02-10 11:40:51

标签: java synchronization

我正在学习 Java 同步。我知道这是一个非常基本的问题,但我不知道为什么我在运行代码“计数”后无法得到 24000。

// clientWriteRequestHandler handles all write request to clients
func (cache Cache)clientWriteRequestHandler() error {
    for {
        select {
        case writeRequest := <-cache.clientWriteRequestCh:
            if err := util.WriteFrame(writeRequest.receivingClient, writeRequest.data); err != nil {
                return err
            }
        }
    }
}

2 个答案:

答案 0 :(得分:2)

synchronized 在单独的对象之间不协调

当我运行您的代码时,我得到诸如 217882400020521 之类的结果。

出现各种结果的原因是您的 synchronized 是对两个单独对象中的每一个的锁。 t1t2 引用的对象都有自己的锁 (monitor)。在每个线程的执行中,执行 synchronized 方法 addToCount 会获取该特定线程的监视器,该特定 Example 对象。如此有效地 synchronized 没有效果。您对 synchronized 关键字的使用不是协调两个对象之间的,而是协调每个对象的内部

有关详细信息,请参阅下面的 comment by Mark Rotteveel,并参阅 Synchronized Methods 上的 Oracle 教程。并阅读下面链接的 Brian Goetz 书。

++ 不是原子的

因此,在您的代码中,addToCount synchronized 没有任何用途。

您的两个 Example 对象中的每一个都是一个单独的线程,每个都访问一个共享资源,即 static int count 变量。每个都抓取当前值,有时它们同时抓取和递增相同的值。例如,它们都可能是值 42,每加一个得到 43 的结果,然后将 43 放入该变量中。

Java 中的 ++ 运算符不是 atomic。在源代码中,我们程序员将其视为单个操作。但实际上是多次操作。见Why is i++ not atomic?

从概念上(不是字面意思),您可以将 Example.count++; 代码视为:

int x = Example.count ;  // Make a copy of the current value of `count`, and put that copy into `x` variable.
x = ( x + 1 ) ;          // Change the value of `x` to new incremented value.
Example.count = x ;      // Replace the value of `count` with value of `x`.

在执行 fetch-increment-replace 的多个步骤时,在任何时候操作都可能被挂起,因为该线程的执行会暂停以等待其他线程执行一段时间。线程可能已经完成了第一步,获取了 42 的副本,然后在线程挂起时暂停。在此暂停期间,另一个线程可能会获取相同的 42 值,将其增加到 43,然后替换回 count。当第一个线程恢复时,已经抓取了 42,这个第一个线程也会增加到 43 并存储回 count。第一个线程不知道第二个线程已经在那里增加并存储了 43。所以 43 最终被存储了两次,使用了我们的两个 for 循环。

这种巧合,每个线程都踩到另一个线程的脚趾,是不可预测的。在此代码的每次运行中,线程的调度可能会根据主机操作系统和 JVM 中的当前瞬时条件而变化。如果我们的结果是 21788,那么我们知道运行经历了 2,212 次碰撞 ( 24,000 - 21,788 = 2,212 )。当我们的结果是 24,000 时,我们知道我们碰巧没有发生这样的碰撞,纯粹是靠运气。

您还有另一个问题。 (并发棘手。)继续阅读。

能见度问题

由于 CPU 架构的原因,两个线程可能会看到同一个 static 变量的不同值。您需要研究Java Memory Model中的可见性

AtomicInteger

您可以使用 Atomic… 类解决可见性和同步问题。在这种情况下,AtomicInteger。此类包装一个整数值,提供一个 thread-safe 容器。

标记 AtomicInteger 字段 final 以保证我们永远只有一个 AtomicInteger 对象,防止重新分配。

final private static AtomicInteger count = new AtomicInteger() ;

要执行加法,请调用 incrementAndGet 等方法。无需将您自己的方法标记为 synchronizedAtomicInteger 会为您处理。

public void  addToCount() {
    int newValue = Example.count.incrementAndGet() ; 
    System.out.println( "newValue " + newValue + " in thread " + Thread.currentThread().getId() + "." ) ;
}

使用这种代码,两个线程将同一个 AtomicInteger 对象递增 12,000 次,结果为 24,000。

有关详细信息,请参阅此类似问题,Why is incrementing a number in 10 Java threads not resulting in a value of 10?

执行服务

您的代码的另一个问题是,在现代 Java 中,我们通常不再直接处理 Thread 类。相反,请使用添加到 Java 5 的执行程序框架。

使您的代码变得棘手的部分原因是它混合了线程管理(作为 Thread 的子类)与试图完成工作的工蜂(增加计数器)。这违反了通常会导致更好设计的 single-responsibility principle。通过使用执行程序服务,我们可以将线程管理与计数器递增这两个职责分开。

织机计划

已经在 Stack Overflow 的许多页面上展示了使用执行程序服务。所以搜索以了解更多信息。相反,如果 Project Loom 技术成为 Java 的一部分,我将展示更简单的未来方法。基于抢先体验 Java 17 的实验版本为 available now

try-with-resources 语法等待提交的任务

在 Loom 中,ExecutorServiceAutoCloseable。这意味着我们可以使用 try-with-resources 语法。 try 块仅在所有提交的任务完成/失败/取消后退出。并且当退出 try 块时,executor 服务会自动为我们关闭。

这是我们的 Incremental 类,其中包含名为 AtomicInteger 的静态 count。该类包括一个增加该原子对象的方法。这个类是一个 Runnable,带有一个 run 方法来完成 12,000 次循环。

package work.basil.example;

import java.time.Instant;
import java.util.concurrent.atomic.AtomicInteger;

public class Incremental implements Runnable
{
    // Member fields
    static final public AtomicInteger count = new AtomicInteger();  // Make `public` for demonstration purposes (not in real work).

    public int addToCount ( )
    {
        return this.count.incrementAndGet();  // Returns the new incremented value stored as payload within our `AtomicInteger` wrapper.
    }

    @Override
    public void run ( )
    {
        for ( int i = 1 ; i <= 12_000 ; i++ )
        {
            int newValue = this.addToCount();
            System.out.println( "Thread " + Thread.currentThread().getId() + " incremented `count` to: " + newValue + " at " + Instant.now() );
        }
    }
}

来自 main 方法的代码,以利用该类。我们通过 ExecutorService 中的工厂方法实例化一个 Executors。然后在 try-with-resources 中,我们提交两个 Incremental 实例,每个实例都在自己的线程中运行。

根据您的原始问题,我们仍然有两个对象、两个线程、每个线程中有 12000 个增量命令,结果存储在名为 static 的单个 count 变量中。

// Exercise the `Incremental` class by running two instances, each in its own thread.
System.out.println( "INFO - `main` starting the demo. " + Instant.now() );
Incremental incremental = new Incremental();
try (
        ExecutorService executorService = Executors.newVirtualThreadExecutor() ;
)
{
    executorService.submit( new Incremental() );
    executorService.submit( new Incremental() );
}

System.out.println( "INFO - At this point all submitted tasks are done/failed/canceled, and executor service is shutting down. " + Instant.now() );
System.out.println( "DEBUG - Incremental.count.get()  = " + Incremental.count.get() );  // Access the static `AtomicInteger` object.
System.out.println( "INFO - `main` ending. " + Instant.now() );

运行时,您的输出可能如下所示:

INFO - `main` starting the demo. 2021-02-10T22:38:06.235503Z
Thread 14 incremented `count` to: 2 at 2021-02-10T22:38:06.258267Z
Thread 14 incremented `count` to: 3 at 2021-02-10T22:38:06.274143Z
Thread 14 incremented `count` to: 4 at 2021-02-10T22:38:06.274349Z
Thread 14 incremented `count` to: 5 at 2021-02-10T22:38:06.274551Z
Thread 14 incremented `count` to: 6 at 2021-02-10T22:38:06.274714Z
Thread 16 incremented `count` to: 1 at 2021-02-10T22:38:06.258267Z
Thread 16 incremented `count` to: 8 at 2021-02-10T22:38:06.274916Z
Thread 16 incremented `count` to: 9 at 2021-02-10T22:38:06.274992Z
Thread 16 incremented `count` to: 10 at 2021-02-10T22:38:06.275061Z
…
Thread 14 incremented `count` to: 23998 at 2021-02-10T22:38:06.667193Z
Thread 14 incremented `count` to: 23999 at 2021-02-10T22:38:06.667197Z
Thread 14 incremented `count` to: 24000 at 2021-02-10T22:38:06.667204Z
INFO - At this point all submitted tasks are done/failed/canceled, and executor service is shutting down. 2021-02-10T22:38:06.667489Z
DEBUG - Incremental.count.get()  = 24000
INFO - `main` ending. 2021-02-10T22:38:06.669359Z

阅读 Brian Goetz 等人的优秀经典著作 Java Concurrency in Practice

答案 1 :(得分:2)

正如其他人所暗示的那样,原因是当您将方法声明为同步时实际上正在同步:

  • 如果方法是静态,那么您正在上进行同步;一次只能有一个线程可以进入该方法。
  • 如果该方法不是静态的,那么您就是在类的各个实例上进行同步。多个线程可以在类的不同实例上(但不能在同一个实例上)同时调用相同的方法。由于在您的情况下每个线程都有自己的实例,因此它们可以同时调用该方法,每个线程都在其单独的实例上。

所以解决方案基本上是一致的:要么(a)有一个共享的静态变量(如你所愿),然后使方法静态;或者 (b),让每个单独的实例都有自己的变量,然后在操作结束时对变量求和以获得总计数。 (作为 (b) 的变体,您还可以有多个线程引用和访问同一个实例。)

相关问题