MST

星途 面试题库

面试题:Java内存模型下的锁机制与并发编程的联系

说明Java内存模型里锁操作(如synchronized)是怎样与内存可见性、原子性以及有序性相关联的,在多线程环境下,锁的使用不当会导致哪些与内存模型相关的问题。
39.9万 热度难度
编程语言Java

知识考点

AI 面试

面试题答案

一键面试

锁操作与内存可见性、原子性、有序性的关联

  1. 内存可见性
    • 当一个线程获取锁时,它会从主内存中重新读取共享变量的值。这是因为锁的获取操作会强制线程刷新本地缓存,使得线程能看到其他线程对共享变量的最新修改。
    • 当一个线程释放锁时,它会将共享变量的最新值刷新回主内存。这样其他线程在获取锁后,就能看到这些最新的修改,从而保证了内存可见性。例如,以下代码:
public class VisibilityExample {
    private static int value = 0;
    public static synchronized void increment() {
        value++;
    }
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                increment();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                increment();
            }
        });
        thread1.start();
        thread2.start();
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Final value: " + value);
    }
}
  • 在这个例子中,increment 方法被 synchronized 修饰,当一个线程执行完 increment 方法并释放锁时,value 的最新值会被刷新回主内存,另一个线程获取锁进入 increment 方法时,会从主内存读取 value 的最新值。
  1. 原子性
    • 锁操作能保证被锁保护的代码块是原子性的。例如,对于 synchronized 修饰的方法或代码块,在同一时刻只有一个线程能够进入执行。以简单的自增操作 i++ 为例,它实际上包含了读取、加1、写回三个步骤,不是原子性的。但如果使用 synchronized 修饰,就可以保证这三个步骤在一个线程执行时不会被其他线程打断,从而保证了原子性。如下代码:
public class AtomicityExample {
    private static int count = 0;
    public static synchronized void increment() {
        count++;
    }
    public static void main(String[] args) {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    increment();
                }
            });
            threads[i].start();
        }
        for (Thread thread : threads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("Final count: " + count);
    }
}
  • 这里 increment 方法被 synchronized 修饰,确保了 count++ 操作的原子性,即使多个线程同时调用 increment 方法,也不会出现数据竞争导致 count 结果错误的情况。
  1. 有序性
    • 锁操作会建立一个 happens - before 关系。获取锁的操作 happens - before 于该锁保护的代码块中的所有操作,而该锁保护的代码块中的所有操作 happens - before 于释放锁的操作。这就保证了在锁保护的代码块内,操作是有序执行的。例如:
public class OrderingExample {
    private static int x = 0;
    private static int y = 0;
    public static synchronized void method1() {
        x = 1;
        y = 2;
    }
    public static synchronized void method2() {
        System.out.println("x: " + x + ", y: " + y);
    }
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            method1();
        });
        Thread thread2 = new Thread(() -> {
            method2();
        });
        thread1.start();
        thread2.start();
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  • 在这个例子中,当 method1 执行完释放锁后,method2 获取锁执行,由于锁建立的 happens - before 关系,method2 能看到 method1 中对 xy 的正确赋值顺序,不会出现重排序导致 y 先于 x 被赋值的情况(在没有锁的情况下可能会因指令重排序出现这种情况)。

锁使用不当导致与内存模型相关的问题

  1. 可见性问题
    • 如果锁使用不当,例如没有对共享变量的访问进行正确的加锁,可能会导致可见性问题。假设多个线程访问共享变量,但只有部分线程对其访问进行了加锁,那么未加锁的线程可能看不到其他线程对共享变量的最新修改。如下代码:
public class BadVisibilityExample {
    private static int data = 0;
    public static void write() {
        data = 1;
    }
    public static void read() {
        System.out.println("Data: " + data);
    }
    public static void main(String[] args) {
        Thread writer = new Thread(() -> {
            write();
        });
        Thread reader = new Thread(() -> {
            read();
        });
        writer.start();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        reader.start();
    }
}
  • 在这个例子中,writeread 方法都没有加锁,reader 线程可能会打印出 0,即使 writer 线程已经将 data 赋值为 1,因为 reader 线程可能没有从主内存中读取到最新值。
  1. 原子性问题
    • 锁范围设置不当会导致原子性问题。如果只对部分操作加锁,而不是对整个需要原子执行的操作序列加锁,就会出现原子性问题。例如:
public class BadAtomicityExample {
    private static int count = 0;
    public static void increment() {
        synchronized (BadAtomicityExample.class) {
            int temp = count;
            temp++;
        }
        count = temp;
    }
    public static void main(String[] args) {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    increment();
                }
            });
            threads[i].start();
        }
        for (Thread thread : threads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("Final count: " + count);
    }
}
  • increment 方法中,只对 int temp = count;temp++; 加锁,而 count = temp; 没有加锁,这就导致在多线程环境下,count 的更新不是原子性的,最终 count 的值可能不正确。
  1. 有序性问题
    • 错误的锁使用也可能破坏有序性。例如,在没有正确的锁机制下,不同线程对共享变量的操作可能会发生重排序,导致程序出现逻辑错误。假设两个线程分别对两个共享变量进行操作:
public class BadOrderingExample {
    private static int a = 0;
    private static int b = 0;
    public static void thread1() {
        a = 1;
        b = 2;
    }
    public static void thread2() {
        if (b == 2) {
            System.out.println("a: " + a);
        }
    }
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            thread1();
        });
        Thread t2 = new Thread(() -> {
            thread2();
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  • 在没有加锁的情况下,thread1 中的 a = 1;b = 2; 可能会发生重排序,thread2 可能会先看到 b = 2,但此时 a 可能还没有被赋值为 1,从而打印出错误的结果。如果在 thread1thread2 对共享变量的访问上正确加锁,就能避免这种因重排序导致的有序性问题。