Java 并发基础知识(二):理解 synchronized

  1. synchronized 的用法?
    答:synchronized 可以用于修饰类的实例方法静态方法、和代码块

  2. synchronized 修饰实例方法的基本原理?
    答:

    • 加了 synchronized 后,方法内的代码就变成了原子操作
    • synchronized 实例方法实际保护的是同一个对象的方法调用,确保同时只能有一个线程执行。
    • synchronized 实例方法保护的是当前实例对象,即 thisthis 对象有一个锁和一个等待队列,锁只能被一个线程持有,其他试图获得同样锁的线程需要等待。
  3. 执行 synchronized 实例方法的过程大致是?
    答:synchronized 的实际执行过程很复杂,而且 Java 虚拟机采用了多种优化方式以提高性能。从概念上,可以如下简单理解:

    • 尝试获得锁,如果能够获得锁,继续下一步,否则加入等待队列,阻塞并等待唤醒。
    • 执行实例方法体代码。
    • 释放锁,如果等待队列上有等待的线程,从中取一个并唤醒,如果有多个等待的线程,唤醒哪一个是不一定的,不保证公平性。
    • 当前线程不能获得锁的时候,它会加入等待队列等待,线程的状态会变为 BLOCKED
  4. synchronized 修饰实例方法的注意事项?
    答:

    • synchronized 保护的是对象而非代码,只要访问的是同一个对象的 synchronized 方法,即使是不同的代码,也会被同步顺序访问。
    • synchronized 方法不能防止非 synchronized 方法被同时执行。
    • 一般在保护变量时,需要在所有访问该变量的方法上加上 synchronized
  5. synchronized 修饰静态方法的基本原理?
    答:

    • synchronized 保护的是对象,对静态方法,synchronized 保护的是类对象
    • 每个对象都有一个锁和一个等待队列,类对象也不例外
    • synchronized 静态方法和 synchronized 实例方法保护的是不同的对象。不同的两个线程,可以一个执行 synchronized 静态方法,另一个执行 synchronized 实例方法
  6. synchronized 包装代码块的基本原理?
    答:

    • synchronized 括号里面的就是要保护的对象
    • synchronized 同步的对象可以是任意对象,任意对象都有一个锁和等待队列。或者说,任何对象都可以作为锁对象
  7. 【笔试题】请用代码块的方式写出下面代码的等价形式?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 用 synchronized 修饰的 Counter 类
    public class Counter {
    private int count;
    public synchronized void incr() {
    count++;
    }
    public synchronized int getCount() {
    return count;
    }
    }
1
2
3
4
5
6
7
8
9
10
11

// synchronized 修饰静态方法
public class StaticCounter {
private static int count = 0;
public static synchronized void incr() {
count++;
}
public static synchronized int getCount() {
return count;
}
}
答:
1
2
3
4
5
6
7
8
9
// synchronized 代码块修饰的 Counter 类
public class Counter {
private int count;
public void incr() {
synchronized(this) {
count++;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// synchronized 代码块修饰的 StaticCounter 类
public class StaticCounter {
private static int count = 0;
public static void incr() {
synchronized(StaticCounter.class) {
count++;
}
}
public static int getCount() {
synchronized(StaticCounter.class) {
return count;
}
}
}
  1. 【笔试题】使用单独对象作为锁的 Counter 类?
    答:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class Counter {
    private int count;
    private Object lock = new Object();
    public void incr() {
    synchronized(lock) {
    count++;
    }
    }
    public int getCount() {
    synchronized(lock) {
    return count;
    }
    }
    }
  1. synchronized 可重入性的概念?
    答:

    • 概念:对同一个执行线程,它在获得了锁之后,在调用其他需要同样锁的代码时,可以直接调用。
    • 举例:在一个 synchronized 实例方法内,可以直接调用其他 synchronized 实例方法。
    • 范围:可重入是一个非常自然的属性,很容易理解,但并不是所有的锁都是可重入的
  2. synchronized 的可重入性是怎样实现的?
    答:

    • 原理:可重入是通过记录锁的持有线程持有数量来实现的。
    • 细节:当调用被 synchronized 保护的代码时,检查对象是否已被锁。如果是,再检查是否被当前线程锁定。如果是,增加持有数量。如果不是被当前线程锁定,才加入等待队列。当释放锁时,减少持有数量,当数量变为 0 时才释放整个锁。
  3. 怎样理解 synchronized 保证内存可见性?
    答:

    • synchronized 除了保证原子操作外,还有一个重要的作用,就是保证内存可见性
    • 在释放锁时,所有写入都会写回内存;在获得锁后,都会从内存中读最新数据。
  4. 关键字 volatile 的作用及原理
    答:

    • 作用:给变量加 volatile 修饰符是更轻量级的保证内存可见性的方式。如果只是为了保证内存可见性,使用 synchronized 的成本有点高
    • 原理:加了 volatile 之后,Java 会在操作对应变量时插入特殊的指令,保证读写到内存最新值,而非缓存的值
  5. 什么叫死锁?
    答:死锁就是类似这种现象,比如,有 ab 两个线程,a 持有锁 A,在等待锁 B;而 b 持有锁 B,在等待锁 A。线程 ab 陷入了互相等待,最后谁都执行不下去。

  6. 【笔试题】手写一个死锁 Demo?
    答:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    public class DeadLockDemo {
    private static Object lockA = new Object();
    private static Object lockB = new Object();

    private static void startThreadA() {
    Thread aThread = new Thread() {
    @Override
    public void run() {
    synchronized(lockA) {
    try {
    Thread.sleep(1000);
    } catch(InterruptedException e) {

    }

    synchronized(lockB) {

    }

    }
    }
    };
    aThread.start();
    }

    private static void startThreadB() {
    Thread bThread = new Thread() {
    @Override
    public void run() {
    synchronized(lockB) {
    try {
    Thread.sleep(1000);
    } catch(InterruptedException e) {

    }

    synchronized(lockA) {

    }
    }
    }
    };
    bThread.start();
    }

    public static void main(String[] args) { // 运行后 aThread 和 bThread 陷入了互相等待
    startThreadA();
    startThreadB();
    }
    }
  1. 怎样解决死锁?
    答:

    • 首先,应该尽量避免在持有一个锁的同时去申请另一个锁,如果确实需要多个锁,所有代码都应该按照相同的约定顺序去申请锁。
    • 不过,在复杂的项目中,这种约定可能难以做到。此时,可以使用显示锁接口 Lock,它支持尝试获取锁(tryLock)和带时间限制的获取锁方法,使用这些方法可以在获取不到锁的时候释放已经持有的锁,然后再次尝试获取锁或干脆放弃,以避免死锁。
    • 如果还是出现了死锁,Java 不会主动处理。此时,可以借助一些工具发现运行中的死锁,比如,Java 自带的 jstack 命令会报告发现的死锁。
  2. Java 中的同步容器内部是怎样实现的?
    答:

    • Collection 中有一些方法,可以返回线程安全的同步容器。
    • 它们是给所有容器方法都加上 synchronized 来实现安全的。
  3. 加了 synchronized,所有方法调用变成了原子操作,客户端在调用时,是不是就绝对安全了呢?
    答:不是,至少有以下情况需要注意:

    • 复合操作(比如先检查再更新)。
    • 伪同步(同步错对象)。
    • 迭代(遍历时给整个容器对象加锁)。
  4. 简要描述一下 Java 中的并发容器?
    答:

    • 同步容器的性能是比较低的,当并发访问量比较大的时候性能比较差。
    • Java 中有很多专为并发设计的容器类,比如:

      • CopyOnWriteArrayList
      • ConcurrentHashMap
      • ConcurrentLinkedQueue
      • ConcurrentSkipListSet
    • 这些容器类都是线程安全的,但都没有使用 synchronized,没有迭代问题,直接支持一些复合操作,性能也高得多