【JavaSE】之JUC并发编程(下)


前言

本文为JUC并发编程相关知识,Java全栈学习路线可参考:【Java全栈学习路线】最全的Java学习路线及知识清单,Java自学方向指引,内含最全Java全栈学习技术清单~
本文上接:【JavaSE】之JUC并发编程(上)

十、阻塞队列

1.阻塞队列简介

集合框架:
在这里插入图片描述

阻塞队列特点:

  • 先进先出
  • 写入时,如果队列满了就必须阻塞等待
  • 取出时,如果队列为空就必须阻塞等待

2.四组API

方式抛出异常有返回值,不抛出异常阻塞,一直等待阻塞,超时等待
添加add()offer()put()offer(,,)
移除remove()pull()take()pull(,)
检测队首元素element()peek()--

代码示例:

public class Test {
    public static void main(String[] args) throws InterruptedException {
        test4();
    }
    // 抛出异常:java.lang.IllegalStateException: Queue full
    public static void test1(){
        // 队列的大小为3
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
        // add()方法返回boolean值
        boolean flag1 = blockingQueue.add("a");
        boolean flag2 = blockingQueue.add("b");
        boolean flag3 = blockingQueue.add("c");
        boolean flag4 = blockingQueue.add("d");// add添加元素超过队列的长度会抛出异常java.lang.IllegalStateException: Queue full
        System.out.println(blockingQueue.element());// 获得队首元素
        System.out.println("=========");
        // remove()返回本次移除的元素
        Object e1 = blockingQueue.remove();
        Object e2 = blockingQueue.remove();
        Object e3 = blockingQueue.remove();
        Object e4 = blockingQueue.remove();// 队列中没有元素仍继续移除元素会抛出异常java.util.NoSuchElementException
    }
    // 有返回值,不抛出异常
    public static void test2(){
        // 队列的大小为3
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
        // offer返回boolean值
        boolean flag1 = blockingQueue.offer("a");
        boolean flag2 = blockingQueue.offer("b");
        boolean flag3 = blockingQueue.offer("c");
        //boolean flag4 = blockingQueue.offer("d");// offer添加元素超过队列的长度会返回false
        System.out.println(blockingQueue.peek());// 获得队首元素
        System.out.println("=========");
        // poll()返回本次移除的元素
        Object poll1 = blockingQueue.poll();
        Object poll2 = blockingQueue.poll();
        Object poll3 = blockingQueue.poll();
        Object poll4 = blockingQueue.poll();// 队列中没有元素仍继续移除元素会打印出null
    }
    // 阻塞,一直等待
    public static void test3() throws InterruptedException {
        // 队列的大小为3
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
        // put没有返回值
        blockingQueue.put("a");
        blockingQueue.put("b");
        blockingQueue.put("c");
        //blockingQueue.put("d");// put添加元素超过队列的长度会一直等待
        System.out.println("=========");
        // take()返回本次移除的元素
        Object take1 = blockingQueue.take();
        Object take2 = blockingQueue.take();
        Object take3 = blockingQueue.take();
        Object take4 = blockingQueue.take();// 队列中没有元素仍继续移除元素会一直等待
    }
    // 阻塞,超时等待
    public static void test4() throws InterruptedException {
        // 队列的大小为3
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
        // offer返回boolean值
        boolean flag1 = blockingQueue.offer("a");
        boolean flag2 = blockingQueue.offer("b");
        boolean flag3 = blockingQueue.offer("c");
        // offer添加元素超过队列的长度会返回false;并且等待指定时间后推出,向下执行
        boolean flag4 = blockingQueue.offer("d", 2, TimeUnit.SECONDS);
        System.out.println("=========");
        // poll()返回本次移除的元素
        Object poll1 = blockingQueue.poll();
        Object poll2 = blockingQueue.poll();
        Object poll3 = blockingQueue.poll();
        // 队列中没有元素仍继续移除元素会打印出null,等待指定之间后退出。
        Object poll4 = blockingQueue.poll(2,TimeUnit.SECONDS);
    }
}

3.SynchronousQueue同步队列

  • 特点:进去一个元素,必须等待取出这个元素后,才能放下一个元素
  • 添加元素方法:put()
  • 取出元素方法:take()

十一、线程池

1.线程池简介

  • 程序的运行,本质:占用系统的资源! (优化资源的使用 => 池化技术)
  • 池化技术:事先准备好一些资源,有人要用,就来我这里拿,用完之后还给我
  • 线程池的好处:1、降低系统资源的消耗;2、提高响应的速度;3、方便管理

对于线程池的一句话总结: 3大方法、7大参数、4大拒绝策略

2.三大方法

  • 创建单个线程的线程池:Executors.newSingleThreadExecutor();
  • 创建一个固定大小的线程池: Executors.newFixedThreadPool(5);
  • 创建一个可伸缩的线程池:Executors.newCachedThreadPool();

代码示例:

package com.wang.pool;
import java.util.concurrent.ExecutorService;
import java.util.List;
import java.util.concurrent.Executors;
public class Demo01 {
    public static void main(String[] args) {
        // Executors 工具类、3大方法
        // Executors.newSingleThreadExecutor();// 创建单个线程的线程池
        // Executors.newFixedThreadPool(5);// 创建一个固定大小的线程池
        // Executors.newCachedThreadPool();// 创建一个可伸缩的线程池
        
        // 单个线程的线程池
        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        try {
            for (int i = 1; i < 100; i++) {
                // 使用了线程池之后,使用线程池来创建线程
                threadPool.execute(()->{
                    System.out.println(
                        Thread.currentThread().getName()+" ok");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 线程池用完,程序结束,关闭线程池
            threadPool.shutdown();
        }
    }
}

3.七大参数

int corePoolSize:核心线程池大小
int maximumPoolSize:最大核心线程池大小
long keepAliveTime:超时没有人调用就会释放
TimeUnit unit:超时单位
BlockingQueue workQueue:阻塞队列
ThreadFactory threadFactory:线程工厂:创建线程的,一般 不用动
RejectedExecutionHandler handle:拒绝策略

源码分析:

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService (
        new ThreadPoolExecutor(
            1, 
            1,
            0L, 
            TimeUnit.MILLISECONDS, 
            new LinkedBlockingQueue<Runnable>())); 
}
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(
        5, 
        5, 
        0L, 
        TimeUnit.MILLISECONDS, 
        new LinkedBlockingQueue<Runnable>()); 
}
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(
        0, 
        Integer.MAX_VALUE, 
        60L, 
        TimeUnit.SECONDS, 
        new SynchronousQueue<Runnable>()); 
}
// 本质ThreadPoolExecutor() 
public ThreadPoolExecutor(int corePoolSize, // 核心线程池大小 
                          int maximumPoolSize, // 最大核心线程池大小 
                          long keepAliveTime, // 超时没有人调用就会释放 
                          TimeUnit unit, // 超时单位 
                          // 阻塞队列 
                          BlockingQueue<Runnable> workQueue, 
                          // 线程工厂:创建线程的,一般 不用动
                          ThreadFactory threadFactory,  
                          // 拒绝策略
                          RejectedExecutionHandler handle ) {
    if (corePoolSize < 0 
        || maximumPoolSize <= 0 
        || maximumPoolSize < corePoolSize 
        || keepAliveTime < 0) 
        throw new IllegalArgumentException(); 
    if (workQueue == null 
        || threadFactory == null 
        || handler == null) 
        throw new NullPointerException(); 
    this.acc = System.getSecurityManager() == null 
        ? null : AccessController.getContext(); 
    this.corePoolSize = corePoolSize; 
    this.maximumPoolSize = maximumPoolSize; 
    this.workQueue = workQueue; 
    this.keepAliveTime = unit.toNanos(keepAliveTime); 
    this.threadFactory = threadFactory; 
    this.handler = handler; 
}

因为实际开发中工具类Executors 不安全,所以需要手动创建线程池,自定义7个参数。
示例代码:

package com.wang.pool;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
// Executors 工具类、3大方法
// Executors.newSingleThreadExecutor();// 创建一个单个线程的线程池
// Executors.newFixedThreadPool(5);// 创建一个固定大小的线程池
// Executors.newCachedThreadPool();// 创建一个可伸缩的线程池
/**
 * 四种拒绝策略:
 *
 * new ThreadPoolExecutor.AbortPolicy() 
 * 银行满了,还有人进来,不处理这个人的,抛出异常
 *
 * new ThreadPoolExecutor.CallerRunsPolicy() 
 * 哪来的去哪里!比如你爸爸 让你去通知妈妈洗衣服,妈妈拒绝,让你回去通知爸爸洗
 *
 * new ThreadPoolExecutor.DiscardPolicy() 
 * 队列满了,丢掉任务,不会抛出异常!
 *
 * new ThreadPoolExecutor.DiscardOldestPolicy() 
 * 队列满了,尝试去和最早的竞争,也不会抛出异常!
 */
public class Demo01 {
    public static void main(String[] args) {
        // 自定义线程池!工作 ThreadPoolExecutor
        ExecutorService threadPool = new ThreadPoolExecutor(
                2,// int corePoolSize, 核心线程池大小(候客区窗口2个)
                5,// int maximumPoolSize, 最大核心线程池大小(总共5个窗口) 
                3,// long keepAliveTime, 超时3秒没有人调用就会释,放关闭窗口 
                TimeUnit.SECONDS,// TimeUnit unit, 超时单位 秒 
                new LinkedBlockingDeque<>(3),// 阻塞队列(候客区最多3人)
                Executors.defaultThreadFactory(),// 默认线程工厂
                // 4种拒绝策略之一:
                // 队列满了,尝试去和 最早的竞争,也不会抛出异常!
                new ThreadPoolExecutor.DiscardOldestPolicy());  
        //队列满了,尝试去和最早的竞争,也不会抛出异常!
        try {
            // 最大承载:Deque + max
            // 超过 RejectedExecutionException
            for (int i = 1; i <= 9; i++) {
                // 使用了线程池之后,使用线程池来创建线程
                threadPool.execute(()->{
                    System.out.println(
                        Thread.currentThread().getName()+" ok");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 线程池用完,程序结束,关闭线程池
            threadPool.shutdown();
        }
    }
}

4.四大拒绝策略

  • new ThreadPoolExecutor.AbortPolicy() :线程队列满了,不处理该线程,抛出异常!
  • new ThreadPoolExecutor.CallerRunsPolicy() :交给主线程去处理!
  • new ThreadPoolExecutor.DiscardPolicy() :队列满了,丢掉任务,不会抛出异常!
  • new ThreadPoolExecutor.DiscardOldestPolicy() :队列满了,尝试去和最早的竞争,也不会抛出异常!

5.最大线程应该如何设置?

  • CPU密集型:最大线程数,CPU几核的就是几,可以保持CPU效率最高。
  • IO密集型:判断程序中十分耗IO的线程数量,大于这个数,一般是这个数的两倍。

十二、四大函数式接口

函数式接口:只有一个方法的接口。
四大函数式接口:

  • Function函数型接口:有一个输入参数,有一个输出(返回值)。
  • predicate断定型接口:有一个输入参数,返回值只能是boolean值。
  • consumer消费型接口:有一个输入参数,没有返回值。
  • supplier供给型接口:没有输入参数,有一个输出(返回值)。

十三、stream流式计算

  • Stream 流式计算是在jdk 1.8后引入的新特性,将集合或数组转换成一种流的元素序列。流不是集合中的元素,也不是一种数据结构,不负责数据的存储。Stream 流也不会改变源对象(源集合)。
  • Stream 接口中几乎所有方法的参数都是四大函数式接口接口类型的参数。而函数式接口可以使用 lambda 表达式来简化开发,并且Stream 接口中的方法基本都是返回对象本身(返回对象本身的方法可以使用链式编程)。所以在使用 Stream流式计算时,基本上都用到了函数式接口、lambda表达式 和 链式编程。
  • Collection接口在 jdk 1.8 后新增了一个 stream() 方法,调用该方法会得到一个 Stream流对象。通过这个对象可以调用 Stream接口相应的方法对集合进行过滤、排序、截断(丢弃)、截断(获取)、转换、遍历、计数、拼接、取最大值、取最小值 等操作。

代码示例:

/**
 * 筛选用户,五个条件
 * 1.ID是偶数 2.年龄大于23岁 3.用户名转化为大写字母 4.用户名字母倒着排序 5.只输出一个用户
 */
public class Test {
    public static void main(String[] args) {
        User u1 = new User(1,"A",23);
        User u2 = new User(2,"B",21);
        User u3 = new User(3,"C",25);
        User u4 = new User(4,"D",30);
        User u4 = new User(5,"E",28);
        //集合就是存储
        List<User> list = Arrays.asList(u1,u2,u3,u4);
        //计算交给Stream
        list.stream()
                .filter(user -> {return user.getId()%2==0;})
                .filter(user -> {return user.getAge()>23;})
                .map(user -> {return user.getName().toUpperCase();})
                .sorted((uu1,uu2)->{return uu2.compareTo(uu1);})
                .limit(1)
                .forEach(System.out::println);
    }
}

十四、Forkjoin详解

1.什么是ForkJoin

  • ForkJoin也是一种线程池,只不过ForkJoin是专为CPU密集型任务而建立的线程池,它能大大提高CPU密集型任务的执行效率。主要用于并行执行任务,提高效率,大数据量。
  • ForkJoin是使用分治算法实现的,主要的原理就是将一个大的任务拆分为若干个小任务分发给若干个线程去处理,最后将若干的线程处理好后的结果进行汇总,从而达到提升计算效率的结果。

2.forkjoinPool

  • (1)通过forkjoinPool来执行ForkJoin
  • (2)计算任务:forkjoinPool.execute(ForkJoinTask task)

代码示例:

public class ForkJoinDemo extends RecursiveTask<long> {
    private Long start;//1
    private Long end;//19990009
    //临界值
    private Long temp = 10000L;
    public ForkJoinDemo(Long start, Long end) {
        this.start = start;
        this.end = end;
    }
    //计算方法
    @Override
    protected long compute() {
        if ((end-start)<temp){
            int sum = 0;
            for (int i=1;i<10_0000_0000;i++){
                sum+=i;
            }
            return sum;
        }else {
            //取一个中间值
            long middle = (start + end) / 2;
            ForkJoinDemo forkJoinDemo1 = new ForkJoinDemo(start,middle);
            //拆分任务,把任务压到线程队列
            forkJoinDemo1.fork();
            ForkJoinDemo forkJoinDemo2 = new ForkJoinDemo(middle+1,end);
            //拆分任务,把任务压到线程队列
            forkJoinDemo2.fork();
            return forkJoinDemo1.join()+forkJoinDemo2.join();
        }
    }
}

3.三种方式进行计算任务

public static void test1(){
        Long sum = 0L;
        long start = System.currentTimeMillis();
        for (int i=1;i<10_0000_0000;i++){
            sum+=i;
        }
        long end = System.currentTimeMillis();
        System.out.println("sum="+sum+"时间:"+(end-start));
    }
    public static void test2() throws ExecutionException, InterruptedException {
        long start = System.currentTimeMillis();
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinTask<Long> task = new ForkJoinDemo(0L,10_0000_0000L);
        ForkJoinTask<Long> submit = forkJoinPool.submit(task);
        Long sum = submit.get();
        long end = System.currentTimeMillis();
        System.out.println("sum="+sum+"时间:"+(end-start));
    }
    public static void test3(){
        long start = System.currentTimeMillis();
        //stream并行流
        long sum = LongStream.rangeClosed(0L, 10_0000_0000L).parallel().reduce(0, Long::sum);
        long end = System.currentTimeMillis();
        System.out.println("sum="+sum+"时间:"+(end-start));
    }

结果:test3快于test2快于test1。

十五、异步回调

1.Future

  • future设计的初衷就是对将来某个事件结果进行建模
  • 本质就是前端发送Ajax异步请求给后端
  • 实现类中使用比较多的事CompletableFuture

2.没有返回值的runAsync异步回调

public static void main(String[] args) throws ExecutionException, InterruptedException {
        //发起一个请求
        CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(()->{
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"执行");
        });
        System.out.println("1111");
        //获取阻塞执行结果
        completableFuture.get();
    }

3.有返回值的supplyAsync异步回调

  • whenComplete()方法有两个参数,T代表正常的返回结果,U代表的是抛出异常的错误信息
  • 如果发生异常的话get()方法会得到exceptionally里的信息
public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(()->{
            System.out.println(Thread.currentThread().getName()+"执行");
            return 1024;
        });
        System.out.println(completableFuture.whenComplete((t,u)->{
            System.out.println("t:"+t+";u="+u);
        }).exceptionally((e)->{
            System.out.println(e.getMessage());
            return 233;
        }).get());
    }

十六、JMM

1.Volatile

  • Volatile其实就是java虚拟机提供的轻量级的同步机制
  • Volatile有三个特点:保证可见性,不保证原子性,禁止指令重排

保证可见性

//不加volatile程序就会死循环,可以保证可见性
  private volatile static int num = 0;
  public static void main(String[] args) {
      new Thread(()->{//线程1  对内存的变化是不知道的
          while (num == 0){
          }
      }).start();
      try {
          TimeUnit.SECONDS.sleep(2);
      } catch (InterruptedException e) {
          e.printStackTrace();
      }
      num = 1;
      System.out.println(num);
  }

不保证原子性
原子性:即不可分割。线程A在执行任务时,不可被打扰也不可能被分割,要么同时成功,要么同时失败

public class tvolatile {
    //不保证原子性的验证
    private volatile static int num = 0;
    public void add(){
        num++;
    }
    public static void main(String[] args) {
        //理论上结果应该是20000,但其实是达不到20000的
        for (int i=0;i<=20;i++){
            new Thread(()->{
                for (int j=0;j<1000;j++){
                    add();
                }
            }).start();
        }
    }
}

禁止指令重排

  • 什么是指令重排?程序并不是按照我们写的那样执行,编译器会先优化重排,指令并行也可能会重排,内存系统也会重排。处理器在进行指令重排的时候,会考虑数据之间的依赖性
  • 指令重排可能会造成影响的结果,对于多线程的情况,可能会出现影响,解决的方式就是Volatile
  • Volatile可以避免指令重排:(1)内存屏障:就是一个CPU指令,可以保证特定的操作顺序,可以保证某些变量的内存可见性,利用这些特性就可以使Volatile实现了可见性;(2)Volatile可以保证可见性,但是不能保证原子性,由于内存屏障,可以保证避免指令重排的现象发生

2.JMM是什么

JMM是一种规定,一种概念,即java内存模型。
关于JMM有一些同步的约定:

  • 线程解锁前,必须把共享变量立刻刷回主存中
  • 线程加锁钱,必须读取主存中的最新值到工作内存中
  • 加锁和解锁是同一把锁

3.JMM的8种操作

  • Read(读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用;
  • load(载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中;
  • Use(使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令;
  • assign(赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中;
  • store(存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用;
  • write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中;
  • lock(锁定):作用于主内存的变量,把一个变量标识为线程独占状态;
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;

4.JMM的8种操作的相关规定

  • 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
  • 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
  • 不允许一个线程将没有assign的数据从工作内存同步回主内存
  • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过assign和load操作
  • 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
  • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
  • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
  • 对一个变量进行unlock操作之前,必须把此变量同步回主内存

十七、单例模式

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

1.饿汉式单例模式

/**
 * 饿汉式单例模式,一加载就创建单例对象
 */
public class Hungry {
    private Hungry(){
    }
    private static final Hungry hungry = new Hungry();
    public static Hungry getInstance(){
        return hungry;
    }
}

2.懒汉式单例模式

/**
 * 懒汉式单例模式
 */
public class LazyMan {
    private LazyMan() {
        System.out.println(Thread.currentThread().getName());
    }
    private static LazyMan lazyMan;
    public static LazyMan getInstance(){
        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();
        }
    }
}
// 打印
// Thread-1
// Thread-0
// Thread-4
// Thread-2

3.双重检测锁单例模式DCL

  • 相对比较安全创建单例的模式,但是反射可以破坏
/**
 * 懒汉式单例模式
 */
public class LazyMan01 {
    private LazyMan01(){
        System.out.println(Thread.currentThread().getName());
    }
    // 加上volatile关键字禁止指令重排
    private volatile static LazyMan01 lazyMan01;
    /**
     * 双重检测锁模式,DCL懒汉式
     * @return
     */
    public static LazyMan01 getInstance(){
        if (lazyMan01==null){
            synchronized(LazyMan01.class){
                if (lazyMan01==null){
                    lazyMan01 =  new LazyMan01();// 不是原子性操作,可能会有指令重排
                    /*
                    * 1.分配内存空间
                    * 2.执行构造器,初始化对象
                    * 3.把对象指向内存空间
                    * 正常情况下:按照123执行;发生指令重排,可能会先执行132
                    * 多线程情况下:如果第一个线程执行了13,此时第二个线程过来可能就会判断lazyMan01不为空,直接就返回了lazyMan01
                    * 此时,lazyMan01对象内存空是空的。
                    * */
                }
            }
        }
        return lazyMan01;
    }
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                LazyMan01.getInstance();
            }).start();
        }
    }
}

4.静态内部类创建单例模式

/**
 * 静态内部类创建单例模式
 */
public class LazyMan02 {
    private LazyMan02(){
    }
    public static LazyMan02 getInstance(){
        return InnerClass.lazyMan02;
    }
    static class InnerClass{
        private static final LazyMan02 lazyMan02 = new LazyMan02();
    }
}

5.枚举创建单例

public enum EnumSingle {
    INSTANCE;
    public static EnumSingle getInstance(){
        return INSTANCE;
    }
}

十八、CAS详解

1.什么是CAS

如果构造lazy时不加volatile,那么这不是一个原子性操作:他会先分配内存空间,再执行构造方法、初始化对象,然后把这个对象指向这个空间,此时lazy可能没有完成操作。
代码示例:

public class Lazy {
    private Lazy(){
    }
    private volatile static Lazy lazy;
    //双重检测锁模式的懒汉式单例:即DCL懒汉式
    public static Lazy getInstance(){
        //加锁
        if (lazy==null){
            synchronized (Lazy.class){
                if (lazy==null){
                    lazy = new Lazy();//如果构造lazy时不加volatile,那么这不是一个原子性操作:先分配内存空间,再执行构造方法、初始化对象,然后把这个对象指向这个空间
                }
            }
        }
        return lazy;//此时lazy可能没有完成操作
    }
    //多线程并发
    public static void main(String[] args) {
        for (int i=0;i<10;i++){
            new Thread(()->{
                Lazy.getInstance();
            }).start();
        }
    }
}

2.compareAndSet()方法

public class CASDemo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2020);
        //compareAndSet()方法中有两个参数,except:期望值、update:更新值
        //如果我期望的值达到了,就更新,否则就不更新
        System.out.println(atomicInteger.compareAndSet(2020,2021));
        System.out.println(atomicInteger.get());
        atomicInteger.getAndIncrement();//相当于number++
        System.out.println(atomicInteger.compareAndSet(2020,2021));
        System.out.println(atomicInteger.get());
    }
}

3.CAS的优点与缺点

优点:

  • 比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么执行,如果不是就一直循环

缺点:

  • 循环会耗时
  • 一次性只能保证一个共享变量的原子性
  • 会出现ABA问题

4.什么是ABA问题

public class CASDemo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2020);
        //捣乱的线程
        System.out.println(atomicInteger.compareAndSet(2020,2021));
        System.out.println(atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(2021,2020));
        System.out.println(atomicInteger.get());
        //期望的线程
        System.out.println(atomicInteger.compareAndSet(2020,2021));
        System.out.println(atomicInteger.get());
    }
}
// ABA问题的结果
// 打印
// true
// 2021
// true
// 2020
// true
// 2021

5.如何解决ABA问题:原子引用

  • 想解决ABA问题必须引入原子引用,对应的思想是:乐观锁
public class CASDemo02 {
    public static void main(String[] args) {
        AtomicStampedReference<Integer> atomicInteger = new AtomicStampedReference<>(1, 1);
        new Thread(()->{
            int stamp = atomicInteger.getStamp();//获得版本号
            System.out.println("a1=>"+stamp);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomicInteger.compareAndSet(1, 2,atomicInteger.getStamp(),atomicInteger.getStamp()+1));
            System.out.println("a2=>"+atomicInteger.getStamp());
            System.out.println(atomicInteger.compareAndSet(2, 1,atomicInteger.getStamp(),atomicInteger.getStamp()+1));
            System.out.println("a3=>"+atomicInteger.getStamp());
        },"a").start();
        new Thread(()->{
            int stamp = atomicInteger.getStamp();//获得版本号
            System.out.println("b1=>"+stamp);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomicInteger.compareAndSet(1,4,stamp,stamp+1);
            System.out.println("b2=>"+atomicInteger.getStamp());
        },"b").start();
    }
}

十、各种锁的理解

1.公平锁与非公平锁

  • 公平锁: 非常公平, 不能够插队,必须先来后到
  • 非公平锁:非常不公平,可以插队 (默认都是非公平)
public ReentrantLock() {
    sync = new NonfairSync(); 
}
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync(); 
}

2.可重入锁

可重入锁(也叫递归锁)
在这里插入图片描述
Synchronized 版代码示例:

package com.wang.lock;
// Synchronized
public class Demo01 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(()->{
            phone.sms();
        },"A").start();
        new Thread(()->{
            phone.sms();
        },"B").start();
    }
}
class Phone{
    public synchronized void sms(){
        System.out.println(Thread.currentThread().getName() 
                                                           + "sms");
        call(); // 这里也有锁(sms锁 里面的call锁)
    }
    public synchronized void call(){
        System.out.println(Thread.currentThread().getName() 
                                                           + "call");
    }
}

Lock 版代码示例:

package com.wang.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo02 {
    public static void main(String[] args) {
        Phone2 phone = new Phone2();
        new Thread(()->{
            phone.sms();
        },"A").start();
        new Thread(()->{
            phone.sms();
        },"B").start();
    }
}
class Phone2{
    Lock lock = new ReentrantLock();
    public void sms(){
        lock.lock(); 
        // 细节问题:lock.lock(); lock.unlock(); 
        // lock 锁必须配对,否则就会死在里面
        // 两个lock() 就需要两次解锁
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() 
                                                           + "sms");
            call(); // 这里也有锁
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
            lock.unlock();
        }
    }
    public void call(){
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() 
                                                           + "call");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

3.自旋锁

  • spinlock又称自旋锁,是为实现保护共享资源而提出的一种轻量级锁机制。
  • 自旋锁与互斥锁比较类似,都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个持有者,即只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,而是一直循环在那里看是否该自旋锁的持有者已经释放了锁,"自旋"一词就是因此而得名。锁一旦被释放,就会被等待的线程立即获取,而不需要经过唤醒和上下文切换。
    自定义一个锁:
package com.wang.lock;
import java.util.concurrent.atomic.AtomicReference;
/**
 - 自旋锁
 */
public class SpinlockDemo {
    // int   0
    // Thread  null
    // 原子引用
    AtomicReference<Thread> atomicReference = 
                                            new AtomicReference<>();
    // 加锁
    public void myLock(){
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() 
                                                       + "==> mylock");
        // 自旋锁
        while (!atomicReference.compareAndSet(null,thread)){
        }
    }
    // 解锁
    // 加锁
    public void myUnLock(){
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName()
                                                   + "==> myUnlock");
        atomicReference.compareAndSet(thread,null);// 解锁
    }
}

对锁的测试:

package com.haust.lock;
import java.util.concurrent.TimeUnit;
public class TestSpinLock {
    public static void main(String[] args) throws 
                                            InterruptedException {
//        ReentrantLock reentrantLock = new ReentrantLock();
//        reentrantLock.lock();
//        reentrantLock.unlock();
        // 底层使用的自旋锁CAS
        SpinlockDemo lock = new SpinlockDemo();// 定义锁
        new Thread(()-> {
            lock.myLock();// 加锁
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.myUnLock();// 解锁
            }
        },"T1").start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(()-> {
            lock.myLock();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.myUnLock();
            }
        },"T2").start();
    }
}

测试结果:
在这里插入图片描述

4.死锁

定义

  • 如果一个进程集合里面的每个进程都在等待这个集合中的其他一个进程(包括自身)才能继续往下执行,若无外力他们将无法推进,这种情况就是死锁,处于死锁状态的进程称为死锁进程。
    在这里插入图片描述
    产生死锁的四个必要条件
  • (1)互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源。
  • (2)请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放。
  • (3)不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放。
  • (4)环路等待条件:是指进程发生死锁后,必然存在一个进程–资源之间的环形链。

处理死锁的基本方法

  • (1)预防死锁:通过设置一些限制条件,去破坏产生死锁的必要条件
  • (2)避免死锁:在资源分配过程中,使用某种方法避免系统进入不安全的状态,从而避免发生死锁
  • (3)检测死锁:允许死锁的发生,但是通过系统的检测之后,采取一些措施,将死锁清除掉
  • (4)解除死锁:该方法与检测死锁配合使用

后记

Java全栈学习路线可参考:【Java全栈学习路线】最全的Java学习路线及知识清单,Java自学方向指引,内含最全Java全栈学习技术清单~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小新要变强

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值