JAVA学习笔记(并发编程-叁)- 线程安全性

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/bingdianone/article/details/83386819


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

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

线程安全性-原子性

原子性-Atomic包

说到原子性,就不得不提及JDK里的atomic包,该包中提供了很多Atomic的类,本小节主要介绍该包中常用的几个类。这些类都是通过CAS来实现原子性的,atomic包提供了如下具有原子性的类(下面是演示如何在IDEA中找到atomic类;图中的AtomicExample1类可以在下面代码中找到):
在这里插入图片描述

atomic实际上就是原子操作, 所谓原子, 就是不可再分割, 已经是最小的操作单位了(所谓操作指的是对内存的读写);一个数据的线程安全, 简单说就是这部分的数据即使有多个线程同时读写, 也不会出现数据错乱的情况, 内存的最后状态总是可以预见的, 如果这块内存的数据被一个多线程读写之后, 出现的结果是不可预见的, 那么就可以说这块内存是"线程不安全的".
  
  atomic: 原子属性, 线程安全, 效率较低. 可以当成人通过一扇门, 一次只能通过一个人.
  atomic实际上相当于一个引用计数器, 如果被标记了atomic, 那么被标记了的内存本身就有了一个引用计数器, 第一个占用这块内存的线程, 会给这个计数器+1, 在这个线程操作这块内存期间, 其他线程在访问这个内存的时候, 如果发现"引用计数器"不为0, 则阻塞, 实际上阻塞并不等于休眠, 它是基于CPU轮询, 休眠除非被唤醒, 否则无法继续执行, 阻塞则不同, 每个CPU轮询片到这个线程的时候都会尝试继续往下执行, 可见阻塞相对于休眠来讲, 阻塞是主动的, 休眠是被动的, 如果引用计数器为0, 轮询片到这,则先给这块内存的引用计数器+1, 然后再去操作, atomic实现操作序列化的具体过程大概是就是这样。

AtomicXXX: CAS, Unsafe.compareAndSwapInt

普通的多线程

package com.mmall.concurrency.example.count;

import com.mmall.concurrency.annoations.NotThreadSafe;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

@Slf4j
@NotThreadSafe
public class CountExample1 {
    //请求总数
    public static int clientTotal=5000;
    //同事并发执行的线程数
    public static int threadTotal=200;
    //计数
    public static int count=0;

    public static void main(String[] args) throws InterruptedException {
        //定义一个线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        //信号量(并发数)
        final Semaphore semaphore = new Semaphore(threadTotal);
        //计数器(线程总数)
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            executorService.execute(()->{
                try {
                    semaphore.acquire();//当前进程是否可以执行(并发是否小于200)
                    add();
                    semaphore.release();//释放进程
                } catch (InterruptedException e) {
                    log.error("exception",e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();//关闭线程池
        log.info("count:{}",count);
    }

    private static void add(){
        count++;
    }
}
/*安全情况下结果为5000
21:42:11.011 [main] INFO com.mmall.concurrency.example.count.CountExample1 - count:4981
*/

以上这个例子,每次执行输出的结果是不确定的,这种就是线程不安全的。如果将以上例子中的count类型换成 AtomicInteger,让这个变量具有原子性的话,就能够保证线程安全了。修改代码如下:

AtomicInteger的测试代码

package com.mmall.concurrency.example.count;


import com.mmall.concurrency.annoations.ThreadSafe;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * AtomicInteger测试
 */
@Slf4j
@ThreadSafe
public class CountExample2 {
    //请求总数
    public static int clientTotal=5000;
    //同事并发执行的线程数
    public static int threadTotal=200;
    //计数
    public static AtomicInteger count=new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        //定义一个线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        //信号量(并发数)
        final Semaphore semaphore = new Semaphore(threadTotal);
        //计数器(线程总数)
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            executorService.execute(()->{
                try {
                    semaphore.acquire();//当前进程是否可以执行(并发是否小于200)
                    add();
                    semaphore.release();//释放进程
                } catch (InterruptedException e) {
                    log.error("exception",e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();//关闭线程池
        log.info("count:{}",count.get());
    }

    private static void add(){
        //先增加操作再获取当前值
        count.incrementAndGet();
        //先获取当前值再增加操作
        //count.getAndIncrement();
    }
}

/**结果正确
 * 13:59:38.822 [main] INFO com.mmall.concurrency.example.count.CountExample2 - count:5000
 */

将count变量的类型修改成 AtomicInteger 后,每次执行输出的结果都会是5000,这样就具有了线程安全性。那么这是怎么做到的呢?我们可以看一下incrementAndGet方法的源码:
incrementAndGet() 使用了unsafe


    /**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

可以看到,在这个方法里实际是通过U这个对象调用了getAndAddInt方法,往该方法里传入了当前对象以及当前对象的值偏移量和增量值1。
然后来看看Unsafe中getAndAddInt方法的实现代码:

public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
//var1 传来的对象;var2当前值;var5底层当前值;var2和var5同等则+var4
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
        return var5;
    }

CAS指的就是compareAndSwap在这里插入图片描述
什么是CAS:

CAS (compareAndSwap),中文叫比较交换,一种无锁原子算法。过程是这样:它包含 3 个参数 CAS(V,E,N),V表示要更新变量的值,E表示预期值,N表示新值。仅当 V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做两个更新,则当前线程则什么都不做。最后,CAS 返回当前V的真实值。CAS 操作时抱着乐观的态度进行的,它总是认为自己可以成功完成操作。
  当多个线程同时使用CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会挂起,仅是被告知失败,并且允许再次尝试,当然也允许实现的线程放弃操作。基于这样的原理,CAS 操作即使没有锁,也可以发现其他线程对当前线程的干扰。
  与锁相比,使用CAS会使程序看起来更加复杂一些,但由于其非阻塞的,它对死锁问题天生免疫,并且,线程间的相互影响也非常小。更为重要的是,使用无锁的方式完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,因此,他要比基于锁的方式拥有更优越的性能。
  简单的说,CAS 需要你额外给出一个期望值,也就是你认为这个变量现在应该是什么样子的。如果变量不是你想象的那样,哪说明它已经被别人修改过了。你就需要重新读取,再次尝试修改就好了。

CAS的缺点:

CAS 看起来非常的吊,但是它仍然有缺点,最著名的就是 ABA 问题:在CAS操作的时候,其他线程将变量的值A改成了B,然后又改成了A。本线程使用期望值A与当前变量进行比较的时候,发现A变量没有变,于是CAS就将A值进行了交换操作。这个时候实际上A值已经被其他线程改变过,这与设计思想是不符合的。
  如果只是在基本类型上是没有问题的,但如果是引用类型呢?这个对象中有多个变量,我怎么知道有没有被改过?聪明的你一定想到了,加个版本号啊。每次修改就检查版本号,如果版本号变了,说明改过。这样只要变量被某一个线程修改过,该变量版本号就会发生递增操作,从而解决了ABA问题

CAS 底层原理:

CAS是如何将比较和交换这两个操作,变成一个原子操作呢?这归功于硬件指令集的发展,实际上,我们可以使用同步将这两个操作变成原子的,但是这么做就没有意义了。所以我们只能靠硬件来完成,硬件保证一个从语义上看起来需要多次操作的行为只通过一条处理器指令就能完成。这类指令常用的有:

  • 1.测试并设置(Tetst-and-Set)
  • 2.获取并增加(Fetch-and-Increment)
  • 3.交换(Swap)
  • 4.比较并交换(Compare-and-Swap)
  • 5.加载链接/条件存储(Load-Linked/Store-Conditional)

其中,前面的3条是20世纪时,大部分处理器已经有了,后面的2条是现代处理器新增的。而且这两条指令的目的和功能是类似的,在IA64,x86 指令集中有 cmpxchg 指令完成 CAS 功能,在 sparc-TSO 也有 casa 指令实现,而在 ARM 和 PowerPC 架构下,则需要使用一对 ldrex/strex 指令来完成 LL/SC 的功能。

CPU 实现原子指令有2种方式:

  1. 通过总线锁定来保证原子性:

总线锁定其实就是处理器使用了总线锁,所谓总线锁就是使用处理器提供的一个 LOCK# 信号,当一个处理器咋总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。但是该方法成本太大。因此有了下面的方式。

  1. 通过缓存锁定来保证原子性:

所谓 缓存锁定 是指内存区域如果被缓存在处理器的缓存行中,并且在Lock 操作期间被锁定,那么当他执行锁操作写回到内存时,处理器不在总线上声言 LOCK# 信号,而时修改内部的内存地址,并允许他的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改两个以上处理器缓存的内存区域数据(这里和 volatile 的可见性原理相同),当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效。

注意:有两种情况下处理器不会使用缓存锁定:

  • 当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行时,则处理器会调用总线锁定。
  • 有些处理器不支持缓存锁定,对于 Intel 486 和 Pentium 处理器,就是锁定的内存区域在处理器的缓存行也会调用总线锁定。

AtomicLong LongAdder

package com.mmall.concurrency.example.atomic;


import com.mmall.concurrency.annoations.ThreadSafe;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicLong;

/**
 * AtomicLong测试
 */
@Slf4j
@ThreadSafe
public class AtomicExample2 {
    //请求总数
    public static int clientTotal=5000;
    //同事并发执行的线程数
    public static int threadTotal=200;
    //计数
    public static AtomicLong count=new AtomicLong(0);

    public static void main(String[] args) throws InterruptedException {
        //定义一个线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        //信号量(并发数)
        final Semaphore semaphore = new Semaphore(threadTotal);
        //计数器(线程总数)
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            executorService.execute(()->{
                try {
                    semaphore.acquire();//当前进程是否可以执行(并发是否小于200)
                    add();
                    semaphore.release();//释放进程
                } catch (InterruptedException e) {
                    log.error("exception",e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();//关闭线程池
        log.info("count:{}",count.get());
    }

    private static void add(){
        //先增加操作再获取当前值
        count.incrementAndGet();
        //先获取当前值再增加操作
        //count.getAndIncrement();
    }
}
/*
14:06:10.636 [main] INFO com.mmall.concurrency.example.atomic.AtomicExample2 - count:5000
 */

在之前的例子中,可以看到AtomicInteger在执行CAS操作的时候,是用死循环的方式,如果线程竞争非常激烈,那么失败量就会很高,性能也就会受到影响。而AtomicLong也是一样的,所有的AtomicXXX它们调用的都是Unsafe里面的方法,只不过是方法参数类型不一样而已,实现思想是一样的。既然有了AtomicLong为什么还需要LongAdder呢?自然是因为LongAdder有区别于AtomicLong的优点。

package com.mmall.concurrency.example.atomic;

import com.mmall.concurrency.annoations.ThreadSafe;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.LongAdder;
/*
LongAdder测试
AtomicXXX底层中CAS底层实现是在一个死循环内不断修改目标值直到修改成功;
如果竞争不激烈修改成功的概率很高;反之则失败概率很高;在失败概率高时这些原子操作就会进行多次尝试;
因此性能会受到影响。
LongAdder将热点数据分离;比如说,它可以将atomiclong内部核心数据value分离成一个数组;每个线程访问时
通过hash等算法预测到其中一个数字进行计数;而最终的计数结果时这个数组的求和累加。
其中热点数据value会被分离成多个单元的excel;每个excel独立维护内部的值;当前对象的值由所有的excel累计合成
这样热点就进行了有效的分离并提高了并行度。
LongAdder相当于在atomic的基础上将单点的更新压力分散到各个节点上。
在低并发的时候可以通过base的直接更新可以很好的保证和atomic的性能基本一致
而在高并发的时候通过分散保证高性能。
缺点是有并发更新的时候可能会导致数据误差;
实际使用中使用多LongAdder而不是使用AtomicLong
当然在线程很低的情况下进行计数还是Atomc比较直接简单
序列号生成全局唯一性使用AtomicLong就不建议使用LongAdder了。

 */
@Slf4j
@ThreadSafe
public class AtomicExample3 {
    //请求总数
    public static int clientTotal=5000;
    //同事并发执行的线程数
    public static int threadTotal=200;
    //计数
    public static LongAdder count=new LongAdder();

    public static void main(String[] args) throws InterruptedException {
        //定义一个线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        //信号量(并发数)
        final Semaphore semaphore = new Semaphore(threadTotal);
        //计数器(线程总数)
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            executorService.execute(()->{
                try {
                    semaphore.acquire();//当前进程是否可以执行(并发是否小于200)
                    add();
                    semaphore.release();//释放进程
                } catch (InterruptedException e) {
                    log.error("exception",e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();//关闭线程池
        log.info("count:{}",count);
    }

    private static void add(){
        //直接加1
        count.increment();
    }
}
/*
14:07:52.036 [main] INFO com.mmall.concurrency.example.atomic.AtomicExample3 - count:5000
 */

JVM会将long,double这些64位的变量的读写操作拆分成两个32位的操作,而LongAdder的设计思想也类似于此。LongAdder的设计思想:

核心是将热点数据分离,可以将AtomicLong的内部数据value分成一个数组,每个线程访问时,通过hash等算法映射到其中一个数字进行计数,而最终计数结果为这个数组的求和累加。其中热点数据value会被分离成多个单元的cell,每个cell独自维护内部的值,当前对象内value的实际值由所有的cell累积合成,从而使热点进行了有效的分离,提高了并行度。这样一来 LongAdder 相当于是在AtomicLong的基础上将单点的更新压力分散到各个节点上,在低并发的时候通过对base的直接更新,可以很好的保证和Atomic的性能基本一致,在高并发的场景,通过将热点分散来提高并行度

缺点:在统计的时候如果有并发更新,可能会导致统计结果有些误差。

LongAdder.add方法的源码:

/**
 * Adds the given value.
 *
 * @param x the value to add
 */
public void add(long x) {
    Cell[] cs; long b, v; int m; Cell c;
    if ((cs = cells) != null || !casBase(b = base, b + x)) {
        boolean uncontended = true;
        if (cs == null || (m = cs.length - 1) < 0 ||
            (c = cs[getProbe() & m]) == null ||
            !(uncontended = c.cas(v = c.value, v + x)))
            longAccumulate(x, null, uncontended);
    }
}

在实际使用中,处理高并发的时候,可以优先使用LongAdder,而不是继续使用AtomicLong。但在需要保证数据无误差的情况下,则需使用全局唯一的AtomicLong

AtomicReference、 AtomicReferenceFieldUpdater

AtomicReference和AtomicInteger非常类似,不同之处就在于AtomicInteger是对整数的封装,而AtomicReference是对对象引用的封装,AtomicReference用于保证对象引用的原子性。AtomicReference的用法同AtomicInteger一样,只不过是可以放各种对象。AtomicReference的使用示例如下:

package com.mmall.concurrency.example.atomic;


import com.mmall.concurrency.annoations.ThreadSafe;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.LongAdder;
/*
AtomicReference

 */
@Slf4j
@ThreadSafe
public class AtomicExample4 {
    private static AtomicReference<Integer> count=new AtomicReference<>(0);

    public static void main(String[] args) {
        count.compareAndSet(0,2); //是0更新成2//count=2
        count.compareAndSet(0,1); //是0更新成1//不执行
        count.compareAndSet(1,3); //是1更新成3//不执行
        count.compareAndSet(2,4); //是2更新成4//count=4
        count.compareAndSet(3,5); //是3更新成5//不执行
        log.info("count:{}",count.get());
    }
}

/*
14:32:35.237 [main] INFO com.mmall.concurrency.example.atomic.AtomicExample4 - count:4
 */

AtomicReferenceFieldUpdater有基本类型的实现,例如AtomicIntegerFieldUpdater ,它们的核心作用是可以原子性的去更新某一个类的实例里所指定的某一个字段。AtomicReferenceFieldUpdater可以原子性的更新对象类型的字段,而AtomicIntegerFieldUpdater 则只可以更新整型字段。如下示例:

package com.mmall.concurrency.example.atomic;


import com.mmall.concurrency.annoations.ThreadSafe;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import java.util.concurrent.atomic.AtomicReference;
/*
AtomicIntegerFieldUpdater
主要用来更新指定类某个字段的值newUpdater(AtomicExample5.class,"count")
并且这个字段需要volatile修饰而且不是static字段
 */
@Slf4j
@ThreadSafe
//实际用到不多
public class AtomicExample5 {
    private static AtomicIntegerFieldUpdater<AtomicExample5> updater=
            AtomicIntegerFieldUpdater.newUpdater(AtomicExample5.class,"count");

    @Getter
    public volatile int count=100;

    public static void main(String[] args) {
        AtomicExample5 example5=new AtomicExample5();
        //example5中的值如果是100则更新成120
        if(updater.compareAndSet(example5,100,120)){
            log.info("updata success 1,{}",example5.getCount());
        }

        if(updater.compareAndSet(example5,100,120)){
            log.info("updata success 2,{}",example5.getCount());
        }else {
            log.info("updata faied,{}",example5.getCount());
        }
    }
}
/*
14:36:58.489 [main] INFO com.mmall.concurrency.example.atomic.AtomicExample5 - updata success 1,120
14:36:58.506 [main] INFO com.mmall.concurrency.example.atomic.AtomicExample5 - updata faied,120
 */

AtomicStampReference : CAS的ABA问题

在上文中提到了CAS里关于ABA的问题,AtomicStampReference类的主要作用就是用于解决CAS里的ABA问题,该类的方法加上了stamp(戳记)进比较,这个stamp是自行定义的,常见的有使用时间戳等。AtomicStampReference里的核心方法源码:
每次更新stamp(版本号)都会加1(A[1]=>B[1]=>A[2])

/**
     * Atomically sets the value of both the reference and stamp
     * to the given update values if the
     * current reference is {@code ==} to the expected reference
     * and the current stamp is equal to the expected stamp.
     *
     * @param expectedReference the expected value of the reference
     * @param newReference the new value for the reference
     * @param expectedStamp the expected value of the stamp
     * @param newStamp the new value for the stamp
     * @return {@code true} if successful
     */
    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)));
    }

AtomicLongArray

相对于AtomicLong多了一个索引(数组下标);主要作用是可以原子性的更新一个数组里指定索引位置的值,所以AtomicLongArray里的方法都会需要传入一个索引值

    /**
     * Atomically sets the element at position {@code i} to the given value
     * and returns the old value.
     *
     * @param i the index
     * @param newValue the new value
     * @return the previous value
     */
    public final long getAndSet(int i, long newValue) {
        return unsafe.getAndSetLong(array, checkedByteOffset(i), newValue);
    }

    /**
     * Atomically sets the element at position {@code i} to the given
     * updated value if the current value {@code ==} the expected value.
     *
     * @param i the index
     * @param expect the expected value
     * @param update the new value
     * @return {@code true} if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(int i, long expect, long update) {
        return compareAndSetRaw(checkedByteOffset(i), expect, update);
    }

    private boolean compareAndSetRaw(long offset, long expect, long update) {
        return unsafe.compareAndSwapLong(array, offset, expect, update);
    }

AtomicBoolean(平时用的比较多)

经过之前的铺垫,就已经知道AtomicBoolean可以原子性的操作boolean值。举一个例子,可以利用AtomicBoolean.compareAndSet方法来实现控制某一段代码只会执行一次。示例代码如下:

package com.mmall.concurrency.example.atomic;


import com.mmall.concurrency.annoations.ThreadSafe;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

@Slf4j
@ThreadSafe
/**
 *AtomicBoolean
 */
public class AtomicExample6 {
    private static AtomicBoolean isHappened = new AtomicBoolean(false);
    //请求总数
    public static int clientTotal=5000;
    //同事并发执行的线程数
    public static int threadTotal=200;
    public static void main(String[] args) throws Exception {

        //定义一个线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        //信号量(并发数)
        final Semaphore semaphore = new Semaphore(threadTotal);
        //计数器(线程总数)
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            executorService.execute(()->{
                try {
                    semaphore.acquire();//当前进程是否可以执行(并发是否小于200)
                    test();
                    semaphore.release();//释放进程
                } catch (InterruptedException e) {
                    log.error("exception",e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();//关闭线程池
        log.info("isHappened:{}",isHappened.get());
    }

    private static void test(){
        if(isHappened.compareAndSet(false,true)){
            log.info("execute");
        }
    }
}
/*
调用5000次只执行一次
14:50:08.449 [pool-1-thread-1] INFO com.mmall.concurrency.example.atomic.AtomicExample6 - execute
14:50:08.856 [main] INFO com.mmall.concurrency.example.atomic.AtomicExample6 - isHappened:true
 */

原子性-锁

synchronized :依赖JVM

我们知道原子性提供了互斥访问,同一时刻只能有一个线程来对它进行操作。在Java里能保证同一时刻只有一个线程来对其进行操作的,除了atomic包之外,还有锁机制。
JDK提供的锁主要分两种:

  1. synchronized:依赖JVM (主要依赖JVM实现锁,因此在这个关键字作用对象的作用范围内,都是同一时刻只能有一个线程可以进行操作的)
  2. Lock:代码层面的锁,依赖特殊的CPU指令,通过代码实现。Lock是一个接口,常用的实现类是ReentrantLock

synchronized是Java中的一个关键字,它是一种同步锁,其修饰的内容有如下四种:

  • 修饰代码块:被修饰的代码称之为同步语句块,作用的范围是大括号括起来的代码,作用于调用这个代码块的对象
  • 修饰方法:被修饰的方法称之为同步方法,作用的范围是整个方法,作用于调用这个方法的对象
  • 修饰静态方法:作用的范围是整个静态方法,作用于这个类的所有对象
  • 修饰类:作用的范围是大括号括起来的部分,作用于这个类的所有对象
    实现原子性方式的对比:
    在这里插入图片描述
    这里的需要注意的是:如果锁的是类对象的话,尽管new多个实例对象,但他们仍然是属于同一个类依然会被锁住,即线程之间保证同步关系
    现在来看看synchronized的具体底层实现。先写一个简单的demo:
public class SynchronizedDemo {
    public static void main(String[] args) {
        synchronized (SynchronizedDemo.class) {
        }
        method();
    }
    private static void method() {
    }
}

上面的代码中有一个同步代码块,锁住的是类对象,并且还有一个同步静态方法,锁住的依然是该类的类对象。编译之后,切换到SynchronizedDemo.class的同级目录之后,然后用javap -v SynchronizedDemo.class查看字节码文件:
在这里插入图片描述
SynchronizedDemo.class
  如图,上面用黄色高亮的部分就是需要注意的部分了,这也是添Synchronized关键字之后独有的。执行同步代码块后首先要先执行monitorenter指令,退出的时候monitorexit指令。通过分析之后可以看出,使用Synchronized进行同步,其关键就是必须要对对象的监视器monitor进行获取,当线程获取monitor后才能继续往下执行,否则就只能等待。而这个获取的过程是互斥的,即同一时刻只有一个线程能够获取到monitor。上面的demo中在执行完同步代码块之后紧接着再会去执行一个静态同步方法,而这个方法锁的对象依然就这个类对象,那么这个正在执行的线程还需要获取该锁吗?答案是不必的,从上图中就可以看出来,执行静态同步方法的时候就只有一条monitorexit指令,并没有monitorenter获取锁的指令。这就是锁的重入性,即在同一锁程中,线程不需要再次获取同一把锁。Synchronized先天具有重入性。每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一。
  synchronized拥有强制原子性的内部锁机制,是一个可重入锁。因此,在一个线程使用synchronized方法时调用该对象另一个synchronized方法,即一个线程得到一个对象锁后再次请求该对象锁,是永远可以拿到锁的。
  在Java内部,同一个线程调用自己类中其他synchronized方法/块时不会阻碍该线程的执行,同一个线程对同一个对象锁是可重入的,同一个线程可以获取同一把锁多次,也就是可以多次重入。原因是Java中线程获得对象锁的操作是以线程为单位的,而不是以调用为单位的。
  每个锁关联一个线程持有者和一个计数器。当计数器为0时表示该锁没有被任何线程持有,那么任何线程都都可能获得该锁而调用相应方法。当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器计为1。此时其他线程请求该锁,则必须等待。而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增。当线程退出一个synchronized方法/块时,计数器会递减,如果计数器为0则释放该锁。

附synchronized测试代码:

package com.mmall.concurrency.example.sync;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/*
Synchronized修饰一个代码块,修饰一个方法
 */
@Slf4j
public class SynchronizedExample1 {
    //修饰一个代码块
    public void test1(int j) {
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                log.info("test1 {} - {}", j,i);
            }
        }
    }

    //修饰一个方法
    public synchronized void test2() {
        for (int i = 0; i < 10; i++) {
            log.info("test2 - {}", i);
        }
    }

    public static void main(String[] args) {
        SynchronizedExample1 example1 = new SynchronizedExample1();
        SynchronizedExample1 example2 = new SynchronizedExample1();
        //使用线程池;启动两个进程去执行
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(() -> {
            example1.test1(1);
        });
        executorService.execute(()->{
            example2.test1(2);
        });
    }
}
/*
14:55:47.378 [pool-1-thread-1] INFO com.mmall.concurrency.example.sync.SynchronizedExample1 - test1 1 - 0
14:55:47.388 [pool-1-thread-2] INFO com.mmall.concurrency.example.sync.SynchronizedExample1 - test1 2 - 0
14:55:47.396 [pool-1-thread-2] INFO com.mmall.concurrency.example.sync.SynchronizedExample1 - test1 2 - 1
14:55:47.396 [pool-1-thread-1] INFO com.mmall.concurrency.example.sync.SynchronizedExample1 - test1 1 - 1
14:55:47.396 [pool-1-thread-2] INFO com.mmall.concurrency.example.sync.SynchronizedExample1 - test1 2 - 2
14:55:47.396 [pool-1-thread-1] INFO com.mmall.concurrency.example.sync.SynchronizedExample1 - test1 1 - 2
14:55:47.396 [pool-1-thread-2] INFO com.mmall.concurrency.example.sync.SynchronizedExample1 - test1 2 - 3
14:55:47.397 [pool-1-thread-2] INFO com.mmall.concurrency.example.sync.SynchronizedExample1 - test1 2 - 4
14:55:47.397 [pool-1-thread-1] INFO com.mmall.concurrency.example.sync.SynchronizedExample1 - test1 1 - 3
14:55:47.397 [pool-1-thread-2] INFO com.mmall.concurrency.example.sync.SynchronizedExample1 - test1 2 - 5
14:55:47.397 [pool-1-thread-1] INFO com.mmall.concurrency.example.sync.SynchronizedExample1 - test1 1 - 4
14:55:47.397 [pool-1-thread-2] INFO com.mmall.concurrency.example.sync.SynchronizedExample1 - test1 2 - 6
14:55:47.397 [pool-1-thread-1] INFO com.mmall.concurrency.example.sync.SynchronizedExample1 - test1 1 - 5
14:55:47.397 [pool-1-thread-2] INFO com.mmall.concurrency.example.sync.SynchronizedExample1 - test1 2 - 7
14:55:47.397 [pool-1-thread-1] INFO com.mmall.concurrency.example.sync.SynchronizedExample1 - test1 1 - 6
14:55:47.397 [pool-1-thread-2] INFO com.mmall.concurrency.example.sync.SynchronizedExample1 - test1 2 - 8
14:55:47.397 [pool-1-thread-1] INFO com.mmall.concurrency.example.sync.SynchronizedExample1 - test1 1 - 7
14:55:47.397 [pool-1-thread-2] INFO com.mmall.concurrency.example.sync.SynchronizedExample1 - test1 2 - 9
14:55:47.397 [pool-1-thread-1] INFO com.mmall.concurrency.example.sync.SynchronizedExample1 - test1 1 - 8
14:55:47.397 [pool-1-thread-1] INFO com.mmall.concurrency.example.sync.SynchronizedExample1 - test1 1 - 9
 */
package com.mmall.concurrency.example.sync;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/*
Synchronized修饰一个类和静态方法
 */
@Slf4j
public class SynchronizedExample2 {
    //修饰一个类
    public static void test1(int j) {
        synchronized (SynchronizedExample2.class) {
            for (int i = 0; i < 10; i++) {
                log.info("test1 {} - {}", j,i);
            }
        }
    }

    //修饰一个静态方法
    public static synchronized void test2() {
        for (int i = 0; i < 10; i++) {
            log.info("test2 - {}", i);
        }
    }

    public static void main(String[] args) {
        SynchronizedExample2 example1 = new SynchronizedExample2();
        SynchronizedExample2 example2 = new SynchronizedExample2();
        //使用线程池;启动两个进程去执行
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(() -> {
            example1.test1(1);
        });
        executorService.execute(()->{
            example2.test1(2);
        });
    }
}
/*
14:58:42.791 [pool-1-thread-1] INFO com.mmall.concurrency.example.sync.SynchronizedExample2 - test1 1 - 0
14:58:42.820 [pool-1-thread-1] INFO com.mmall.concurrency.example.sync.SynchronizedExample2 - test1 1 - 1
14:58:42.820 [pool-1-thread-1] INFO com.mmall.concurrency.example.sync.SynchronizedExample2 - test1 1 - 2
14:58:42.820 [pool-1-thread-1] INFO com.mmall.concurrency.example.sync.SynchronizedExample2 - test1 1 - 3
14:58:42.821 [pool-1-thread-1] INFO com.mmall.concurrency.example.sync.SynchronizedExample2 - test1 1 - 4
14:58:42.821 [pool-1-thread-1] INFO com.mmall.concurrency.example.sync.SynchronizedExample2 - test1 1 - 5
14:58:42.821 [pool-1-thread-1] INFO com.mmall.concurrency.example.sync.SynchronizedExample2 - test1 1 - 6
14:58:42.821 [pool-1-thread-1] INFO com.mmall.concurrency.example.sync.SynchronizedExample2 - test1 1 - 7
14:58:42.821 [pool-1-thread-1] INFO com.mmall.concurrency.example.sync.SynchronizedExample2 - test1 1 - 8
14:58:42.821 [pool-1-thread-1] INFO com.mmall.concurrency.example.sync.SynchronizedExample2 - test1 1 - 9
14:58:42.821 [pool-1-thread-2] INFO com.mmall.concurrency.example.sync.SynchronizedExample2 - test1 2 - 0
14:58:42.821 [pool-1-thread-2] INFO com.mmall.concurrency.example.sync.SynchronizedExample2 - test1 2 - 1
14:58:42.821 [pool-1-thread-2] INFO com.mmall.concurrency.example.sync.SynchronizedExample2 - test1 2 - 2
14:58:42.821 [pool-1-thread-2] INFO com.mmall.concurrency.example.sync.SynchronizedExample2 - test1 2 - 3
14:58:42.821 [pool-1-thread-2] INFO com.mmall.concurrency.example.sync.SynchronizedExample2 - test1 2 - 4
14:58:42.821 [pool-1-thread-2] INFO com.mmall.concurrency.example.sync.SynchronizedExample2 - test1 2 - 5
14:58:42.821 [pool-1-thread-2] INFO com.mmall.concurrency.example.sync.SynchronizedExample2 - test1 2 - 6
14:58:42.821 [pool-1-thread-2] INFO com.mmall.concurrency.example.sync.SynchronizedExample2 - test1 2 - 7
14:58:42.821 [pool-1-thread-2] INFO com.mmall.concurrency.example.sync.SynchronizedExample2 - test1 2 - 8
14:58:42.821 [pool-1-thread-2] INFO com.mmall.concurrency.example.sync.SynchronizedExample2 - test1 2 - 9
 */

原子性对比

synchronized:不可中断锁,适合竞争不激烈,可读性好
Lock:可中断锁,能多样化同步,竞争激烈时能维持常态(相关介绍待整理)
Atomic:竞争激烈时能维持常态,比Lock性能好,但只能同步一个值

线程安全性-可见性

可见性是让一个线程对主内存的修改可以及时的被其他线程观察到。
与可见性相反的就是不可见性,导致共享变量在线程中不可见的一些主要原因:

  • 线程交叉执行
  • 重排序结合线程交叉执行
  • 共享变量更新后的值没有在工作内存与主内存间及时更新

Java提供了synchronized 和 volatile 两种方法来确保可见性

JMM(java内存模型)关于synchronized的两条规定:

  1. 线程解锁前,必须把共享变量的最新值刷新到主内存
  2. 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意,加锁和解锁是同一把锁)

可见性-volatile,volatile通过加入内存屏障和禁止重排序优化来实现可见性:

  • 对volatile 变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存
  • 对volatile变量读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量

可见性-volatile写
在这里插入图片描述
可见性-volatile读
在这里插入图片描述
但是volatile关键字能保证其所修饰的变量是线程安全的吗?实际上并不能,volatile能阻止重排序实现可见性,但是并不具有原子性。来看以下这个例子:

package com.mmall.concurrency.example.count;


import com.mmall.concurrency.annoations.NotThreadSafe;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

/**
 * volatile
 */
@Slf4j
@NotThreadSafe
public class CountExample4 {
    //请求总数
    public static int clientTotal=5000;
    //同事并发执行的线程数
    public static int threadTotal=200;
    //计数
    public static volatile int count=0;

    public static void main(String[] args) throws InterruptedException {
        //定义一个线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        //信号量(并发数)
        final Semaphore semaphore = new Semaphore(threadTotal);
        //计数器(线程总数)
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            executorService.execute(()->{
                try {
                    semaphore.acquire();//当前进程是否可以执行(并发是否小于50)
                    add();
                    semaphore.release();//释放进程
                } catch (InterruptedException e) {
                    log.error("exception",e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();//关闭线程池
        log.info("count:{}",count);
    }
/**
     * 这个方法是线程不安全的,会有多个线程同时操作count变量
     * 因为volatile只能保证以下三步执行的顺序不会被重排序
     * 但是不保证这三步能够原子执行,所以volatile是不具备原子性的
     * 也就是说还是有可能会有两个线程交叉执行这三步,导致执行结果不能确定
     */
    private static void add(){
        count++;
        //volatile为什么不安全
        //两个线程回同时执行以下步骤会丢掉一次操作
        //1、取出count
        //2、+1 写入count主存
    }
}
/*线程安全下结果是5000
15:05:12.610 [main] INFO com.mmall.concurrency.example.count.CountExample4 - count:4998
 */

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  1. 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(某个线程改变它后;其他线程会重新载入主存中的值);
  2. 禁止进行指令重排序。可以如下使用:

综上,volatile特别适合用来做线程标记量,如下示例:


volatile boolean inited = false;

// 线程1
context = loadContext();
inited = true;

// 线程2
while(!inited){
    sleep();
}
doSomethingWithConfig(context);

在这个例子中定义了一个用volatile修饰的共享变量inited,其主要作用是用于线程2判断线程1是否已完成context的初始化。当线程1初始化context完成时,会修改inited变量的值为true。然后由于volatile的可见性,所以此时线程2马上就能获取到inited的值为true,接着就可以使用初始化好的context了。

线程安全性-有序性

◆ Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性
◆ volatile, synchronized, Lock
  
  在Java里,可以通过volatile关键字保证一定的有序性,另外也可以通过synchronized和Lock来保证有序性。很显然,synchronized和Lock可以保证在同一时间,只会有一个线程执行同步代码,相当于是让线程有序的执行同步代码,自然就保证了有序性。
  另外,Java内存模型具备一些先天的有序性,就是可以不需要通过任何手段就能够得到保证的有序性,这个通常称之为Happens-before原则。如果两个操作的执行次序无法从Happens-before原则中推导出来,那么这两个操作就不能保证有序性,虚拟机就可以随意的对它们进行重排序。
  Happens-before原则(先行发生原则),Java内存模型一共列出了八条Happens-before原则,如果两个操作的次序不能从这八种规则中推倒出来,则不能保证有序性:

  1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生书写在后面的操作
  2. 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作
  3. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  4. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  5. 线程启动规则:Thread对象的start() 方法先行发生于此线程的每一个动作
  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  7. 线程终结规则:线程中所有操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束,可以通过Thread.isAlive()的返回值检测到线程已经终止执行
  8. 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
    第一条规则要注意理解,这里只是程序的运行结果看起来像是顺序执行,虽然结果是一样的,但JVM会对没有变量值依赖的操作进行重排序,这个规则只能保证单线程下执行的有序性,不能保证多线程下的有序性

猜你喜欢

转载自blog.csdn.net/bingdianone/article/details/83386819