爬梯:JUC并发编程(三)

学习资源整理自:B站《狂神说》
书接上回

JUC并发编程

12、CompletableFuture 异步回调

理解

父类:Future,对将来的某个事件的结果进行建模

可以用ajax进行理解。

从1.8开始

场景:需要阻塞等待的任务,使用异步,可以让后面的任务继续进行,不出现阻塞,提高系统性能。

在这里插入图片描述

官方文档

java.util.concurrent 
Interface Future<V>

参数类型 
V - 未来的 get方法返回的结果类型 
All Known Subinterfaces: 
Response <T>, RunnableFuture <V>, RunnableScheduledFuture <V>, ScheduledFuture < 
所有已知实现类: 
CompletableFuture , CountedCompleter , ForkJoinTask , FutureTask , RecursiveAction , RecursiveTask , SwingWorker 

代码实现

runAsync()

public static void main(String args[]) throws ExecutionException, InterruptedException {
    
    
    //执行,不返回,没有返回结果
    CompletableFuture<Void> runAsync = CompletableFuture.runAsync(()->{
    
    
        try {
    
    
            TimeUnit.SECONDS.sleep(2);
            System.out.println(Thread.currentThread().getName()+"==> runAsync");
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    });
    System.out.println("111");
    System.out.println(runAsync.get());//返回: null
    System.out.println("222");
}

console

111
ForkJoinPool.commonPool-worker-1==> runAsync
null
222

supplyAsync()

public static void main(String args[]) throws ExecutionException, InterruptedException {
    
    
    // 使用supplyAsync异步,有返回值,供给型函数式接口
    CompletableFuture<String> supplyAsync = CompletableFuture.supplyAsync(()->{
    
    
        try {
    
    
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() +"==> supplyAsync");
        //int i = 1/0; //产生异常
        return "石似心";
    });

    String result = supplyAsync.whenComplete((s, t) -> {
    
     //当执行结束时
        System.out.println("爬梯结束 s=> " + s);// 正常执行完s有结果,异常s为null
        System.out.println("爬梯结束 t=> " + t);// 正常执行完t为null,异常t为Throwable
    }).exceptionally(t -> {
    
     //当执行异常时(onError)
        t.printStackTrace();
        System.out.println(t.getMessage());
        return "再接再厉";
    }).get();

    System.out.println("最终结果==> "+result);
}

console

ForkJoinPool.commonPool-worker-1==> supplyAsync
爬梯结束 s=> 石似心
爬梯结束 t=> null
最终结果==> 石似心

13、JMM

java memory model java内存模型,是一个不存在的东西,是一种概念,约定。

关于JMM的同步约定:

1、线程解锁前,必须把共享变量立即刷回主内;

2、线程加锁前,必须读取主存中的最新值到工作内存中;

3、加锁和解锁是通一把锁

图解

在这里插入图片描述

引用网络资料

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

  • lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态

  • unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定

  • read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用

  • load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中

  • use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令

  • assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中

  • store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用

  • write  (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中

JMM对这八种指令的使用,制定了如下规则:

  • 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write

  • 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存

  • 不允许一个线程将没有assign的数据从工作内存同步回主内存

  • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作

  • 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁

  • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值

  • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量

  • 对一个变量进行unlock操作之前,必须把此变量同步回主内存

代码实测

private static boolean flag=true;
public static void main(String args[]) throws InterruptedException {
    
    
    new Thread(() -> {
    
    
        while (flag){
    
    
            //无限循环
        }
    }).start();
    // 一秒后 主线程修改了flag
    TimeUnit.SECONDS.sleep(1);
    flag = false;
    System.out.println(flag);//打印false
    /**
     * 程序一直运行
     */
}

引发问题:

​ 当线程A操作过的flag没有写入到主内存中时,线程B将flag更改写入了主内存,此时线程A不知道主线程中的flag已经变更。将会造成线程A的数据紊乱、丢失问题!

14、Volatile

Volatile 是java虚拟机提供的轻量级同步机制

volatile有三个特性!!!

1、保证可见性

2、不保证原子性

3、禁止指令重排

验证 保证可见性

代码实测

//添加volatile关键字
private volatile static boolean flag=true;
public static void main(String args[]) throws InterruptedException {
    
    
    new Thread(() -> {
    
    
        while (flag){
    
    
            //无限循环
        }
    }).start();
    // 一秒后 主线程修改了flag
    TimeUnit.SECONDS.sleep(1);
    flag = false;
    System.out.println(flag);//打印false
    /**
     * 程序结束
     */
}

验证 不保证原子性

原子性:不可分割

/**
 * 验证volatile的非原子性
 * @author: stone
 * @create: 2020-08-25  
 */
public class Test03 {
    
    
    //定义简单变量
    private volatile static int num = 0;
    //实现加一方法
    private static void incr(){
    
    
        num++;
    }
    public static void main(String[] args) {
    
    
        //多线程情况下,共同修改 num ,查看最终结果是否正常
        for (int i = 0; i < 10; i++) {
    
    
            //创建10个线程去跑
            new Thread(() -> {
    
    
                //多次修改num值
                for (int j = 0; j < 1000; j++) {
    
    
                    incr();
                }
            }).start();
        }
        //10个线程跑完后,输出num
        while (Thread.activeCount()>2){
    
    
            Thread.yield();
        }
        System.out.println(num); // 结果 并不是 10*1000
    }
}

由此得:volatile并不保证修饰对象的原子性!

引出一个非Synchronized、非Lock的高效方法保证原子性

使用原子类 java.util.concurrent.atomic.AtomicInteger

private static AtomicInteger num = new AtomicInteger(0);

这些类的底层都直接和操作系统挂钩,都是native方法,在内存中修改值,Unsaft类是一个很特殊的存在。

验证 禁止指令重排

什么是指令重排

程序代码,在计算机中并不一定是按照代码的顺序去执行的。

源代码 --> 编译器优化重排 --> 指令并行可能会重排 --> 内存系统也会重排 --> 执行

举例:

1int a = 1;
2int b = 2;
3:a = a + 3;
4:b = a + 4;

在以上代码中,正常的重排是不会影响代码逻辑的,如:第一行和第二行重排对执行结果是没有影响的;

但!当两条以上的线程操作共享数据时:每个线程内的重排就可能会造成不同的结果:

定义:a、b、c、d四个变量都为 0

线程1 线程2
a=b c=d
d=2 b=1

若线程1中的c=2重排为第一个执行,则会对线程2的结算结果产生影响:a=1,c=2

volatile禁止指令重排的原理

这里有两个计算机专业的概念:内存屏障、CPU指令

当volatile修饰的代码,会在前后加入一层内存屏障,保证执行顺序是不可更改的。

在这里插入图片描述

15、玩转单例模式

首先展示三种常规的单例操作

饿汉式

/**
 * 单例模式 饿汉模式
 * 可能会造成内存浪费
 * @author: stone
 * @create: 2020-08-25 17:59
 */
public class Hungry {
    
    
    private Hungry(){
    
    }
    //单一不可修改
    private final static Hungry hungry = new Hungry();
    public Hungry getInstance(){
    
    
        return hungry;
    }
}

懒汉式

/**
 * 单例模式 懒汉式
 * @author: stone
 * @create: 2020-08-25 18:32
 */
public class LazyMan {
    
    

    private LazyMan(){
    
    
        System.out.println(Thread.currentThread().getName());
    }

    private static LazyMan lazyMan;

    /**
     * 创建对象,在底层为三行代码
     * 1、开辟内存空间
     * 2、执行构造方法,初始化对象
     * 3、对象指向内存空间
     * 故此可能发生指令重排  132
     * 而第二条线程则误以为lazyMan!=null,将会导致错误
     * 所以synchronized保证原子性
     */
    public static LazyMan getInstance(){
    
    
        // DCL懒汉模式  双重检查锁  double check lock。。。
        if(lazyMan==null){
    
    
            synchronized (LazyMan.class){
    
    
                if (lazyMan==null){
    
    
                    lazyMan = new LazyMan();

                }
            }
        }
        return lazyMan;
    }
    public static void main(String args[]){
    
    
        for (int i = 0; i < 10; i++) {
    
    
            new Thread(() -> {
    
    
                LazyMan.getInstance();
            }).start();
        }
    }
}

静态内部类

/**
 * 静态内部类实现单例模式
 * @author: stone
 * @create: 2020-08-25 23:45
 */
public class Holder {
    
    
    private Holder(){
    
    }

    private static class InnerClass{
    
    
        //在内部类创建对象
        private static Holder holder = new Holder();
    }
    public Holder getInstance(){
    
    
        return InnerClass.holder;
    }
}

反射与单例的斗争

懒汉类:ReflectSingle.class

1、破坏DCL懒汉式单例

public static void main(String args[]) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    
    
    //正常获取一个懒汉单例
    ReflectSingle lazyMan = ReflectSingle.getInstance();

    //获取单例的无参构造器
    Constructor<ReflectSingle> declaredConstructor = ReflectSingle.class.getDeclaredConstructor();
    //开启 访问private权限
    declaredConstructor.setAccessible(true);
    ReflectSingle lazyMan2 = declaredConstructor.newInstance();
}

2、破坏三重锁的懒汉式单例

懒汉类升级了构造器,判断私有字段lazyMan,实现三重验证

private ReflectSingle(){
    
    
    synchronized(ReflectSingle.class){
    
    
        if(lazyMan!=null){
    
    
            throw new RuntimeException("禁止重新创建此类");
        }
    }
}

破坏思路:不是用getInstance方法创建第一个对象,保持lazyMan一直为空

public static void main(String args[]) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    
    
    //正常获取一个懒汉单例
    //ReflectSingle lazyMan = ReflectSingle.getInstance();

    //获取单例的无参构造器
    Constructor<ReflectSingle> declaredConstructor = ReflectSingle.class.getDeclaredConstructor();
    //开启 访问private权限
    declaredConstructor.setAccessible(true);
    ReflectSingle lazyMan2 = declaredConstructor.newInstance();
    ReflectSingle lazyMan3 = declaredConstructor.newInstance();
}

3、破坏带有flag的构造器

懒汉升级:使用flag标示构造方法只能走一次

private ReflectSingle(){
    
    
    synchronized(ReflectSingle.class){
    
    
        if(!prFlag_ssx){
    
    
            prFlag_ssx = true;
        }else{
    
    
            throw new RuntimeException("禁止重新创建此类");
        }
    }
}
private static boolean prFlag_ssx = false;

这时只有第一条走完构造器的代码可以获取到对象。

反射破坏:得知标识字段名后,可以强行获取,破坏私有权限,直接修改值

public static void main(String args[]) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
    
    
    //正常获取一个懒汉单例
    //ReflectSingle lazyMan = ReflectSingle.getInstance();

    //知道flag字段名后,可以强行获取,破坏私有权限,直接修改值
    Field flagField = ReflectSingle.class.getDeclaredField("prFlag_ssx");
    flagField.setAccessible(true);

    //获取单例的无参构造器
    Constructor<ReflectSingle> declaredConstructor = ReflectSingle.class.getDeclaredConstructor();
    //开启 访问private权限
    declaredConstructor.setAccessible(true);
    ReflectSingle lazyMan2 = declaredConstructor.newInstance();
    //修改标示,让构造器可以再走一次
    flagField.set(declaredConstructor,false);
    ReflectSingle lazyMan3 = declaredConstructor.newInstance();
}

枚举登场

枚举在java里就是一个类,其底层也是继承了一个 Enum的类

使用工具反编译枚举类

jad -sjava EnumSingle.class
package com.ssx.single;


public final class EnumSingle extends Enum
{
    
    

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

    public static EnumSingle valueOf(String name)
    {
    
    
        return (EnumSingle)Enum.valueOf(com/ssx/single/EnumSingle, name);
    }
	//得知枚举中其实有一个 使用两个参数的构造方法
    private EnumSingle(String s, int i)
    {
    
    
        super(s, i);
    }

    public EnumSingle getInstance()
    {
    
    
        return INSTANCE;
    }

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

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

反射尝试破解

/**
 * 枚举本身就是一个单例模式
 * 其底层有写禁止反射创建对象的代码
 * @author: stone
 * @create: 2020-08-26 00:32
 */
public enum EnumSingle {
    
    

    INSTANCE;
    public EnumSingle getInstance(){
    
    
        return INSTANCE;
    }
}
class Test{
    
    
    public static void main(String args[]) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    
    
        //尝试使用反射破解枚举单例模式
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
        declaredConstructor.setAccessible(true);
        EnumSingle enumSingle = declaredConstructor.newInstance();
    }
}

结果

Cannot reflectively create enum objects

newInstrance() 源码

在这里插入图片描述

java在反射的底层禁止了对enum的反射创建

16、Atomic,深入理解CAS(底层进阶)

什么是CAS

比较并交换

unsafe:java为C++留的后门,里面全是native方法

由于java无法操作内存,所以java需要调用C++操作内存

在这里插入图片描述

java层面的cas

public static void main(String args[]){
    
    
    AtomicInteger atomicInteger = new AtomicInteger(2020);
    /**
     * 比较与设置
     * public final boolean compareAndSet(int expect, int update)
     * 比较期望值,如果==则设置为更新值,如果不相等,则返回false
     */
    atomicInteger.compareAndSet(2020,2021);
    System.out.println(atomicInteger.get());
}

底层的CAS

比较当前工作内存中的值和主内存中的值(保证原子性),如果这个值是期望的,那么则执行操作!如果不是则一直循环。

里面有个do while自旋锁
在这里插入图片描述

个人理解:计算之前,先获取内存中的值,然后比较,然后计算。原理上有点像乐观锁。

缺点:

1、循环耗时

2、一次性只能保证一个共享变量

3、造成ABA问题

ABA问题

public static void main(String args[]){
    
    
    //共享变量
    AtomicInteger atomicInteger = new AtomicInteger(2020);

    //线程2,意料之外的线程操作
    atomicInteger.compareAndSet(2020,2021);
    System.out.println(atomicInteger.get());
    atomicInteger.compareAndSet(2021,2020);
    System.out.println(atomicInteger.get());

    //线程1,正常业务线程
    //这个时候,其实已经不是原来的那个2020了,被替换过
    atomicInteger.compareAndSet(2020,2021);
    System.out.println(atomicInteger.get());
}

原子引用解决ABA问题

java.util.concurrent.atomic.AtomicStampedReference

引用乐观锁的原理,使用版本号

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

/**
 * 原子引用解决ABA问题
 * @author: stone
 * @create: 2020-08-26 13:09
 */
public class TestABA2 {
    
    

    public static void main(String args[]){
    
    
        //创建原子引用对象,赋予初始值和初始版本号
        //源码:public AtomicStampedReference(V initialRef, int initialStamp)
        AtomicStampedReference<Integer> atomic = new AtomicStampedReference<>(1,1);

        //使用两个线程模拟ABA现象
        //线程1
        new Thread(() -> {
    
    
            Integer stamp = atomic.getStamp();//获取版本号

            try {
    
    
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }

            // 修改值 期望是1 则改为2,期望版本号stamp,新的版本号+1
            System.out.println(atomic.compareAndSet(1, 2, stamp, stamp + 1));
            System.out.println("线程1= stamp ==》"+atomic.getStamp());
            // 修改值 期望是2 则改为1,期望版本号stamp,新的版本号+1
            System.out.println(atomic.compareAndSet(2, 1, atomic.getStamp(), stamp + 1));
            System.out.println("线程1= stamp ==》"+atomic.getStamp());
        }).start();

        //线程2
        new Thread(() -> {
    
    
            Integer stamp = atomic.getStamp();//获取版本号

            try {
    
    
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }

            // 修改值 期望是1 则改为2,期望版本号stamp,新的版本号+1
            // 由于最初获取的版本号已经被修改,所以这里是set失败的
            System.out.println(atomic.compareAndSet(1, 3, stamp, stamp + 1));
            System.out.println("线程2= stamp ==》"+atomic.getStamp());
        }).start();

        /**
         * 最终结果:
         * true
         * 线程1= stamp ==》2
         * true
         * 线程1= stamp ==》2
         * false
         * 线程2= stamp ==》2
         */
    }
}

题外话,Integer的一个坑

在这里插入图片描述

17、各种锁

公平锁、非公平锁

公平锁:先来后到,排队逐个获得锁、施放锁,不可插队

非公平锁:可以插队 java.util.concurrent.locks.ReentrantLock

可重入锁

(递归锁)

锁中包含的锁,会一同获取,需要整个大锁结束,才会施放。

自旋锁

手写自旋锁

/**
 * 手写自旋锁
 * @author: stone
 * @create: 2020-08-26 13:54
 */
public class SpinLock {
    
    
    //使用原子引用
    //无参构造器,初始值为null
    AtomicReference<Thread> atomic = new AtomicReference<>();

    /**
     * 加锁方法
     */
    public void lock(){
    
    
        //如果当前原子引用不是null,则无法退出循环,形成自旋
        while (!atomic.compareAndSet(null,Thread.currentThread())){
    
    
        }
        System.out.println(Thread.currentThread().getName()+" -- 获得 了锁 --");
    }
    /**
     * 解锁方法
     */
    public void unLock(){
    
    
        Thread thread = Thread.currentThread();
        atomic.compareAndSet(thread,null);
        System.out.println(thread.getName()+" -- 释放 了锁 --");
    }
}

class Test{
    
    
    public static void main(String args[]) throws InterruptedException {
    
    
        SpinLock spinLock = new SpinLock();

        new Thread(()->{
    
    
            try {
    
    
                spinLock.lock();//获得锁
                TimeUnit.SECONDS.sleep(2);
            } catch (Exception e) {
    
    
                e.printStackTrace();
            } finally {
    
    
                spinLock.unLock();//施放锁
            }
        },"石似心").start();

        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{
    
    
            try {
    
    
                spinLock.lock();//获得锁
                TimeUnit.SECONDS.sleep(1);
            } catch (Exception e) {
    
    
                e.printStackTrace();
            } finally {
    
    
                spinLock.unLock();//施放锁
            }
        },"周杰伦").start();
    }
}

死锁的排查(面试加分)

场景模拟

/**
 * 模拟死锁场景
 * 交叉锁
 * @author: stone
 * @create: 2020-08-26 14:17
 */
public class DeadLock {
    
    
    public static void main(String args[]){
    
    
        //资源锁
        Lock lockA = new ReentrantLock();
        Lock lockB = new ReentrantLock();

        new Thread(new MyThread(lockA,lockB),"石似心").start();
        new Thread(new MyThread(lockB,lockA),"悟空").start();
    }
}

class MyThread implements Runnable{
    
    
    //模拟交叉
    private Lock lockA;
    private Lock lockB;
    public MyThread(Lock lockA, Lock lockB) {
    
    
        this.lockA = lockA;
        this.lockB = lockB;
    }
    @Override
    public void run() {
    
    
        System.out.println(Thread.currentThread().getName()+"获得了lockA ");
        lockA.lock();//锁住A
        System.out.println(Thread.currentThread().getName()+"尝试获取lockB");
        lockB.lock();
        //..死锁....这里是进行不下去的
        lockA.unlock();
        lockB.unlock();
    }
}

解决方案

1、使用jps命令获取进程号

E:\workspace_self\study-juc\study-juc>jps -l
16080 org.jetbrains.kotlin.daemon.KotlinCompileDaemon
13588 org.jetbrains.jps.cmdline.Launcher
1732 sun.tools.jps.Jps
12440 com.ssx.MyLock.DeadLock
8792

2、使用jstack命令查找堆栈信息

E:\workspace_self\study-juc\study-juc>jstack 12440

......
Found one Java-level deadlock:
=============================
"悟空":
  waiting for ownable synchronizer 0x00000000db0d6fb8, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
  which is held by "石似心"
"石似心":
  waiting for ownable synchronizer 0x00000000db0d6fe8, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
  which is held by "悟空"
......

可以很清楚的看到当前存在一个死锁现象,都各自拥有一个锁,然后等待对方的锁。

猜你喜欢

转载自blog.csdn.net/qq845484236/article/details/108239665