#

多个程序操作同一个资源时, 有可能出现资源混乱的情况, 所以需要上锁.

# synchronized

# 锁的是什么

synchronized 关键字在锁的时候, 锁的是一个对象.

private Object lock = new Object();

public void test1() {
  synchronized (lock) {
    System.out.println("开始执行方法...");
  }
}
1
2
3
4
5
6
7

由上方代码我看可以看出, synchronized 锁住的其实是 lock 对象, 而不是方法. 当一个线程获取到 lock 对象的锁的时候, 才有权利去执行后面的代码.

注意: 锁的时候不能用基础数据类型(包括包装类), 也不能用字符串常量

同时也不建议使用 String 类型

注意: 如果被锁定的某个对象的属性发生了改变, 不影响锁的使用, 但是如果把对象变成了别的对象(重新赋值), 则锁定的对象发生改变.

# 用法

若每次锁的时候都要 new 一个对象, 也是不太方便的, 所以我们也可以使用如下的方式进行加锁.

public void test2() {
  synchronized (this) {
    System.out.println("开始执行方法...");
  }
}

public synchronized void test3() {
  System.out.println("开始执行方法...");
}
1
2
3
4
5
6
7
8
9

上面代码中两种锁的形式是等价的.

如果在静态方法中怎么锁?

public static void test4() {
  synchronized (SynchronizedDemo.class) {
    System.out.println("开始执行方法...");
  }
}

public static synchronized void test5() {
  System.out.println("开始执行方法...");
}
1
2
3
4
5
6
7
8
9

我们知道, 静态方法跟类是无关的, 所有静态方法中是没有 this 对象的, 如果 synchronized 作用在静态方法上的时候, 其等同于锁定方法所在的类的 class 对象.

锁定方法和非锁定方法是可以同时执行的.

# HotSpot 的实现

JVM 并没有对 synchronized 做规范约束, 其锁的实现方式均由虚拟机自己去实现.

HotSpot 是通过对象头信息(对象的头信息有 64 位)中的两位去标记实现的, 两位的组合分别是不同锁的类型.

# 可重录性

在一个线程调用 synchronized 方法的同时在其方法体内部调用该对象另一个 synchronized 方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是 synchronized 的可重入性。

private Object lock1 = new Object();

public void m1() {
  synchronized(lock1) {
    System.out.println("m1 执行...");
    m2();
  }
}

public void m2() {
  synchronized(lock1) {
    System.out.println("m2 执行...");
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

如代码所示, 方法 m1()m2() 锁的对象均是 lock1 对象. m1() 获取到 lock1 对象的锁后调用了 m2() 方法, 如果 synchronized 是不可重录锁, 就会造成死锁.

可重录的必要性:

class Parent{
    synchronized void m() {
        System.out.println("parent run...");
    }
}

class Children extends Parent{
    @Override
    synchronized void m() {
        System.out.println("children run...");
        super.m();
        System.out.println("children end...");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

如上一个父类 Parent() 和一个子类 Children(), 如果 synchronize 是不可重录的, 则子类重写的 m() 方法中调用父类的 m() 方法一定会报错.

# 底层实现

早期时 synchronize 是重量级的, 每次都会去操作系统去申请锁.

JDK1.5 升级后的实现(HotPost):

  1. 第一个线程去申请锁时, 先在 Object 上记录(markword) 一下 -- 偏向锁
  2. 如果有线程征用, 则升级锁 -- 自旋锁(默认10次)
  3. 升级为重量级锁 -- 去操作系统申请锁

# 异常和锁

如果出现异常后, 默认情况下锁会被释放.

public static void main(String[] args) {
  ExceptionAndLock exceptionAndLock = new ExceptionAndLock();

  new Thread(() -> {
    exceptionAndLock.m();
  }, "thread1").start();

  new Thread(() -> {
    exceptionAndLock.m();
  }, "thread2").start();
}

public synchronized void m() {
  for (int i = 0; i < 3; i++) {
    System.out.println(Thread.currentThread().getName() + ": " + i);
    try {
      Thread.sleep(100);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    if (i == 1) {
      int j = i / 0;
    }
  }
}
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

此处代码输出结果为

thread1: 0
thread1: 1
Exception in thread "thread1" ...
thread2: 0
thread2: 1
Exception in thread "thread2" ...
1
2
3
4
5
6

执行 thread1 时, 当 i 为 1 时抛出了异常, 此时 thread2 得到的执行的机会进入了线程的执行.

# 锁的类型

# 自旋锁(CAS)

自旋锁的处理效率比重量级锁要高, 但是其会暂用 CPU. 重量级锁时把执行线程放到执行队列中, 不会占用 CPU.

实际使用: 执行时间短, 线程数少, 使用自旋锁. 执行时间长, 线程数多, 使用重量级锁.

# volatile

volatile(可见的; 易见的;) 有两大特性, 保证线程可见性禁止指令重排序.

/*volatile*/ boolean running = true;
void m() {
  System.out.println("m start...");
  while (running){
  }
  System.out.println("m end...");
}

public static void main(String[] args) {

  VolatileDemo volatileDemo = new VolatileDemo();

  new Thread(volatileDemo::m, "t1")
    .start();

  try {
    Thread.sleep(100);
  } catch (InterruptedException e) {
    e.printStackTrace();
  }

  volatileDemo.running = false;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

上面代码中, m() 方法会不断执行.

如果给 running 参数加上 volatile 关键字, m() 方法就会正常结束.

# 保证线程可见性

线程执行时, 会把一些参数 load 到自己的线程内部的专属空间中. 这时, 如果一个线程改变了堆内存中的数据, 另一个线程内部的副本值并不会实实改变. 如果加了 volatile 关键字, 一个线程改变了某个值, 则会通知 CPU. 这时其他的线程也会发生改变.

扩展: 多个 CPU 处理时会使用缓存一致性协议(MESI), 用于保证多个 CPU 之间的数据一致性.

# MESI

多个 CPU 在处理多核之间数据一致性的时候, 使用的协议.

# 禁止指令重排序

指令重排序是指 CPU 执行指令时, 执行的顺序跟代码中的顺序不一致.

单例模式的双重检查实现, 就是很好的一个示例.

JVM 初始化一个对象的顺序是

  1. 给对象申请内存, 给成员设置初始值.
  2. 给对象的成员变量初始化, 这时才会设置真正的值.
  3. 把内存的内容赋值给对象(让对象的索引指向堆内存).
/**
 * 测试程序的入口
 * 调用工具查看此方法的汇编指令
 * view -> Show Bytecode With jclasslib
 * 查看 方法 -> main -> Code 会发现其指令如下
 * 0 new #2 <java/lang/Object>
 * 3 dup
 * 4 invokespecial #1 <java/lang/Object.<init>>
 * 7 astore_1
 * 8 return
 *
 * @param args 传入
 * Author DanielLi
 * Version 2.0.0-RELEASE
 * DateTime 2020/11/2 10:45 下午
 */
public static void main(String[] args) {

    Object o = new Object();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

如果出现了指令重排序, 有可能出现 1 - 3 - 2 的执行情况, 如果出现了这种情况, 第二个线程获取到的对象的成员变量的值有可能为初始值, 而不是想要设定的真正的值.

如果加入了 volatile 关键字, 则会禁止指令重排序, 就不会出现上述的问题了.

# 锁的优化

# 锁的细化

如果一个方法中只有一小部分代码的操作需要加锁, 那么加锁的时候就不应该给整个方法加锁, 而是只加在那部分需要加锁的地方.

# 锁的粗化

由于锁的粒度越来越细, 如果一个方法中有很多处地方都需要加锁, 倒不如给整个方法加锁.

上次更新时间: 2020/11/10 上午1:34:34