线程同步
1071字约4分钟
2024-08-08
介绍
当多个线程同时运行时,线程的调度由操作系统决定,程序本身无法决定。因此,任何一个线程都有可能在任何指令处被操作系统暂停,然后在某个时间段后继续执行
这个时候,有个单线程模型下不存在的问题就来了:如果多个线程同时读写共享变量,会出现数据不一致的问题
我们来看一个例子
public static void main(String[] args) throws InterruptedException {
Thread add = new AddThread();
Thread dec = new DecThread();
add.start();
dec.start();
add.join();
dec.join();
System.out.println(Counter.count);
}
class Counter {
public static int count = 0;
}
class AddThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
Counter.count += 1;
}
}
}
class DecThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
Counter.count -= 1;
}
}
}
上面的代码很简单,两个线程同时对一个 int
变量进行操作,一个加 1000
次,一个减 1000
次,最后结果应该是 0
,但是,每次运行,结果实际上都是不一样的
这是因为对变量进行读取和写入时,结果要正确,必须保证是原子操作。原子操作是指不能被中断的一个或一系列操作
多线程模型下,要保证逻辑正确,对共享变量进行读写时,必须保证一组指令以原子方式执行:即某一个线程执行时,其他线程必须等待
保证一段代码的原子性就是通过加锁和解锁实现的。Java
程序使用 synchronized
关键字对一个对象进行加锁
public static void main(String[] args) throws InterruptedException {
Thread add = new AddThread();
Thread dec = new DecThread();
add.start();
dec.start();
add.join();
dec.join();
System.out.println(Counter.count);
}
class Counter {
public static final Object lock = new Object();
public static int count = 0;
}
class AddThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
synchronized (Counter.lock) {
Counter.count += 1;
}
}
}
}
class DecThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
synchronized (Counter.lock) {
Counter.count -= 1;
}
}
}
}
synchronized(Counter.lock) { // 获取锁
...
} // 释放锁
它表示用 Counter.lock
实例作为锁,两个线程在执行各自的 synchronized(Counter.lock) { ... }
代码块时,必须先获得锁,才能进入代码块进行。执行结束后,在 synchronized
语句块结束会自动释放锁。这样一来,对 Counter.count
变量进行读写就不可能同时进行。上述代码无论运行多少次,最终结果都是 0
使用 synchronized
解决了多线程同步访问共享变量的正确性问题。但是,它的缺点是带来了性能下降。因为 synchronized
代码块无法并发执行。此外,加锁和解锁需要消耗一定的时间,所以,synchronized
会降低程序的执行效率
我们来概括一下如何使用 synchronized
找出修改共享变量的线程代码块
选择一个共享实例作为锁
使用
synchronized(lockObject) { ... }
在使用 synchronized
的时候,不必担心抛出异常。因为无论是否有异常,都会在 synchronized
结束处正确释放锁
不需要 synchronized
的操作
JVM
规范定义了几种原子操作
基本类型(
long
和double
除外)赋值,例如:int n = m
引用类型赋值,例如:
List<String> list = anotherList
long
和 double
是 64
位数据,JVM
没有明确规定 64
位赋值操作是不是一个原子操作,不过在 x64
平台的 JVM
是把 long
和 double
的赋值作为原子操作实现的
单条原子操作的语句不需要同步。例如:
public void set(int m) {
synchronized(lock) {
this.value = m;
}
}
就不需要同步,对引用也是类似,例如
public void set(String s) {
this.value = s;
}
如果是多行赋值语句,就必须保证是同步操作
class Pair {
int first;
int last;
public void set(int first, int last) {
synchronized(this) {
this.first = first;
this.last = last;
}
}
}
有些时候,通过一些巧妙的转换,可以把非原子操作变为原子操作。例如,上述代码如果改造成
class Pair {
int[] pair;
public void set(int first, int last) {
int[] ps = new int[] { first, last };
this.pair = ps;
}
}
就不再需要同步,因为 this.pair = ps
是引用赋值的原子操作。而语句
int[] ps = new int[] { first, last };
这里的 ps
是方法内部定义的局部变量,每个线程都会有各自的局部变量,互不影响,并且互不可见,并不需要同步