锁操作与内存可见性、原子性、有序性的关联
- 内存可见性
- 当一个线程获取锁时,它会从主内存中重新读取共享变量的值。这是因为锁的获取操作会强制线程刷新本地缓存,使得线程能看到其他线程对共享变量的最新修改。
- 当一个线程释放锁时,它会将共享变量的最新值刷新回主内存。这样其他线程在获取锁后,就能看到这些最新的修改,从而保证了内存可见性。例如,以下代码:
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
的最新值。
- 原子性
- 锁操作能保证被锁保护的代码块是原子性的。例如,对于
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
结果错误的情况。
- 有序性
- 锁操作会建立一个 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
中对 x
和 y
的正确赋值顺序,不会出现重排序导致 y
先于 x
被赋值的情况(在没有锁的情况下可能会因指令重排序出现这种情况)。
锁使用不当导致与内存模型相关的问题
- 可见性问题
- 如果锁使用不当,例如没有对共享变量的访问进行正确的加锁,可能会导致可见性问题。假设多个线程访问共享变量,但只有部分线程对其访问进行了加锁,那么未加锁的线程可能看不到其他线程对共享变量的最新修改。如下代码:
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();
}
}
- 在这个例子中,
write
和 read
方法都没有加锁,reader
线程可能会打印出 0
,即使 writer
线程已经将 data
赋值为 1
,因为 reader
线程可能没有从主内存中读取到最新值。
- 原子性问题
- 锁范围设置不当会导致原子性问题。如果只对部分操作加锁,而不是对整个需要原子执行的操作序列加锁,就会出现原子性问题。例如:
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
的值可能不正确。
- 有序性问题
- 错误的锁使用也可能破坏有序性。例如,在没有正确的锁机制下,不同线程对共享变量的操作可能会发生重排序,导致程序出现逻辑错误。假设两个线程分别对两个共享变量进行操作:
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
,从而打印出错误的结果。如果在 thread1
和 thread2
对共享变量的访问上正确加锁,就能避免这种因重排序导致的有序性问题。