共计 2038 个字符,预计需要花费 6 分钟才能阅读完成。
我们知道 Java 程序依靠 synchronized
对线程进行同步,使用 synchronized
的时候,锁住的是哪个对象非常重要。
让线程自己选择锁对象往往会使得代码逻辑混乱,也不利于封装。更好的方法是把 synchronized
逻辑封装起来。例如,我们编写一个计数器如下:
public class Counter {private int count = 0;
public void add(int n) {synchronized(this) {count += n;}
}
public void dec(int n) {synchronized(this) {count -= n;}
}
public int get() {return count;
}
}
这样一来,线程调用 add()
、dec()
方法时,它不必关心同步逻辑,因为 synchronized
代码块在 add()
、dec()
方法内部。并且,我们注意到,synchronized
锁住的对象是 this
,即当前实例,这又使得创建多个Counter
实例的时候,它们之间互不影响,可以并发执行:
var c1 = Counter();
var c2 = Counter();
// 对 c1 进行操作的线程:
new Thread(() -> {c1.add();
}).start();
new Thread(() -> {c1.dec();
}).start();
// 对 c2 进行操作的线程:
new Thread(() -> {c2.add();
}).start();
new Thread(() -> {c2.dec();
}).start();
现在,对于 Counter
类,多线程可以正确调用。
如果一个类被设计为允许多线程正确访问,我们就说这个类就是“线程安全”的(thread-safe),上面的 Counter
类就是线程安全的。Java 标准库的 java.lang.StringBuffer
也是线程安全的。
还有一些不变类,例如String
,Integer
,LocalDate
,它们的所有成员变量都是final
,多线程同时访问时只能读不能写,这些不变类也是线程安全的。
最后,类似 Math
这些只提供静态方法,没有成员变量的类,也是线程安全的。
除了上述几种少数情况,大部分类,例如 ArrayList
,都是非线程安全的类,我们不能在多线程中修改它们。但是,如果所有线程都只读取,不写入,那么ArrayList
是可以安全地在线程间共享的。
提示
没有特殊说明时,一个类默认是非线程安全的。
我们再观察 Counter
的代码:
public class Counter {public void add(int n) {synchronized(this) {count += n;}
}
...
}
当我们锁住的是 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;
}
...
}
就必须要同步了。
小结
用 synchronized
修饰方法可以把整个方法变为同步代码块,synchronized
方法加锁对象是this
;
通过合理的设计和数据封装可以让一个类变为“线程安全”;
一个类没有特殊说明,默认不是 thread-safe;
多线程能否安全访问某个非线程安全的实例,需要具体问题具体分析。