同步方法
约 825 字大约 3 分钟
2024-08-08
我们知道 Java 程序依靠 synchronized 对线程进行同步,使用 synchronized 的时候,锁住的是哪个对象非常重要
让线程自己选择锁对象往往会使得代码逻辑混乱,也不利于封装。更好的方法是把 synchronized 逻辑封装起来。例如,我们编写一个计数器如下
class Counter {
public static int count = 0;
public void add(int n) {
synchronized (this) {
count += n;
}
}
public void dec(int n) {
synchronized (this) {
count -= n;
}
}
public int getCount() {
return count;
}
}这样一来,线程调用 add()、dec() 方法时,它不必关心同步逻辑,因为 synchronized 代码块在 add()、dec() 方法内部。并且,我们注意到,synchronized 锁住的对象是 this,即当前实例,这又使得创建多个 Counter 实例的时候,它们之间互不影响,可以并发执行
Counter c1 = new Counter();
Counter c2 = new Counter();
// 对 c1 进行操作的线程:
new Thread(() -> {
for (int i = 0; i < 100000; i++) {
c1.add(1);
}
}).start();
new Thread(() -> {
for (int i = 0; i < 100000; i++) {
c1.dec(1);
}
}).start();
// 对 c2 进行操作的线程:
new Thread(() -> {
for (int i = 0; i < 100000; i++) {
c2.add(1);
}
}).start();
new Thread(() -> {
for (int i = 0; i < 100000; i++) {
c2.dec(1);
}
}).start();如果一个类被设计为允许多线程正确访问,我们就说这个类就是"线程安全"的(thread-safe),上面的 Counter 类就是线程安全的。Java 标准库的 java.lang.StringBuffer 也是线程安全的
还有一些不变类,例如 String,Integer,LocalDate,它们的所有成员变量都是 final,多线程同时访问时只能读不能写,这些不变类也是线程安全的
最后,类似 Math 这些只提供静态方法,没有成员变量的类,也是线程安全的
除了上述几种少数情况,大部分类,例如 ArrayList,都是非线程安全的类,我们不能在多线程中修改它们。但是,如果所有线程都只读取,不写入,那么 ArrayList 是可以安全地在线程间共享的
当我们锁住的是 this 实例时,实际上可以用 synchronized 修饰这个方法。下面两种写法是等价的
public void add(int n) {
synchronized(this) { // 锁住this
count += n;
} // 解锁
}public synchronized void add(int n) { // 锁住this
count += n;
} // 解锁因此,用 synchronized 修饰的方法就是同步方法,它表示整个方法都必须用 this 实例加锁
如果对一个静态方法添加synchronized修饰符,它锁住的是哪个对象?
public synchronized static void test(int n) {
...
}对于 static 方法,是没有 this 实例的,因为 static 方法是针对类而不是实例。但是我们注意到任何一个类都有一个由 JVM 自动创建的 Class 实例,因此,对 static 方法添加 synchronized,锁住的是该类的 Class 实例。上述 synchronized static 方法实际上相当于
public class Counter {
public static void test(int n) {
synchronized(Counter.class) {
...
}
}
}我们再考察Counter的get()方法
public class Counter {
private int count;
public int get() {
return count;
}
...
}它没有同步,因为读一个 int 变量不需要同步,如果我们把代码稍微改一下,返回一个包含两个 int 的对象,就必须要同步了
public class Counter {
private int first;
private int last;
public Pair get() {
Pair p = new Pair();
p.first = first;
p.last = last;
return p;
}
...
}