juc总结

1.volatile

1.1java内存模型

共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入时,能对另一个线程可见。

从抽象的角度来看,JMM定义了线程和主内存(堆内存)之间的抽象关系:**线程之间的共享变量存储在主内存(main memory)中每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。**本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。

JMM同步规定:

1.线程解锁前,必须把共享变量的值刷新到主内存去

2.线程加锁前,必须读取到主内存的最新值到自己的工作内存去

3.加锁解锁是同一把锁

由于JVM运行程序的实体就是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方为称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型总规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行**,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再讲变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,这个就是可见性**

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7oW7GOWR-1602212222834)(JUCImage\jmm1.png)]

从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:

  1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。

  2. 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yv4v7Kq0-1602212222839)(JUCImage/jmm2.png)]

如上图所示,本地内存A和B有主内存中共享变量x的副本。假设初始时,这三个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。

当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。

从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。

总结:什么是Java内存模型:java内存模型简称jmm,定义了一个线程对另一个线程可见。共享变量存放在主内存中,每个线程都有自己的本地内存,当多个线程同时访问一个数据的时候,可能本地内存没有及时刷新到主内存,所以就会发生线程安全问题。

1.2Vilatile的定义

volatile是java虚拟机提供的轻量级的同步机制,当多个线程进行操作共享数据时,可以保证内存中的数据可见性

相较与synchronized是一种较为轻量级的同步策略。

1.3Vilatile的可见性

各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后写回到主内存中的。这就可能存在一个线程AAA修改了共享变量X的值但还未写回主内存时,另外一个线程BBB有对主内存中同一个共享变量X进行操作,但此时A线程工作内存中共享变量x对线程B来说是并不可见的,这种工作内存与主内存同步延迟线程就造成了可见性问题,

class MyData{
   volatile int number = 1;

    public void updateNumber(){
        this.number = 60;
    }
}
/**
 * 验证volatitle的可见性
 *
 * 1.1假如int number =0;number 变量之前根本没有添加volatile关键字修饰
   1.2添加volatile关键修饰
 */
public class VolatileDemo {

    public static void main(String[] args) throws Exception{
        MyData myData = new MyData();
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"\t common in");
            //暂停一会儿线程
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myData.updateNumber();
            System.out.println(Thread.currentThread().getName()+"\t update number\t"+myData.number);
        },"AA").start();
        while (myData.number==1){
            //等于0一直等待
        }
        System.out.println("main number over\t"+myData.number);
    }
注意:
1.volatile不具备"互斥性"
 2.volatile 不能保证变量的原子性

1.3不保证原子性

原子性:不可分割,完整性,也即某个线程正在做某个业务时,中间不可以被加载或者被分割。需要整体完整要么同时成功,要么同时失败,也就是最终一致性

验证可见性和不保证原子性

不保证原子性的解决方法:

1.加sync

2.直接使用JUC下AtomicInteger

原子变量: jdk1.5后java.util.concurrent.atomic 提供了常用的原子变量:
1.volatile保证有可见性
        2.CAS(Compare-And-Swap)算法保证数据的原子性
            CAS算法是硬件对于并发操作共享数据的支持
           CAS包含了三个操作值:
                内存值V
                预估值A
                更新值B
                当且仅当V==A时,V=B,否则不进行操作
class MyData{
   volatile int number = 0;
    public void addNumber(){
        this.number = 60;
    }
    public void addAtomicity(){
        this.number++;
    }
    AtomicInteger atomicInteger = new AtomicInteger();
    public void addMyAtomic(){
        atomicInteger.getAndIncrement();
    }
}

/**
 * 1验证volatile的可见性
 *    1.1假如int number=0;number变量之前根本没有添加volatile关键字修饰,没有可见性
 *    1.2添加了volatile,可以解决可见性问题
 * 2验证volatitle不保证原子性
 *      原子性:不可分割,完整性,也即某个线程正在做某个业务时,中间不可以被加载或者被分割。
 *      需要整体完整要么同时成功,要么同时失败
 *
 */
public class VolatileDemo01 {
    public static void main(String[] args) {
        MyData myData = new MyData();
        for (int i = 0; i < 20; i++) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        for (int j = 0; j < 10000; j++) {
                            myData.addAtomicity();
                            myData.addMyAtomic();
                        }
                    }
                }).start();
        }
        //需要等待上面20个线程都全部计算完成后,再用main线程取得最终的结果看是多少?
        while(Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+"最终结果值:"+myData.number);
        System.out.println(Thread.currentThread().getName()+"最终结果值:"+myData.atomicInteger);
    }

    //volatile可见保证可见性,及时通知其他线程,主物理内存的值以及被修改了
    public  static void seeVolatile(){
        MyData myData = new MyData();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("修改之前的值:"+myData.number);
                //等待三秒
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //调用改变的方法
                myData.addNumber();
                System.out.println("修改之后的值:"+myData.number);
            }
        }).start();

        while(myData.number==0){
            //一直循环等待
        }
        System.out.println(Thread.currentThread().getName()+":"+myData.number);
    }
}

1.4禁止指令重排(有序性)

volatile 禁止指令重排

JMM 有序性

计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令重排,一般分为以下3种

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Uo0b77qM-1602212222842)(JUCImage/指令重排.png)]

单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致

处理器在进行重排序时必须要考虑指令之间的数据依赖性,多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保住一致性是无法确定的,结果无法预测

public class ResouceStore {
    int a = 0;
    boolean flag = false;
    public void method1(){
        int a = 1;
        boolean flag = true;
    }
    /*处理器在进行重排序时必须要考虑指令之间的数据依赖性,
    多线程环境中线程交替执行,由于编译器优化重排的存在,
    两个线程中使用的变量能否保住一致性是无法确定的,结果无法预测*/
    public void method2(){
         if (flag){
             a = a+5;//语句三
             System.out.println("value:"+a);
     	    }
    }
}

禁止指令重排总结

volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象

先了解一下概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:

一是保证特定操作的执行顺序,

二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。

由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和折腾Memory Barrier指令重排序,

也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

JMM线程安全得到保证

工作内存和主内存同步延时现象导致的可见性问题,可以使用synchronize或者volatile关键字进行解决,他们都可以使一个线程修改后的变量立即对其他线程可见。

对于指令重排导致的可见性问题和有序性问题可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化

那些地方用过volatile

1.单例模式DCL代码 2单例模式volatile分析

public class VolatileDemo02 {

    private static volatile VolatileDemo02 instance = null;
    private VolatileDemo02(){
        System.out.println(Thread.currentThread().getName()+"构造函数");
    }

    //DCL(Double Check Lock双端检测机制)
    public static  VolatileDemo02 getInstance(){
        if (instance == null) {
            synchronized (VolatileDemo02.class){
                if (instance == null) {
                    instance = new VolatileDemo02();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
//        System.out.println(VolatileDemo02.getInstance()==VolatileDemo02.getInstance());
//        System.out.println(VolatileDemo02.getInstance()==VolatileDemo02.getInstance());
//        System.out.println(VolatileDemo02.getInstance()==VolatileDemo02.getInstance());
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    VolatileDemo02.getInstance();
                }
            }).start();
        }
    }
}

DCL(Double Check Lock双端检测机制)

DCL(双端检索)机制不一定线程安全,原因四有指令重排序的存在,加入volatile可以禁止指令重排

原因在与某一个执行到第一次检测,读取到的instance不为null时间,instance的引用对象可能没有完成初始化。

instance = new SingletonDemo();可以分为以下三步完成(伪代码)

memory = allocate();//1.分配对象内存空间

instance(memory );//2初始化对象

instance = memory //3设置instance执行分配的内存地址,此时instance!=null

步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中没有变化,因此这种重排优化是允许的

memory = allocate();//1.分配对象内存空间

instance = memory //3设置instance执行分配的内存地址,此时instance!=null但是对象还没有初始化

instance(memory );//2初始化对象

但是指令重排只会保证串行语义的执行的一致性(单线程),但不会关系多线程间的语义一致性。

所以当一条数据线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。

2.CAS

2.1CAS定义

CAS:Compare and Swap,即比较再交换。 CAS是一种硬件对并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问,CAS是一种无锁的非阻塞算法的实现

CAS包含了3个操作数:

     ```java

1.需要读写到内存值v
2.进行比较的值A
3.更新的值B
当且仅当V的值等于A时,CAS通过原子方式更新B来更新V的值,否则不进行任何操作
```

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-To1GYKyc-1602212222845)(JUCImage/CAS1.png)]

代码案例1:

/**
 * CAS算法:
 *  CAS是一种硬件对并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享
 * 数据的并发访问
 * CAS是一种无锁的非阻塞算法的实现
 * CAS包含了3个操作数:
 *        1. 需要读写到内存值v
 *         2.进行比较的值A
 *         3.更新的值
 * 当且仅当V的值等于A时,CAS通过原子方式更新B来更新V的值,否则不进行任何操作
 *
 * CopyOnWriteArrayList:"写入并复制"
 * 注意:添加操作多时,效率低,因为每次添加操作都会进行复制,开销非常大。并发迭代操作多时可以选择
 */
public class TestCas {

    public static void main(String[] args) {

        CompareAndSwap compareAndSwap = new CompareAndSwap();
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                int expectedValue = compareAndSwap.get();
                boolean b = compareAndSwap.compareAndSet(expectedValue, (int) Math.random() * 101);
                System.out.println(b);
            }).start();
        }
    }
}
class CompareAndSwap{
    private int value;

    public synchronized int get(){
        return value;
    }
    //比较
    public synchronized int compareAndSwap(int excetedValue,int newVlaue){
        int oldValue = value;
        if (excetedValue == oldValue){
            return newVlaue;
        }
        return oldValue;
    }
    //设置值
    public synchronized  boolean compareAndSet(int excetedValue,int newVlaue){
        return excetedValue == compareAndSwap(excetedValue,newVlaue);
    }
}

代码案例:

/**
 * CAS是什么?
 *  比较并交换
 */
public class CASDemo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(5);
        /**
         * 意思和5进行比较,如果是5的话就修改为2019
         * 真实值和期望值相同修改成功,不相同,修改失败
         */
        System.out.println(atomicInteger.compareAndSet(5,2019)
                +":t current data"+atomicInteger.get());//结果true:t current data2019
        System.out.println(atomicInteger.compareAndSet(5,1024)
                +":t current data"+atomicInteger.get());//结果:false:t current data2019
    }
}

2.2CAS的底层原理

CAS底层原理:

1.自旋锁

2.UnSafe

AtomicInteger atomicInteger = new AtomicInteger(5);
atomicInteger.getAndIncrement();

atomicInteger.getAndIncrement()源码

//this:当前对象
//valueOffset:内存的偏移量(内存地址) 
public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

2.3 UnSafe类

private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

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

    private volatile int value;

由上诉代码可以看出:

CAS能够保证原子性,靠的是Unsafe类

UnSafe是CAS的核心类,由于java无法直接访问底层系统,需要通过本地(native)方法来访问,UnSafe相当于一个后门,基于该类可以直接操作特定内存的数据。UnSafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为java中CAS操作的执行依赖于UnSafe类的方法

注意UnSafe类中的所有方法都是native修饰的,也就是说UnSafe类中的方法都直接调用操作系统底层资源执行相应任务

1.变量valueOffset,表示该变量值在内存中的偏移地址(内存地址),因为UnSafe就是内存地址偏移地址获取数据的

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

2.变量value用volatile修饰,保证了多线程之间的内存可见性

2.4CAS详解

CAS的全程为Compare-And-Swap,它是一条CPU并发源语。

它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。

CAS指令在Intel CPU上称为CMPXCHG指令,它的作用是将指定内存地址的内容与所给的某个值相比,如果相等,则将其内容替换为指令中提供的新值,如果不相等,则更新失败。这一比较并交换的操作是原子的,不可以被中断。初一看,CAS也包含了读取、比较 (这也是种操作)和写入这三个操作,和之前的i++并没有太大区别,是的,的确在操作上没有区别,但CAS是通过硬件命令保证了原子性,而i++没有,且硬件级别的原子性比i++这样高级语言的软件级别的运行速度要快地多。虽然CAS也包含了多个操作,但其的运算是固定的(就是个比较),这样的锁定性能开销很小。

从内存领域来说这是乐观锁因为它在对共享变量更新之前会先比较当前值是否与更新前的值一致,如果是,则更新,如果不是,则无限循环执行(称为自旋),直到当前值与更新前的值一致为止,才执行更新。

简单的来说,**CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则返回V。**这是一种乐观锁的思路,它相信在它修改之前,没有其它线程去修改它;而Synchronized是一种悲观锁,它认为在它修改之前,一定会有其它线程去修改它,悲观锁效率很低。

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}
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保证原子性靠的是 CAS的汇编指令发出的CPU原语,偏硬件,执行必须连续,不会被打断的

unsafe.getAndAddInt

var1 AtomicInteger对象本身

var2 该对象值得引用地址

var4需要变动的数量

var5是用过var1 var2 找出的主内存中的真实的值。

该对象当前的值与var5比较:

如果相同,更新var5+var4并且返回true,

如果不同,继续取值然后在比较,直至更新完成。

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

getAndIncrement()方法底层是CAS思想,靠的是UnSafe类的CPU指令原语来保证原子性,也就是说原子性
靠的是UnSafe类,工作的思想是CAS,CAS是比较并交换,真实值和期望值相比较一致,修改成功,成功之后+1,反之,真实值和期望值相比较不一致,修改失败,失败之后再来一次,直至成功,

过程详解 :

假设线程A和线程B两个线程B同时执行getAndAddInt操作(分别跑在不同CPU上):
1.AtomicInteger里面的value值为3,即主内存中AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存。

2.线程A通过getIntVolatile(var1,var2)拿到value值3,这时线程A被挂起。(CPU底层调度)

3.线程B也通过getIntVolatile(var1,var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存值也为3,成功修改内存值为4,线程B打完收工,一切ok。

4.这时线程A恢复,执行compareAndSwapInt方法进行比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值已经会被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取再来一遍了。

5.线程A重新获取value的值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到的,线程A继续执行compareAndSwapInt进行比较替换,直到成功。

CAS与synchronize的区别

synchronize保证了一致性,但是降低了并发性
CAS即保证了一致性(volatile),也提高了并发性

简单小总结

底层思想: UnSafe+CAS思想(自旋)

CAS(CompareAndSwap)

比较当前工作内存中的值和主内存的值,如果相同则执行规定操作,否则继续比较直到内存和工作内存中的值一致为止。

CAS的应用

CAS有3个操作数,内存值,
旧的预期值A,要修改的更新值B。当且仅当预期值A和内存值V相同时,将内存V修改为B,否则什么都不会做

加深理解的参考网站

https://blog.csdn.net/seulzz/article/details/77930800

2.5 CAS缺点

1.循环时间长,开销大

在执行getAndAddInt方法的时,有一个do while

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失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销

2.只能保证一个共享变量的原子操作

当对一个共享变量执行操作时,可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。

3.ABA问题

CAS→UnSafe→CAS底层思想→ABA→原子引用更新→如何规避ABA问题

CAS会导致"ABA问题"

CAS算法实现一个重要前提取出内存中某一时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。比如说一个线程1从内存位置V中读取A,这个时候另一个线程2也从内存中取出A,并且线程2行了一些操作将值变成了B,然后线程2又将V位置的数据变成了A,这个时候线程1进行CAS操作发现内存中仍然是A,然后线程1操作成功,这期间存在一个时间差尽管线程1是CAS操作成功,但是不代表这个过程就是没有问题的

原子引用

class User{
  String username;
  int age;

    public User(String username, int age) {
        this.username = username;
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" + "username='" + username + '\'' + ", age=" + age + '}';
    }
}
public class AtomicReferenceDemo {
    public static void main(String[] args) {
        User user1 = new User("z3",12);
        User user2 = new User("lisi",14);
        AtomicReference<User> atomicReference = new AtomicReference<>();
        atomicReference.set(user1);
        //结果true    User{username='lisi', age=14}
  System.out.println(atomicReference.compareAndSet(user1,user2)+"\t"+atomicReference.get());
        //结果false    User{username='lisi', age=14}
        //如果想对某个类进行包装,可以参考AtomicReference
  System.out.println(atomicReference.compareAndSet(user1,user2)+"\t"+atomicReference.get());
    }
}

解决ABA问题:

理解原子引用+新增一种机制,那就是修改版本号(类似于时间戳)


public class ABADemo {
    static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
    //时间戳的原子引用
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100,1);
    public static void main(String[] args) {
          System.out.println("=========以下是ABA问题的产生===============");
            new Thread(()->{
                atomicReference.compareAndSet(100,101);
                atomicReference.compareAndSet(101,100);
             },"AA").start();
        new Thread(()->{
            try {
                //暂停2s钟等BB线程,保证上面的AA线程执行完ABA操作
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomicReference.compareAndSet(100, 2019)+"\t"+atomicReference.get());
        },"BB").start();
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("=========以下是解决ABA问题===============");
        new Thread(()->{
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t 第一次版本号:"+stamp);
            try {
                //暂停2s钟
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"\t 第二次版本号"+atomicStampedReference.getStamp());
            atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"\t 第三次版本号"+atomicStampedReference.getStamp());
        },"CC").start();
        new Thread(()->{
            int stamp = atomicStampedReference.getStamp();
            try {
                //暂停2s钟等DD线程,保证上面的CC线程执行完ABA操作
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean result = atomicStampedReference.compareAndSet(100, 101, stamp, stamp + 1);
            System.out.println(Thread.currentThread().getName()+"修改成功与否:"+result+"\t"+"当前的实际版本号:"+stamp);
            System.out.println("当前的值为:"+atomicStampedReference.getReference());
        },"DD").start();
    }
}

效果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jwQy5RTx-1602212222849)(JUCImage/ABA问题.png)]

有ABA问题(即在更新前的值是A,但在操作过程中被其他线程更新为B,又更新为 A),这时当前线程认为是可以执行的,其实是发生了不一致现象,如果这种不一致对程序有影响(真正有这种影响的场景很少,除非是在变量操作过程中以此变量为标识位做一些其他的事,比如初始化配置),则需要使用AtomicStampedReference(除了对更新前的原值进行比较,也需要用更新前的 stamp标志位来进行比较)。

总结:
可以用CAS在无锁的情况下实现原子操作,但要明确应用场合,非常简单的操作且又不想引入锁可以考虑使用CAS操作,当想要非阻塞地完成某一操作也可以考虑CAS。不推荐在复杂操作中引入CAS,会使程序可读性变差,且难以测试,同时会出现ABA问题

3.Lock

在 Lock 接口出现之前, Java 中的应用程序对于多线程的并发安全处理只能基于synchronized 关键字来解决。但是 synchronized 在有些场景中会存在一些短板,也就是它并不适合于所有的并发场景。 但是在 Java5 以后, Lock 的出现可以解决synchronized 在某些场景中的短板,它比 synchronized 更加灵活。

3.1 Lock 的实现

Lock 本质上是一个接口,它定义了释放锁和获得锁的抽象方法, 定义成接口就意味着它定义了锁的一个标准规范,也同时意味着锁的不同实现。 实现 Lock 接口的类有很多,以下为几个常见的锁实现ReentrantLock:表示重入锁,它是唯一一个实现了 Lock 接口的类。重入锁指的是线程在获得锁之后,再次获取该锁不需要阻塞,而是直接关联一次计数器增加重入次数

ReentrantReadWriteLock:重入读写锁,它实现了 ReadWriteLock 接口,在这个类中维护了两个锁,一个是 ReadLock,一个是 WriteLock,他们都分别实现了 Lock接口。读写锁是一种适合读多写少的场景下解决线程安全问题的工具,基本原则是: 读和读不互斥、读和写互斥、写和写互斥。也就是说涉及到影响数据变化的操作都会存在互斥。

3.2Lock 的类关系图

Lock 有很多的锁的实现,但是直观的实现是 ReentrantLock 重入锁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XXSMIcio-1602212222851)(JUCImage/lock1.png)]

void lock() // 如果锁可用就获得锁,如果锁不可用就阻塞直到锁释放
void lockInterruptibly() // 和lock()方法相似, 但阻塞的线程可中断抛出java.lang.InterruptedException 异常
boolean tryLock() // 非阻塞获取锁;尝试获取锁,如果成功返回 true
boolean tryLock(long timeout, TimeUnit timeUnit)//带有超时时间的获取锁方法
void unlock() // 释放锁

3.3 ReentrantLock 重入锁

重入锁,表示支持重新进入的锁,也就是说,如果当前线程 t1 通过调用 lock 方法获取了锁之后,再次调用 lock,是不会再阻塞去获取锁的,直接增加重试次数就行了。 synchronized 和 ReentrantLock 都是可重入锁。

重入锁的设计目的

比如调用 demo 方法获得了当前的对象锁,然后在这个方法中再去调用demo2, demo2 中的存在同一个实例锁,这个时候当前线程会因为无法获得demo2 的对象锁而阻塞,就会产生死锁。重入锁的设计目的是避免线程的死锁

基于synchronized重入锁的代码实现:

public class ReentrantDemo {
    public synchronized void demo1(){
        System.out.println("begin:demo1");
        demo02();
    }
    public  void demo02(){
        System.out.println("begin:demo2");
        synchronized (this){

        }
    }

    public static void main(String[] args) {
        ReentrantDemo rd = new ReentrantDemo();
        new Thread(rd::demo1).start();
    }
}

ReentrantLock 的使用案例 :

public class AtomicDemo implements Runnable{

    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        get();
    }
    public void get(){
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getId()+"\t get()");
            set();
        } catch (Exception ioe) {
            ioe.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void set(){
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getId()+"\t set()");
        } catch (Exception ioe) {
            ioe.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public static void main(String[] args) {
        AtomicDemo atomicDemo = new AtomicDemo();
       Thread t1 = new Thread(atomicDemo,"t1");
       Thread t2 = new Thread(atomicDemo,"t2");
       t1.start();
       t2.start();
    }

}

3.4ReentrantLock 的实现原理

在 Lock 中,用到了一个同步队列 AQS,全称 AbstractQueuedSynchronizer,它是一个同步工具也是 Lock 用来实现线程同步的核心组件。

AQS 的两种功能:

独占锁,每次只能有一个线程持有锁,比如前面给大家演示 ReentrantLock 就是以独占方式实现的互斥锁
共 享 锁 , 允 许 多 个 线 程 同 时 获 取 锁 , 并 发 访 问 共 享 资 源 , 比 如ReentrantReadWriteLock

AQS 的内部实现:

AQS 队列内部维护的是一个 FIFO 的双向链表,这种结构的特点是每个数据结构 , 都有两个指针,分别指向直接的后继节点和直接前驱节点。所以双向链表可以从任意一个节点开始很方便的访问前驱和后继。每个 Node 其实是由线程封装,当线程争抢锁失败后会封装成 Node 加入到 ASQ 队列中去; 当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ybPcU9Nq-1602212222853)(JUCImage/AQS.png)]

Node 的组成:

 static final class Node {
        static final Node SHARED = new Node();
        static final Node EXCLUSIVE = null;
        static final int CANCELLED =  1;
        static final int SIGNAL    = -1;
        static final int CONDITION = -2;
        static final int PROPAGATE = -3;
        volatile int waitStatus;
        volatile Node prev;//前驱节点
        volatile Node next;//下一个节点
        volatile Thread thread;
        Node nextWaiter;//存储condition队列的后续节点
        //是否是共享锁
        final boolean isShared() {
            return nextWaiter == SHARED;
        }
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }
        //构建一个Node添加为等待队列
        Node(Thread thread, Node mode) {   
            this.nextWaiter = mode;
            this.thread = thread;
        }
       //这个会在condition队列使用
        Node(Thread thread, int waitStatus) { 
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

释放锁以及添加线程对于队列的变化当出现锁竞争以及释放锁的时候, AQS 同步队列中的节点会发生变化,首先看一 下添加节点的场景 。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-odGwtnrw-1602212222854)(JUCImage/AQS1.png)]

里会涉及到两个变化

  1. 新的线程封装成 Node 节点追加到同步队列中,设置 prev 节点以及修改当前节点的前置节点的 next 节点指向自己

  2. 通过 CAS 讲 tail 重新指向新的尾部节点head 节点表示获取锁成功的节点,当头结点在释放同步状态时,会唤醒后继节点,如果后继节点获得锁成功,会把自己设置为头结点,节点的变化过程如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7V1SgWxj-1602212222855)(JUCImage/AQS3.png)]

这个过程也是涉及到两个变化

  1. 修改 head 节点指向下一个获得锁的节点
  2. 新的获得锁的节点,将 prev 的指针指向 null

设置 head 节点不需要用 CAS,原因是设置 head 节点是由获得锁的线程来完成的,而同步锁只能由一个线程获得,所以不需要 CAS 保证,只需要把 head 节点设置为原首节点的后继节点,并且断开原 head 节点的 next 引用即可

3.5ReentrantLock 的源码分析

以 ReentrantLock 作为切入点,来看看在这个场景中是如何使用 AQS 来实现线程的同步的

调用 ReentrantLock 中的 lock()方法,源码的调用过程我使用了时序图来展现。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aycTblpu-1602212222856)(JUCImage/AQS4.png)]

ReentrantLock.lock()

这个是 reentrantLock 获取锁的入口

public void lock() {
  sync.lock();
}

sync 实际上是一个抽象的静态内部类,它继承了 AQS 来实现重入锁的逻辑,

前面说过 AQS 是一个同步队列,它能够实现线程的阻塞以及唤醒, 但它并不具备业务功能, 所以在不同的同步场景中,会继承 AQS 来实现对应场景的功能Sync 有两个具体的实现类,分别是:
NofairSync: 表示可以存在抢占锁的功能,也就是说不管当前队列上是否存在其他线程等待,新线程都有机会抢 占锁
FailSync: 表示所有线程严格按照 FIFO 来获取锁

NofairSync.lock

以非公平锁为例,来看看 lock 中的实现

  1. 非公平锁和公平锁最大的区别在于,在非公平锁中我抢占锁的逻辑是,不管有没有线程排队,我先上来 cas 去抢占一下
  2. CAS 成功,就表示成功获得了锁
  3. CAS 失败,调用 acquire(1)走锁竞争逻辑
final void lock() {
   if (compareAndSetState(0, 1))
    setExclusiveOwnerThread(Thread.currentThread());
else
    acquire(1);
}

CAS原理:

  protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

通过 cas 乐观锁的方式来做比较并替换, 这段代码的意思是,如果当前内存中的state 的值和预期值 expect 相等,则替换为 update。更新成功返回 true,否则返回 false.
这个操作是原子的,不会出现线程安全问题,这里面涉及到Unsafe这个类的操作,以及涉及到 state 这个属性的意义。
state 是 AQS 中的一个属性,它在不同的实现中所表达的含义不一样, 对于重入
锁的实现来说,表示一个同步状态。它有两个含义的表示

  1. 当 state=0 时,表示无锁状态
  2. 当 state>0 时,表示已经有线程获得了锁,也就是 state=1,但是因为
    ReentrantLock 允许重入,所以同一个线程多次获得同步锁的时候, state 会递增,
    比如重入 5 次,那么 state=5。 而在释放锁的时候,同样需要释放 5 次直到 state=0
    其他线程才有资格获得锁

Unsafe 类

Unsafe 类是在 sun.misc 包下,不属于 Java 标准。但是很多 Java 的基础类库,包括一些被广泛使用的高性能开发库都是基于 Unsafe 类开发的,比如 Netty、Hadoop、 Kafka 等;Unsafe 可认为是 Java 中留下的后门,提供了一些低层次操作,如直接内存访问、线程的挂起和恢复、 CAS、线程同步、内存屏障而 CAS 就是 Unsafe 类中提供的一个原子操作, 第一个参数为需要改变的对象,第二个为偏移量(即之前求出来的 headOffset 的值),第三个参数为期待的值,第四个为更新后的值整个方法的作用是如果当前时刻的值等于预期值 var4 相等,则更新为新的期望值 var5,如果更新成功,则返回 true,否则返回 false;

stateOffset
一个 Java 对象可以看成是一段内存,每个字段都得按照一定的顺序放在这段内存里,通过这个方法可以准确地告诉你某个字段相对于对象的起始内存地址的字节偏移。用于在后面的 compareAndSwapInt 中,去根据偏移量找到对象在内存中的具体位置所以 stateOffset 表示 state 这个字段在 AQS 类的内存中相对于该类首地址的偏移量

AQS.accquire

acquire 是 AQS 中的方法,如果 CAS 操作未能成功,说明 state 已经不为 0,此
时继续 acquire(1)操作
➢ acquire 方法中的 1 的参数是用来做什么呢?
这个方法的主要逻辑是

  1. 通过 tryAcquire 尝试获取独占锁,如果成功返回 true,失败返回 false
  2. 如果 tryAcquire 失败,则会通过 addWaiter 方法将当前线程封装成 Node 添加
    到 AQS 队列尾部
  3. acquireQueued,将 Node 作为参数,通过自旋去尝试获取锁。
   public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

NonfairSync.tryAcquire

这个方法的作用是尝试获取锁,如果成功返回 true,不成功返回 false它是重写 AQS 类中的 tryAcquire 方法,并且大家仔细看一下 AQS 中 tryAcquire方法的定义,并没有实现,而是抛出异常。按照一般的思维模式,既然是一个不实现的模版方法,那应该定义成 abstract,让子类来实现呀?

ReentrantLock.nofairTryAcquire

  1. 获取当前线程,判断当前的锁的状态

  2. 如果 state=0 表示当前是无锁状态,通过 cas 更新 state 状态的值

  3. 当前线程是属于重入,则增加重入次数

     final boolean nonfairTryAcquire(int acquires) {
                final Thread current = Thread.currentThread();//获取当前执行的线程
                int c = getState();//获得 state 的值
                if (c == 0) {//表示无锁状态
                    if (compareAndSetState(0, acquires)) {//cas替换state的值,cas 成功表示获取锁成功
                        setExclusiveOwnerThread(current);//保存当前获得锁的线程,下次再来的时候不要再尝试竞争锁
                        return true;
                    }
                }
                else if (current == getExclusiveOwnerThread()) {//如果同一个线程来获得锁,直接增加重入次数
                    int nextc = c + acquires;
                    if (nextc < 0) 
                        throw new Error("Maximum lock count exceeded");
                    setState(nextc);
                    return true;
                }
                return false;
            }
    

    AQS.addWaiter

    当 tryAcquire 方法获取锁失败以后,则会先调用 addWaiter 将当前线程封装成Node.
    入参 mode 表示当前节点的状态,传递的参数是 Node.EXCLUSIVE,表示独占状态。意味着重入锁用到了 AQS 的独占锁功能

    1. 将当前线程封装成 Node

    2. 当前链表中的 tail 节点是否为空,如果不为空,则通过 cas 操作把当前线程的 node 添加到 AQS 队列 3. 如果为空或者 cas 失败,调用 enq 将节点添加到 AQS 队列

          private Node addWaiter(Node mode) {
              Node node = new Node(Thread.currentThread(), mode);//把当前线程封装为 Node
              Node pred = tail;//tail 是 AQS 中表示同比队列队尾的属性,默认是 null
              if (pred != null) {//tail 不为空的情况下,说明队列中存在节点
                  node.prev = pred;//把当前线程的 Node 的 prev 指向 tail
                  if (compareAndSetTail(pred, node)) {//通过 cas 把 node加入到 AQS 队列,也就是设置为 tail
                      pred.next = node;//设置成功以后,把原 tail 节点的 next指向当前 node
                      return node;
                  }
              }
              enq(node);//tail=null,把 node 添加到同步队列
              return node;
          }
      

      enq

      enq 就是通过自旋操作把当前节点加入到队列中

         private Node enq(final Node node) {
              for (;;) {
                  Node t = tail;
                  if (t == null) { // Must initialize
                      if (compareAndSetHead(new Node()))
                          tail = head;
                  } else {
                      node.prev = t;
                      if (compareAndSetTail(t, node)) {
                          t.next = node;
                          return t;
                      }
                  }
              }
          }
      

      图解分析

      假设 3 个线程来争抢锁, 那么截止到 enq 方法运行结束之后, 或者调用 addwaiter方法结束后, AQS 中的链表结构图

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AYqbgSri-1602212222858)(JUCImage/AQS5.png)]

AQS.acquireQueued

通过 addWaiter 方法把线程添加到链表后, 会接着把 Node 作为参数传递给
acquireQueued 方法,去竞争锁

  1. 获取当前节点的 prev 节点

  2. 如果 prev 节点为 head 节点,那么它就有资格去争抢锁,调用 tryAcquire 抢占

  3. 抢占锁成功以后,把获得锁的节点设置为 head,并且移除原来的初始化 head
    节点

  4. 如果获得锁失败,则根据 waitStatus 决定是否需要挂起线程

  5. 最后,通过 cancelAcquire 取消获得锁的操作

      final boolean acquireQueued(final Node node, int arg) {
            boolean failed = true;
            try {
                boolean interrupted = false;
                for (;;) {
                    final Node p = node.predecessor();//获取当前节点的 prev 节点
                    if (p == head && tryAcquire(arg)) {//如果是 head 节点,说明有资格去争抢锁
                        setHead(node);//获取锁成功,也就是ThreadA 已经释放了锁,然后设置 head 为 ThreadB 获得执行权限
                        p.next = null; //把原 head 节点从链表中移除
                        failed = false;
                        return interrupted;
                    }
                    //ThreadA 可能还没释放锁,使得 ThreadB 在执行 tryAcquire 时会返回 false
                    if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                        interrupted = true;//并且返回当前线程在等待过程中有没有中断过。
                }
            } finally {
                if (failed)
                    cancelAcquire(node);
            }
        }
    

    NofairSync.tryAcquire

    这个方法在前面分析过,就是通过 state 的状态来判断是否处于无锁状态,然后在通过 cas 进行竞争锁操作。成功表示获得锁,失败表示获得锁失败

    shouldParkAfterFailedAcquire

    如果 ThreadA 的锁还没有释放的情况下, ThreadB 和 ThreadC 来争抢锁肯定是会失败,那么失败以后会调用 shouldParkAfterFailedAcquire 方法
    Node 有 5 中状态,分别是: CANCELLED(1), SIGNAL(-1)、 CONDITION(-2)、 PROPAGATE(-3)、默认状态(0)
    CANCELLED: 在同步队列中等待的线程等待超时或被中断,需要从同步队列中取
    消该 Node 的结点, 其结点的 waitStatus 为 CANCELLED,即结束状态,进入该状
    态后的结点将不会再变化
    SIGNAL: 只要前置节点释放锁,就会通知标识为 SIGNAL 状态的后续节点的线程

CONDITION: 和 Condition 有关系
PROPAGATE: 共享模式下, PROPAGATE 状态的线程处于可运行状态

0:初始状态
这个方法的主要作用是,通过 Node 的状态来判断, ThreadA 竞争锁失败以后是
否应该被挂起。

  1. 如果 ThreadA 的 pred 节点状态为 SIGNAL,那就表示可以放心挂起当前线程
  2. 通过循环扫描链表把 CANCELLED 状态的节点移除
  3. 修改 pred 节点的状态为 SIGNAL,返回 false

返回 false 时,也就是不需要挂起,返回 true,则需要调用 parkAndCheckInterrupt
挂起当前线程

  private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;//前置节点的waitStatus
        if (ws == Node.SIGNAL)//如果前置节点为 SIGNAL, 意味着只需要等待其他前置节点的线程被释放,
          
            return true;//返回 true,意味着可以直接放心的挂起了
        if (ws > 0) {//ws 大于 0,意味着 prev 节点取消了排队,直接移除这个节点就行
          
            do {
                //相当于: pred=pred.prev;node.prev=pred;
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);//这里采用循环,从双向列表中移除 CANCELLED 的节点
            pred.next = node;
        } else {
            //利用 cas 设置 prev 节点的状态为 SIGNAL(-1)
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

parkAndCheckInterrupt

使用 LockSupport.park 挂起当前线程编程 WATING 状态Thread.interrupted,返回当前线程是否被其他线程触发过中断请求,也就是thread.interrupt(); 如果有触发过中断请求,那么这个方法会返回当前的中断标识true,并且对中断标识进行复位标识已经响应过了中断请求。 如果返回 true,意味着在 acquire 方法中会执行selfInterrupt()。

private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

selfInterrupt: 标识如果当前线程在 acquireQueued 中被中断过,则需要产生一个中断请求,原因是线程在调用 acquireQueued 方法的时候是不会响应中断请求的

 static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }

图解分析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3FU2C1lI-1602212222859)(JUCImage/lock2.png)]

LockSupport 类是 Java6 引入的一个类,提供了基本的线程同步原语。LockSupport实际上是调用了 Unsafe 类里的函数,归结到 Unsafe 里,只有两个函数

    public native void unpark(Object var1);

    public native void park(boolean var1, long var2);

unpark 函数为线程提供“许可(permit)”,线程调用 park 函数则等待“许可”。这个有点像信号量,但是这个“许可”是不能叠加的, “许可”是一次性的。permit 相当于 0/1 的开关,默认是 0,调用一次 unpark 就加 1 变成了 1.调用一次 park 会消费 permit,又会变成 0。 如果再调用一次 park 会阻塞,因为 permit 已经是 0 了。直到 permit 变成 1.这时调用 unpark 会把 permit 设置为 1.每个线程都有一个相关的 permit, permit 最多只有一个,重复调用 unpark 不会累积

锁的释放流程

在 unlock 中,会调用 release 方法来释放锁

   public final boolean release(int arg) {
        if (tryRelease(arg)) {//释放锁成功
            Node h = head;//得到 aqs 中 head 节点
            if (h != null && h.waitStatus != 0)//如果 head 节点不为空并且状态! =0.调用 unparkSuccessor(h)唤醒后续节点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

ReentrantLock.tryRelease

这个方法可以认为是一个设置锁状态的操作, 通过将 state 状态减掉传入的参数值(参数是 1),如果结果状态为 0,就将排它锁的 Owner 设置为 null,以使得其它 的线程有机会进行执行。在排它锁中,加锁的时候状态会增加 1(当然可以自己修改这个值),在解锁的时候减掉 1,同一个锁,在可以重入后,可能会被叠加为 2、 3、 4 这些值,只有 unlock()的次数与 lock()的次数对应才会将 Owner 线程设置为空,而且也只有这种情况下才会返回 true。


        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

unparkSuccessor


    private void unparkSuccessor(Node node) {
   
        int ws = node.waitStatus;//获得 head 节点的状态
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);// 设置 head 节点状态为 0

        //如果下一个节点为 null 或者 status>0 表示 cancelled 状态.
        //通过从尾部节点开始扫描,找到距离 head 最近的一个waitStatus<=0 的节点
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)//next 节点不为空,直接唤醒这个线程即可
            LockSupport.unpark(s.thread);
    }

为什么在释放锁的时候是从 tail 进行扫描

再回到 enq那个方法、。在标注为红色部分的代码来看一个新的节点是如何加入到链表中的

  1. 将新的节点的 prev 指向 tail

  2. 通过 cas 将 tail 设置为新的节点,因为 cas 是原子操作所以能够保证线程安全性

  3. t.next=node;设置原 tail 的 next 节点指向新的节点

        private Node enq(final Node node) {
            for (;;) {
                Node t = tail;
                if (t == null) { // Must initialize
                    if (compareAndSetHead(new Node()))
                        tail = head;
                } else {
                    node.prev = t;
                    if (compareAndSetTail(t, node)) {
                        t.next = node;
                        return t;
                    }
                }
            }
        }
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5nAmGgmq-1602212222860)(JUCImage/locl3.png)]

在 cas 操作之后, t.next=node 操作之前。 存在其他线程调用 unlock 方法从 head开始往后遍历,由于 t.next=node 还没执行意味着链表的关系还没有建立完整。就会导致遍历到 t 节点的时候被中断。 所以从后往前遍历,一定不会存在这个问题。

图解分析

通过锁的释放,原本的结构就发生了一些变化。head 节点的 waitStatus 变成了 0,ThreadB 被唤醒

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WQ9e186T-1602212222862)(JUCImage/lock4.png)]

原本挂起的线程继续执行

通过 ReentrantLock.unlock,原本挂起的线程被唤醒以后继续执行, 原来被挂起的线程是在 acquireQueued 方法中,所以被唤醒以后继续从这个方法开始执行 AQS.acquireQueued 这个方法前面已经完整分析过了,我们只关注一下 ThreadB 被唤醒以后的执行流程。由于 ThreadB 的 prev 节点指向的是 head,并且 ThreadA 已经释放了锁。所以这个时候调用 tryAcquire 方法时,可以顺利获取到锁

  1. 把 ThreadB 节点当成 head

  2. 把原 head 节点的 next 节点指向为 null

        final boolean acquireQueued(final Node node, int arg) {
            boolean failed = true;
            try {
                boolean interrupted = false;
                for (;;) {
                    final Node p = node.predecessor();
                    if (p == head && tryAcquire(arg)) {
                        setHead(node);
                        p.next = null; // help GC
                        failed = false;
                        return interrupted;
                    }
                    if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                        interrupted = true;
                }
            } finally {
                if (failed)
                    cancelAcquire(node);
            }
        }
    

    图解分析

    1. 设置新 head 节点的 prev=null

    2. 设置原 head 节点的 next 节点为 null

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s9W6Jqjr-1602212222864)(JUCImage/lock5.png)]

公平锁和非公平锁的区别

锁的公平性是相对于获取锁的顺序而言的,如果是一个公平锁, 那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是 FIFO。 在上面分析的例子来说,只要CAS 设置同步状态成功,则表示当前线程获取了锁,而公平锁则不一样,差异点有两个

FairSync.tryAcquire

 final void lock() {
    acquire(1);
 }

非公平锁在获取锁的时候,会先通过 CAS 进行抢占,而公平锁则不会

FairSync.tryAcquire

   protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

这个方法与 nonfairTryAcquire(int acquires)比较, 不同的地方在于判断条件多了hasQueuedPredecessors()方法, 也就是加入了[同步队列中当前节点是否有前驱节点]的判断,如果该方法返回 true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁

公平锁和非公平锁
公平锁:是指多个线程按照申请的顺序获取锁,类似排队打饭,先来后到
非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比现申请的线程优先获取在高并发情况下,有可能造成优先级反转或者饥饿现象

区别:

公平锁/非公平锁
并发包中ReentrantLock创建可以指定构造函数的boolean类型来得到公平锁或非公平锁,默认非公平锁

关于两者区别:

公平锁:就是很公平,在并发情况下,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等等队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。
非公平锁:比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式

java ReenTrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的有点在与吞吐量比公平锁打。

对于synchronize而言,也是一种非公平锁

4. Condition

学习 synchronized 的时候, 有讲到 wait/notify 的基本使用, 结合synchronized 可以实现对线程的通信。那么这个时候我就在思考了,既然 J.U.C 里面提供了锁的实现机制,那 J.U.C 里面有没有提供类似的线程通信的工具呢? 于是找阿找,发现了一个 Condition 工具类。Condition 是一个多线程协调通信的工具类,可以让某些线程一起等待某个条件(condition),只有满足条件时,线程才会被唤醒

生产者和消费者案例:

package com.yehui.juc;

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

/**
 * Condition接口描述了可能会与锁有关联的条件变量。这些变量在用法上与使用Object.wait访问的
 * 影视监听器类似,但提供了更强大的功能。需要特别指出的是,单个Lock可能与多个Condition对象关联
 *。
 * 在Condition对象中,与wait、notify和notifyAll方法对应的分别是awit、signal和signalAll。
 *  Condition实例实质上被绑定到一个锁。要为特定的Lock实例获得Condition实例,请使用
 *   Lock lock = new ReentrantLock();
 *     Condition condition = lock.newCondition();
 */
public class TestLock {
    public static void main(String[] args) {
        ClerkLock clerk = new ClerkLock();
        ProductLock  product = new ProductLock(clerk);
        ConsomerLock  consomer = new ConsomerLock(clerk);
        new Thread(product).start();
        new Thread(consomer).start();
    }
}
/**
 * 店员
 */
class ClerkLock{
    private int product = 0;
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

    /**
     * 进货
     */
    public  void get(){
        lock.unlock();
        try {
            while (product>=1){//为了避免虚假问题,应该总是使用在循环中
                System.out.println("产品已经满了");
                try {
                    //等待
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()+":"+(++product));
        }catch (Exception e){
        }finally {
            //唤醒
            condition.signalAll();
        }
    }
    /**
     * 卖货
     */
    public  void sale(){
        lock.lock();
        try {
            while (product<=0){
                System.out.println("产品已经卖完了");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //唤醒
            this.notifyAll();
            System.out.println(Thread.currentThread().getName()+":"+(--product));
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}
//生产者
class ProductLock implements Runnable{

    private ClerkLock clerk;

    public ProductLock(ClerkLock clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            clerk.get();
        }
    }
}
//消费者
class ConsomerLock implements Runnable{

    private ClerkLock clerk;

    public ConsomerLock(ClerkLock clerk) {
        this.clerk = clerk;
    }
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            clerk.sale();
        }
    }
}

通过这个案例简单实现了 wait 和 notify 的功能,当调用 await 方法后,当前线程会释放锁并等待,而其他线程调用 condition 对象的 signal 或者 signalall 方法通知并被阻塞的线程,然后自己执行 unlock 释放锁,被唤醒的线程获得之前的锁继续执行,最后释放锁。所以, condition 中两个最重要的方法,一个是 await,一个是 signal 方法await:把当前线程阻塞挂起signal:唤醒阻塞的线程

调用 Condition,需要获得 Lock 锁,所以意味着会存在一个 AQS 同步队列, 在上面那个案例中,假如两个线程同时运行的话,那么 AQS 的队列可能是下面这种情况

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3ppbQ7Z3-1602212222866)(JUCImage/condition1.png)]

那么这个时候 ThreadA 调用了 condition.await 方法,它做了什么事情呢?

condition.await

调用 Condition 的 await()方法(或者以 await 开头的方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从 await()方法返回时,当前线程一定获取了Condition 相关联的锁 。

 public final void await() throws InterruptedException {
            if (Thread.interrupted())//表示 await 允许被中断
                throw new InterruptedException();
            Node node = addConditionWaiter();//创建一个新的节点,节点状态为 condition,采用的数据结构仍然是链表
            long savedState = fullyRelease(node);//释放当前的锁,得到锁的状态,并唤醒 AQS 队列中的一个线程
            int interruptMode = 0;
     //如果当前节点没有在同步队列上,即还没有被 signal,则将当前线程阻塞
            while (!isOnSyncQueue(node)) {//判断这个节点是否在 AQS 队列上,第一次判断的是 false,因为前面已经释放锁了
                LockSupport.park(this);//通过 park 挂起当前线程
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
     // 当这个线程醒来,会尝试拿锁, 当 acquireQueued返回 false 就是拿到锁了.
// interruptMode != THROW_IE -> 表示这个线程没有成功将 node 入队,但 signal 执行了 enq 方法让其入队了.
         // 将这个变量设置成 REINTERRUPT.
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
     // 如果 node 的下一个等待者不是 null, 则进行清理,清理 Condition 队列上的节点.
// 如果是 null ,就没有什么好清理的了
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
     // 如果线程被中断了,需要抛出异常.或者什么都不做
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

addConditionWaiter

这个方法的主要作用是把当前线程封装成 Node,添加到等待队列。 这里的队列不再是双向链表,而是单向链表

   private Node addConditionWaiter() {
            Node t = lastWaiter;
             如 果 lastWaiter 不 等 于 空 并 且waitStatus 不等于 CONDITION 时,把冲好这个节点从链表中移除
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
       //构建一个 Node,waitStatus=CONDITION。这里的链表是一个单向的,所以相比 AQS 来说会简单很多
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }

图解分析

执行完 addConditionWaiter 这个方法之后,就会产生一个这样的 condition 队列

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LLTJ6qpC-1602212222867)(JUCImage/condition5.png)]

fullyRelease

fullRelease,就是彻底的释放锁,什么叫彻底呢,就是如果当前锁存在多次重入,那么在这个方法中只需要释放一次就会把所有的重入次数归零。

 final long fullyRelease(Node node) {
        boolean failed = true;
        try {
            long savedState = getState();
            //获得重入的次数
            if (release(savedState)) {  //释放锁并且唤醒下一个同步队列中的线程
                failed = false;	
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }

图解分析

此时,同步队列会触发锁的释放和重新竞争。 ThreadB 获得了锁。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YsHBZFU5-1602212222869)(JUCImage/condition2.png)]

isOnSyncQueue

final boolean isOnSyncQueue(Node node) {    
    if (node.waitStatus == Node.CONDITION || node.prev == null)       
        return false;   
    if (node.next != null) 
        return true;
return findNodeFromTail(node);

判断当前节点是否在同步队列中, 返回 false 表示不在,返回 true 表示在如果不在 AQS 同步队列,说明当前节点没有唤醒去争抢同步锁,所以需要把当前线程阻塞起来,直到其他的线程调用 signal 唤醒如果在 AQS 同步队列,意味着它需要去竞争同步锁去获得执行程序执行权限为什么要做这个判断呢?原因是在 condition 队列中的节
点会重新加入到 AQS 队列去竞争锁。 也就是当调用 signal的时候,会把当前节点从 condition 队列转移到 AQS 队列

如何去判断ThreadA 这个节点是否存在于 AQS 队列中呢?

  1. 如果 ThreadA 的 waitStatus 的状态为 CONDITION,说明它存在于 condition 队列中,不在 AQS 队列。因为
    AQS 队列的状态一定不可能有 CONDITION
  2. 如果 node.prev 为空,说明也不存在于 AQS 队列,原因是 prev=null 在 AQS 队列中只有一种可能性,就是它是head 节点, head 节点意味着它是获得锁的节点。
  3. 如果 node.next 不等于空,说明一定存在于 AQS 队列中,因为只有 AQS 队列才会存在 next 和 prev 的关系
  4. findNodeFromTail,表示从 tail 节点往前扫描 AQS 队列, 一旦发现 AQS 队列的节点和当前节点相等,说明节点一定存在于 AQS 队列中

Condition.signal
await 方法会阻塞 ThreadA,然后 ThreadB 抢占到了锁获得了执行权限,这个时候在 ThreadB 中调用了 Condition的 signal()方法,将会唤醒在等待队列中节点

   public final void signal() {
            if (!isHeldExclusively())//先判断当前线程是否获得了锁,这个判断比较简单,直接用获得锁的线程和当前线程相比即可
                throw new IllegalMonitorStateException();// 拿到 Condition队列上第一个节点
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }

Condition.doSignal

对 condition 队列中从首部开始的第一个 condition 状态的节点,执行 transferForSignal 操作,将 node 从 condition队列中转换到 AQS 队列中,同时修改 AQS 队列中原先尾节点的状态

  private void doSignal(Node first) {
            do {
                //从 Condition 队列中删除 first 节点
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;// 将 next 节点设置成 null
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

AQS.transferForSignal

该方法先是 CAS 修改了节点状态,如果成功,就将这个节点放到 AQS 队列中,然后唤醒这个节点上的线程。此时,那个节点就会在 await 方法中苏醒

  final boolean transferForSignal(Node node) {
   
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;//更新节点的状态为 0,如果更新失败,只有一种可能就是节点被 CANCELLED 了
        Node p = enq(node);//调用 enq,把当前节点添加到AQS 队列。并且返回返回按当前节点的上一个节点,也就是原tail 节点
        int ws = p.waitStatus;
      // 如果上一个节点的状态被取消了, 或者尝试设置上一个节点的状态为 SIGNAL 失败了(SIGNAL 表示: 他的 next节点需要停止阻塞),
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);// 唤醒节点上的线程
        return true;//如果 node 的 prev 节点已经是signal 状态,那么被阻塞的 ThreadA 的唤醒工作由 AQS 队列来完成
    }

图解分析

执行完 doSignal 以后,会把 condition 队列中的节点转移到 aqs 队列上, 逻辑结构图如下这个时候会判断 ThreadA 的 prev 节点也就是 head 节点的 waitStatus, 如果大于 0 或者设置 SIGNAL 失败,表示节点被设置成了 CANCELLED 状态。 这个时候会唤醒ThreadA 这个线程。 否则就基于 AQS 队列的机制来唤醒,也就是等到 ThreadB 释放锁之后来唤醒 ThreadA

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fbYYvQnz-1602212222871)(JUCImage/condition3.png)]

被阻塞的线程唤醒后的逻辑

前面在分析 await 方法时,线程会被阻塞。而通过 signal被唤醒之后又继续回到上次执行的逻辑中标注为红色部分的代码checkInterruptWhileWaiting 这个方法是干嘛呢?其实从名字就可以看出来,就是 ThreadA 在 condition 队列被阻塞的过程中,有没有被其他线程触发过中断请求

       public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            Node node = addConditionWaiter();
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

checkInterruptWhileWaiting

如果当前线程被中断,则调用transferAfterCancelledWait 方法判断后续的处理应该是抛出InterruptedException 还是重新中断。 这里需要注意的地方是,如果第一次 CAS 失败了,则不能判断当前线程是先进行了中断还是先进行了 signal 方法的调用,可能是先执行了 signal 然后中断,也可能是先执行了中断,后执行了 signal,当然,这两个操作肯定是发生在 CAS 之前。这时需要做的就是等待当前线程的 node被添加到 AQS 队列后,也就是 enq 方法返回后,返回false 告诉 checkInterruptWhileWaiting 方法返回REINTERRUPT(1),后续进行重新中断。 简单来说,该方法的返回值代表当前线程是否在 park 的时候被中断唤醒,如果为 true 表示中断在 signal 调用之前, signal 还未执行,那么这个时候会根据 await 的语义,在 await 时遇到中断需要抛出interruptedException,返回 true 就是告诉checkInterruptWhileWaiting 返回 THROW_IE(-1)。如果返回 false,否则表示 signal 已经执行过了,只需要重新响应中断即可

   private int checkInterruptWhileWaiting(Node node) {
            return Thread.interrupted() ?
                (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
                0;
        }
//使用 cas 修改节点状态,如果还能修改成功, 说明线程被中断时, signal 还没有被调用。
// 这里有一个知识点,就是线程被唤醒,并不一定是在 java 层面执行了locksupport.unpark,也可能是调用了线程的 interrupt()方法,这个方法会更新一个中断标识,并且会唤醒处于阻塞状态下的线程。
final boolean transferAfterCancelledWait(Node node) {
        if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
            enq(node);//如果 cas 成功,则把node 添加到 AQS 队列
            return true;
        }
    //如果 cas 失败,则判断当前 node 是否已经在 AQS 队列上,如果不在,则让给其他线程执行
//当 node 被触发了 signal 方法时, node 就会被加到 aqs 队列上
        while (!isOnSyncQueue(node))//循环检测 node 是否已经成功添加到 AQS 队列中。如果没有,则通过 yield,
            Thread.yield();
        return false;
    }

acquireQueued

这个方法在讲 aqs 的时候说过,是的当前被唤醒的节点ThreadA 去抢占同步锁。并且要恢复到原本的重入次数状
态。调用完这个方法之后, AQS 队列的状态如下将 head 节点的 waitStatus 设置为-1, Signal 状态。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YZJJaPu0-1602212222873)(JUCImage/condition4.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ic3I0Peo-1602212222875)(JUCImage/condition6.png)]

reportInterruptAfterWait

根据 checkInterruptWhileWaiting 方法返回的中断标识来进行中断上报。如果是 THROW_IE,则抛出中断异常
如果是 REINTERRUPT,则重新响应中断

   private void reportInterruptAfterWait(int interruptMode)
            throws InterruptedException {
            if (interruptMode == THROW_IE)
                throw new InterruptedException();
            else if (interruptMode == REINTERRUPT)
                selfInterrupt();
        }

Condition 总结

await 和 signal 的总结

线程 awaitThread 先通过 lock.lock()方法获取锁成功后调用了 condition.await 方法进入等待队列,而另一个线程 signalThread 通过 lock.lock()方法获取锁成功后调用了 condition.signal 或者 signalAll 方法,使得线awaitThread 能够有机会移入到同步队列中,当其他线程 释放 lock 后使得线程 awaitThread 能够有机会获取
lock,从而使得线程 awaitThread 能够从 await 方法中退出执行后续操作。如果 awaitThread 获取 lock 失败会直
接进入到同步队列。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SOdHedlJ-1602212222877)(JUCImage/condition7.png)]

阻塞: await()方法中,在线程释放锁资源之后,如果节点不在 AQS 等待队列,则阻塞当前线程,如果在等待队列,则自旋等待尝试获取锁
释放: signal()后,节点会从 condition 队列移动到 AQS等待队列,则进入正常锁的获取流程

有一个面试题联系:

package com.yehui.juc;

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

/*定制资源类
 */
class ShareData{
    private int num = 1;//AA为1,BB为2,CC为3
    Lock lock = new ReentrantLock();
   Condition cd1 = lock.newCondition();
   Condition cd2 = lock.newCondition();
   Condition cd3 = lock.newCondition();
    /**
     * 打印5次
     */
    public void print5(int total){
        lock.lock();
        try {
            //判断
            while (num!=1){
                cd1.await();
            }
            //干活
            for (int i1 = 0; i1 < 5; i1++) {
                System.out.println(Thread.currentThread().getName()+"打印"+":"+total+"\t"+i1);
            }
            num=2;
            //通知
            cd1.signal();
        } catch (Exception e) {
          e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    /**
     * 打印10次
     */
    public void print10(int total){
        lock.lock();
        try {
            //判断
            while (num!=2){
                cd1.await();
            }
            //干活
            for (int i1 = 0; i1 < 10; i1++) {
                System.out.println(Thread.currentThread().getName()+"打印"+":"+total+"\t"+i1);
            }
            num=3;
            //通知
            cd1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    /**
     * 打印10次
     */
    public void print15(int total){
        lock.lock();
        try {
            //判断
            while (num!=3){
                cd1.await();
            }
            //干活
            for (int i1 = 0; i1 < 15; i1++) {
                System.out.println(Thread.currentThread().getName()+"打印"+":"+total+"\t"+i1);
            }
            num=1;
            //通知
            cd1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}
/**
 *    多线程之间按顺序调用,实现A->B->C
 *      3个线程启动,要求如下:
 *      AA打印5次,BB打印10次,CC打印15次
 *    接着
 *      AA打印5次,BB打印10次,CC打印15次
 * ...来10轮
 */
public class ConditionDemo {
    public static void main(String[] args) {
      ShareData shareData = new ShareData();
          new Thread(()->{
              for (int i = 0; i < 10; i++) {
                  shareData.print5(i);
              }
           },"AA").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                shareData.print10(i);
            }
        },"BB").start();
        new Thread(()->{
            for (int i = 0; i < 15; i++) {
                shareData.print15(i);
            }
        },"CC").start();

    }
}

5.CountDownLatch

countdownlatch 是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完毕再执行。从命名可以解读到 countdown 是倒数的意思,类似于我们倒计时的概念。countdownlatch 提供了两个方法,一个是 countDown,一个是 await, countdownlatch 初始化的时候需要传入一个整数,在这个整数倒数到 0 之前,调用了 await 方法的程序都必须要等待,然后通过 countDown 来倒数。

使用案例 :

/**
 * CountDownLatch(闭锁):在完成某些运算时,只有其他线程全部完成,当前线程才进行执行
 *
 *
 */
public class TestCountDownLatch {
    public static void main(String[] args) {
        CountDownLatch countDownLatch = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"执行任务");
                countDownLatch.countDown();
            },String.valueOf(i)).start();
        }
        //等待子线程执行结束
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("子线程执行完毕,主线程开始执行");

    }
}

从代码的实现来看,有点类似 join 的功能,但是比 join 更加灵活。 CountDownLatch 构造函数会接收一个 int 类型的参数作为计数器的初始值,当调用 CountDownLatch 的countDown 方法时,这个计数器就会减一。
通过 await 方法去阻塞去阻塞主流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u5nhgDZ8-1602212222879)(JUCImage/CountDownLatch1.png)]

模拟高并发

public class TestCountDownLatch {
    public static void main(String[] args) {
        CountDownLatch countDownLatch = new CountDownLatch(10);
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"执行任务");
                countDownLatch.countDown();
            },String.valueOf(i)).start();
        }
        //等待子线程执行结束
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("子线程执行完毕,主线程开始执行");

    }
}

总的来说,凡事涉及到需要指定某个人物在执行之前,要等到前置人物执行完毕之后才执行的场景,都可以使用
CountDownLatch

CountDownLatch 源码分析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H2ODfj5N-1602212222880)(JUCImage/CountDownLatch3.png)]

对于 CountDownLatch,我们仅仅需要关心两个方法,一个是 countDown() 方法,另一个是 await() 方法。countDown() 方法每次调用都会将 state 减 1,直到state 的值为 0;而 await 是一个阻塞方法,当 state 减
为 0 的时候, await 方法才会返回。 await 可以被多个线程调用, 大家在这个时候脑子里要有个图:所有调用了
await 方法的线程阻塞在 AQS 的阻塞队列中,等待条件满足(state == 0),将线程从队列中一个个唤醒过来。

acquireSharedInterruptibly

countdownlatch 也用到了 AQS,在 CountDownLatch 内部写了一个 Sync 并且继承了 AQS 这个抽象类重写了 AQS中的共享锁方法。首先看到下面这个代码,这块代码主要是 判 断 当 前 线 程 是 否 获 取 到 了 共 享 锁 ; ( 在
CountDownLatch 中 , 使 用 的 是 共 享 锁 机 制 , 因 为CountDownLatch 并不需要实现互斥的特性)

  public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)//state 如果不等于 0,说明当前线程需要加入到共享锁队列中
            doAcquireSharedInterruptibly(arg);
    }

doAcquireSharedInterruptibly

  1. addWaiter 设置为 shared 模式。
  2. tryAcquire 和 tryAcquireShared 的返回值不同,因此会 多出一个判断过程
  3. 在 判 断 前 驱 节 点 是 头 节 点 后 , 调 用 了setHeadAndPropagate 方法,而不是简单的更新一下头
    节点。
 private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.SHARED);
     //创建一个共享模式的节点添加到队列中
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    //就判断尝试获取锁
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {//r>=0 表示获取到了执行权限,这个时候因为 state!=0,所以不会执行这段代码
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                //阻塞线程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

**图解分析
**加入这个时候有 3 个线程调用了 await 方法, 由于这个时候 state 的值还不为 0,所以这三个线程都会加入到 AQS队列中。并且三个线程都处于阻塞状态

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OmPqvMcx-1602212222881)(JUCImage/countDownLatch6.png)]

CountDownLatch.countDown

由于线程被 await 方法阻塞了,所以只有等到countdown 方法使得 state=0 的时候才会被唤醒,我们
来看看 countdown 做了什么

  1. 只有当 state 减为 0 的时候, tryReleaseShared 才返回 true, 否则只是简单的 state = state - 1
  2. 如果 state=0, 则调用 doReleaseShared唤醒处于 await 状态下的线程
 public void countDown() {
        sync.releaseShared(1);
    }

用自旋的方法实现 state 减 1

   public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

AQS. doReleaseShared

共享锁的释放和独占锁的释放有一定的差别前面唤醒锁的逻辑和独占锁是一样,先判断头结点是不是SIGNAL 状态,如果是,则修改为 0,并且唤醒头结点的下一个节点

   
//PROPAGATE: 标识为 PROPAGATE 状态的节点,是共享锁模式下的节点状态,处于这个状态下的节点,会对线程的唤醒进行传播
private void doReleaseShared() {
 
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;          
                    unparkSuccessor(h);
                }
                // 这个 CAS 失败的场景是:执行到这里的时候,刚好有一个节点入队,入队会将这个 ws 设置为 -1
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;              
            }
            // 如果到这里的时候,前面唤醒的线程已经占领了 head,那么再循环
// 通过检查头节点是否改变了,如果改变了就继续循环
            if (h == head)                  
                break;
        }
    }

h == head:说明头节点还没有被刚刚用unparkSuccessor 唤醒的线程(这里可以理解为ThreadB)占有,此时 break 退出循环。h != head:头节点被刚刚唤醒的线程(这里可以理解为ThreadB)占有,那么这里重新进入下一轮循环,唤醒下一个节点(这里是 ThreadB )。我们知道,等到ThreadB 被唤醒后,其实是会主动唤醒ThreadC

doAcquireSharedInterruptibly

一旦 ThreadA 被唤醒,代码又会继续回到doAcquireSharedInterruptibly 中来执行。 如果当前 state
满足=0 的条件,则会执行 setHeadAndPropagate 方法

   private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {//被唤醒的线程进入下一次循环继续判断
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; //把当前节点移除 aqs 队列
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

setHeadAndPropagate

这个方法的主要作用是把被唤醒的节点,设置成 head 节点。 然后继续唤醒队列中的其他线程。由于现在队列中有 3 个线程处于阻塞状态,一旦 ThreadA被唤醒,并且设置为 head 之后,会继续唤醒后续的ThreadB

private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);

        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

图解分析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0NNbcEUO-1602212222883)(JUCImage/countdownLatch4.png)]

6 .Semaphore

semaphore 也就是我们常说的信号灯,semaphore 可以控制同时访问的线程个数,通过 acquire 获取一个许可,如果没有就等待,通过 release 释放一个许可。有点类似限流的作用。叫信号灯的原因也和他的用处有关,比如某商场就 5 个停车位,每个停车位只能停一辆车,如果这个时候来了 10 辆车,必须要等前面有空的车位才能进入。

使用案例:

/**
 * Semaphore:semaphore 也就是我们常说的信号灯,semaphore 可以控
 * 制同时访问的线程个数,通过 acquire 获取一个许可,如
 * 果没有就等待,通过 release 释放一个许可。有点类似限流
 * 的作用。叫信号灯的原因也和他的用处有关,比如某商场
 * 就 5 个停车位,每个停车位只能停一辆车,如果这个时候
 * 来了 10 辆车,必须要等前面有空的车位才能进入。
 */
public class SemaphoreDemo {
    public static void main(String[] args) {
        //模拟有5个车位
        Semaphore semaphore = new Semaphore(5);
        for (int i = 0; i <10 ; i++) {//来了10辆车
            new Thread(()->{
                try {
                    semaphore.acquire();//获取信号量
                    System.out.println(Thread.currentThread().getName()+"\t"+"占有车位");
                    TimeUnit.SECONDS.sleep(3);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"停了3s钟\t"+"离开车位");
                semaphore.release();//释放信号量
            },String.valueOf(i)).start();
        }
    }
}

**使用场景
**Semaphore 比较常见的就是用来做限流操作了。

Semaphore 源码分析

从 Semaphore 的功能来看,我们基本能猜测到它的底层实现一定是基于 AQS 的共享所,因为需要实现多个线程共享一个领排池 创建 Semaphore 实例的时候,需要一个参数 permits,这个基本上可以确定是设置给 AQS 的 state 的,然后每个线程调用 acquire 的时候,执行 state = state - 1, release 的时候执行 state = state + 1,当然, acquire 的时候,如果 state = 0,说明没有资源了,需要等待其他线程 release。

Semaphore 分公平策略和非公平策略

FairSync

   static final class FairSync extends Sync {
        private static final long serialVersionUID = 2014338818796000944L;

        FairSync(int permits) {
            super(permits);
        }

        protected int tryAcquireShared(int acquires) {
            for (;;) {
                // 区别就在于是不是会先判断是否有线程在排队,然后才进行 CAS 减操作
                if (hasQueuedPredecessors())
                    return -1;
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }
    }

NofairSync

通过对比发现公平和非公平的区别就在于是否多了一个hasQueuedPredecessors 的判断

static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -2694183684443567898L;

        NonfairSync(int permits) {
            super(permits);
        }

        protected int tryAcquireShared(int acquires) {
            return nonfairTryAcquireShared(acquires);
        }
    }

基于共享锁的实现

7 CyclicBarrier

CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续工作。CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用 await 方法告诉 CyclicBarrier 当前线程已经到达了屏障,然后当前 线程被阻塞
使用场景
当存在需要所有的子任务都完成时,才执行主任务,这个时候就可以选择使用 CyclicBarrier

public class CyclicBarrierDemo {
    public static void main(String[] args) throws Exception{
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
            System.out.println("集齐七科龙珠完毕");
        });
        for (int i = 0; i < 7; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"\t"+"正在搜集龙珠");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}

注意点
1)对于指定计数值 parties,若由于某种原因,没有足够的线程调用 CyclicBarrier 的 await,则所有调用 await 的线程都会被阻塞;
2)同样的 CyclicBarrier 也可以调用 await(timeout, unit),设置超时时间,在设定时间内,如果没有足够线程到达,则解除阻塞状态,继续工作;
3)通过 reset 重置计数,会使得进入 await 的线程出现BrokenBarrierException;
4 ) 如 果 采 用 是 CyclicBarrier(int parties, RunnablebarrierAction) 构造方法,执行 barrierAction 操作的是最
后一个到达的线程
实现原理
CyclicBarrier 相比 CountDownLatch 来说,要简单很多,源码实现是基于 ReentrantLock 和 Condition 的组合使用。看如下示意图, CyclicBarrier 和 CountDownLatch 是不是很像,只是 CyclicBarrier 可以有不止一个栅栏,因为它的栅栏(Barrier)可以重复使用(Cyclic)

7.ConcurrentHashMap

7.1 ConcurrentHashMap 的初步使用及场景

ConcurrentHashMap 是 J.U.C 包里面提供的一个线程安全并且高效的 HashMap,所以ConcurrentHashMap 在并发编程的场景中使用的频率比较高, ConcurrentHashMap 是 Map 的派生类,所以 api 基本和 Hashmap 是类似,主要就是 put、get 这些方法,接下来基于 ConcurrentHashMap 的 put 和 get 这两个方法作为切入点来分
析 ConcurrentHashMap 的源码实现

7.2ConcurrentHashMap的源码分析

7.2.1 JDK1.7 和 Jdk1.8 版本的变化

ConcurrentHashMap 和 HashMap 的实现原理是差不多的,但是因为 ConcurrentHashMap需要支持并发操作,所以在实现上要比 hashmap 稍微复杂一些。在 JDK1.7 的 实 现 上 , ConrruentHashMap 由 一 个 个 Segment 组 成 , 简 单 来 说 ,ConcurrentHashMap 是一个 Segment 数组,它通过继承 ReentrantLock 来进行加锁,通过每次锁住一个 segment 来保证每个 segment 内的操作的线程安全性从而实现全局线程安全。
整个结构图如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kgBdahk0-1602212222884)(JUCImage/ConcurrentHashMap.png)]

当每个操作分布在不同的 segment 上的时候,默认情况下,理论上可以同时支持 16 个线程的并发写入。

相比于 1.7 版本,它做了两个改进

  1. 取消了 segment 分段设计,直接使用 Node 数组来保存数据,并且采用 Node 数组元素作
    为锁来实现每一行数据进行加锁来进一步减少并发冲突的概率

  2. 将原本数组+单向链表的数据结构变更为了数组+单向链表+红黑树的结构。为什么要引入红黑树呢?在正常情况下, key hash 之后如果能够很均匀的分散在数组中,那么 table 数组中的每个队列的长度主要为 0 或者 1.但是实际情况下,还是会存在一些队列长度过长的情况。如果还采用单向列表方式,那么查询某个节点的时间复杂度就变为 O(n); 因此对于队列长度超过 8 的列表, JDK1.8 采用了红黑树的结构,那么查询的时间复杂度就会降低到O(logN),可以提升查找的性能;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-39OJwX4F-1602212222886)(JUCImage/concurrentHashMap2.png)]

这个结构和 JDK1.8 版本中的 Hashmap 的实现结构基本一致,但是为了保证线程安全性,ConcurrentHashMap 的实现会稍微复杂一下。

7.2.2 put 方法第一阶段

 public V put(K key, V value) {
        return putVal(key, value, false);
    }
 final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());//计算 hash 值
        int binCount = 0;//用来记录链表的长度
        for (Node<K,V>[] tab = table;;) {//这里其实就是自旋操作,当出现线程竞争时不断自旋
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)//如果数组为空,则进行数组初始化
                tab = initTable//初始化数组
                //通过 hash 值对应的数组下标得到第一个节点; 以 volatile 读的方式来读取 table 数组中的元素,保证每次拿到的数据都是最新的
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                //如果该下标返回的节点为空,则直接通过 cas 将新的值封装成 node 插入即可;如果 cas 失败,说明存在竞争,则进入下一次循环
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                  
            }
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

假如在上面这段代码中存在两个线程, 在不加锁的情况下:线程 A 成功执行 casTabAt 操作
后,随后的线程 B 可以通过 tabAt 方法立刻看到 table[i]的改变。原因如下:线程 A 的casTabAt 操作,具有 volatile 读写相同的内存语义,根据 volatile 的 happens-before 规则:线程 A 的 casTabAt 操作,一定对线程 B 的 tabAt 操作可见

initTable

```java

private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings(“unchecked”)
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
```

数组初始化方法,这个方法比较简单,就是初始化一个合适大小的数组sizeCtl 这个要单独说一下,如果没搞懂这个属性的意义,可能会被搞晕这个标志是在 Node 数组初始化或者扩容的时候的一个控制位标识,负数代表正在进行初始化或者扩容操作-1 代表正在初始化
-N 代表有 N-1 有二个线程正在进行扩容操作,这里不是简单的理解成 n 个线程, sizeCtl 就
是-N,这块后续在讲扩容的时候会说明
0 标识 Node 数组还没有被初始化,正数代表初始化或者下一次扩容的大小

8.ReentrantReadWriteLock

读写锁维护了一对锁,一个读锁、一个写锁;一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量.

class MyChae{
    private volatile Map<String,String> map = new HashMap<String,String>();
    private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();

    /**
     * 写入数据
     * @param key
     * @param value
     */
    public void set(String key,String value){
        Lock writeLock = rw.writeLock();
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"\t"+"开始写入数据");
            map.put(key,value);
            TimeUnit.SECONDS.sleep(1);
            System.out.println(Thread.currentThread().getName()+"\t"+"写入数据完成");
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            writeLock.unlock();
        }
    }

    /**
     * 读取数据
     * @param key
     */
    public void get(String key){
        ReentrantReadWriteLock.ReadLock readLock = rw.readLock();
        readLock.lock();
        try {
            String  value = map.get(key);
            System.out.println(Thread.currentThread().getName()+"\t"+"读取到的数据"+value);
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            readLock.unlock();
        }
    }
}
public class ReetrantReadWriterLockDemo {
    public static void main(String[] args) {
        MyChae myChae = new MyChae();
        for (int i = 0; i < 5; i++) {
            int finalI = i;
            new Thread(()->{
                myChae.set(String.valueOf(finalI), UUID.randomUUID().toString());
            },String.valueOf(i)).start();
        }
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 0; i < 5; i++) {
            int finalI = i;
            new Thread(()->{
                myChae.get(String.valueOf(finalI));
            },String.valueOf(i)).start();
        }
    }
}

在这个案例中,通过 hashmap 来模拟了一个内存缓存,然后使用读写所来保证这个内存缓存的线程安全性。当执行读操作的时候,需要获取读锁,在并发访问的时候,读锁不会被阻塞,因为读操作不会影响执行结果。在执行写操作是,线程必须要获取写锁,当已经有线程持有写锁的情况下,当前线程会被阻塞,只有当写锁释放以后,其他读写操作才能继续执行。 使用读写锁提升读操作的并发性,也保证每次写操作对所有的读写操作的可见性
⚫ 读锁与读锁可以共享
⚫ 读锁与写锁不可以共享(排他)
⚫ 写锁与写锁不可以共享(排他)

9.阻塞队列

10.线程池

10.1线程池的定义

在 Java 中,如果每个请求到达就创建一个新线程,创建和销毁线程花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。如果在一个 Jvm 里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足为了解决这个问题,就有了线程池的概念,线程池的核心逻辑是提前创建好若干个线程放在一个容器中。如果有任务需要处理,则将任务直接分配给线程池中的线程来执行就行,任务处理完以后这个线程不会被销毁,而是等待后续分配任务。同时通过线程池来重复管理线程还可以避免创建大量线程增加开销。

10.2为什么使用线程池

11.

ock();

/**
 * 写入数据
 * @param key
 * @param value
 */
public void set(String key,String value){
    Lock writeLock = rw.writeLock();
    writeLock.lock();
    try {
        System.out.println(Thread.currentThread().getName()+"\t"+"开始写入数据");
        map.put(key,value);
        TimeUnit.SECONDS.sleep(1);
        System.out.println(Thread.currentThread().getName()+"\t"+"写入数据完成");
    } catch (Exception e) {
        e.printStackTrace();
    }finally {
        writeLock.unlock();
    }
}

/**
 * 读取数据
 * @param key
 */
public void get(String key){
    ReentrantReadWriteLock.ReadLock readLock = rw.readLock();
    readLock.lock();
    try {
        String  value = map.get(key);
        System.out.println(Thread.currentThread().getName()+"\t"+"读取到的数据"+value);
    } catch (Exception e) {
        e.printStackTrace();
    }finally {
        readLock.unlock();
    }
}

}
public class ReetrantReadWriterLockDemo {
public static void main(String[] args) {
MyChae myChae = new MyChae();
for (int i = 0; i < 5; i++) {
int finalI = i;
new Thread(()->{
myChae.set(String.valueOf(finalI), UUID.randomUUID().toString());
},String.valueOf(i)).start();
}
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 5; i++) {
int finalI = i;
new Thread(()->{
myChae.get(String.valueOf(finalI));
},String.valueOf(i)).start();
}
}
}


  在这个案例中,通过 hashmap 来模拟了一个内存缓存,然后使用读写所来保证这个内存缓存的线程安全性。当执行读操作的时候,需要获取读锁,在并发访问的时候,读锁不会被阻塞,因为读操作不会影响执行结果。在执行写操作是,线程必须要获取写锁,当已经有线程持有写锁的情况下,当前线程会被阻塞,只有当写锁释放以后,其他读写操作才能继续执行。 使用读写锁提升读操作的并发性,也保证每次写操作对所有的读写操作的可见性
⚫ 读锁与读锁可以共享
⚫ 读锁与写锁不可以共享(排他)
⚫ 写锁与写锁不可以共享(排他)  

# 9.阻塞队列

# 10.线程池

## 10.1线程池的定义

在 Java 中,如果每个请求到达就创建一个新线程,创建和销毁线程花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。如果在一个 Jvm 里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足为了解决这个问题,就有了线程池的概念,线程池的核心逻辑是提前创建好若干个线程放在一个容器中。如果有任务需要处理,则将任务直接分配给线程池中的线程来执行就行,任务处理完以后这个线程不会被销毁,而是等待后续分配任务。同时通过线程池来重复管理线程还可以避免创建大量线程增加开销。

## 10.2为什么使用线程池

# 11.





































































  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值