JUC 并发编程(续)

339 阅读26分钟

volatile

JMM

JMM(Java 内存模型)定义了 Java 虚拟机 (JVM) 在计算机内存(RAM)中的工作方式,是一个抽象概念。
JVM 在设计时候考虑到,如果 JAVA 线程每次读取和写入变量都直接操作主内存,对性能影响比较大,所以每条线程拥有各自的工作内存,工作内存中的变量是主内存中的一份拷贝,线程对变量的读取和写入,直接在工作内存中操作,而不能直接去操作主内存中的变量。但是这样就会出现一个问题,当一个线程修改了自己工作内存中变量,对其他线程是不可见的,会导致线程不安全的问题。因此 JMM 制定了一套标准来保证开发者在编写多线程程序的时候,能够控制什么时候内存会被同步给其他线程。

内存交互操作

内存交互操作有 8 种,虚拟机实现必须保证每一个操作都是原子的,不可再分的(对于 double 和 long 类型的变量来说,load、store、read 和 write 操作在某些平台上允许例外)

  • lock(锁定):作用于主内存的变量,把一个变量标识为线程独占状态
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  • read(读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用
  • load(载入):作用于工作内存的变量,它把 read 操作从主存中变量放入工作内存中
  • use(使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
  • assign(赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
  • store(存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的 write 使用
  • write(写入):作用于主内存中的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中 JMM 对这八种指令的使用,制定了如下规则:
  • 不允许 read 和 load、store 和 write 操作之一单独出现。即使用了 read 必须 load,使用了 store 必须 write
  • 不允许线程丢弃他最近的 assign 操作,即工作变量的数据改变了之后,必须告知主存
  • 不允许一个线程将没有 assign 的数据从工作内存同步回主内存
  • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施 use、store 操作之前,必须经过 assign 和 load 操作
  • 一个变量同一时间只有一个线程能对其进行 lock。多次 lock 后,必须执行相同次数的 unlock 才能解锁
  • 如果对一个变量进行 lock 操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新 load 或 assign 操作初始化变量的值
  • 如果一个变量没有被 lock,就不能对其进行 unlock 操作。也不能 unlock 一个被其他线程锁住的变量
  • 对一个变量进行 unlock 操作之前,必须把此变量同步回主内存 JMM 中关于同步的一些约定:
  1. 线程解锁前,必须把共享变量立刻刷回主存。
  2. 线程加锁前,必须读取主存中的最新值到工作内存中。
  3. 加锁和解锁是同一把锁。

可见性问题

内存交互操作
CPU 中运行的线程 A 和线程 B 从主存中拷贝共享数据 number 到它们的工作内存,其中线程 A 把 number 改为 2。但这个变更对线程 B 不可见,因为这个更改还没有 flush 到主存中。
要解决可见性问题,可以使用 volatile 关键字(要求被修改之后的变量立即更新到主内存,每次使用前从主内存处读取)或者加锁(lock 前从主内存中读数据,unlock 前必须把数据更新到主内存)。

原子性问题

线程 A 和线程 B 共享 number。假设线程 A 从主存读取 number 到自己的工作内存,同时,线程 B 也读取了 number 到它的工作内存,并且这两个线程都对 number 做了加一操作。此时,number 加一操作被执行了两次,不过都在不同的工作内存中。如果这两个加一操作是串行执行的,那么 number 便会在原始值上加 2,最终主存中的 number 会是 3。然而图中两个加一操作是并行的,不管是线程 A 还是线程 B 先 flush 计算结果到主存,最终主存中的 number 只会增加 1 次变成 2,尽管一共有两次加一操作。
要解决原子性问题,可以加锁

有序性问题

Java 的有序性跟线程相关。如果在线程内部观察,会发现当前线程的一切操作都是有序的。如果在线程的外部来观察的话,会发现线程的所有操作都是无序的。因为 JMM 的工作内存和主内存之间存在延迟,而且 Java 会对一些指令进行重新排序。
要解决有序性问题,可以使用 volatile 关键字或者加锁,它们能保证指令不进行重排序,即保证程序的有序性。

指令重排:编译器优化的重排、指令并行也可能会重排、内存系统也会重排。经过这些重排,代码才会被执行! 现有两个线程需要执行一些步骤,a 和 b 默认为 0: |线程 A|线程 B| |:-:|:-:| |a = 1;
x = b;|b = 2;
y = a;|

因为线程执行顺序的不可确定性,有可能出现如下结果:

  1. x = 0; y = 1;
  2. x = 2; y = 0;
  3. x = 2; y = 1; 那么有没有可能出现 x = 0; y = 0; 呢?
    答案很明显是有可能的!因为指令重排,有可能执行的时候是下面这种情况: |线程 A|线程 B| |:-:|:-:| |x = b;
    a = 1;|y = a;
    b = 2;|

指令重排不管怎么对指令进行排序,都必须保证单线程下执行结果不会被改变(as-if-serial)。那么对于线程 A 和线程 B 来说先执行哪一步都是可以的,所以就可能变成上面那种情况(线程 A 中的 x 变量依赖于 b,而 b 已经在程序运行前创建好了,只要满足 x = b; 这一步在 b 创建之后就可以。这就是依赖性)。

volatile

保证可见性

volatile 要求被修改之后的变量立即更新到主内存,每次使用前从主内存处读取。

package com.example.volat;

import java.util.concurrent.TimeUnit;

public class VolatileTest {
  private static boolean flag = true;

  public static void main(String[] args) throws InterruptedException {
    new Thread(() -> {
      while (flag) {
      }
    }).start();
    TimeUnit.SECONDS.sleep(1);
    flag = false;
    System.out.println("flag: " + flag);
  }
}

上面的程序正常逻辑应该是 flag 改为 false 后,开启的线程退出循环,进而程序终止。然而实际上,如下:
因为线程有自己的工作内存,线程使用变量时将变量拷贝到自己的工作内存中,而另一个线程改变变量的值后,本线程并不知道!只需要给 flag 加上 volatile 修饰词,就可以保证可见性。

  private volatile static boolean flag = true;

不保证原子性

package com.example.volat;

import java.util.concurrent.TimeUnit;

public class VolatileTest {
  private volatile static int number = 0;

  private static void increment() {
    number++;
  }

  public static void main(String[] args) throws InterruptedException {
    for (int i = 0; i < 10; i++) {
      new Thread(() -> {
        for (int j = 0; j < 200; j++) increment();
      }).start();
    }
    TimeUnit.SECONDS.sleep(2);
    System.out.println(number);
  }
}

上面的程序运行完毕,结果应该是 2000,然而不是,并且 idea 也给出提示了!
那么问题来了:number++; 只是一行代码,怎么不是原子性操作呢?
因为 number++; 实际上是:number = number + 1;,分两步:第一步执行加一操作,第二步把值赋给 number。使用 javap 反编译 class 文件 javap -p -c VolatileTest.class,如下:
如果不加 lock 或 synchronized,怎么保证原子性?

使用原子类保证原子性

原子类

AtomicInteger 就是 Integer 的原子类。将 int 替换成 AtomicInteger 就可以解决原子性问题,代码如下:

package com.example.volat;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class VolatileTest {
  private volatile static AtomicInteger number = new AtomicInteger();

  private static void increment() {
    number.getAndIncrement();
  }

  public static void main(String[] args) throws InterruptedException {
    for (int i = 0; i < 10; i++) {
      new Thread(() -> {
        for (int j = 0; j < 200; j++) increment();
      }).start();
    }
    TimeUnit.SECONDS.sleep(2);
    System.out.println(number);
  }
}

那么为什么 AtomicInteger 可以保证原子性呢?

// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

发现调用的 Unsafe 对象方法,相关代码如下:

public final int getAndAddInt(Object var1, long var2, int var4) {
  int var5;
  do {// 这里用了自旋锁,稍候解释自旋锁
    var5 = this.getIntVolatile(var1, var2);
  } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
  return var5;
}

// 发现调用的本地方法(compareAndSwap 即 CAS,比较再交换,乐观锁,下面再详细介绍)
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

Unsafe 类非常特殊,方法基本是本地方法,和操作系统挂钩。

保证有序性

volatile 可以避免指令重排。 Memory Barrier(内存屏障)可以禁止指令重排,Java 编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序,从而让程序按我们预想的流程去执行。

  1. 保证特定操作的执行顺序。
  2. 保证某些数据(或则是某条指令的执行结果)的内存可见性。

volatile 总结

volatile 可以保证可见性;不保证原子性;由于内存屏障,可以避免指令重排,从而保证有序性。
那么哪里使用 volatile 最多呢?单例模式!

单例模式

饿汉式

package com.example.single;

public class HungryMan {
  private static final HungryMan HUNGRY_MAN = new HungryMan();

  private HungryMan() {
  }

  public static HungryMan getInstance() {
    return HUNGRY_MAN;
  }
}

线程安全,但是存在一个问题,就是如果 HungryMan 中含有大量变量并存储了大量数据,这样势必导致资源的浪费。

懒汉式

package com.example.single;

public class LazyMan {
  private static LazyMan lazyMan;

  private LazyMan() {
  }

  public static LazyMan getInstance() {
    if (lazyMan == null) {
      lazyMan = new LazyMan();
    }
    return lazyMan;
  }
}

线程不安全!

DCL 懒汉式

package com.example.single;

public class DCLLazyMan {
  private volatile static DCLLazyMan lazyMan;

  private DCLLazyMan() {
  }

  public static DCLLazyMan getInstance() {
    if (lazyMan == null) {
      synchronized (DCLLazyMan.class) {
        if (lazyMan == null) {
          /* 对象赋值并不是原子性操作,因为它有三步:
           * 1. 分配内存空间
           * 2、执行构造方法,初始化对象
           * 3、把这个对象指向这个空间
           * 如果是 1,2,3 顺序执行,不会产生问题;
           * 如果是 1,3,2 顺序执行,那么在 1 和 3 执行完毕后,
           *   另一个线程调用本方法,此时 lazyMan 不为 null,直接返回,
           *   但是此时 lazyMan 还没有完成初始化!
           * 所以必须给 lazyMan 加上 volatile 修饰词。
           */
          lazyMan = new DCLLazyMan();
        }
      }
    }
    return lazyMan;
  }
}

线程安全,没有资源占用的情况,一切看起来都很完美!

静态内部类

package com.example.single;

public class StaticInnerClass {
  private StaticInnerClass() {
  }

  public static StaticInnerClass getInstance() {
    return InnerClass.INSTANCE;
  }

  private static class InnerClass {
    private static final StaticInnerClass INSTANCE = new StaticInnerClass();
  }
}

外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化 INSTANCE,故而不占内存。即当 StaticInnerClass 第一次被加载时,并不需要去加载 InnerClass,只有当 getInstance() 方法第一次被调用时,才会去初始化 INSTANCE,第一次调用 getInstance() 方法会导致虚拟机加载 InnerClass 类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。

上面四种真的安全吗?

以 DCL 懒汉式举例:

  1. 如果实现了 Serializable 序列化接口,那么可以通过序列化和反序列化得到新的对象,代码如下:
package com.example.single;

import java.io.Serializable;

public class DCLLazyMan implements Serializable {
  public static final long serialVersionUID = 1L;

  private volatile static DCLLazyMan lazyMan;

  private DCLLazyMan() {
  }

  public static DCLLazyMan getInstance() {
    if (lazyMan == null) {
      synchronized (DCLLazyMan.class) {
        if (lazyMan == null) {
          lazyMan = new DCLLazyMan();
        }
      }
    }
    return lazyMan;
  }
}

DCLLazyMan.java 代码

package com.example.single;

import java.io.*;

public class SecurityTest {
  public static void main(String[] args) throws IOException, ClassNotFoundException {
    DCLLazyMan instance = DCLLazyMan.getInstance();
    String filename = "d://dcl-lazyman";
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filename));
    oos.writeObject(instance);
    oos.close();
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
    Object o = ois.readObject();
    System.out.println(o.getClass().getName());
    System.out.println(o);
    System.out.println(instance);
  }
}

SecurityTest.java 测试代码 测试结果 测试结果:内存地址不一样,说明是两个对象!破解成功。

  1. 使用反射破解单例模式,代码如下:
package com.example.single;

public class DCLLazyMan {
  private volatile static DCLLazyMan lazyMan;
  // 加上一个标记,标记是否实例化过
  private static boolean initialFlag = false;

  private DCLLazyMan() {
    synchronized (DCLLazyMan.class) {
      if (initialFlag) {
        throw new RuntimeException("不要试图使用反射破坏单例异常");
      } else {
        initialFlag = true;
      }
    }
  }

  public static DCLLazyMan getInstance() {
    if (lazyMan == null) {
      synchronized (DCLLazyMan.class) {
        if (lazyMan == null) {
          lazyMan = new DCLLazyMan();
        }
      }
    }
    return lazyMan;
  }
}

DCLLazyMan.java 代码

package com.example.single;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

public class SecurityTest {
  public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException {
    DCLLazyMan instance = DCLLazyMan.getInstance();
    // 上一步执行完毕后,initialFlag 为 true,如果不将 initialFlag 置为 false,势必报错!
    Field initialFlag = DCLLazyMan.class.getDeclaredField("initialFlag");
    initialFlag.setAccessible(true);
    initialFlag.setBoolean(instance, false);
    Constructor<DCLLazyMan> constructor = DCLLazyMan.class.getDeclaredConstructor();
    constructor.setAccessible(true);
    DCLLazyMan newInstance = constructor.newInstance();
    System.out.println(instance);
    System.out.println(newInstance);
  }
}

SecurityTest.java 测试代码 测试结果 测试结果:内存地址不一样,说明是两个对象!破解成功。 那么怎样才能实现安全的单例模式呢?枚举!

枚举

package com.example.single;

public enum SingleEnum {
  INSTANCE;

  public SingleEnum getInstance() {
    return INSTANCE;
  }
}

SingleEnum.java

  1. 使用序列化破解(枚举类不需要实现序列化接口)
package com.example.single;

import java.io.*;

public class EnumSecurityTest {
  public static void main(String[] args) throws IOException, ClassNotFoundException {
    SingleEnum instance = SingleEnum.getInstance();
    String filename = "d://enum";
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filename));
    oos.writeObject(instance);
    oos.close();
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
    Object o = ois.readObject();
    System.out.println(o.getClass().getName());
    System.out.println(o);
    System.out.println(instance);
    System.out.println(instance == o);
  }
}

EnumSecurityTest.java 测试代码 测试结果 测试结果:使用 == 判断仍为 true!是同一个对象。

  1. 使用反射破解
package com.example.single;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class EnumSecurityTest {
  public static void main(String[] args) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException {
    SingleEnum instance = SingleEnum.getInstance();
    Constructor<SingleEnum> constructor = SingleEnum.class.getDeclaredConstructor();
    constructor.setAccessible(true);
    SingleEnum newInstance = constructor.newInstance();
    System.out.println(instance);
    System.out.println(newInstance);
  }
}

EnumSecurityTest.java 测试代码 测试结果 测试结果:居然报了无此方法的异常,什么情况! 分析:代码是不会骗人的,说明该枚举类编译后就是没有无参构造器。 idea 反编译 使用 idea 打开 class 文件,发现居然有无参构造器,可能是 idea 欺骗了我们。 javap 反编译 使用 javap -p SingleEnum.class 反编译后有两个发现:1. 枚举类编译后也是普通类;2. 居然也有无参构造器。可能是 javap 欺骗了我们。

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   SingleEnum.java

package com.example.single;


public final class SingleEnum extends Enum
{

    public static SingleEnum[] values()
    {
        return (SingleEnum[])$VALUES.clone();
    }

    public static SingleEnum valueOf(String name)
    {
        return (SingleEnum)Enum.valueOf(com/example/single/SingleEnum, name);
    }

    private SingleEnum(String s, int i)
    {
        super(s, i);
    }

    public static SingleEnum getInstance()
    {
        return INSTANCE;
    }

    public static final SingleEnum INSTANCE;
    private static final SingleEnum $VALUES[];

    static 
    {
        INSTANCE = new SingleEnum("INSTANCE", 0);
        $VALUES = (new SingleEnum[] {
            INSTANCE
        });
    }
}

使用 jad 反编译软件,执行命令 jad.exe -p SingleEnum.class > SingleEnum.java,得到最终的反编译源码如上,发现两个参数的私有构造器。jad 反编译软件下载地址 那么继续使用反射破解!

package com.example.single;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class EnumSecurityTest {
  public static void main(String[] args) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException {
    SingleEnum instance = SingleEnum.getInstance();
    Constructor<SingleEnum> constructor = SingleEnum.class.getDeclaredConstructor(String.class, int.class);
    constructor.setAccessible(true);
    SingleEnum newInstance = constructor.newInstance("INSTANCE", 0);
    System.out.println(instance);
    System.out.println(newInstance);
  }
}

修改一下代码~ 测试结果 测试结果:最终发现在反射里面做了限制,禁止实例化枚举类。所以单例建议使用枚举~


CAS

Compare And Swap 的缩写,即先比较,相等则修改,否则不修改。

package com.example.cas;

import java.util.concurrent.atomic.AtomicInteger;

public class CasTest {
  public static void main(String[] args) {
    AtomicInteger integer = new AtomicInteger(2020);
    System.out.println(integer.compareAndSet(2020, 2021));
    System.out.println(integer.get());
    System.out.println(integer.compareAndSet(2020, 2021));
    System.out.println(integer.get());
    System.out.println(integer.getAndIncrement());
  }
}

查看 getAndIncrement() 方法的源码,发现如下:

private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;// 是 value 属性的内存偏移地址

static {
    try {
        valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

private volatile int value;

public final int getAndIncrement() {
    // 最终调用的 Unsafe 类中的 getAndAddInt 方法
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

------------- 以下是 Unsafe 类的 getAndAddInt 方法 --------------
public final int getAndAddInt(Object var1, long var2, int var4) {
  int var5;
  do {
    var5 = this.getIntVolatile(var1, var2);// 获取内存地址中的值
  } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
  // 内存操作,效率极高
  // 自旋锁
  return var5;
}

CAS:比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么则执行操作;如果不是就一直循环。 缺点:

  1. 循环会耗时;
  2. 一次只能保证一个共享变量的原子性;
  3. 会产生 ABA 问题。

ABA 问题

狸猫换太子:线程 A 要操作共享变量 instance,线程 B 也要操作共享变量 instance(假如此时 instance.number = 2020),线程 A 先将其改为 2021,之后又改为 2020,线程 B 对此一无所知,然后操作数据。实际上 instance.number 已经发生过变更,线程 B 去操作时,应该失败!

package com.example.aba;

import java.util.concurrent.atomic.AtomicInteger;

public class AbaTest {
  public static void main(String[] args) {
    AtomicInteger integer = new AtomicInteger(2020);
    // ============== 捣乱的线程 ==================
    System.out.println(integer.compareAndSet(2020, 2021));
    System.out.println(integer.get());
    System.out.println(integer.compareAndSet(2021, 2020));
    System.out.println(integer.get());
    // ============== 期望的线程 ==================
    System.out.println(integer.compareAndSet(2020, 6666));
    System.out.println(integer.get());
  }
}

如何解决 ABA 问题?带版本号的原子操作。

原子操作

package com.example.aba;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;

public class AtomicReferenceTest {
  private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1, 1);

  public static void main(String[] args) {
    // 捣乱的线程,将目标值修改,再复原,但是 stamp 一直在增加
    new Thread(() -> {
      int stamp = atomicStampedReference.getStamp(); // 获得版本号
      System.out.println("a-stamp-1: " + stamp);
      try {
        TimeUnit.SECONDS.sleep(1);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      atomicStampedReference.compareAndSet(1, 2, stamp, stamp + 1);
      stamp = atomicStampedReference.getStamp();
      System.out.println("a-stamp-2: " + stamp);
      System.out.println(atomicStampedReference.compareAndSet(2, 1, stamp, stamp + 1));
      System.out.println("a-stamp-3: " + atomicStampedReference.getStamp());
    }).start();
    // 与乐观锁的原理相同
    // 先获取到版本号,然后进行业务操作,最后更新数据
    new Thread(() -> {
      int stamp = atomicStampedReference.getStamp(); // 获得版本号
      System.out.println("b-stamp-1: " + stamp);
      try {
        TimeUnit.SECONDS.sleep(2);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println(atomicStampedReference.compareAndSet(1, 6, stamp, stamp + 1));
      System.out.println("b-stamp-2: " + atomicStampedReference.getStamp());
    }).start();
  }
}

注意:compareAndSet() 方法底层使用的 == 去比较的,所以注意必须是同一个对象,否则执行出问题。包装类里面缓存了一些数据,当使用包装方法 valueOf() 时,会判断缓存中是否存在,存在就返回缓存中的对象,不存在就创建新的。

------------------- 以下是 AtomicStampedReference 的源码 ----------------------
public boolean compareAndSet(V expectedReference, V newReference,
                             int expectedStamp, int newStamp) {
    Pair<V> current = pair;
    return expectedReference == current.reference &&
        expectedStamp == current.stamp &&
        ((newReference == current.reference &&
          newStamp == current.stamp) ||
         casPair(current, Pair.of(newReference, newStamp)));
}

------------------- 以下是 Integer 的源码 ----------------------
public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

private static class IntegerCache {
    static final int low = -128;// 固定从 -128 开始
    static final int high;
    static final Integer cache[];// 缓存数组

    static {
        // high 的值可以通过配置获得
        int h = 127;
        String integerCacheHighPropValue =
            sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        if (integerCacheHighPropValue != null) {
            try {
                int i = parseInt(integerCacheHighPropValue);
                i = Math.max(i, 127);// 取配置的值和 127 中较大的一个
                // 数组的最大长度为 Integer.MAX_VALUE,
                // 确保 i 不会超过 Integer.MAX_VALUE - (-low) -1,
                // 否则数组长度就超过 Integer.MAX_VALUE 了
                h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
            } catch( NumberFormatException nfe) {
                // 如果没有配置,或者不是数字,那么忽略
            }
        }
        high = h;
        cache = new Integer[(high - low) + 1];
        int j = low;
        for(int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);
        // range [-128, 127] must be interned (JLS7 5.1.7)
        assert IntegerCache.high >= 127;
    }

    private IntegerCache() {}
}

加锁与解锁的操作必须成对出现,否则会出现问题。 synchronized 不会出现上面的问题,但是 Lock 会出现问题,代码如下:

package com.example.lock;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockTest {
  private Lock lock = new ReentrantLock();

  public static void main(String[] args) {
    LockTest lockTest = new LockTest();
//    new Thread(lockTest::test1, "A").start();
//    new Thread(lockTest::test1, "B").start();
    new Thread(lockTest::test2, "A").start();
    new Thread(lockTest::test2, "B").start();
  }

  // 多加了一个锁,会出现死锁的情况!
  public void test1() {
    lock.lock();
    lock.lock();
    try {
      System.out.println(Thread.currentThread().getName() + ": test1");
    } finally {
      lock.unlock();
    }
  }

  // 多解了一个锁,会报 IllegalMonitorStateException
  public void test2() {
    lock.lock();
    try {
      System.out.println(Thread.currentThread().getName() + ": test2");
    } finally {
      lock.unlock();
      lock.unlock();
    }
  }
}

乐观锁 / 悲观锁

乐观锁与悲观锁并不是特指某两种类型的锁,是人们定义出来的概念或思想,主要是指看待并发同步的角度。

  1. 乐观锁:很乐观,认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。
    • 在 Java 中是通过使用无锁编程来实现,最常采用的是 CAS 算法,Java 原子类中的递增操作就是通过 CAS 自旋实现的。
    • 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
  2. 悲观锁:认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
    • 在 Java 中,synchronized 和 Lock 的实现类都是悲观锁。
    • 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。

独享锁 / 共享锁

独享锁和共享锁同样是一种概念。

  1. 独享锁:也叫排他锁,是指该锁一次只能被一个线程所持有。
    • 如果线程 T 对数据 A 加上排它锁后,则其他线程不能再对 A 加任何类型的锁。
    • 获得排它锁的线程即能读数据又能修改数据。
    • Java 中的 synchronized 和 ReentrantLock、ReentrantReadWriteLock.WriteLock 就是互斥锁。
  2. 共享锁:指该锁可被多个线程所持有。
    • 如果线程 T 对数据 A 加上共享锁后,则其他线程只能对 A 再加共享锁,不能加排它锁。
    • 获得共享锁的线程只能读数据,不能修改数据。
    • Java 中的 ReentrantReadWriteLock.ReadLock 就是共享锁。

独享锁与共享锁是通过 AQS 来实现的,通过实现不同的方法,来实现独享或者共享。

互斥锁 / 读写锁

上面的独享锁 / 共享锁就是一种广义的说法,互斥锁 / 读写锁就是具体的实现。

  1. 互斥锁在 Java 中的具体实现就是 ReentrantLock。
  2. 读写锁在 Java 中的具体实现就是 ReentrantReadWriteLock,可以参见 JUC 并发编程--ReadWriteLock,其表现为:读读可以并存;读写不能并存;写写也不能并存。

可重入锁 / 非可重入锁

  1. 可重入锁:又名递归锁,是指同一个线程在外层方法获取锁的时候,再次进入内层方法时会自动获取锁(前提:锁对象得是同一个对象或者 class),不会因为之前已经获取过还没释放而阻塞。
    • Java 中 synchronized 和 Lock 实现类都是可重入锁。
    • 可重入锁的一个优点是可一定程度避免死锁。
  2. 不可重入锁:是指同一个线程在外层方法获取锁的时候,再次进入内层方法时必须先释放外层方法获取到的锁,如果不释放,会出现死锁。

公平锁和非公平锁

  1. 公平锁:很公平,获取锁需要排队,先到先得。
  2. 非公平锁:不公平,获取锁可以插队。

默认使用非公平锁,是为了公平!因为线程 A 执行业务需要 1 小时,线程 B 执行业务需要 1 秒,线程 A 先排队,让线程 B 等待线程 A 执行完显然不合适。 synchronized 使用的非公平锁,ReentrantLock 默认非公平锁,但可以选择使用公平锁,代码如下:

public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

偏向锁 / 轻量级锁 / 重量级锁

这三种锁是指锁的状态,并且是针对 synchronized。在 Java 5 通过引入锁升级的机制来实现高效 synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。

  1. 偏向锁:指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
  2. 轻量级锁:指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
  3. 重量级锁:指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让他申请的线程进入阻塞,性能降低。

分段锁

分段锁是一种锁的设计,并不是具体的一种锁,对于 ConcurrentHashMap (JDK1.7,JDK1.8 中已经不使用 Segment 了) 而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。 以 ConcurrentHashMap (JDK1.7,JDK1.8 中已经不使用 Segment 了) 来说一下分段锁的含义以及设计思想,ConcurrentHashMap 中的分段锁称为 Segment,它即类似于 HashMap(JDK7 和 JDK8 中 HashMap 的实现)的结构,即内部拥有一个 Entry 数组,数组中的每个元素又是一个链表;同时又是一个 ReentrantLock(Segment 继承了 ReentrantLock)。

  1. 当需要 put 元素的时候,并不是对整个 hashmap 进行加锁,而是先通过 hashcode 来知道他要放在哪一个分段中,然后对这个分段进行加锁,所以当多线程 put 的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
  2. 但是,在统计 size 时,就是获取 hashmap 全局信息的时候,需要获取所有的分段锁才能统计。
  3. 分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

自旋锁

在 Java 中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,好处是减少线程上下文切换的消耗,缺点是循环会消耗 CPU 资源。自旋锁的实现原理也是 CAS。
Unsafe

上图就是典型的自旋锁。

手写非可重入自旋锁

非可重入自旋锁代码如下:

package com.example.lock;

import java.util.concurrent.atomic.AtomicReference;

public class MyLock {
  // atomicReference 中 value 默认值为 null,null 表示没有线程获取到锁;非 null 表示有线程获取到锁
  private AtomicReference<Thread> atomicReference = new AtomicReference<>();

  public void lock() {
    Thread currentThread = Thread.currentThread();
    System.out.println(currentThread.getName() + " want to get lock!");
    // 如果 atomicReference 中 value 不是 null,那么一直循环,这里就是 非可重入锁
    while (!atomicReference.compareAndSet(null, currentThread)) {
      Thread.yield();// 可以放弃当前分配的时间片,减少 CPU 资源消耗,不过也带来了线程上下文切换的消耗
    }
    System.out.println(currentThread.getName() + " get lock!");
  }

  public void unlock() {
    Thread currentThread = Thread.currentThread();
    boolean flag = atomicReference.compareAndSet(currentThread, null);
    if (flag) {
      System.out.println(currentThread.getName() + " release lock!");
    } else {
      throw new RuntimeException("线程 " + currentThread.getName() + " 没有获取到锁,不能释放!");
    }
  }
}

测试代码如下:

package com.example.lock;

import java.util.concurrent.TimeUnit;

public class MyLockTest {
  private MyLock lock = new MyLock();

  public static void main(String[] args) throws InterruptedException {
    MyLockTest myLockTest = new MyLockTest();
    new Thread(myLockTest::sms, "A").start();
    TimeUnit.SECONDS.sleep(1);
    new Thread(myLockTest::call, "B").start();
//    new Thread(myLockTest::both, "C").start();
  }

  public void sms() {
    lock.lock();
    try {
      TimeUnit.SECONDS.sleep(4);
      System.out.println(Thread.currentThread().getName() + ": sms");
    } catch (InterruptedException e) {
      e.printStackTrace();
    } finally {
      lock.unlock();
    }
  }

  public void call() {
    lock.lock();
    try {
      System.out.println(Thread.currentThread().getName() + ": call");
    } finally {
      lock.unlock();
    }
  }

  public void both() {
    lock.lock();
    try {
      System.out.println(Thread.currentThread().getName() + ": sms");
      call();
    } finally {
      lock.unlock();
    }
  }
}

测试结果如下:

  1. 多线程获取锁,没有问题
  2. 单线程重复获取锁,因为非可重入,所以会发生死锁

手写可重入自旋锁

可重入自旋锁代码如下:

package com.example.lock;

import java.util.concurrent.atomic.AtomicStampedReference;

public class MyReentrantLock {
  // atomicStampedReference 中 reference 表示持有锁的线程,默认值为 null,null 表示没有线程获取到锁;非 null 表示有线程获取到锁
  // atomicStampedReference 中 stamp 表示锁的深度,默认值为 0,0 表示没有线程获取到锁;非 0 表示有线程获取到锁
  private AtomicStampedReference<Thread> atomicStampedReference = new AtomicStampedReference<>(null, 0);

  public void lock() {
    Thread currentThread = Thread.currentThread();
    System.out.println(currentThread.getName() + " want to get lock!");
    // 如果 atomicStampedReference 中 reference 不是 null;
    // 并且 atomicStampedReference 中 reference 不是 currentThread;
    // 那么一直循环
    while (!atomicStampedReference.compareAndSet(null, currentThread, 0, 1)
        && !atomicStampedReference.compareAndSet(currentThread, currentThread, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1)) {
      Thread.yield();// 可以放弃当前分配的时间片,减少 CPU 资源消耗,不过也带来了线程上下文切换的消耗
    }
    System.out.println(currentThread.getName() + " get lock!");
  }

  public void unlock() {
    Thread currentThread = Thread.currentThread();
    int stamp;
    boolean flag = atomicStampedReference.getReference() == currentThread // 判断是否为当前线程
        && ((stamp = atomicStampedReference.getStamp()) > 1 ? // 执行赋值操作,并判断是否大于 1
        // 如果大于 1,则将深度减 1
        atomicStampedReference.compareAndSet(currentThread, currentThread, stamp, stamp - 1)
        // 如果等于 1,那么重置,否则返回 false
        : (stamp == 1 && atomicStampedReference.compareAndSet(currentThread, null, 1, 0)));
    if (flag) {
      System.out.println(currentThread.getName() + " release lock!");
    } else {
      throw new RuntimeException("线程 " + currentThread.getName() + " 没有获取到锁,不能释放!");
    }
  }
}

测试代码如下:

package com.example.lock;

import java.util.concurrent.TimeUnit;

public class MyReentrantLockTest {
  private MyReentrantLock lock = new MyReentrantLock();

  public static void main(String[] args) throws InterruptedException {
    MyReentrantLockTest myLockTest = new MyReentrantLockTest();
    new Thread(myLockTest::sms, "A").start();
    TimeUnit.SECONDS.sleep(1);
    new Thread(myLockTest::call, "B").start();
//    new Thread(myLockTest::both, "C").start();
//    for (int i = 0; i < 10; i++) {
//      new Thread(myLockTest::both, "D" + i).start();
//    }
  }

  public void sms() {
    lock.lock();
    try {
      TimeUnit.SECONDS.sleep(4);
      System.out.println(Thread.currentThread().getName() + ": sms");
    } catch (InterruptedException e) {
      e.printStackTrace();
    } finally {
      lock.unlock();
    }
  }

  public void call() {
    lock.lock();
    try {
      System.out.println(Thread.currentThread().getName() + ": call");
    } finally {
      lock.unlock();
    }
  }

  public void both() {
    lock.lock();
    try {
      System.out.println(Thread.currentThread().getName() + ": sms");
      call();
    } finally {
      lock.unlock();
    }
  }
}

测试结果如下:

  1. 多线程获取锁,没有问题
  2. 单线程重复获取锁,没有问题
  3. 多线程重复获取锁,没有问题

死锁

死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
Java 死锁产生的四个必要条件:

  1. 互斥使用:资源被一个线程使用(占有)时,别的线程不能使用。
  2. 不可抢占:资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
  3. 请求和保持:资源请求者在请求其他资源的同时保持对原有资源的占有。
  4. 循环等待:P1 占有 P2 的资源,P2 占有 P3 的资源,P3 占有 P1 的资源。形成了一个等待环路。

当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。

手写一个死锁

package com.example.die;

import java.util.concurrent.TimeUnit;

@SuppressWarnings("ALL")
public class DieLockTest {
  public static void main(String[] args) {
    String A = "A", B = "B", C = "C";
    new Thread(() -> {// 持有 A,想要 B
      synchronized (A) {
        System.out.println(Thread.currentThread().getName() + " has " + A);
        try {
          TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " wants " + B);
        synchronized (B) {
          System.out.println(Thread.currentThread().getName() + " has " + B);
        }
      }
    }, "T1").start();
    new Thread(() -> {// 持有 B,想要 C
      synchronized (B) {
        System.out.println(Thread.currentThread().getName() + " has " + B);
        try {
          TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " wants " + C);
        synchronized (C) {
          System.out.println(Thread.currentThread().getName() + " has " + C);
        }
      }
    }, "T2").start();
    new Thread(() -> {// 持有 C,想要 A
      synchronized (C) {
        System.out.println(Thread.currentThread().getName() + " has " + C);
        try {
          TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " wants " + A);
        synchronized (A) {
          System.out.println(Thread.currentThread().getName() + " has " + A);
        }
      }
    }, "T3").start();
  }
}

运行后发现程序产生死锁:

排查死锁

  1. jps + jstack:都是 jdk 提供的命令工具

jps -l:查看进程号
jstack 进程号:查看堆栈信息

  1. jconsole:jdk 提供的一个可视化的工具,在 jdk/bin 目录下

打开 jconsole 连接指定进程 可能出现安全连接失败,选择不安全连接 选择线程,点击检测死锁 随便点击一个线程,可以看到资源被其他线程占有

  1. jvisualvm:jdk 提供的一个非常强大的排查 java 程序问题的工具,可以监控程序的性能、查看 jvm 配置信息、堆快照、线程堆栈信息,在 jdk/bin 目录下

双击选择进程,点击线程,可以发现直接提示“检测到死锁!” 点击“线程 Dump”,拖动到最后,可以发现和 jstack 生成的内容一样

破坏死锁

破坏死锁,只需要破坏四个条件之一。

  1. 破坏互斥使用:互斥是非共享资源所必须的,不仅不能改变,还应加以保证。
  2. 破坏不可抢占:
    • 当一个已经持有了一些资源的线程在提出新的资源请求没有得到满足时,它必须释放已经保持的所有资源,待以后需要使用的时候再重新申请。
      • 该种方法实现起来比较复杂,且代价也比较大。释放已经保持的资源很有可能会导致线程之前的工作失效等,反复的申请和释放资源会导致线程的执行被无限的推迟,这不仅会延长线程的周转周期,还会影响系统的吞吐量。
  3. 破坏请求和保持:有两种方法。
    • 资源请求者在开始时一次性地申请其在整个运行过程中所需要的全部资源。
      • 优点:简单易实施且安全。
      • 缺点:因为某项资源不满足,无法启动,而其他已经满足了的资源也不会得到利用,严重降低了资源的利用率,造成资源浪费。
    • 资源请求者分阶段申请部分资源,每一阶段开始时申请一部分资源,结束后释放这些资源。
      • 是对上面方法的改进,资源的利用率会得到提高。
  4. 破坏循环等待:只要按照顺序加锁,逆序解锁就可以了。
    • 比如上方的测试代码,要求按照 A -> B -> C 的顺序加锁,那么破坏循环等待,只需要改动线程 T3,代码如下:
    new Thread(() -> {// 先持有 A,再持有 C,就可以解决死锁问题
      synchronized (A) {
        System.out.println(Thread.currentThread().getName() + " has " + A);
        try {
          TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " wants " + C);
        synchronized (C) {
          System.out.println(Thread.currentThread().getName() + " has " + C);
        }
      }
    }, "T3").start();