我正在学习 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
}
}
}
}
答案 0 :(得分:2)
synchronized
在单独的对象之间不协调当我运行您的代码时,我得到诸如 21788
、24000
、20521
之类的结果。
出现各种结果的原因是您的 synchronized
是对两个单独对象中的每一个的锁。 t1
和 t2
引用的对象都有自己的锁 (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
等方法。无需将您自己的方法标记为 synchronized
。 AtomicInteger
会为您处理。
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。
在 Loom 中,ExecutorService
是 AutoCloseable
。这意味着我们可以使用 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) 的变体,您还可以有多个线程引用和访问同一个实例。)