MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java内存模型中的happens-before关系

2021-01-082.8k 阅读

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是共享变量,存储在主内存中。thread1thread2都有自己的工作内存。thread1value从主内存拷贝到自己的工作内存,修改为1后写回主内存。thread2value从主内存拷贝到自己的工作内存并读取其值。但是,由于线程执行的不确定性以及内存模型的规则,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规则

  1. 程序顺序规则(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。这是因为在单线程环境下,程序的执行顺序是确定的,前面的操作结果对后续操作可见。

  1. 监视器锁规则(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();
    }
}

在这个例子中,thread1lock的解锁操作happens - before thread2lock的加锁操作。这就保证了thread1value的修改对thread2是可见的。当thread2获取到锁时,它能看到thread1修改后的value值。

  1. 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变量,thread1value的写操作happens - before thread2value的读操作。这确保了thread2能读到thread1修改后的value值。

  1. 线程启动规则(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()之后的操作)是可见的。

  1. 线程终止规则(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()返回时,主线程能看到threadvalue的修改。

  1. 线程中断规则(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())。

  1. 对象终结规则(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的赋值对主线程后续的操作是可见的。

  1. 传递性(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();
    }
}

在这个例子中,首先根据监视器锁规则,thread1lock的解锁操作happens - before thread2lock的加锁操作。然后,由于value是volatile变量,thread1value的写操作happens - before thread2value的读操作。通过这两个规则的结合,确保了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变量,threadvalue的写操作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关系保证thread1value的写操作对thread2的读操作可见,就可能出现thread2读到旧值(0)的情况,这就是数据竞争和可见性问题。

利用happens - before关系进行正确的多线程编程

为了避免多线程编程中的数据竞争和可见性问题,我们需要正确利用happens - before关系。

  1. 使用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 threadstopFlag的读操作,确保了thread能及时感知到stopFlag的变化并停止。

  1. 使用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的值是正确的累加结果。

  1. 合理使用线程间通信机制:如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程序,避免多线程编程中常见的数据竞争和可见性问题。在实际的开发中,需要根据具体的业务需求和场景,合理选择合适的同步机制和编程模型,以确保程序的正确性和性能。