繁体   English   中英

如何同步 Java 中的两个线程

[英]How can I synchronize two threads in Java

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

public class Example extends Thread {

    private static int count;

    public static void main(String[] args) throws InterruptedException {
        Example t1 = new Example();
        Example t2 = new Example();
        
        t1.start();
        t2.start();
        
        t1.join();
        t2.join();
        
        System.out.println(count);
    }

    public void run() {
        for (int i = 0; i < 12000; i++) {
            addToCount();
        }
    }

    public synchronized void addToCount() {
        Example.count++;
    }
}

在单独的对象上synchronized不会在它们之间进行协调

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

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

有关更多信息,请参阅下方Mark Rotteveel 的评论,并参阅 Oracle 同步方法教程。 并阅读下面链接的 Brian Goetz 书。

++不是原子的

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

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

Java 中的++运算符不是atomic 在源代码中,我们程序员将其视为单个操作。 但实际上是多次操作。 请参阅为什么 i++ 不是原子的? .

从概念上(不是字面意思),您可以想到您的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 这个 class 包装了一个 integer 值,提供了一个线程安全的容器。

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

final private static AtomicInteger count = new AtomicInteger() ;

要执行加法,请调用诸如incrementAndGet之类的方法。 无需将您自己的方法标记为synchronized AtomicInteger为您处理。

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

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

有关更多信息,请参阅这个类似的问题,为什么在 10 个 Java 线程中增加一个数字不会导致值 10? .

执行服务

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

使您的代码变得棘手的部分原因在于它将线程管理(作为Thread的子类)与试图完成工作的工蜂(递增计数器)混合在一起。 这违反了通常会带来更好设计的单一职责原则 通过使用执行器服务,我们可以分离两个职责,线程管理与计数器递增。

项目织机

已经在 Stack Overflow 的许多页面上展示了使用 executor 服务。 因此,搜索以了解更多信息。 相反,如果Project Loom技术成为 Java 的一部分,我将展示更简单的未来方法。 基于早期访问Java 17的实验版本现已推出

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

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

这是我们的Incremental class ,其中包含名为count的 static AtomicInteger class 包括增加原子 object 的方法。 而这个 class 是一个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方法编写代码,以利用该 class。 我们通过Executors ExecutorService 然后在 try-with-resources 中,我们提交两个Incremental实例,每个实例都在各自的线程中运行。

根据您最初的问题,我们仍然有两个对象、两个线程、每个线程中的一万二千条增量命令,并且结果存储在一个名为countstatic变量中。

// 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() );

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

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

正如其他人所暗示的那样,原因与您将方法声明为同步时实际同步的内容有关

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM