![](/img/trans.png)
[英]How can I synchronize two threads in Java by extending the Thread class?
[英]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
不会在它们之间进行协调当我运行您的代码时,我会得到诸如21788
、 24000
、 20521
之类的结果。
各种结果的原因是您的synchronized
是对两个单独对象中的每一个的锁定。 t1
和t2
引用的对象都有自己的锁( 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的实验版本现已推出。
在 Loom 中, ExecutorService
是AutoCloseable
。 这意味着我们可以使用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
实例,每个实例都在各自的线程中运行。
根据您最初的问题,我们仍然有两个对象、两个线程、每个线程中的一万二千条增量命令,并且结果存储在一个名为count
的static
变量中。
// 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 。
正如其他人所暗示的那样,原因与您将方法声明为同步时实际同步的内容有关:
所以解决方案基本上是一致的:要么(a)有一个共享的 static 变量(如你所见),然后制作方法 static; 或者(b),让每个单独的实例都有自己的变量,然后在操作结束时对变量求和以获得总计数。 (作为 (b) 的变体,您还可以有多个线程引用和访问同一个实例。)
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.