当我使用 2 个线程在同一对象上调用方法(新 Counter())时,while 循环的运行次数超过了循环限制。有人可以用简单的语言解释相同的原因吗?
主类 :
public class MainClass {
public static void main(String[] args) {
Counter c1 = new Counter();
MyRunnable r1 = new MyRunnable(c1);
Thread t1 = new Thread(r1, "t1");
Thread t2 = new Thread(r1, "t2");
t1.start();
t2.start();
}
}
可运行 :
public class MyRunnable implements Runnable {
Counter counter;
public MyRunnable(Counter counter) {
this.counter = counter;
}
@Override
public void run() {
counter.add();
}
}
柜台类别 :
public class Counter {
int i = 1;
public void add() {
while (i <= 5) {
System.out.println(i + " " + Thread.currentThread().getName());
i++;
}
}
}
输出:
1 t1
1 t2
2 t1
3 t2
4 t1
5 t2
我想知道,如果限制是 5,那么为什么它会输出 6 个值。有时它还输出 5 个值?如果有人对此有所了解,那就太好了。谢谢。
代码存在两个并发问题。
-
共享的 int
i
既没有为并发任务进行适当的声明,也没有访问。 它至少需要是易失性的,但为了正确处理原子增量应该是一个AtomicInteger
。 易失性将有助于保证其他线程可以看到值是否更改(JVM 无法保证这一点),但原子整数更进一步,并提供原子操作,这是您在这里真正需要的。 -
两个线程可以同时访问您的循环,因为条件检查与变量的增量是分开的。 为了正确处理运行循环的并发线程,您需要在此处
while
循环条件,以便将增量作为单个原子操作执行。 要么这样,要么你需要添加一个同步块。
也许尝试以下未经测试的代码:
public class Counter {
private final AtomicInteger i = new AtomicInteger(1);
public void add() {
for (;;)
{
int localCount = i.getAndIncrement();
if (localCount > 5) { break; }
System.out.println(localCount + " " + Thread.currentThread().getName());
}
}
}
逐行解释上述代码:
- 我们
i
切换到AtomicInteger
,因为它允许我们获取当前值并以原子方式将下一个值递增 1。 这种原子性很重要,因为否则两个竞争线程可能会递增到相同的值,而不是在所有线程中读取和递增到每个值一次。 我们可以通过将代码的递增部分放在同步块中来实现相同的目的。 - 我们切换到
for (;;)
循环,以便我们可以将循环控制/终止代码移动到循环主体内。 这使我们能够轻松地将原子增量的结果用于控制代码和循环主体中的用法(例如在println
语句中)。 i.getAndIncrement()
原子地递增我们的计数器,并在递增之前将值返回给我们。 此操作保证在运行的任何线程中工作,并确保我们不会错过或重复任何数字。 将之前的计数值分配给localCount
int 为我们提供了在循环中使用的值。 如果我们尝试在循环中重新访问i
而不是使用 localCount,我们最终会在某个时候得到一个不同的数字,因为其他线程同时递增i
.- 我们检查
localCount
,看看它是否超过了我们的终止限制,如果超过了我们的终止限制,我们就打破了我们的循环。 我们不检查i
因为它可能已经被另一个线程修改了,我们只对该线程的当前数量是否超过限制感兴趣。 - 最后,我们使用
localCount
而不是i
来做我们的 println,因为当代码到达这里时i
可能已经被另一个线程修改了(这将导致打印重复的行)。
请注意,此代码不保证计数器的数字将按顺序打印。大多数时候可能是这种情况,但肯定不能保证,也不应该依赖。 您的问题没有说明实际的预期/所需结果,如果这是一个要求,那么您很可能需要围绕增量和 println 语句进行同步,以确保没有乱序执行。
例如同步解决方案:
public class Counter {
int i = 1;
public void add() {
for (;;) {
synchronized(this) {
if (i > 5) { break; }
System.out.println(i + " " + Thread.currentThread().getName());
i++;
}
}
}
}
这是一个简单的问题。
- 您创建了一个名为
c1
的计数器; - 你用这个计数器做了一个可运行的;
- 您将相同的可运行对象传递给两个线程。两个线程现在将在同一个计数器对象上运行
c1
; - 在
add()
方法中运行 while 循环。
add()
方法在一行上打印某些内容,在下一行上递增i
。
当前输出的情况下,在第一个线程执行其 print 语句后,它被中断,第二个线程控制了 CPU。第二个线程设法执行其打印。然后任何一个线程递增计数器(我们无法知道是哪一个),打印和计数器增量的两个步骤继续。
问题是您同时访问变量i
。 Java 将变量缓存在 CPU 缓存中。由于每个线程可以在不同的内核中执行,因此每个线程中的i
值可能不同。即使您正在执行i++
底层代码也是i = i + 1
的。您正在读取变量,然后正在写入,因此在这种情况下,多个线程也可能具有不同的值。
此问题的解决方案是在您的情况下使用Atomic
变量,例如AtomicInteger
。这些原子变量是线程安全的,并且像在事务中一样读取和写入,在更新变量时阻止对线程的访问。
在您的情况下,您可以声明int
而不是声明
private volatile AtomicInteger atomicInteger = new AtomicInteger(1);
而不是使用i++
你可以使用atomicInteger.addAndGet(1)
你刚刚发现的是一个竞争条件(https://en.m.wikipedia.org/wiki/Race_condition) 线程可以不受任何限制地访问counter
对象。 即使i
是一个 int,也不能保证,例如,当 't2' 读取它时,"t1"已完成递增值。尝试运行该程序几次,输出可能会有所不同。
要解决您的问题,您需要实现一种锁,或使用synchronized
关键字。通常,在实现使用多线程的应用程序时,需要确定关键部分并确保避免争用条件。
编辑:我不敢相信我写了protected
而不是synchronized
,不知道这是怎么发生的。就在今天偶然发现这个,编辑所以没有其他人被误导。