线程安全性

线程安全性

        当多个线程访问某个类是,不管运行时环境采用何种调度方式或者这些进程如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

  1. 原子性:提供了互斥访问,统一时刻只能有一个线程来对它进行操作;
  2. 可见性:一个线程对主存的修改可以及时的被其他线程观察到;
  3. 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序;

1. 原子性 - Atomic 包

使用CAS原理实现院子性,使用昨天的代码演示AtomicInteger的使用

1. AtomicInteger

高并发下使用AtomicInteger实现计数功能

每次运行发现结果都为 5000,没问题,说明AtomicInteger这个类是线程安全的;

下面我们看一下AtomicInteger的源码实现

1.        首先发现AtomicInteger使用了一个unsafe的类

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

2.        查看getAndAddInt的实现,到unsafe中,发现是使用一个do-while语句实现的,里面有一个compareAndSwapInt的方法,这个方法就是我们上述提到的CAS原理,

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;
    }

3.        我们来观察一下CAS的实现,发现这个方法使用native修饰的,也就是调用了 Java 底层的api,不是 Java 实现的,需要我们特别注意,这里就不深入研究了,记住就好了;

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

4.        回过头来继续看一下我们的这个方法,首先呢,如果我们要执行的操作是 2 + 1 = 3 这个操作,那么这里面的这几个参数的含义如下,var1使我们传递进来的对象也就是我们上述的count对象,var2是 2,var4 是 1,var5 是调用底层 api 获取到的底层当前值。如果没有线程处理我们的这个对象 count 的话,var5 返回的应该是 2。所以,调用compareAndSwapInt方法时传递进去的值分别是:

  • [x] 当前的 count 对象 var1
  • [x] 当前值 2;
  • [x] 当前从底层传递来的值 var5;
  • [x] 当前从底层传递来的值 var5 加上 我们的添加量 1 var4;

        如果当前的值 2 与底层的值 var5 相同的话,更新为 var5 + var4,也就是执行 2 + 1 操作,并把结果赋予给 count var1;通过不行的循环判断,只有期望的值与底层的值完全相同的时候才执行相加操作,并把底层的值 var5 覆盖掉。这就是CAS的核心;

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;
    }

2. AtomicLong & LongAdder

int -> AtomicInteger

long -> AtomicLong

        说道AtomicLong,jdk 8 新增了一个LongAdder,为什么有了AtomicLong又添加了一个LongAdder,原因是因为如果竞争不激烈的情况下,CAS的修改效率是可以的,但是,在竞争比较激烈的情况下,CAS的失败率就比较高了,然后就会不停的进行尝试比较,性能会受到一定影响。LongAdder 就是为了解决这个问题而设计的。对与基本数据类型 Long 和 Double类型,Jvm允许吧一个 64 位的数据类型拆成两个 32 位的进行操作。LongAdder 核心是将热点数据分离,比如可以将内部核心数据 value 分离成一个数组,每个线程访问时,通过 hash 等算法映射到其中一个数字进项操作,而最终的计数结果为这个数组的求和累加。其中,热点数据 value 会被分成多个单元数据 cell,每个 cell 独立维护内部的值,当前对象的实际值,由所有 cell 的值累计合成,这样呢热点就有效的进行了分离,并提高了并行度。LongAdder相当于在AtomicLong的基础上把单点的更新压力分散到各个节点上,在低并发的情况下,通过对 base 的直接更新可以保证和AtomicLong的性能基本一致,而在高并发情况下通过分散提高了性能。

        LongAdder也是有自己的缺陷的,如果在统计的时候有并发更新,可能导致统计的数据会有误差,实际使用中需要具体分析,比如全局唯一的数据 ID AtomicLong肯定是首选。

        除了 compareAndSwapInt这个方法外,还有一个方法 compareAndSet,这个方法需要注意一下,这个是AtomicBoolean使用的方法,可以用来控制同一时间内,只有一个线程可以执行某段操作,当我们把值改为 false 之后,又可以被执行一次。

3. AtomicReference & AtomicReferenceFiledUpdater

        AtomicReference 跟 AtomicInteger有点像,我们这里只写一个简单的例子,根据期望值更新reference的值

AtomicReference对int类型的简答使用

最后返回结果为 3,符合预期,没问题。

        AtomiceReferenceFiledUpdate的简单使用,代码中有注释,比较好理解。

AtomicIntegerFiledUpdate更新某个对象的int类型字段

需要特别注意的是这里的 count 必须是非 static 的,并且必须使用 volatile 修饰,不使用 volatile jvm会给出错误信息,关于 volatile 的具体使用我们以后会有讲解。

Exception in thread "main" java.lang.ExceptionInInitializerError
Caused by: java.lang.IllegalArgumentException: Must be volatile type
at java.util.concurrent.atomic.AtomicIntegerFieldUpdater$AtomicIntegerFieldUpdaterImpl.(AtomicIntegerFieldUpdater.java:412)
at java.util.concurrent.atomic.AtomicIntegerFieldUpdater.newUpdater(AtomicIntegerFieldUpdater.java:88)
at com.mmall.concurrency.example.atomic.bigd.AtomicExample5.(AtomicExample5.java:16)

4. AtomicStamReference

        这个类主要是为了解决CAS的ABA问题,那么什么是ABA问题呢,是说如果有两个线程修改同一个变量,线程一把变量的值由 A 修改为了 B,随后线程一又把变量的值由 B 修改为了 A, 这时候线程二在修改此变量的时候在执行compareAndSwap
操作是,发现期望值与现有值是一致的,所以进行了修改操作,这与预期是想违背的。里面多个一个队stamp 值得比较,读者自己繁育源码看一下吧。

5. AtomicLongArray

        AtomicLongArray 比 AtomicLong,的compareAndSet方法多了一个参数 index, 可以针对数组的某一个 index 上的值进行修改;

6. AtomicBoolean

        AtomicBoolean 在实际项目中使用场景还是挺多的,下面给个例子,来看代码演示:

使用AtomicBoolean让一段代码有且仅有执行一次

        在我们上述的例子中,在我们的 test 方法中,我们判断 checkHappened 当前值是否为 false, 如果为 false 更改为 true, 我们在 100 并发下执行了 50 次 test方法,发现仅且只有一次把 checkHappened 的值由 false 更改为了 true,其他的 4999 次都没有执行此操作。这个例子可以应用到流程化操作中,让某一段代码仅且执行一次。

        关于 atomic 包下的一些类的使用就介绍到这里了。

2. 原子性 - 锁

        原子性提供了互斥访问,同一时刻只能有一个线程对其访问, Java 中保证同一时刻只有一个线程对其访问的除了 atomic 包下的类之外还有锁 jdk 提供锁主要分两种:

  • [x] 1. synchronized 锁,java 的关键字,依赖 jvm 实现锁 ,在这个关键字作用对象的作用范围内,都是在同一时刻只能有一个线程对其进行操作的
  • [x] 2. Lock 接口类,代码层面的锁,依赖特殊的 CPU 指令,实现类中比较有代表性的是 ReentrantLock,这个我们会在后面单独重点讲解;

1. 原子性 - synchronized

         synchronized, java的关键字, 修饰的对象主要有四种:

  • [x] 1. 修饰代码块:大括号括起来的代码,被修饰的代码块被称为同步语句块,作用的范围是大括号括起来的代码,作用的对象是调用这个代码块的对象;
  • [x] 2. 修饰一个方法:整个方法,被修饰的方法被称为同步方法,作用的范围是整个方法,作用的对象是调用这个方法的对象;
  • [x] 3. 修饰静态方法:整个静态方法,作用的范围是整个静态方法,作用的对象是这个类的所有对象;
  • [x] 4. 修饰类:括号括起来的部分,作用的范围是 synchronized 后面阔气来的部分,作用的对象是这个类的所有对象;

        下面分别演示这四种使用

  1. syncchronized 修饰代码块的详细用法,使用线程池模拟并发调用 syncCodeBlock 方法,发现执行结果每次都是 从 0 到 9,然后,在执行第二遍,输出结果;因为synchronized修饰的是调用这个代码块的对象,所以,如果我们在线程池中使用不同的对象,访问此代码块的时候,理论上输出结果应该是交叉执行的,从输出结果来看,符合我们的预期;

  2. syncchronized 修饰同步方法的详细用法,使用线程池模拟并发调用 syncMethod 方法,发现执行结果每次都是 从 0 到 9,然后,在执行第二遍,输出结果,同理,使用不同的对象访问输出结果也符合我们的预期。

        从输出结果可以看出,如果一个方法里整个是一个同步代码块的时候,它跟修饰一个方法达到的效果是一样的。

        如果当前类是父类,被子类继承,在父类被 synchronized 修饰的方法,在子类中是没有这个关键字修饰的,原因是 synchronizde 不属于方法声明的一部分。如果子类想要使用同步方法,需要子类显示加上 sychronized 关键字。

  1. synchronized 修饰一个静态方法,作用的对象是这个类的所有对象,因此,此处使用不同的对象来测试,同一时间只有一个线程执行,我们预测先执行 obeject 1 的 0 -9 ,然后执行 object 2的 0-9,查看结果,预期一致的。

  2. synchronized 修饰一个类,作用的对象是这个类的所有对象,因此,此处使用不同的对象来测试,同一时间只有一个线程执行,我们预测先执行 obeject 1 的 0 - 9 ,然后执行 object 2的 0 - 9,查看结果,预期一致的。

        同样的,一个方法里面,如果所有需要执行的代码部分,都是被一个 synchronized 修饰的一个类的来包围的时候,那么它和 synchronized 修饰的一个静态方法的表现是一致的。

        使用 synchronized 保证我们的计数是线程安全的
使用synchronized 修饰静态方法 add()

对比

synchronized :不可中断锁,一旦执行到 synchronized 作用范围内时,是不能中断的,必须等待代码执行完毕。适合竞争不激烈的时候使用,可读性好,在竞争激烈的时候性能下降的非常的快;

lock : 可中断的锁,执行 unlock 就可以了,在竞争激烈的时候仍然能保持常态

Atomic : 竞争激烈是能维持常态,比 lock 性能好,缺点是只能同步一个值;

~~ the end

你可能感兴趣的:(线程安全性)