5 回答
TA贡献1880条经验 获得超4个赞
鉴于这段代码:
public class Test {
volatile static private int a;
static private int b;
public static void main(String [] args) throws Exception {
for (int i = 0; i < 100; i++) {
new Thread() {
@Override
public void run() {
int tt = b; // makes the jvm cache the value of b
while (a==0) {
}
if (b == 0) {
System.out.println("error");
}
}
}.start();
}
b = 1;
a = 1;
}
}
的易变存储a发生在 的正常存储之后b。因此,当线程运行并看到时a != 0,由于 JMM 中定义的规则,我们必须看到b == 1。
JRE 中的 bug 允许线程进入生产error线并随后得到解决。如果您没有a定义为,这肯定会失败volatile。
TA贡献1862条经验 获得超7个赞
这可能会重现问题,至少在我的电脑上,我可以在一些循环后重现它。
假设你有一个
Counter
类:class Holder { boolean flag = false; long modifyTime = Long.MAX_VALUE; }
设为
thread_A
,flag
将true
时间存入modifyTime
。让另一个线程,比方说
thread_B
,阅读Counter
的flag
。如果晚于thread_B
仍然get even ,那么我们可以说我们已经重现了问题。false
modifyTime
示例代码
class Holder {
boolean flag = false;
long modifyTime = Long.MAX_VALUE;
}
public class App {
public static void main(String[] args) {
while (!test());
}
private static boolean test() {
final Holder holder = new Holder();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
holder.flag = true;
holder.modifyTime = System.currentTimeMillis();
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
long lastCheckStartTime = 0L;
long lastCheckFailTime = 0L;
while (true) {
lastCheckStartTime = System.currentTimeMillis();
if (holder.flag) {
break;
} else {
lastCheckFailTime = System.currentTimeMillis();
System.out.println(lastCheckFailTime);
}
}
if (lastCheckFailTime > holder.modifyTime
&& lastCheckStartTime > holder.modifyTime) {
System.out.println("last check fail time " + lastCheckFailTime);
System.out.println("modify time " + holder.modifyTime);
return true;
} else {
return false;
}
}
}
结果
last check time 1565285999497
modify time 1565285999494
这意味着从提交的时间thread_B获取,甚至将其设置为时间(早 3 毫秒)。falseCounterflag1565285999497thread_Atrue1565285999494
TA贡献1860条经验 获得超9个赞
使用的示例太糟糕,无法证明内存一致性问题。让它工作将需要脆弱的推理和复杂的编码。然而,您可能无法看到结果。多线程问题是由于时机不巧而发生的。如果有人想增加观察问题的机会,我们需要增加不幸时机的机会。以下程序实现了它。
public class ConsistencyIssue {
static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new Increment(), "Thread-1");
Thread thread2 = new Thread(new Increment(), "Thread-2");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(counter);
}
private static class Increment implements Runnable{
@Override
public void run() {
for(int i = 1; i <= 10000; i++)
counter++;
}
}
}
执行1输出:10963,执行2输出:14552
最终计数应该是 20000,但它比那个少。原因是count++是多步操作,1.读count 2.increment count 3.store it
两个线程可能会同时读取计数 1,将其递增到 2。然后写出 2。但如果它是串行执行,则应该是 1++ -> 2++ -> 3。
我们需要一种方法使所有 3 个步骤成为原子。即一次只能由一个线程执行。
解决方案 1:Synchronized 用 Synchronized 包围增量。由于计数器是静态变量,您需要使用类级同步
@Override
public void run() {
for (int i = 1; i <= 10000; i++)
synchronized (ConsistencyIssue.class) {
counter++;
}
}
现在输出:20000
解决方案 2:AtomicInteger
public class ConsistencyIssue {
static AtomicInteger counter = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new Increment(), "Thread-1");
Thread thread2 = new Thread(new Increment(), "Thread-2");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(counter.get());
}
private static class Increment implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 10000; i++)
counter.incrementAndGet();
}
}
}
我们可以使用信号量,也可以使用显式锁定。但是对于这个简单的代码,AtomicInteger 就足够了
TA贡献1826条经验 获得超6个赞
请再看看你的源代码中是如何介绍这个例子的。
避免内存一致性错误的关键是理解 happens-before 关系。这种关系只是保证一个特定语句对内存的写入对另一个特定语句可见。要看到这一点,请考虑以下示例。
这个例子说明了多线程不是确定性的,因为你不能保证不同线程操作的执行顺序,这可能会导致多次运行的不同观察结果。但是并不能说明内存一致性错误!
要了解什么是内存一致性错误,您需要首先了解内存一致性。Lamport 在 1979 年引入了最简单的内存一致性模型。这是原始定义。
任何执行的结果都是一样的,就好像所有进程的操作都按某种顺序执行,并且每个进程的操作都按照其程序指定的顺序出现在这个序列中
现在,考虑这个示例多线程程序,请看一下最近一篇关于顺序一致性的研究论文中的这张图片。它说明了真正的内存一致性错误可能是什么样子。
要最终回答您的问题,请注意以下几点:
内存一致性错误始终取决于底层内存模型(特定的编程语言可能允许更多行为以进行优化)。什么是最好的内存模型仍然是一个悬而未决的研究问题。
上面给出的例子给出了一个违反顺序一致性的例子,但是不能保证你可以用你喜欢的编程语言观察到它,原因有两个:它取决于编程语言精确的内存模型,并且由于不确定性,你没有强制执行特定错误执行的方法。
TA贡献1818条经验 获得超7个赞
有时当我试图重现一些真正的并发问题时,我会使用调试器。在 print 上创建一个断点,在 increment 上创建一个断点并运行整个过程。释放不同顺序的断点会得到不同的结果。
也许很简单,但它对我有用。
添加回答
举报