同步方法

让线程自己选择锁对象往往会使得代码逻辑混乱,也不利于封装。更好的方法是把synchronized逻辑封装起来。例如,我们编写一个计数器如下:

这样一来,线程调用add()dec()方法时,它不必关心同步逻辑,因为synchronized代码块在add()dec()方法内部。并且,我们注意到,synchronized锁住的对象是this,即当前实例,这又使得创建多个Counter实例的时候,它们之间互不影响,可以并发执行:

  1. var c1 = Counter();
  2. var c2 = Counter();
  3. // 对c1进行操作的线程:
  4. new Thread(() -> {
  5. c1.add();
  6. }).start();
  7. new Thread(() -> {
  8. c1.dec();
  9. }).start();
  10. // 对c2进行操作的线程:
  11. new Thread(() -> {
  12. c2.add();
  13. }).start();
  14. c2.dec();
  15. }).start();

现在,对于Counter类,多线程可以正确调用。

如果一个类被设计为允许多线程正确访问,我们就说这个类就是“线程安全”的(thread-safe),上面的Counter类就是线程安全的。Java标准库的也是线程安全的。

还有一些不变类,例如StringIntegerLocalDate,它们的所有成员变量都是final,多线程同时访问时只能读不能写,这些不变类也是线程安全的。

最后,类似Math这些只提供静态方法,没有成员变量的类,也是线程安全的。

没有特殊说明时,一个类默认是非线程安全的。

我们再观察Counter的代码:

  1. public class Counter {
  2. public void add(int n) {
  3. synchronized(this) {
  4. count += n;
  5. }
  6. }
  7. ...
  8. }

当我们锁住的是this实例时,实际上可以用synchronized修饰这个方法。下面两种写法是等价的:

  1. public synchronized void add(int n) { // 锁住this
  2. count += n;
  3. } // 解锁

因此,用synchronized修饰的方法就是同步方法,它表示整个方法都必须用this实例加锁。

我们再思考一下,如果对一个静态方法添加synchronized修饰符,它锁住的是哪个对象?

  1. public synchronized static void test(int n) {
  2. ...
  3. }

对于static方法,是没有this实例的,因为static方法是针对类而不是实例。但是我们注意到任何一个类都有一个由JVM自动创建的实例,因此,对static方法添加synchronized,锁住的是该类的class实例。上述synchronized static方法实际上相当于:

  1. public class Counter {
  2. public int get() {
  3. return count;
  4. }
  5. ...
  6. }

它没有同步,因为读一个int变量不需要同步。

然而,如果我们把代码稍微改一下,返回一个包含两个int的对象:

  1. public class Counter {
  2. private int first;
  3. private int last;
  4. public Pair get() {
  5. Pair p = new Pair();
  6. p.first = first;
  7. p.last = last;
  8. return p;
  9. }
  10. ...

就必须要同步了。

synchronized修饰方法可以把整个方法变为同步代码块,synchronized方法加锁对象是;

通过合理的设计和数据封装可以让一个类变为“线程安全”;

一个类没有特殊说明,默认不是thread-safe;

读后有收获可以支付宝请作者喝咖啡,读后有疑问请加微信群讨论

同步方法 - 图1