Java内存模型中的happens-before关系
Java内存模型基础
在深入探讨happens - before关系之前,我们先来回顾一下Java内存模型(Java Memory Model,JMM)的一些基础知识。
JMM定义了Java程序中多线程访问共享变量时的规则。Java的内存分为主内存(Main Memory)和工作内存(Working Memory)。主内存是所有线程共享的,存储了共享变量的值。而每个线程都有自己的工作内存,线程对共享变量的操作都需要先将变量从主内存拷贝到自己的工作内存,然后在工作内存中进行操作,操作完成后再将变量写回到主内存。
例如,考虑以下简单的代码:
public class MemoryModelExample {
private static int value = 0;
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
value = 1;
});
Thread thread2 = new Thread(() -> {
int localValue = value;
System.out.println("Thread 2 reads value: " + localValue);
});
thread1.start();
thread2.start();
}
}
在这个例子中,value
是共享变量,存储在主内存中。thread1
和thread2
都有自己的工作内存。thread1
将value
从主内存拷贝到自己的工作内存,修改为1后写回主内存。thread2
将value
从主内存拷贝到自己的工作内存并读取其值。但是,由于线程执行的不确定性以及内存模型的规则,thread2
不一定能读到thread1
修改后的值。这就引出了happens - before关系的重要性。
happens - before关系的定义
happens - before关系是JMM中定义的一种偏序关系(partial order relation),用于描述两个操作之间的顺序。如果操作A happens - before操作B,那么操作A的结果对操作B是可见的,并且操作A按顺序排在操作B之前。
需要注意的是,happens - before并不意味着操作A在时间上一定先于操作B执行。它更多的是一种内存可见性的保证。即使操作A在时间上后于操作B执行,但只要满足happens - before关系,操作A的结果对操作B仍然是可见的。
happens - before规则
- 程序顺序规则(Program Order Rule):在一个线程内,按照程序代码的顺序,前面的操作happens - before后续的操作。例如:
public class ProgramOrderExample {
public static void main(String[] args) {
int a = 1; // 操作1
int b = a + 1; // 操作2
System.out.println(b); // 操作3
}
}
在这个例子中,操作1 happens - before操作2,操作2 happens - before操作3。这是因为在单线程环境下,程序的执行顺序是确定的,前面的操作结果对后续操作可见。
- 监视器锁规则(Monitor Lock Rule):对一个锁的解锁操作happens - before后续对同一个锁的加锁操作。考虑以下代码:
public class MonitorLockExample {
private static final Object lock = new Object();
private static int value = 0;
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lock) {
value = 1;
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock) {
int localValue = value;
System.out.println("Thread 2 reads value: " + localValue);
}
});
thread1.start();
thread2.start();
}
}
在这个例子中,thread1
对lock
的解锁操作happens - before thread2
对lock
的加锁操作。这就保证了thread1
对value
的修改对thread2
是可见的。当thread2
获取到锁时,它能看到thread1
修改后的value
值。
- volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作happens - before后续对同一个volatile变量的读操作。例如:
public class VolatileExample {
private static volatile int value = 0;
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
value = 1;
});
Thread thread2 = new Thread(() -> {
int localValue = value;
System.out.println("Thread 2 reads value: " + localValue);
});
thread1.start();
thread2.start();
}
}
由于value
是volatile变量,thread1
对value
的写操作happens - before thread2
对value
的读操作。这确保了thread2
能读到thread1
修改后的value
值。
- 线程启动规则(Thread Start Rule):Thread对象的start()方法的调用操作happens - before此线程的每一个操作。例如:
public class ThreadStartExample {
private static int value = 0;
public static void main(String[] args) {
Thread thread = new Thread(() -> {
value = 1;
System.out.println("Thread set value to: " + value);
});
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main thread sees value: " + value);
}
}
在这个例子中,thread.start()
的调用happens - before thread
内的所有操作。这保证了主线程启动thread
后,thread
内对value
的修改对主线程后续的操作(如thread.join()
之后的操作)是可见的。
- 线程终止规则(Thread Termination Rule):线程中的所有操作happens - before对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。例如:
public class ThreadTerminationExample {
private static int value = 0;
public static void main(String[] args) {
Thread thread = new Thread(() -> {
value = 1;
});
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main thread sees value: " + value);
}
}
在这个例子中,thread
内的所有操作happens - before主线程调用thread.join()
之后的操作。当thread.join()
返回时,主线程能看到thread
对value
的修改。
- 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用操作happens - before被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()或Thread.isInterrupted()检测到是否有中断发生。例如:
public class ThreadInterruptionExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// 线程执行任务
}
System.out.println("Thread interrupted");
});
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
}
}
在这个例子中,主线程调用thread.interrupt()
的操作happens - before thread
内检测到中断的操作(Thread.currentThread().isInterrupted()
)。
- 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)happens - before它的finalize()方法的开始。例如:
public class FinalizerExample {
private static FinalizerExample instance;
@Override
protected void finalize() throws Throwable {
instance = this;
}
public static void main(String[] args) {
FinalizerExample example = new FinalizerExample();
example = null;
System.gc();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (instance != null) {
System.out.println("Instance is not null in main thread");
}
}
}
在这个例子中,FinalizerExample
对象的构造函数执行结束happens - before finalize()
方法的开始。当对象被垃圾回收触发finalize()
方法时,finalize()
方法中对instance
的赋值对主线程后续的操作是可见的。
- 传递性(Transitivity):如果A happens - before B,且B happens - before C,那么A happens - before C。例如:
public class TransitivityExample {
private static int a = 0;
private static int b = 0;
private static int c = 0;
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
a = 1; // 操作1
b = 2; // 操作2
});
Thread thread2 = new Thread(() -> {
c = b; // 操作3
System.out.println("c = " + c); // 操作4
});
thread1.start();
try {
thread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.start();
}
}
在这个例子中,根据程序顺序规则,操作1 happens - before操作2。又因为thread1.join()
保证了thread1
内所有操作完成后thread2
才开始,所以操作2 happens - before操作3。通过传递性,操作1 happens - before操作3和操作4。这就保证了thread2
在读取b
时能看到thread1
修改后的b
值。
复合场景下的happens - before关系分析
在实际的多线程编程中,往往会遇到多种happens - before规则同时起作用的场景。例如,结合监视器锁和volatile变量:
public class CompositeExample {
private static volatile int value = 0;
private static final Object lock = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lock) {
value = 1;
}
});
Thread thread2 = new Thread(() -> {
int localValue;
synchronized (lock) {
localValue = value;
}
System.out.println("Thread 2 reads value: " + localValue);
});
thread1.start();
thread2.start();
}
}
在这个例子中,首先根据监视器锁规则,thread1
对lock
的解锁操作happens - before thread2
对lock
的加锁操作。然后,由于value
是volatile变量,thread1
对value
的写操作happens - before thread2
对value
的读操作。通过这两个规则的结合,确保了thread2
能读到thread1
修改后的value
值。
再看一个结合线程启动和volatile变量的例子:
public class CompositeExample2 {
private static volatile int value = 0;
public static void main(String[] args) {
Thread thread = new Thread(() -> {
value = 1;
});
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main thread reads value: " + value);
}
}
这里,根据线程启动规则,thread.start()
的调用happens - before thread
内的所有操作。同时,由于value
是volatile变量,thread
对value
的写操作happens - before主线程在thread.join()
之后对value
的读操作。这保证了主线程能读到thread
修改后的value
值。
违反happens - before关系可能导致的问题
如果不遵循happens - before关系,可能会导致数据竞争(Data Race)和可见性问题。数据竞争是指多个线程同时访问共享变量,并且至少有一个线程对该变量进行写操作,而这些操作之间没有合适的happens - before关系。例如:
public class DataRaceExample {
private static int value = 0;
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
value = 1;
});
Thread thread2 = new Thread(() -> {
int localValue = value;
System.out.println("Thread 2 reads value: " + localValue);
});
thread1.start();
thread2.start();
}
}
在这个例子中,由于没有任何happens - before关系保证thread1
对value
的写操作对thread2
的读操作可见,就可能出现thread2
读到旧值(0)的情况,这就是数据竞争和可见性问题。
利用happens - before关系进行正确的多线程编程
为了避免多线程编程中的数据竞争和可见性问题,我们需要正确利用happens - before关系。
- 使用volatile关键字:当共享变量需要在多线程间可见时,将其声明为volatile。例如,实现一个简单的标志位来控制线程的停止:
public class VolatileFlagExample {
private static volatile boolean stopFlag = false;
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (!stopFlag) {
// 线程执行任务
}
System.out.println("Thread stopped");
});
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
stopFlag = true;
}
}
在这个例子中,stopFlag
被声明为volatile,主线程对stopFlag
的写操作happens - before thread
对stopFlag
的读操作,确保了thread
能及时感知到stopFlag
的变化并停止。
- 使用synchronized关键字:在需要保证线程安全的代码块或方法上使用synchronized。例如,实现一个简单的计数器:
public class SynchronizedCounterExample {
private static int count = 0;
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
synchronized (SynchronizedCounterExample.class) {
count++;
}
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
synchronized (SynchronizedCounterExample.class) {
count++;
}
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + count);
}
}
在这个例子中,通过synchronized
关键字,保证了对count
的操作满足监视器锁规则,避免了数据竞争,确保最终count
的值是正确的累加结果。
- 合理使用线程间通信机制:如
Thread.join()
、wait()
、notify()
等方法,来保证线程间操作的顺序和可见性。例如,实现一个生产者 - 消费者模型:
import java.util.LinkedList;
import java.util.Queue;
public class ProducerConsumerExample {
private static final int MAX_SIZE = 5;
private static Queue<Integer> queue = new LinkedList<>();
public static void main(String[] args) {
Thread producer = new Thread(() -> {
int value = 0;
while (true) {
synchronized (queue) {
while (queue.size() == MAX_SIZE) {
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.add(value++);
System.out.println("Produced: " + (value - 1));
queue.notify();
}
}
});
Thread consumer = new Thread(() -> {
while (true) {
synchronized (queue) {
while (queue.isEmpty()) {
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int consumed = queue.poll();
System.out.println("Consumed: " + consumed);
queue.notify();
}
}
});
producer.start();
consumer.start();
}
}
在这个例子中,通过synchronized
结合wait()
和notify()
方法,保证了生产者和消费者之间操作的顺序和共享数据(queue
)的可见性,满足了happens - before关系,实现了正确的生产者 - 消费者模型。
通过正确理解和应用happens - before关系,我们能够编写出更加健壮、线程安全的Java程序,避免多线程编程中常见的数据竞争和可见性问题。在实际的开发中,需要根据具体的业务需求和场景,合理选择合适的同步机制和编程模型,以确保程序的正确性和性能。