volatile 关键字原子性

Volatile keyword atomicity

我正在尝试学习 volatile field modifier 多线程。我遇到了这个声明:

Volatile is preferred in cases when one thread reads and writes a shared variable and other threads just read the same. Whereas if there are more than 2 threads performing read and write both on the shared variable then only volatile is not enough, you need to have synchronization as well.

我知道 volatile 提供可见性和先行发生保证,但是是否可以给出一个简单的小代码示例来演示上面需要同步块的语句?

有很多这样的例子...这是一个:

volatile int i = 0;

// Thread #1
while (true) {
    i = i + 1;
}

// Thread #2
while (true) {
    Console.WriteLine(i);
}

在这种情况下,线程 #1 和线程 #2 都在读取变量 i,但只有线程 #1 在写入它。线程 #2 将始终看到递增的值 i

如果没有 volatile 关键字,您偶尔会看到奇怪的行为,通常是在多处理器机器或多核 CPUs 上。发生的事情(这里稍微简化)是线程 #1 和 #2 每个都是 运行 自己的 CPU 并且每个都有自己的 i 副本(在它的 CPU缓存 and/or 寄存器)。如果没有 volatile 关键字,它们可能永远不会相互更新更改后的值。

与本例对比:

static volatile int i = 0;

// Thread #1
while (true) {
    i = i + 1;
}

// Thread #2
while (true) {
    if (i % 2 == 0) 
        i == 0;
    else
        Console.WriteLine(i);
}

所以在这里,线程 #1 试图单调递增 i,而线程 #2 要么将 i 设置为 0(如果 i 是偶数),要么打印如果我很奇怪,它会发送到控制台。您会认为线程 #2 永远不会向控制台打印偶数,对吗?

事实并非如此。因为您在访问 i 时没有同步,线程 #2 可能会看到一个奇数,移动到 else 分支,然后线程 #1 增加 [=13= 的值],导致线程 #2 打印偶数。

在这种情况下,解决该问题的一种方法是使用基本锁定作为一种同步形式。因为我们不能锁定一个原语,所以我们引入一个空白对象来锁定:

static volatile int i = 0;
static Object lockOnMe = new Object();

// Thread #1
while (true) {
    lock (lockOnMe) {
        i = i + 1;
    }
}

// Thread #2
while (true) {
    lock (lockOnMe) {
        if (i % 2 == 0) 
            i == 0;
        else
            Console.WriteLine(i);
    }
}
public class TwoInts {
    private volatile int i1; 
    private volatile int i2;  

    public void set(int i1, int i2) {
        this.i1 = i1;
        this.i2 = i2;
    }

    public boolean same() {
        return i1 == i2;
    }
}

现在,如果您有一个线程在执行此操作:

while (true) {
    twoInts.set(i, i); 
    i++;
}

第二个线程执行此操作:

while (true) {
    if (!twoInts.same()) {
        System.out.println("Ooops!!");
    }
}

然后您将观察到引用文本所谈论的问题。如果您重写 TwoInts class 使方法 synchronized 那么 "Oooops!!" 消息将停止。

假设您有 int i 和两个线程,您希望每个线程都读取 i 并设置 i = i + 1.

像这样:

public class Main {

    private static volatile int i = 0;

    public static void main(String[] args) throws Exception{

        Runnable  first = new Runnable() {
            @Override
            public void run() {
                System.out.println("Thread_1 see i = " + i);
                i++;
                System.out.println("Thread_1 set i = " + i);
            }
        };

        Runnable second = new Runnable() {
            @Override
            public void run() {
                System.out.println("Thread_2 see i = " + i);
                i++;
                System.out.println("Thread_2 set i = " + i);
            }
        };
        new Thread(first).start();
        new Thread(second).start();
    }
}

结果是:

Thread_1 see i = 0
Thread_2 see i = 0
Thread_1 set i = 1
Thread_2 set i = 2

如你所见,Thread_2获取0并设置2(因为Thread_1已经将i更新为1),这预计不会。


添加同步后,

public class Main {

    private static volatile int i = 0;

    public static void main(String[] args) throws Exception{

        Runnable  first = new Runnable() {
            @Override
            public void run() {
                synchronized (Main.class) {
                    System.out.println("Thread_1 see i = " + i);
                    i++;
                    System.out.println("Thread_1 set i = " + i);
                }
            }
        };

        Runnable second = new Runnable() {
            @Override
            public void run() {
                synchronized (Main.class) {
                    System.out.println("Thread_2 see i = " + i);
                    i++;
                    System.out.println("Thread_2 set i = " + i);
                }
            }
        };
        new Thread(first).start();
        new Thread(second).start();
    }
}

有效:

Thread_2 see i = 0
Thread_2 set i = 1
Thread_1 see i = 1
Thread_1 set i = 2