volatile 以及同步相关内容

volatile Fields

The Java programming language allows threads to access shared variables (§17.1). As a rule, to ensure that shared variables are consistently and reliably updated, a thread should ensure that it has exclusive use of such variables by obtaining a lock that, conventionally, enforces mutual exclusion for those shared variables.

The Java programming language provides a second mechanism, volatile fields, that is more convenient than locking for some purposes.

A field may be declared volatile, in which case the Java Memory Model ensures that all threads see a consistent value for the variable

It is a compile-time error if a final variable is also declared volatile.

如果一个字段被声明为 volatile,那么Java内存模型可以保证所有线程看到这个字段的值都是一致的。

简单示例

以下示例中,如果 keepRunning字段不声明为volatile,那么代码运行时会陷入死循环一直运行。

public class TestDemo {

    private volatile static boolean  keepRunning = true;

    public static void main(String[] args)  throws Exception {
        new Thread(
                ()->{
                    while (keepRunning){
                        //(1) empty loop
                    }
                    System.out.println("loop terminates");
                }
        ).start();
        Thread.sleep(1000);
        keepRunning = false;
        System.out.println("keepRunning is false now");
    }
}

Sleep and Yield

Java语言规范中有说:

Thread.sleep causes the currently executing thread to sleep (temporarily cease execution) for the specified duration, subject to the precision and accuracy of system timers and schedulers. The thread does not lose ownership of any monitors, and resumption of execution will depend on scheduling and the availability of processors on which to execute the thread.

It is important to note that neither Thread.sleep nor Thread.yield have any synchronization semantics. In particular, the compiler does not have to flush writes cached in registers out to shared memory before a call to Thread.sleep or Thread.yield, nor does the compiler have to reload values cached in registers after a call to Thread.sleep or Thread.yield.

For example, in the following (broken) code fragment, assume that this.done is a non-volatile boolean field:

while (!this.done)
    Thread.sleep(1000);
The compiler is free to read the field this.done just once, and reuse the cached value in each execution of the loop. This would mean that the loop would never terminate, even if another thread changed the value of this.done.

那么按照上面所说的,下面代码执行应该也会无限循环才对,但实际几乎一修改循环条件就立刻执行完了:

import java.util.concurrent.TimeUnit;

public class TestDemo {
    private static boolean keepRunning = true;

    static void endlessLoop() {
        while (keepRunning) {
            //(1) sleep 1 second
            try {
                TimeUnit.MILLISECONDS.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("loop terminates");
    }

    public static void main(String[] args) throws Exception {
        new Thread(() -> endlessLoop()).start();
        TimeUnit.MILLISECONDS.sleep(1000);
        keepRunning = false;
        System.out.println("keepRunning is false now");
    }
}

将(1)处休眠部分代码换成System.out.println(“something”) 的输出语句也能让程序正常执行完毕而不陷入无限循环。 这里大致原因是因为PrintStream的println方法内使用了同步语句:

    public void println(String x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }

Synchronization

Threads communicate primarily by sharing access to fields and the objects reference fields refer to. This form of communication is extremely efficient, but makes two kinds of errors possible: thread interference and memory consistency errors. The tool needed to prevent these errors is synchronization.

The Java programming language provides two basic synchronization idioms: synchronized methods and synchronized statements.

Intrinsic Locks and Synchronization

Synchronization is built around an internal entity known as the intrinsic lock or monitor lock. (The API specification often refers to this entity simply as a “monitor.”) Intrinsic locks play a role in both aspects of synchronization: enforcing exclusive access to an object’s state and establishing happens-before relationships that are essential to visibility.

Every object has an intrinsic lock associated with it. By convention, a thread that needs exclusive and consistent access to an object’s fields has to acquire the object’s intrinsic lock before accessing them, and then release the intrinsic lock when it’s done with them. A thread is said to own the intrinsic lock between the time it has acquired the lock and released the lock. As long as a thread owns an intrinsic lock, no other thread can acquire the same lock. The other thread will block when it attempts to acquire the lock.

When a thread releases an intrinsic lock, a happens-before relationship is established between that action and any subsequent acquisition of the same lock.

Locks In Synchronized Methods

When a thread invokes a synchronized method, it automatically acquires the intrinsic lock for that method’s object and releases it when the method returns. The lock release occurs even if the return was caused by an uncaught exception.

You might wonder what happens when a static synchronized method is invoked, since a static method is associated with a class, not an object. In this case, the thread acquires the intrinsic lock for the Class object associated with the class. Thus access to class’s static fields is controlled by a lock that’s distinct from the lock for any instance of the class.

Synchronized Statements

Another way to create synchronized code is with synchronized statements. Unlike synchronized methods, synchronized statements must specify the object that provides the intrinsic lock:

public void addName(String name) {
    synchronized(this) {
        lastName = name;
        nameCount++;
    }
    nameList.add(name);
}

In this example, the addName method needs to synchronize changes to lastName and nameCount, but also needs to avoid synchronizing invocations of other objects’ methods. (Invoking other objects’ methods from synchronized code can create problems that are described in the section on Liveness.) Without synchronized statements, there would have to be a separate, unsynchronized method for the sole purpose of invoking nameList.add.

Synchronized methods enable a simple strategy for preventing thread interference and memory consistency errors: if an object is visible to more than one thread, all reads or writes to that object’s variables are done through synchronized methods.

知乎上 下面的代码 Java 线程结束原因是什么? 也有讨论。

但是循环体中有休眠语句也能起效果,这是为什么?Java文档中也是说sleep方法没有同步语义,在休眠前不必将缓存寄存器中的值更新到主存,休眠后也必重新加载缓存寄存器中的值,那是什么原因导致上面示例在keepRunning值被修改后,几乎是立刻就读取到修改的值而结束循环呢?

Comments are closed