本文介绍了如何在Java中同步两个线程的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I am learning synchronization in Java. I know this is a very basic question, but I can't find out why I can't get 24000 as a result after running the code to "count".

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 on separate objects does not coordinate between them

When I run your code, I get results such as 21788, 24000, 20521.

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

The reason for the various results is that your synchronized is a lock on each of two separate objects. The objects referenced by t1 and t2 each have their own lock (monitor). Within each thread's execution, executing the synchronized method addToCount grabs the monitor for that particular thread, that particular Example object. So effectively the synchronized has no effect. Your use of the synchronized keyword is not coordinating between the two objects, only within each single object.

有关详细信息,请参阅下方 Mark Rotteveel 的评论,并查看 上的 Oracle 教程同步方法.并阅读下面链接的 Brian Goetz 书.

For more info, see comment by Mark Rotteveel below, and see the Oracle Tutorial on Synchronized Methods. And read the Brian Goetz book linked below.

因此,在您的代码中,使 addToCount synchronized 没有任何意义.

So in your code, no purpose is served by making addToCount synchronized.

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

Each of your two Example objects is a separate thread, each accessing a shared resource, the static int count variable. Each is grabbing the current value, and sometimes they are simultaneously grabbing and incrementing the very same value. For example, they both may the value 42, each adding one gets a result of 43, and each put 43 into that variable.

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

The ++ operator in Java is not atomic. Within the source code, we programmers think of it as a single operation. But it is actually multiple operations. See Why is i++ not atomic?.

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

Conceptually (not literally), you can think of your Example.count++; code as being:

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 循环.

While the multiple steps of fetch-increment-replace are being executed, at any point the operations may be suspended as the execution of that thread pauses for some other thread to execute a while. The thread might have completed the first step, fetching a copy of 42, then paused when thread suspends. During this suspension, the other thread may grab the same 42 value, increment it to 43, and replace back into count. When the first thread resumes, having already grabbed 42, this first thread also increments to 43 and stores back into count. The first thread will have no idea that the second thread slipped in there to already increment and store 43. So 43 ends up being stored twice, using two of our for loops.

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

This coinidence, where each thread steps on the toes of the other, is unpredictable. On each run of this code, the scheduling of the threads may vary based on current momentary conditions within the host OS and the JVM. If our result is 21788, then we know that run experienced 2,212 collisions ( 24,000 - 21,788 = 2,212 ). When our result is 24,000, we know we happened to have no such collisions, by sheer arbitrary luck.

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

You have yet another problem as well. (Concurrency is tricky.) Read on.

由于 CPU 架构的原因,两个线程可能会看到同一个 static 变量的不同值.您需要在 Java 内存模型中研究可见性.

Because of CPU architecture, the two threads might see different values for the same single static variable. You will need to study visibility in the Java Memory Model.

您可以通过使用 Atomic… 类来解决可见性和同步问题.在这种情况下,AtomicInteger.此类包装一个整数值,提供一个线程安全容器.

You can solve both the visibility and synchronization problems by using the Atomic… classes. In this case, AtomicInteger. This class wraps an integer value, providing a thread-safe container.

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

Mark the AtomicInteger field final to guarantee that we have one and only one AtomicInteger object ever, preventing re-assignment.

final private static AtomicInteger count = new AtomicInteger() ;

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

To perform addition, call a method such as incrementAndGet. No need to mark your own method as synchronized. The AtomicInteger handles that for you.

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

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

With this kind of code, two threads incrementing the same AtomicInteger object for 12,000 times each results in a value of 24,000.

有关详细信息,请参阅此类似问题,为什么在 10 个 Java 线程中递增一个数字不会导致值为10?.

For more info, see this similar Question, Why is incrementing a number in 10 Java threads not resulting in a value of 10?.

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

One more issue with your code is that in modern Java, we generally no longer address the Thread class directly. Instead, use the executors framework added to Java 5.

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

Part of what makes your code tricky is that it mixes the thread-management (being a subclass of Thread) with being a worker-bee trying to get a job done (incrementing a counter). This violates the single-responsibility principle that generally leads to better designs. By using an executor service, we can separate the two responsibilities, thread-management versus counter-incrementing.

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

Using an executor service has been shown on many many pages of Stack Overflow already. So search to learn more. Instead, I will show the simpler future approach if and when Project Loom technology becomes a part of Java. Experimental releases based on early-access Java 17 are available now.

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

In Loom, the ExecutorService is AutoCloseable. This means we can use try-with-resources syntax. The try block exits only after all submitted tasks are done/failed/canceled. And when exiting the try block, the executor service is automatically closed for us.

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

Here is our Incremental class that holds the static AtomicInteger named count. The class includes a method to increment that atomic object. And this class is a Runnable with a run method to do your 12,000 loops.

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

And code from a main method, to utilize that class. We instantiate a ExecutorService via the factory methods found in Executors. Then within a try-with-resources we submit two instances of Incremental to each be run in their own thread.

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

As per your original Question, we still have two objects, two threads, twelve thousands increment commands in each thread, and results stored in a single static variable named 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() );

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

When run, your output might look like this:

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


阅读优秀经典书籍Java并发实践 作者:Brian Goetz 等人


Read the excellent and classic book Java Concurrency in Practice by Brian Goetz, et al.

这篇关于如何在Java中同步两个线程的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

05-26 04:49
查看更多