JUC笔记

基础知识

1.并发问题根源:

  • 可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到。

    CPU执行指令时,会先将数据从内存中拷贝到自己的缓存中进行操作,若共享变量没有及时写入主存中,会出现可见性问题。

  • 原子性:一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

    CPU时间片轮转执行,一个操作可能被打断。

  • 有序性:程序执行的顺序按照代码的先后顺序执行。

    编译器和处理器会对指令进行重排序优化。

2.线程安全的实现方法:

  • 互斥同步:synchronized 和 ReentrantLock

  • 非阻塞同步

    • 比较并交换(CAS):CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B
    • AtomicInteger
    • ABA:如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。JUC 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解决这个问题
  • 无同步方案

    如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性。

    • 栈封闭:

      如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性。

    • 线程本地存储(Thread Local Storage):

      如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。

    • 可重入代码:

1、什么是JUC

源码 + 官方文档 面试高频问!

1647673363570

java.util 工具包、包、分类

业务:普通的线程代码 Thread

Runnable 没有返回值、效率相比于 Callable 相对较低!

1647673469443

1647673496383

2、线程和进程

线程、进程,如果不能用一句话说出来的技术,不扎实!

进程:程序的一次动态执行过程,是系统资源分配的基本单位

一个程序,QQ.exe Music.exe 程序集合;

一个进程往往可以包含多个线程,至少包含一个!

Java默认有几个线程?2个 main,GC

线程:独立的执行路径,是CPU调度和执行的单位

开了一个进程Typora,写字,自动保存(线程)

对于Java而言:Thread、Runnable、Callable

Java真的可以开启线程吗? 开不了,只能通过本地方法调用底层C++,Java无法直接操作硬件

并发、并行

并发编程:并发、并行

并发(多线程操作同一个资源)

  • CPU一核,模拟出来多条线程,快速交替执行

并行(多个人一起行走)

  • CPU多核,多个线程可以同时执行;线程池
public class Test01{
    public static void main(String[] args){
        //获取CPU的核数
        //CPU密集型,IO密集型
        System.out.println(Runtime.getRuntime.availableProcessors());
    }
}

并发编程的本质:充分利用CPU的资源

线程有几个状态

新生(NEW)、运行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、超时等待(TIMED_WAITING)、终止(TERMINATED)

wait() / sleep() 区别

1、来自不同的类

wait => Object

sleep => Thread

2、关于锁的释放

wait 会释放锁

sleep 不会释放锁

3、使用范围不同

wait 必须在同步代码块中,因为wait()会释放锁,前提是先获取锁。

sleep 可以使用在任何地方

4、是否需要传入时间参数

wait 可以传入,也可以不传入

  • 不传入时间,需要notify()唤醒,并执行
  • 传入时间,自动唤醒,是否能获取锁并执行,需要看CPU

sleep 必须传入时间参数,自动唤醒,并执行

3、Lock锁(重点)

传统Synchronized

package demo01;

//基本的卖票例子
/**
 * 真正的多线程开发,公司中的开发
 * 线程就是一个单独的资源类,没有任何附属的操作!
 * 1.属性、方法
 */
public class SaleTicketDemo01 {
    public static void main(String[] args) {
        //并发:多个线程操作同一个资源类,把资源类丢入线程就可以了
        Ticket ticket = new Ticket();

        //@FunctionalInterface 函数式接口,jdk1.8 lambda表达式(参数)->{代码}
        new Thread(()->{
            for (int i = 0; i < 60; i++) {
                ticket.sale();
            }
        },"A").start();
        new Thread(()->{
            for (int i = 0; i < 60; i++) {
                ticket.sale();
            }
        },"B").start();
        new Thread(()->{
            for (int i = 0; i < 60; i++) {
                ticket.sale();
            }
        },"C").start();
    }

}

// 资源类 OOP
class Ticket {
    // 属性、方法
    private int number = 50;

    //卖票的方式
    //synchronized 本质:队列,锁
    public synchronized void sale() {
        if (number > 0) {
            System.out.println(Thread.currentThread().getName()+"卖出了"+(number--)+"票,剩余:"+(50-number));
        }
    }
}

Lock接口

1647680694175

1647680757374

1647680955181

公平锁:十分公平,可以先来后到

非公平锁:十分不公平,可以插队(默认)

ReentrantLock 源码分析

ReentrantLock总共有三个内部类,并且三个内部类是紧密相关的,下面先看三个类的关系:

java-thread-x-juc-reentrantlock-1.png (384×254) (pdai.tech)

说明: ReentrantLock类内部总共存在Sync、NonfairSync、FairSync三个类,NonfairSync与FairSync类继承自Sync类,Sync类继承自AbstractQueuedSynchronizer抽象类。下面逐个进行分析。

  • NonfairSync类
    • NonfairSync类继承了Sync类,表示采用非公平策略获取锁,其实现了Sync类中抽象的lock方法。
    • 从lock方法的源码可知,每一次都尝试获取锁,而并不会按照公平等待的原则进行等待,让等待时间最久的线程获得锁。
  • FairSyn类
    • FairSync类也继承了Sync类,表示采用公平策略获取锁,其实现了Sync类中的抽象lock方法。
    • 跟踪lock方法的源码可知,当资源空闲时,它总是会先判断sync队列(AbstractQueuedSynchronizer中的数据结构)是否有等待时间更长的线程,如果存在,则将该线程加入到等待队列的尾部,实现了公平获取原则。

package demo01;

//基本的卖票例子

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

/**
 * 真正的多线程开发,公司中的开发
 * 线程就是一个单独的资源类,没有任何附属的操作!
 * 1.属性、方法
 */
public class SaleTicketDemo02 {
    public static void main(String[] args) {
        //并发:多个线程操作同一个资源类,把资源类丢入线程就可以了
        Ticket2 ticket = new Ticket2();

        //@FunctionalInterface 函数式接口,jdk1.8 lambda表达式(参数)->{代码}
        new Thread(()->{for (int i = 0; i < 60; i++) ticket.sale();},"A").start();
        new Thread(()->{for (int i = 0; i < 60; i++) ticket.sale();},"B").start();
        new Thread(()->{for (int i = 0; i < 60; i++) ticket.sale();},"C").start();
    }
}

// 资源类 OOP
class Ticket2 {
    // 属性、方法
    private int number = 50;

    Lock lock = new ReentrantLock();
    //卖票的方式
    //lock三部曲
    //1.new ReentrantLock();
    //2.lock.lock(); 加锁
    //3.finally ==> lock.unlock()
    public void sale() {
        lock.lock(); //加锁

        try {
            // 业务代码
            if (number > 0) {
                System.out.println(Thread.currentThread().getName()+"卖出了"+(number--)+"票,剩余:"+(50-number));
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

Synchronized 和 Lock 区别

1、Synchronized 内置的Java关键字,Lock 是一个Java类

2、Synchronized 无法判断获取锁的状态,Lock 可以判断是否获取到了锁

3、Synchronized 会自动释放锁,Lock 必须要手动释放锁!如果不 释放锁,死锁

4、Synchronized 线程1(获得锁,阻塞)、线程2(等待,傻傻的等);Lock 就不一定会等待下去,有lock.tryLock()函数;

5、Synchronized 可重入锁(对象获取锁后还可以再次获取该锁),不可以中断的,非公平;Lock 可重入锁,可以判断锁,可以自己设置公平

6、Synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码。

锁是什么,如何判断锁的是谁?

4、生产者和消费者问题

面试:单例模式、排序算法、生产者和消费者、死锁

Synchronized版 wait notify

JUC Lock版

生产者和消费者问题 synchronized 版

package PC;

/**
 * 线程之间的通信问题:生产者和消费者问题   等待唤醒,通知唤醒
 * 线程交替执行   A    B  操作同一个变量    num=0
 * A    num+1
 * B    num-1
 */
public class test01 {
    public static void main(String[] args) {
        Data data = new Data();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"B").start();
    }
}

//判断等待,业务,通知
class Data{
    private int number = 0;

    //+1
    public synchronized void increment() throws InterruptedException {
        if (number != 0) {
            //等待
            this.wait();
        }
        number++;
        System.out.println(Thread.currentThread().getName()+"=>"+number);
        //通知其他线程,我+1完毕了
        this.notifyAll();
    }
    //-1
    public synchronized void decrement() throws InterruptedException {
        if (number == 0) {
            //等待
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName()+"=>"+number);
        //通知其他线程,我-1完毕了
        this.notifyAll();
    }

}

问题存在:A B C D 四个线程!虚假唤醒

1647684622897

if 改为 while

package PC;

/**
 * 线程之间的通信问题:生产者和消费者问题   等待唤醒,通知唤醒
 * 线程交替执行   A    B  操作同一个变量    num=0
 * A    num+1
 * B    num-1
 */
public class test01 {
    public static void main(String[] args) {
        Data data = new Data();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"B").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"C").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"D").start();
    }
}

//判断等待,业务,通知
class Data{
    private int number = 0;

    //+1
    public synchronized void increment() throws InterruptedException {
        while (number != 0) {
            //等待
            this.wait();
        }
        number++;
        System.out.println(Thread.currentThread().getName()+"=>"+number);
        //通知其他线程,我+1完毕了
        this.notifyAll();
    }
    //-1
    public synchronized void decrement() throws InterruptedException {
        while (number == 0) {
            //等待
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName()+"=>"+number);
        //通知其他线程,我-1完毕了
        this.notifyAll();
    }

}

JUC版的生产者和消费者问题

通过Lock 找到 Condition

1647691075733

1647691010038

代码实现:

package PC;

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

public class Test02 {
    public static void main(String[] args) {
        Data2 data = new Data2();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"B").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"C").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"D").start();

    }
}

//判断等待,业务,通知
class Data2{
    private int number = 0;

    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

    // condition.await(); //等待
    // condition.signalAll(); //唤醒全部

    //+1
    public void increment() throws InterruptedException {
        lock.lock();

        try {
            while (number != 0) {
                //等待
                condition.await(); //等待
            }
            number++;
            System.out.println(Thread.currentThread().getName()+"=>"+number);
            //通知其他线程,我+1完毕了
            condition.signalAll(); //唤醒全部
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }


    }
    //-1
    public void decrement() throws InterruptedException {
        lock.lock();

        try {
            while (number == 0) {
                //等待
                condition.await(); //等待
            }
            number--;
            System.out.println(Thread.currentThread().getName()+"=>"+number);
            //通知其他线程,我-1完毕了
            condition.signalAll(); //唤醒全部
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }
}

Condition 精准的通知和唤醒线程

1647692262034

package PC;

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

/*
   A执行完调用B,B执行完调用C,C执行完调用A
*/
public class Test03 {

   public static void main(String[] args) {
       Data3 data = new Data3();

       new Thread(()->{
           for (int i = 0; i < 10; i++) {
               data.printA();
           }
       },"A").start();

       new Thread(()->{
           for (int i = 0; i < 10; i++) {
               data.printB();
           }
       },"B").start();

       new Thread(()->{
           for (int i = 0; i < 10; i++) {
               data.printC();
           }
       },"C").start();
   }
}

class Data3 {//资源类
   private Lock lock = new ReentrantLock();
   private Condition condition1 = lock.newCondition();
   private Condition condition2 = lock.newCondition();
   private Condition condition3 = lock.newCondition();
   private int number = 1; // 1A 2B 3C

   public void printA() {
       lock.lock();
       //判断等待---执行业务---通知唤醒
       try {
           while (number != 1) { //判断等待
               condition1.await();
           }
           System.out.println(Thread.currentThread().getName()+"===>AAAAAA");
           //唤醒,指定唤醒的人,B
           number = 2;
           condition2.signal();

       } catch (Exception e) {
           e.printStackTrace();
       } finally {
           lock.unlock();
       }


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

       try {
           while (number != 2) { //判断等待
               condition2.await();
           }
           System.out.println(Thread.currentThread().getName()+"===>BBBBBB");
           //唤醒,指定的人,C
           number = 3;
           condition3.signal();

       } catch (Exception e) {
           e.printStackTrace();
       } finally {
           lock.unlock();
       }
   }
   public void printC() {
       lock.lock();
       try {
           while (number != 3) {
               condition3.await();
           }
           System.out.println(Thread.currentThread().getName() + "===>CCCCCC");
           //唤醒,指定的人,A
           number = 1;
           condition1.signal();

       } catch (Exception e) {
           e.printStackTrace();
       } finally {
           lock.unlock();
       }
   }

}

5、8锁现象

如何判断锁的是谁,深刻理解锁

  1. 两个方法用的是同一个锁(实例对象的锁),谁先拿到谁执行;
package lock8;

import java.util.concurrent.TimeUnit;

/**
 * 8锁,就是关于锁的8个问题
 * 1、标准情况下,两个线程先打印发短信还是打电话? ans:先发短信 后打电话
 * 2、sendSms延迟4s,两个线程先打印发短信还是打电话? ans:先发短信 后打电话
 */
public class Test1 {
    public static void main(String[] args) {
        Phone phone = new Phone();

        new Thread(()->{
            phone.sendSms();
        },"A").start();

        //休息1s
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            phone.call();
        },"B").start();
    }
}

class Phone {
    // synchronized 锁的对象是方法的调用者 即phone
    // 两个方法用的是同一把锁,谁先拿到谁执行!
    public synchronized void sendSms() {
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    public synchronized void call() {
        System.out.println("打电话");
    }
}

2、两个对象,普通方法锁不是同一把锁


package lock8;

import java.util.concurrent.TimeUnit;

/**
 * 3、增加了一个普通方法hello(),先打印发短信还是hello? ans:先打印hello,再发短信
 * 4、两个对象,两个同步方法,先打印发短信还是打电话? ans:先打电话,后发短信
 */
public class Test2 {
    public static void main(String[] args) {
        // 两个对象,两个调用者,两把锁,互不影响
        Phone2 phone1 = new Phone2();
        Phone2 phone2 = new Phone2();

        new Thread(()->{
            phone1.sendSms();
        },"A").start();

        //休息1s
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            phone2.call();
        },"B").start();
    }
}

class Phone2 {
    // synchronized 锁的对象是方法的调用者 即phone
    // 两个方法用的是同一把锁,谁先拿到谁执行!
    public synchronized void sendSms() {
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    public synchronized void call() {
        System.out.println("打电话");
    }

    //这里没有锁,不是同步方法,不受锁的影响
    public void hello() {
        System.out.println("Hello");
    }
}

3、静态同步代码,锁的是Class模板,同一个类的所有实例对象用的是同一把锁


package lock8;

import java.util.concurrent.TimeUnit;

/**
 * 5、增加两个静态同步方法,只有一个对象,先打印发短信还是hello? ans:先发短信,后打电话
 * 6、两个对象,两个静态同步方法,先发短信还是先打电话? ans:先发短信,后打电话
 */
public class Test3 {
    public static void main(String[] args) {
        // 两个对象,两个调用者,两把锁,互不影响
        Phone3 phone1 = new Phone3();
        Phone3 phone2 = new Phone3();


        new Thread(()->{
            phone1.sendSms();
        },"A").start();

        //休息1s
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            phone2.call();
        },"B").start();
    }
}

// Phone3唯一一个 class 对象
// Phone3.class
class Phone3 {
    // synchronized 锁的对象是方法的调用者 即phone
    // static 静态方法
    // 类一加载就有了! 锁的是 Class 模板
    public static synchronized void sendSms() {
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    public static synchronized void call() {
        System.out.println("打电话");
    }
    
}

4、一个静态同步方法,一个普通同步方法,这是两把锁,即使是一个实例对象,也是两把锁。


package lock8;

import java.util.concurrent.TimeUnit;

/**
 * 7、一个静态同步方法,一个普通同步方法,一个对象,先发短信还是先打电话? ans:先打电话,后发短信
 * 8、一个静态同步方法,一个普通同步方法,两个个对象,先发短信还是先打电话? ans:先打电话,后发短信
 */
public class Test4 {
    public static void main(String[] args) {
        // 两个对象,两个调用者,两把锁,互不影响
        Phone4 phone = new Phone4();


        new Thread(()->{
            phone.sendSms();
        },"A").start();

        //休息1s
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            phone.call();
        },"B").start();
    }
}

// Phone3唯一一个 class 对象
// Phone3.class
class Phone4 {
    // synchronized 锁的对象是方法的调用者 即phone
    // static 静态方法
    // 类一加载就有了! 锁的是 Class 模板
    public static synchronized void sendSms() {
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    //普通同步方法
    public synchronized void call() {
        System.out.println("打电话");
    }
}

小结

new 具体实例对象

static Class 唯一的一个模板

6、集合类不安全

List 不安全

package unsafe;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;

//java.util.ConcurrentModificationException 并发修改异常
public class ListTest {
    public static void main(String[] args) {
        // 并发下 ArrayList 不安全
        /**
         * 解决方案:
         * 1、Vector<>() 是安全的,换成List<String> list = new Vector<>();
         * 2、Collections包 List<String> list = Collections.synchronizedList(new ArrayList<>());
         * 3、JUC包  List<String> list = new CopyOnWriteArrayList<>();
         */
        // CopyOnWrite 写入时复制 COW 计算机程序设计领域的一种优化策略
        // 读写分离
        List<String> list = new CopyOnWriteArrayList<>();


        for (int i = 1; i <= 10; i++) {

            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(list);
            },String.valueOf(i)).start();

        }
    }
}

Set 不安全

package unsafe;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * 同理可证:java.util.ConcurrentModificationException
 * 解决方案:
 * 1、Collections包   Set<String> set = Collections.synchronizedSet(new HashSet<>());
 * 2、JUC包   Set<String> set = new CopyOnWriteArraySet<>();
 */

public class SetTest {
    public static void main(String[] args) {
        Set<String> set = new CopyOnWriteArraySet<>();

        for (int i = 1; i <= 10; i++) {
            new Thread(()->{
                set.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(set);
            },String.valueOf(i)).start();
        }
    }
}

hashSet底层是什么?

public HashSet() {
    map = new HashMap<>();
}

//add  
//set 本质就是 map  key是无法重复的
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}
private static final Object PRESENT = new Object(); //不变的值

Map 不安全

回顾map基本操作

1650875198849

package unsafe;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

//java.util.ConcurrentModificationException

/**
 * 解决方法:
 * 1.Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
 * 2.Map<String, String> map = new ConcurrentHashMap<>();
 */
public class MapTest {
    public static void main(String[] args) {
        // map 是这样用的吗? 不是,工作中不用HashMap
        // 默认等价于什么?new HashMap<>(16,0.75)
        Map<String, String> map = new ConcurrentHashMap<>();
        //容量,加载因子

        for (int i = 1; i <= 10; i++) {
            new Thread(()->{
                map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0,5));
                System.out.println(map);
            },String.valueOf(i)).start();
        }
    }
}

7、Callable

1650948501911

1、可以有返回值

2、可以抛出异常

3、方法不同,run()/call()

代码测试

package callable;

import sun.awt.windows.ThemeReader;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class CallableTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // new Thread(new Runnable()).start();
        // new Thread(new FutureTask<V>()).start();
        // new Thread(new FutureTask<V>( Callable )).start();
        new Thread().start(); //怎么启动callable

        MyThread thread = new MyThread();
        FutureTask futureTask = new FutureTask(thread); //适配类

        new Thread(futureTask,"A").start();
        new Thread(futureTask,"B").start();

        Integer o = (Integer) futureTask.get(); //获取callable的返回结果
        System.out.println(o);
    }
}

class MyThread implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        System.out.println("call()");
        return 1024;
    }
}

细节:

1、有缓存,即state不为new时,run()直接返回,不执行;

if (state != NEW ||
    !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                 null, Thread.currentThread()))
    return;

2、结果可能需要等待,会阻塞!get()方法要等待线程执行完成后,才返回结果。

8、常用的辅助类(必会)

8.1、CountDownLatch

1650951302169

package add;

import java.util.concurrent.CountDownLatch;

// 计数器
public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        //总数是6,必须要执行任务的时候,再使用
        CountDownLatch countDownLatch = new CountDownLatch(6);

        for (int i = 1; i <= 6; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+" Go out");
                countDownLatch.countDown(); //数量-1
            }, String.valueOf(i)).start();
        }

        countDownLatch.await(); //等待计数器归零,然后再向下执行

        System.out.println("Close door");
    }
}

原理:

countDownLatch.countDown(); //数量-1

countDownLatch.await(); //等待计数器归零,然后再向下执行

每次有线程调用 countDown() 数量-1,假设计数器变为0,countDownLatch.await() 就会被唤醒,继续执行!

源码分析:

CountDownLatch没有显示继承哪个父类或者实现哪个父接口, 它底层是AQS是通过内部类Sync来实现的。

CountDownLatch类的内部只有一个Sync类型的属性:

public class CountDownLatch {
    // 同步队列
    private final Sync sync;
}
  1. 构造函数:

    public CountDownLatch(int count) {     
        if (count < 0) throw new IllegalArgumentException("count < 0");     
        // 初始化状态数     
        this.sync = new Sync(count); 
    }
    

    说明: 该构造函数可以构造一个用给定计数初始化的CountDownLatch,并且构造函数内完成了sync的初始化,并设置了状态数。

  2. await():

    此函数将会使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断。其源码如下

    public void await() throws InterruptedException {
        // 转发到sync对象上
        sync.acquireSharedInterruptibly(1);
    }
    

    说明: 由源码可知,对CountDownLatch对象的await的调用会转发为对Sync的acquireSharedInterruptibly(从AQS继承的方法)方法的调用。

    对CountDownLatch的await调用大致会有如下的调用链:

    1654671240513

  3. countDown():

    此函数将递减锁存器的计数,如果计数到达零,则释放所有等待的线程。

    public void countDown() {
        sync.releaseShared(1);
    }
    

    说明: 对countDown的调用转换为对Sync对象的releaseShared(从AQS继承而来)方法的调用。

    对CountDownLatch的countDown调用大致会有如下的调用链:

    1654671610552

8.2、CyclicBarrier

1650953547913

与CountDownLatch区别:

  • CountDownLatch:调用 await() 方法的线程等待另外N个线程执行完毕后,才继续执行

    CyclicBarrier:N个线程互相等待,任何一个线程完成之前,所有线程都必须等待

  • CountDownLatch减计数,CyclicBarrier加计数。

  • CountDownLatch是一次性的,CyclicBarrier可以重用。

  • CountDownLatch和CyclicBarrier都有让多个线程等待同步然后再开始下一步动作的意思,

    但是CountDownLatch的下一步的动作实施者是主线程,具有不可重复性;而CyclicBarrier的下一步动作实施者还是“其他线程”本身,具有往复多次实施动作的特点。

源码分析:

CyclicBarrier没有显示继承哪个父类或者实现哪个父接口, 所有AQS和重入锁不是通过继承实现的,而是通过组合实现的。

  1. 属性

    public class CyclicBarrier {
        
        /** The lock for guarding barrier entry */
        // 可重入锁
        private final ReentrantLock lock = new ReentrantLock();
        /** Condition to wait on until tripped */
        // 条件队列
        private final Condition trip = lock.newCondition();
        /** The number of parties */
        // 参与的线程数量
        private final int parties;
        /* The command to run when tripped */
        // 由最后一个进入 barrier 的线程执行的操作
        private final Runnable barrierCommand;
        /** The current generation */
        // 当前代
        private Generation generation = new Generation();
        // 正在等待进入屏障的线程数量
        private int count;
    }
    

    说明: 该属性有一个为ReentrantLock对象,有一个为Condition对象,而Condition对象又是基于AQS的,所以,归根到底,底层还是由AQS提供支持。

  2. 构造函数

    • CyclicBarrier(int, Runnable)型构造函数

      public CyclicBarrier(int parties, Runnable barrierAction) {
          // 参与的线程数量小于等于0,抛出异常
          if (parties <= 0) throw new IllegalArgumentException();
          // 设置parties
          this.parties = parties;
          // 设置count
          this.count = parties;
          // 设置barrierCommand
          this.barrierCommand = barrierAction;
      }
      

      说明: 该构造函数可以指定关联该CyclicBarrier的线程数量,并且可以指定在所有线程都进入屏障后的执行动作,该执行动作由最后一个进行屏障的线程执行

    • CyclicBarrier(int)型构造函数

      public CyclicBarrier(int parties) {
          // 调用含有两个参数的构造函数
          this(parties, null);
      }
      

      说明: 该构造函数仅仅执行了关联该CyclicBarrier的线程数量,没有设置执行动作。

  3. dowait()

    CyclicBarrier类对外提供的 await() 函数在底层都是调用该了doawait函数。

    dowait方法的逻辑会进行一系列的判断,大致流程如下:

    1654674906585

  4. nextGeneration()

    此函数在所有线程进入屏障后会被调用,即生成下一个版本,所有线程又可以重新进入到屏障中。

    在此函数中会调用AQS的signalAll方法,即唤醒所有等待线程。如果所有的线程都在等待此条件,则唤醒所有线程。

  5. breakBarrier()

    此函数的作用是损坏当前屏障,会唤醒所有在屏障中的线程。

加法计数器

package add;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        /**
         * 集齐7颗龙珠召唤神龙
         */
        //召唤神龙线程
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
            System.out.println("召唤神龙成功");
        });

        for (int i = 1; i <= 7; i++) {
            //lambda表达式无法直接操作到i
            //匿名内部类、lambda表达式需要使用final(临时)变量,保证数据一致性
            final int temp = i;
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"收集 "+temp+" 个龙珠");

                try {
                    cyclicBarrier.await(); //等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

8.3、Semaphore

Semaphore:信号量

1650954971686

Semaphore底层是基于AbstractQueuedSynchronizer来实现的。

Semaphore称为计数信号量,它允许n个任务同时访问某个资源,可以将信号量看做是在向外分发使用资源的许可证,只有成功获取许可证,才能使用资源。

源码分析:

Semaphore总共有三个内部类:Sync、NonfairSync、FairSync。NonfairSync与FairSync类继承自Sync类,Sync类继承自AbstractQueuedSynchronizer抽象类。

  1. Sync类

    其属性相对简单,只有一个版本号,Sync类存在如下方法和作用如下:

    1654676220701

  2. NonfairSync类

    NonfairSync类继承了Sync类,表示采用非公平策略获取资源,其只有一个tryAcquireShared方法,重写了AQS的该方法。

  3. FairSync类

    FairSync类继承了Sync类,表示采用公平策略获取资源,其只有一个tryAcquireShared方法,重写了AQS的该方法。

  4. 构造函数

    • Semaphore(int)型构造函数

      public Semaphore(int permits) {
          sync = new NonfairSync(permits);
      }
      

      说明: 该构造函数会创建具有给定的许可数和非公平的公平设置的Semaphore。

    • Semaphore(int, boolean)型构造函数

      public Semaphore(int permits, boolean fair) {
          sync = fair ? new FairSync(permits) : new NonfairSync(permits);
      }
      

      说明: 该构造函数会创建具有给定的许可数和给定的公平设置的Semaphore。

  5. acquire函数

    此方法从信号量获取一个(多个)许可,在提供一个许可前一直将线程阻塞,或者线程被中断,其源码如下

    public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }
    

    说明: 该方法中将会调用Sync对象的acquireSharedInterruptibly(从AQS继承而来的方法)方法,而acquireSharedInterruptibly方法在上一篇CountDownLatch中已经进行了分析,在此不再累赘。

  6. release函数

    此方法释放(本质是添加)一个(多个)许可,将其返回给信号量,源码如下:

    public void release() {
        sync.releaseShared(1);
    }
    

    说明: 该方法中将会调用Sync对象的releaseShared(从AQS继承而来的方法)方法,而releaseShared方法在上一篇CountDownLatch中已经进行了分析,在此不再累赘。

package add;

import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class SemaphoreDemo {
    public static void main(String[] args) {
        //线程数量:停车位 限流!
        Semaphore semaphore = new Semaphore(3);

        for (int i = 1; i <= 6; i++) {
            new Thread(()->{

                try {
                    // acquire() 得到
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName()+"抢到车位");
                    TimeUnit.SECONDS.sleep(2);
                    System.out.println(Thread.currentThread().getName()+"离开车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // release() 释放
                    semaphore.release();
                }
            },String.valueOf(i)).start();
        }
    }
}

原理:

semaphore.acquire(); 获得,假设已经满了,等待释放为止

semaphore.release(); 释放,会将当前的信号量释放(+1),然后唤醒等待线程

作用:多个共享资源互斥的使用!并发限流,控制最大线程数!

9、读写锁

ReadWriteLock

1650955993222

package rw;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 独占锁(写锁) 一次只能被一个线程占用
 * 共享锁(读锁) 多个线程可以同时占有
 * ReadWriteLock
 * 读-读  可以共存
 * 读-写  不能共存!
 * 写-写  不能共存!
 */
public class ReadWriteLockDemo {
    public static void main(String[] args) {
        //MyCache myCache = new MyCache();
        MyCacheLock myCache = new MyCacheLock();

        //写入
        for (int i = 1; i <= 5; i++) {
            final int temp = i;
            new Thread(()->{
                myCache.put(temp+"",temp+"");
            },String.valueOf(i)).start();
        }

        //写入
        for (int i = 1; i <= 5; i++) {
            final int temp = i;
            new Thread(()->{
                myCache.get(temp+"");
            },String.valueOf(i)).start();
        }
    }
}

//加锁的
class MyCacheLock {
    private volatile Map<String,Object> map = new HashMap<>();
    //读写锁:更加细粒度的控制
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    //存,写入的时候只希望同时只有一个线程写
    public void put(String key, Object value) {
        readWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName()+"写入"+key);
            map.put(key, value);
            System.out.println(Thread.currentThread().getName()+"写入OK");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }

    //取,读,所有人都可以读
    public void get(String key) {
        readWriteLock.readLock().lock();

        try {
            System.out.println(Thread.currentThread().getName()+"读取"+key);
            Object o = map.get(key);
            System.out.println(Thread.currentThread().getName()+"读取OK");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
        }
    }
}

/**
 * 自定义缓存
 */
class MyCache {
    private volatile Map<String,Object> map = new HashMap<>();

    //存,写
    public void put(String key, Object value) {
        System.out.println(Thread.currentThread().getName()+"写入"+key);
        map.put(key, value);
        System.out.println(Thread.currentThread().getName()+"写入OK");
    }

    //取,读
    public void get(String key) {
        System.out.println(Thread.currentThread().getName()+"读取"+key);
        Object o = map.get(key);
        System.out.println(Thread.currentThread().getName()+"读取OK");
    }
}

10、阻塞队列

Blocking Queue (接口)

阻塞

队列

1650973212649

阻塞队列:

1650973333965

什么情况下我们会使用 阻塞队列:多线程并发处理、线程池

学会使用阻塞队列

添加、删除

四组API

  1. 抛出异常
  2. 不会抛出异常
  3. 阻塞等待
  4. 超时等待
方式 抛出异常 不会抛出异常,有返回值 阻塞等待 超时等待
添加 add() offer() put() offer(,timeout,timeunit)
移除 remove() poll() take() poll(timeout,timeunit)
检测队首元素 element() peek()

:无法向一个 BlockingQueue 中插入 null。如果你试图插入 null,BlockingQueue 将会抛出一个 NullPointerException。

/**
* 抛出异常
*/
public static void test1() {
    //参数为队列的大小
    ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);

    System.out.println(blockingQueue.add("a"));
    System.out.println(blockingQueue.add("b"));
    System.out.println(blockingQueue.add("c"));

    // 抛出异常 java.lang.IllegalStateException: Queue full
    // System.out.println(blockingQueue.add("d"));
    System.out.println("=========================");
    System.out.println(blockingQueue.remove());
    System.out.println(blockingQueue.remove());
    System.out.println(blockingQueue.remove());

    // 抛出异常 java.util.NoSuchElementException
    // System.out.println(blockingQueue.remove());
}

/**
* 不抛出异常
*/
public static void test2() {
    //参数为队列的大小
    ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);

    System.out.println(blockingQueue.offer("a"));
    System.out.println(blockingQueue.offer("b"));
    System.out.println(blockingQueue.offer("c"));
    // 不抛出异常 返回false
    // System.out.println(blockingQueue.offer("d"));
    System.out.println("=============================");
    System.out.println(blockingQueue.poll());
    System.out.println(blockingQueue.poll());
    System.out.println(blockingQueue.poll());
    // 不抛出异常  返回null
    // System.out.println(blockingQueue.poll());
}

/**
* 等待,阻塞(一直阻塞)
*/
public static void test3() throws InterruptedException {
    //参数为队列的大小
    ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);


    blockingQueue.put("a");
    blockingQueue.put("b");
    blockingQueue.put("c");
    // 队列没有位置了,一直阻塞
    // blockingQueue.put("d");
    System.out.println("======================");
    System.out.println(blockingQueue.take());
    System.out.println(blockingQueue.take());
    System.out.println(blockingQueue.take());
    // 队列为空,一直等待,阻塞
    // System.out.println(blockingQueue.take());
}

/**
* 等待,阻塞(等待超时)
*/
public static void test4() throws InterruptedException {
    //参数为队列的大小
    ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);

    blockingQueue.offer("a");
    blockingQueue.offer("b");
    blockingQueue.offer("c");
    // 等待,超过2秒就返回false
    blockingQueue.offer("d",2, TimeUnit.SECONDS);
    System.out.println("====================");
    blockingQueue.poll();
    blockingQueue.poll();
    blockingQueue.poll();
    // 等待,超过2秒就返回null
    blockingQueue.poll(2,TimeUnit.SECONDS);
}

Blocking Deque (接口)

双端队列是一个你可以从任意一端插入或者抽取元素的队列。在线程既是一个队列的生产者又是这个队列的消费者的时候可以使用到 BlockingDeque。

1654416639606

具有如下4组方法:

1654416808641

数组阻塞队列 ArrayBlockingQueue (类)

ArrayBlockingQueue 类实现了 BlockingQueue 接口。

ArrayBlockingQueue 是一个有界的阻塞队列,其内部实现是将对象放到一个数组里。

可以在对其初始化的时候设定这个上限,但之后就无法对这个上限进行修改了(译者注: 因为它是基于数组实现的,也就具有数组的特性: 一旦初始化,大小就无法修改)

链阻塞队列 LinkedBlockingQueue (类)

LinkedBlockingQueue 类实现了 BlockingQueue 接口。

LinkedBlockingQueue 内部以一个链式结构(链接节点)对其元素进行存储。

如果需要的话,这一链式结构可以选择一个上限。如果没有定义上限,将使用 Integer.MAX_VALUE 作为上限。

延迟队列 DelayQueue (类)

DelayQueue 实现了 BlockingQueue 接口。

DelayQueue 对元素进行持有直到一个特定的延迟到期。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。

具有优先级的阻塞队列 PriorityBlockingQueue

PriorityBlockingQueue 类实现了 BlockingQueue 接口。

PriorityBlockingQueue 是一个无界的并发队列。

无法向这个队列中插入 null 值。 所有插入到 PriorityBlockingQueue 的元素必须实现 java.lang.Comparable 接口。因此该队列中元素的排序就取决于你自己的 Comparable 实现

同步队列 SynchronousQueue

没有容量,只能存放一个元素!

如果该队列已有一元素的话,试图向队列中插入一个新元素的线程将会阻塞,直到另一个线程将该元素从队列中抽走。同样,如果该队列为空,试图向队列中抽取一个元素的线程将会阻塞,直到另一个线程向队列中插入了一条新的元素。

put()、take()

package bq;

import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;

/**
 * 同步队列
 * 和其他的BlockQueue 不一样,SynchronousQueue 不存储元素
 * put了一个元素,必须从里面先take取出来,否则不能再put进去值!
 */
public class SynchronousQueueDemo {
    public static void main(String[] args) {
        SynchronousQueue<String> synchronousQueue = new SynchronousQueue<>();

        new Thread(()->{

            try {
                System.out.println(Thread.currentThread().getName()+" put 1");
                synchronousQueue.put("1");
                System.out.println(Thread.currentThread().getName()+" put 2");
                synchronousQueue.put("2");
                System.out.println(Thread.currentThread().getName()+" put 3");
                synchronousQueue.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"T1").start();
        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName()+" take "+synchronousQueue.take());
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName()+" take "+synchronousQueue.take());
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName()+" take "+synchronousQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"T2").start();
    }
}

11、线程池(重点)

线程池:三大方法、7大参数、4种拒绝策略

池化技术

程序的运行,本质:占用系统资源!优化资源的使用===>池化技术

线程池、JDBC连接池、内存池、对象池

池化技术:事先准备好一些资源,有人要用,就来我这里拿,用完之后还给我。

线程池的好处:

1、降低资源的消耗

2、提高响应的速度

3、方便管理

线程复用、可以控制最大并发数、管理线程

线程池:三大方法

1651029453658

ExecutorService threadPool = Executors.newSingleThreadExecutor(); // 单个线程
ExecutorService threadPool = Executors.newFixedThreadPool(5); // 固定大小线程池
ExecutorService threadPool = Executors.newCachedThreadPool(); // 可伸缩的
package pool;

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

// Executors 工具类、3大方法

public class Demo01 {
    public static void main(String[] args) {
        //ExecutorService threadPool = Executors.newSingleThreadExecutor(); // 单个线程
        //ExecutorService threadPool = Executors.newFixedThreadPool(5); // 固定大小线程池
        ExecutorService threadPool = Executors.newCachedThreadPool(); // 可伸缩的
        try {

            for (int i = 0; i < 10; i++) {
                // 使用了线程池之后,使用线程池来创建线程
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+" OK");
                });
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 线程池用完,程序结束,关闭线程池
            threadPool.shutdown();
        }
    }
}

7大参数

研究源码

//本质:ThreadPoolExecutor()
public ThreadPoolExecutor(int corePoolSize, //核心线程池大小
                          int maximumPoolSize, //最大线程池大小
                          long keepAliveTime, //超时,没有人调用就释放
                          TimeUnit unit, //超时单位
                          BlockingQueue<Runnable> workQueue, //阻塞队列
                          ThreadFactory threadFactory, //线程工厂,创建线程,一般不用动
                          RejectedExecutionHandler handler //拒绝策略
                         ) {
    ...
}

1651035993555

Execute原理:

当一个任务提交至线程池之后:

  1. 线程池首先当前运行的线程数量是否少于corePoolSize。如果是,则创建一个新的工作线程来执行任务。如果都在执行任务,则进入2.

  2. 判断BlockingQueue是否已经满了,倘若还没有满,则将线程放入BlockingQueue。否则进入3.

  3. 如果创建一个新的工作线程将使当前运行的线程数量超过maximumPoolSize,则交给RejectedExecutionHandler来处理任务。

keepAliveTime是指当线程池中线程数量大于corePollSize时,此时存在非核心线程,keepAliveTime指非核心线程空闲时间达到的阈值会被回收。
java核心线程池的回收由allowCoreThreadTimeOut参数控制,默认为false,若开启为true,则此时线程池中不论核心线程还是非核心线程,只要其空闲时间达到keepAliveTime都会被回收。
但如果这样就违背了线程池的初衷(减少线程创建和开销),所以默认该参数为false

三种类型

newFixedThreadPool:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

线程池的线程数量达corePoolSize后,即使线程池没有可执行任务时,也不会释放线程。

FixedThreadPool的工作队列为无界队列LinkedBlockingQueue(队列容量为Integer.MAX_VALUE), 这会导致以下问题:

  • 线程池里的线程数量不超过corePoolSize,这导致了maximumPoolSize和keepAliveTime将会是个无用参数
  • 由于使用了无界队列, 所以FixedThreadPool永远不会拒绝, 即饱和策略失效

newSingleThreadExecutor:

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

初始化的线程池中只有一个线程,如果该线程异常结束,会重新创建一个新的线程继续执行任务,唯一的线程可以保证所提交任务的顺序执行.

由于使用了无界队列, 所以SingleThreadPool永远不会拒绝, 即饱和策略失效

newCachedThreadPool:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
  • (1) 主线程调用SynchronousQueue的offer()方法放入task, 倘若此时线程池中有空闲的线程尝试读取 SynchronousQueue的task, 即调用了SynchronousQueue的poll(), 那么主线程将该task交给空闲线程. 否则执行(2)

  • (2) 当线程池为空或者没有空闲的线程, 则创建新的线程执行任务.

  • (3) 执行完任务的线程倘若在60s内仍空闲, 则会被终止. 因此长时间空闲的CachedThreadPool不会持有任何线程资源

手动创建一个线程池

package pool;

import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 4大拒绝策略:
 * new ThreadPoolExecutor.AbortPolicy()    //银行满了,还有人进来,不处理这个人,抛出异常
 * new ThreadPoolExecutor.CallerRunsPolicy()    // 哪来的回哪去,由调用线程处理该任务
 * new ThreadPoolExecutor.DiscardPolicy()    // 队列满了,丢掉任务,不会抛出异常
 * new ThreadPoolExecutor.DiscardOldestPolicy()  // 队列满了,将最早进入队列的任务删除,之后再尝试加入队列
 */
public class Demo02 {
    public static void main(String[] args) {
        // 自定义线程池,ThreadPoolExecutor
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2,
                5,
                3,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardOldestPolicy()); // 队列满了,将最早进入队列的任务删除,之后再尝试加入队列

        try {
            // 最大承载 队列容量+maxPoolSize = 3+5
            // 超出,异常 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种拒绝策略

1651037820544

/**
 * 4大拒绝策略:
 * new ThreadPoolExecutor.AbortPolicy()    //银行满了,还有人进来,不处理这个人,抛出异常
 * new ThreadPoolExecutor.CallerRunsPolicy()    // 哪来的回哪去,由调用线程处理该任务
 * new ThreadPoolExecutor.DiscardPolicy()    // 队列满了,丢掉任务,不会抛出异常
 * new ThreadPoolExecutor.DiscardOldestPolicy()  // 队列满了,将最早进入队列的任务删除,之后再尝试加入队列
 */

小结和拓展

池的最大线程数该如何设置?

1、CPU密集型:任务管理器中,CPU线程数量是几就是几,可以保证CPU效率最高

Runtime.getRuntime().availableProcessors(); //获得电脑逻辑CPU数

2、IO密集型:判断程序中十分耗IO的线程数,设置maxPoolSize>2*数 (一般)

12、四大函数式接口(必须掌握)

lambda表达式、链式编程、函数式接口、Stream流式计算

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

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

1651040933604.

Function 函数型接口:有一个输入参数,有一个输出参数

package function;

import java.util.function.Function;

/**
 * Function 函数型,有一个输入参数,有一个输出
 * 只要是函数式接口,就可以用lambda表达式简化
 */
public class Demo01 {
    public static void main(String[] args) {
//        Function<String, String> function = new Function<String, String>() {
//            @Override
//            public String apply(String str) {
//                return str;
//            }
//        };

        Function function = (str)->{return str;};
        System.out.println(function.apply("123"));
    }
}

Predicate 断定型接口:有一个输入参数,返回值只能是 布尔值

package function;

import java.util.function.Predicate;

/**
 * 断定型接口:有一个输入参数,返回值只能是 布尔值
 */
public class Demo02 {
    public static void main(String[] args) {
//        Predicate<String> predicate = new Predicate<String>() {
//            @Override
//            public boolean test(String str) {
//                return str.isEmpty();
//            }
//        };
        Predicate<String> predicate = (str)->{return str.isEmpty();};
        System.out.println(predicate.test("123"));
    }
}

Consumer 消费型接口:只有输入参数,没有返回值

package function;

import java.util.function.Consumer;

/**
 * Consumer 消费型接口:只有输入参数,没有返回值
 */
public class Demo03 {
    public static void main(String[] args) {
//        Consumer<String> consumer = new Consumer<String>() {
//            @Override
//            public void accept(String str) {
//                System.out.println(str);
//            }
//        };

        Consumer<String> consumer = (str)->{System.out.println(str);};
        consumer.accept("123");
    }
}

Supplier 供给型接口:没有输入参数,只有返回值

package function;

import java.util.function.Supplier;

/**
 * Supplier 供给型接口:没有参数,只有返回值
 */
public class Demo04 {
    public static void main(String[] args) {
//        Supplier<Integer> supplier = new Supplier<Integer>() {
//            @Override
//            public Integer get() {
//                return 1024;
//            }
//        };

        Supplier<Integer> supplier = ()->{return 1024;};

        System.out.println(supplier.get());
    }
}

13、Stream流式计算

什么是Stream流式计算

package stream;

import java.util.Arrays;
import java.util.List;

/**
 * 题目要求:一分钟内完成此题,只能用一行代码实现!
 * 现在有5个用户,筛选:
 * 1、ID 必须是偶数
 * 2、年龄必须大于23
 * 3、用户名转为大写
 * 4、用户名字字母倒着排序
 * 5、只输出一个用户
 */
public class Test {
    public static void main(String[] args) {
        User u1 = new User(1, "a", 21);
        User u2 = new User(2, "b", 22);
        User u3 = new User(3, "c", 23);
        User u4 = new User(4, "d", 24);
        User u5 = new User(6, "e", 25);

        //集合就是存储
        List<User> list = Arrays.asList(u1,u2,u3,u4,u5);

        //计算交给流
        //lambda表达式、链式编程、Stream流式计算
        list.stream()
                .filter(u->{return u.getId()%2==0;})
                .filter(u->{return u.getAge()>23;})
                .map(u->{return u.getName().toUpperCase();})
                .sorted((o1,o2)->{return o2.compareTo(o1);})
                .limit(1)
                .forEach(System.out::println);
    }
}

14、ForkJoin

什么是ForkJoin

ForkJoin在JDK1.7,并行执行任务,提高效率

1651063160252

三个模块:

  • 任务对象:ForkJoinTask (包括 RecursiveTaskRecursiveActionCountedCompleter
  • 执行 Fork/Join 任务的线程:ForkJoinWorkerThread
  • 线程池:ForkJoinPool

这三者的关系是: ForkJoinPool可以通过池中的ForkJoinWorkerThread来处理ForkJoinTask任务。

RecursiveTask 是 ForkJoinTask 的子类,是一个可以递归执行的 ForkJoinTask,RecursiveAction 是一个无返回值的 RecursiveTask,CountedCompleter 在任务完成执行后会触发执行一个自定义的钩子函数。

核心思想:分治算法

1654668317895

核心思想:工作窃取

工作窃取算法:线程池内的所有工作线程都尝试找到并执行已经提交的任务,或者是被其他活动任务创建的子任务(如果不存在就阻塞等待)。

在 ForkJoinPool 中,线程池中每个工作线程(ForkJoinWorkerThread)都对应一个任务队列(WorkQueue),工作线程优先处理来自自身队列的任务(LIFO或FIFO顺序,参数 mode 决定),然后以FIFO的顺序随机窃取其他队列中的任务。

具体思路如下:

  • 每个线程都有自己的一个WorkQueue,该工作队列是一个双端队列。
  • 队列支持三个功能push、pop、poll
  • push/pop只能被队列的所有者线程调用,而poll可以被其他线程调用。
  • 划分的子任务调用fork时,都会被push到自己的队列尾(top端)中。
  • 默认情况下,工作线程从自己的双端队列尾(top端)获出任务并执行。(LIFO)
  • 当自己的队列为空时,线程随机从另一个线程的队列首(base端)调用poll方法窃取任务。(FIFO)

1654670146104

向 ForkJoinPool 提交任务有三种方式:

  • invoke()会等待任务计算完毕并返回计算结果;

  • execute()是直接向池提交一个任务来异步执行,无返回结果;

  • submit()也是异步执行,但是会返回提交的任务,在适当的时候可通过task.get()获取执行结果。

ForkJoin的操作

package forkjoin;

import java.util.concurrent.RecursiveTask;

/**
 * 求和计算的任务
 */
public class ForkJoinDemo extends RecursiveTask<Long> {

    private Long start;
    private Long end;

    //临界值
    private Long temp = 10000L;

    public ForkJoinDemo(Long start, Long end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        if ((end - start) < temp) {
            Long sum = 0L;
            for (Long i = start; i <= end; i++) {
                sum += i;
            }
            return sum;
        } else { //ForkJoin
            long mid = start + (end - start) / 2;
            ForkJoinDemo task1 = new ForkJoinDemo(start, mid);
            task1.fork();
            ForkJoinDemo task2 = new ForkJoinDemo(mid + 1, end);
            task2.fork();

            return task1.join() + task2.join();
        }
    }
}
package forkjoin;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.stream.LongStream;

public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //test1(); //5059
        //test2(); //3655
        test3(); //134
    }

    //普通程序员
    public static void test1() {
        Long sum = 0L;
        long start = System.currentTimeMillis();
        for (Long i = 0L; i <= 10_0000_0000; i++) {
            sum += i;
        }
        long end = System.currentTimeMillis();
        System.out.println("sum="+sum+" 时间:"+(end-start));
    }

    //会使用ForkJoin
    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));
    }

    //Stream 并行流
    public static void test3() {
        long start = System.currentTimeMillis();

        long sum = LongStream.rangeClosed(0L, 10_0000_0000L).parallel().reduce(0, Long::sum);

        long end = System.currentTimeMillis();
        System.out.println("sum="+sum+" 时间:"+(end-start));
    }
}

15、异步回调

Future 设计的初衷:对未来的某个事件的结果进行建模

package future;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

/**
 * 异步调用 CompletableFuture
 * 异步执行
 * 成功回调
 * 失败回调
 */
public class Demo01 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 没有返回结果的异步回调 runAsync
//        CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(()->{
//            try {
//                TimeUnit.SECONDS.sleep(2);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//            System.out.println(Thread.currentThread().getName()+"runAsync=>Void");
//        });
//        System.out.println("111111");
//        completableFuture.get(); //获取执行结果

        // 有返回结果的异步回调
        CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName()+"supplyAsync=>Integer");
            return 1024;
        });

        completableFuture.whenComplete((t,u)->{
            System.out.println("t=>"+t); //正常的返回结果
            System.out.println("u=>"+u); //错误信息
        }).exceptionally((e)->{
            System.out.println(e.getMessage());
            return 233; //可以获取到错误的返回结果
        }).get();
    }
}

16、JMM

请你谈谈你对Volatile的理解

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

1、保证可见性

2、不保证原子性

3、禁止指令重排

什么是JMM

JMM: Java内存模型,不存在的东西,是一种约定!

Java内存模型(JMM)总结 - mzone - 博客园 (cnblogs.com)

关于JMM的一些同步的约定:

1、线程解锁前,必须把共享变量立刻刷新回主存

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

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

线程 工作内存、主内存

8种操作:

1651123401281

1651123526726

Java内存模型定义了以下八种操作来完成:

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

Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:

  • 如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作, 如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。
  • 不允许read和load、store和write操作之一单独出现
  • 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
  • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
  • 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现
  • 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
  • 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
  • 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。

17、volatile

1、保证可见性

package volatileTest;

import java.util.concurrent.TimeUnit;

public class JMMDemo {
    // 不加 volatile 程序就会死循环
    // 加 volatile 可以保证可见性
    private volatile static int num = 0;

    public static void main(String[] args) { //main线程

        new Thread(()->{
            while (num == 0) {

            }
        }).start();

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

        num = 1;
        System.out.println(num);
    }
}

2、不保证原子性

原子性:不可分割

线程A在执行任务的时候,不能被打扰的,也不能被分割。要么同时成功,要么同时失败。

package volatileTest;

/**
 * volatile 不保证原子性
 */
public class VDemo02 {

    private volatile static int num = 0;

    public static void add() {
        num++;
    }

    public static void main(String[] args) {
        //理论上 num的结果为 20000
        for (int i = 1; i <= 20; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    add();
                }
            }).start();
        }

        while (Thread.activeCount() > 2) { //main gc
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+" "+num);
    }
}

如果不加lock和synchronized,怎么样保证原子性?

使用原子类,解决原子性问题!

1651125455589.

package volatileTest;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * volatile 不保证原子性
 */
public class VDemo02 {

    //原子类Integer
    private volatile static AtomicInteger num = new AtomicInteger();

    public static void add() {
        // num++; // 不是一个原子性操作
        num.getAndIncrement(); // AtomicInteger 的 +1 方法,CAS
    }

    public static void main(String[] args) {
        //理论上 num的结果为 20000
        for (int i = 1; i <= 20; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    add();
                }
            }).start();
        }

        while (Thread.activeCount() > 2) { //main gc
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+" "+num);
    }
}

指令重排

什么是 指令重排:你写的程序,计算机并不是按照你写的那样去执行的。

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

处理器在进行指令重排的时候,考虑:数据之间的依赖性!

int x = 1; // 1
int y = 2; // 2
x = x + 5; // 3
y = x * x; // 4

我们期望的:1234  但是可能执行的时候变成 2134 1324
不可能是4123

可能造成影响的结果:a b x y 这四个默认都是0;

线程A 线程B
x=a; y=b;
b=1; a=2;

正常的结果:x=0;y=0,但是可能由于指令重排

线程A 线程B
b=1; a=2;
x=a; y=b;

指令重排导致的异常结果:x=2;y=1

volatile 如何避免指令重排

JVM 提供 4 种内存屏障指令,

1651147513632

作用:

1、阻止屏障两侧的指令重排序,保证特定的操作执行顺序

2、强制把写缓冲区/高速缓存中的脏数据等写回主内存,保证某些变量的内存可见性

1651147778789.

18、单例模式

饿汉式 DCL懒汉式

饿汉式

package single;

/**
 * 饿汉式单例:一上来就把对象创建完成
 */
public class Hungry {

    // 可能浪费空间
    private byte[] data1 = new byte[1024*1024];
    private byte[] data2 = new byte[1024*1024];
    private byte[] data3 = new byte[1024*1024];
    private byte[] data4 = new byte[1024*1024];

    private Hungry() {}

    private final static Hungry HUNGRY = new Hungry();

    public static Hungry getInstance() {
        return HUNGRY;
    }

}

DCL懒汉式

package single;

/**
 * 懒汉式单例模式
 * 单线程下OK,多线程使用双重检测锁
 */
public class LazyMan {
    private LazyMan() {
        System.out.println(Thread.currentThread().getName()+" OK");
    }

    private volatile static LazyMan lazyMan;

    // 双重检测锁模式 double check lock (DCL)
    public static LazyMan getInstance() {
        if (lazyMan == null) {
            synchronized (LazyMan.class) {
                if (lazyMan == null) {
                    lazyMan = new LazyMan();
                    /**
                     * 1、分配内存空间
                     * 2、执行构造方法,初始化对象
                     * 3、把这个对象指向这个内存空间
                     *
                     * 123   线程A
                     * 132   线程B  出现了执行重排,需要声明volatile禁止指令重排
                     */
                }
            }
        }

        return lazyMan;
    }

    // 多线程并发
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                lazyMan.getInstance();
            }).start();
        }
    }
}

静态内部类

package single;

// 静态内部类
public class Holder {
    private Holder() {}

    private static Holder getInstance() {return InnerClass.HOLDER;}

    public static class InnerClass {
        private static final Holder HOLDER = new Holder();
    }
}

单例不安全 :反射 破坏了单例

解决:使用枚举类

19、深入理解CAS

什么是CAS

CAS:比较并交换

比较当前工作内存中的值,如果这个值是期望的,那么执行操作,否则就一直循环(自旋锁)!

相对于对于synchronized这种锁机制,CAS是非阻塞算法的一种常见实现。能够保证操作原子性。

缺点:

1、循环耗时

​ 解决方法:pause指令,提高CPU执行效率

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

​ 解决方法:把多个变量放在一个对象中进行CAS操作,JDK提供AtomicReference类保证对象的原子性

3、ABA问题

​ 解决方法:使用版本号,首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全 部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。JDK提供AtomicStampedReference类。

​ 另一个解决方法:使用 AtomicMarkableReference,它不是维护一个版本号,而是维护一个boolean类型的标 记,标记值有修改,则不更新值。

package cas;

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

public class CASDemo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2020);

        //期望、更新
        //public final boolean compareAndSet(int expect, int update)
        //===================捣乱线程====================
        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,6666));
        System.out.println(atomicInteger.get());
    }
}

unsafe 类 AtomicInteger

常用API:

public final int get(); //获取当前的值
public final int getAndSet(int newValue); //获取当前的值,并设置新的值
public final int getAndIncrement(); //获取当前的值,并自增 相当于i++
public final int incrementAndGet(); //先自增,再获取当前的值 相当于++i
public final int getAndDecrement(); //获取当前的值,并自减
public final int getAndAdd(int delta); //获取当前的值,并加上预期的值
void lazySet(int newValue); //最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

AtomicInteger 底层用的是volatile的变量和CAS来进行更改数据的。

  • volatile保证线程的可见性,多线程并发时,一个线程修改数据,可以保证其它线程立马看到修改后的值
  • CAS 保证数据更新的原子性。

1651151781446

1651151907237

1651151966756

CSA:ABA问题(狸猫换太子)

1651152254730

补:原子操作类

原子更新基本类型

  • AtomicBoolean: 原子更新布尔类型。
  • AtomicInteger: 原子更新整型。
  • AtomicLong: 原子更新长整型。

原子更新数组

  • AtomicIntegerArray: 原子更新整型数组里的元素。

  • AtomicLongArray: 原子更新长整型数组里的元素。

  • AtomicReferenceArray: 原子更新引用类型数组里的元素。

常用方法:

  • get(int index):获取索引为index的元素值。

  • getAndAdd(int index, int delta):获取index处的值,并加上delta

  • compareAndSet(int i, int expect, int update): 如果当前值等于预期值,则以原子方式将数组位置i的元素设置为update值。

原子更新引用类型

  • AtomicReference: 原子更新引用类型。
  • AtomicStampedReference: 原子更新引用类型, 内部使用Pair来存储元素值及其版本号。
  • AtomicMarkableReferce: 原子更新带有标记位的引用类型。

原子更新字段类

  • AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。

  • AtomicLongFieldUpdater: 原子更新长整型字段的更新器。

  • AtomicStampedFieldUpdater: 原子更新带有版本号的引用类型。

  • AtomicReferenceFieldUpdater: 上面已经说过此处不在赘述

20、原子引用

解决ABA问题,使用原子引用!对应思想:乐观锁

带版本号的原子操作

Integer 使用了对象缓存机制,默认范围是-128~127,推荐使用静态工厂方法 valueOf 获取对象实例,而不是 new ,因为 valueOf 使用缓存,而 new 一定会创建新的对象分配新的内存空间;

因此,包装类注意 == 和 equals()

1651208268870

package cas;

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

public class CASDemo {
    public static void main(String[] args) {
        // AtomicInteger atomicInteger = new AtomicInteger(2020);

        AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1,1);

        new Thread(()->{
            int stamp = atomicStampedReference.getStamp(); //获得版本号
            System.out.println("a1=>"+stamp);

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

            System.out.println(atomicStampedReference.compareAndSet(1, 2,
                    atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));

            System.out.println("a2=>"+atomicStampedReference.getStamp());

            System.out.println(atomicStampedReference.compareAndSet(2, 1,
                    atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));

            System.out.println("a3=>"+atomicStampedReference.getStamp());
        },"A").start();

        new Thread(()->{
            int stamp = atomicStampedReference.getStamp(); //获得版本号
            System.out.println("b1=>"+stamp);
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(atomicStampedReference.compareAndSet(1, 6,
                    stamp, stamp + 1));

            System.out.println("b2=>"+atomicStampedReference.getStamp());

        },"B").start();
    }
}

21、各种锁的理解

1、公平锁、非公平锁

公平锁:非常公平,不能插队,必须先来先执行!

非公平锁:非常不公平,可以插队,默认都是非公平锁。

public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

2、可重入锁

所有锁都是可重入锁,可重入锁又叫递归锁。

可重入锁:当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。

1651208744371

synchronized

package 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(); // 这里也有锁
    }

    public synchronized void call() {
        System.out.println(Thread.currentThread().getName() + " call");
    }
}

Lock

package 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 锁必须配对,否则就会死在里面

        try {
            System.out.println(Thread.currentThread().getName() + " sms");
            call(); // 这里也有锁
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

    public void call() {
        lock.lock();

        try {
            System.out.println(Thread.currentThread().getName() + " call");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

3、自旋锁

1651210107300

package lock;

import java.util.concurrent.atomic.AtomicReference;

/**
 * 自旋锁
 */
public class SpinLock {
    // 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 lock;

import java.util.concurrent.TimeUnit;

public class TestSpinLock {
    public static void main(String[] args) throws InterruptedException {
        // 底层使用自旋锁CAS
        SpinLock spinLock = new SpinLock();

        new Thread(()->{
            spinLock.myLock();

            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                spinLock.myUnlock();
            }
        },"T1").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(()->{
            spinLock.myLock();

            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                spinLock.myUnlock();
            }

        },"T2").start();
    }
}

4、死锁

1651211759400

package lock;

import java.util.concurrent.TimeUnit;

public class DeadLockDemo {
    public static void main(String[] args) {
        String lockA = "lockA";
        String lockB = "lockB";

        new Thread(new MyThread(lockA,lockB),"T1").start();
        new Thread(new MyThread(lockB,lockA),"T2").start();
    }
}

class MyThread implements Runnable {

    private String lockA;
    private String lockB;

    public MyThread(String lockA, String lockB) {
        this.lockA = lockA;
        this.lockB = lockB;
    }

    @Override
    public void run() {
        synchronized (lockA) {
            System.out.println(Thread.currentThread().getName() + "lock:" + lockA + "=>get"+lockB);

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

            synchronized (lockB) {
                System.out.println(Thread.currentThread().getName() + "lock:" + lockB + "=>get"+lockA);
            }
        }
    }
}

解决死锁

1、使用 jps -l定位进程号

1651213259355

2、使用 jstack 进程号 找到死锁问题

1651213416424

补:AQS

AbstractQueuedSynchronizer(AQS) 是一个用来构建锁和同步器的框架,比如ReentrantLock, Semaphore 等

核心思想

AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)锁实现的,即将暂时获取不到锁的线程加入到队列中。

AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。

private volatile int state;//共享变量,使用volatile修饰保证线程可见性

状态信息通过procted类型的getState,setState,compareAndSetState进行操作

著作权归https://pdai.tech所有。
链接:https://www.pdai.tech/md/java/thread/java-thread-x-lock-AbstractQueuedSynchronizer.html

//返回同步状态的当前值
protected final int getState() {  
        return state;
}
 // 设置同步状态的值
protected final void setState(int newState) { 
        state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

AQS定义两种资源共享方式

  • Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁
  • Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。

AQS 数据结构

AQS底层使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法:

isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。

AbstractQueuedSynchronizer类底层的数据结构是使用CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。

  • Sync queue,即同步队列,是双向链表,包括head结点和tail结点,head结点主要用作后续的调度.
  • Condition queue不是必须的,其是一个单向链表,只有当使用Condition时,才会存在此单向链表。并且可能会有多个Condition queue。

补:ReentrantReadWriteLock

ReentrantReadWriteLock底层是基于ReentrantLock和AbstractQueuedSynchronizer来实现的,所以,ReentrantReadWriteLock的数据结构也依托于AQS的数据结构。

public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {}

ReentrantReadWriteLock有五个内部类,五个内部类之间也是相互关联的。内部类的关系如下图所示:

1654160884950

说明: 如上图所示,Sync继承自AQS、NonfairSync继承自Sync类、FairSync继承自Sync类;ReadLock实现了Lock接口、WriteLock也实现了Lock接口。

Sync类的属性

abstract static class Sync extends AbstractQueuedSynchronizer {
    // 版本序列号
    private static final long serialVersionUID = 6317671515068378041L;        
    // 高16位为读锁,低16位为写锁
    static final int SHARED_SHIFT   = 16;
    // 读锁单位
    static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
    // 读锁最大数量 2^16-1
    static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
    // 写锁最大数量 2^16-1
    static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
    // 本地线程计数器
    private transient ThreadLocalHoldCounter readHolds;
    // 缓存的计数器
    private transient HoldCounter cachedHoldCounter;
    // 第一个读线程
    private transient Thread firstReader = null;
    // 第一个读线程的计数
    private transient int firstReaderHoldCount;
}

构造函数:

  • 无参构造

    public ReentrantReadWriteLock() {
        this(false);
    }
    

    说明: 此构造函数会调用另外一个有参构造函数

  • 有参构造

    public ReentrantReadWriteLock(boolean fair) {
        // 公平策略或者是非公平策略
        sync = fair ? new FairSync() : new NonfairSync();
        // 读锁
        readerLock = new ReadLock(this);
        // 写锁
        writerLock = new WriteLock(this);
    }
    

    说明: 可以指定设置公平策略或者非公平策略,并且该构造函数中生成了读锁与写锁两个对象。

补:ConcurrentHashMap

在JDK1.5~1.7版本,Java使用了分段锁机制实现ConcurrentHashMap.

简而言之,ConcurrentHashMap在对象中保存了一个Segment数组,即将整个Hash表划分为多个分段;而每个Segment元素,即每个分段则类似于一个Hashtable;这样,在执行put操作时首先根据hash算法定位到元素属于哪个Segment,然后对该Segment加锁即可。因此,ConcurrentHashMap在多线程并发编程中可是实现多线程put操作。

1654326722458

简单理解就是,ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。

在JDK1.8中,ConcurrentHashMap的实现原理摒弃了这种设计,而是选择了与HashMap类似的数组+链表+红黑树的方式实现,而加锁则采用CAS和synchronized实现。

1654326793872

ConcurrentHashMap 底层实现和HashMap基本一致,只不过保证了多线程并发安全,主要有一下不同:

  • put() 时,若该位置结点为null,则直接CAS操作放入新值即可

    // 找该 hash 值对应的数组下标,得到第一个节点 f
    else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
        // 如果数组该位置为空,
        //    用一次 CAS 操作将这个新值放入其中即可,这个 put 操作差不多就结束了,可以拉到最后面了
        //          如果 CAS 失败,那就是有并发操作,进到下一个循环就好了
        if (casTabAt(tab, i, null,
                     new Node<K,V>(hash, key, value, null)))
            break;                   // no lock when adding to empty bin
    }
    
  • put() 时,若该位置结点不为null,则获取头结点的监视器锁 synchronized (f)

    // 获取数组该位置的头节点的监视器锁
    synchronized (f) {
        if (tabAt(tab, i) == f) {
            if (fh >= 0) { // 头节点的 hash 值大于 0,说明是链表
                // 用于累加,记录链表的长度
                binCount = 1;
                // 遍历链表
                for (Node<K,V> e = f;; ++binCount) {
                    K ek;
                    // 如果发现了"相等"的 key,判断是否要进行值覆盖,然后也就可以 break 了
                    ...
                    // 到了链表的最末端,将这个新值放到链表的最后面
                    Node<K,V> pred = e;
                    ...
                }
            }
            else if (f instanceof TreeBin) { // 红黑树
                Node<K,V> p;
                binCount = 2;
                // 调用红黑树的插值方法插入新节点
                ...
            }
        }
    }
    
  • 初始化数组 initTable() 通过CAS操作

    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
            // CAS 一下,将 sizeCtl 设置为 -1,代表抢到了锁
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    //初始化操作
                    ...
                } finally {
                    // 设置 sizeCtl 为 sc,我们就当是 12 吧
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }
    
  • 扩容 tryPresize() ,主要是 transfer()

    tryPresize() 方法中多次调用 transfer() ,假设原数组长度为 n,所以我们有 n 个迁移任务,让每个线程每次负责一个小任务是最简单的,每做完一个任务再检测是否有其他没做完的任务,帮助迁移就可以了。

    程序使用了一个 stride,简单理解就是步长,每个线程每次负责迁移其中的一部分,如每次迁移 16 个小任务。所以,我们就需要一个全局的调度者来安排哪个线程执行哪几个任务,这个就是属性 transferIndex 的作用。

    第一个发起数据迁移的线程会将 transferIndex 指向原数组最后的位置,然后从后往前的 stride 个任务属于第一个线程,然后将 transferIndex 指向新的位置,再往前的 stride 个任务属于第二个线程,依此类推。当然,这里说的第二个线程不是真的一定指代了第二个线程,也可以是同一个线程。

补:CopyOnWriteArrayList

  1. 构造函数 :

    • 无参构造

      public CopyOnWriteArrayList() {
          // 设置数组
          setArray(new Object[0]);
      }
      
    • 有参构造:该构造函数用于创建一个按 collection 的迭代器返回元素的顺序包含指定 collection 元素的列表。

      public CopyOnWriteArrayList(Collection<? extends E> c) {
          Object[] elements;
          if (c.getClass() == CopyOnWriteArrayList.class) // 类型相同
              // 获取c集合的数组
              elements = ((CopyOnWriteArrayList<?>)c).getArray();
          else { // 类型不相同
              // 将c集合转化为数组并赋值给elements
              elements = c.toArray();
              // c.toArray might (incorrectly) not return Object[] (see 6260652)
              if (elements.getClass() != Object[].class) // elements类型不为Object[]类型
                  // 将elements数组转化为Object[]类型的数组
                  elements = Arrays.copyOf(elements, elements.length, Object[].class);
          }
          // 设置数组
          setArray(elements);
      }
      
  2. copyOf()

    该函数用于复制指定的数组,截取或用 null 填充(如有必要),以使副本具有指定的长度。

    public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        @SuppressWarnings("unchecked")
        // 确定copy的类型(将newType转化为Object类型,将Object[].class转化为Object类型,判断两者是否相等,若相等,则生成指定长度的Object数组
        // 否则,生成指定长度的新类型的数组)
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        // 将original数组从下标0开始,复制长度为(original.length和newLength的较小者),复制到copy数组中(也从下标0开始)
        System.arraycopy(original, 0, copy, 0,
                            Math.min(original.length, newLength));
        return copy;
    }
    
  3. add() :

    此函数用于将指定元素添加到此列表的尾部

    著作权归https://pdai.tech所有。
    链接:https://www.pdai.tech/md/java/thread/java-thread-x-juc-collection-CopyOnWriteArrayList.html
    
    public boolean add(E e) {
        // 可重入锁
        final ReentrantLock lock = this.lock;
        // 获取锁
        lock.lock();
        try {
            // 元素数组
            Object[] elements = getArray();
            // 数组长度
            int len = elements.length;
            // 复制数组
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            // 存放元素e
            newElements[len] = e;
            // 设置数组
            setArray(newElements);
            return true;
        } finally {
            // 释放锁
            lock.unlock();
        }
    }
    
  4. addIfAbsent()

    该函数用于添加元素(如果数组中不存在,则添加;否则,不添加,直接返回),可以保证多线程环境下不会重复添加元素。

    该函数的流程如下:

    ① 获取锁,获取当前数组为current,current长度为len,判断数组之前的快照snapshot是否等于当前数组current,若不相等,则进入步骤②;否则,进入步骤④

    ② 不相等,表示在snapshot与current之间,对数组进行了修改(如进行了add、set、remove等操作),获取长度(snapshot与current之间的较小者),对current进行遍历操作,若遍历过程发现snapshot与current的元素不相等并且current的元素与指定元素相等(可能进行了set操作),进入步骤⑤,否则,进入步骤③

    ③ 在当前数组中索引指定元素,若能够找到,进入步骤⑤,否则,进入步骤④

    ④ 复制当前数组current为newElements,长度为len+1,此时newElements[len]为null。再设置newElements[len]为指定元素e,再设置数组,进入步骤⑤

    ⑤ 释放锁,返回。

  5. set()

    此函数用于用指定的元素替代此列表指定位置上的元素,也是基于数组的复制来实现的。

  6. remove()

    此函数用于移除此列表指定位置上的元素。

    处理流程如下

    ① 获取锁,获取数组elements,数组长度为length,获取索引的值elements[index],计算需要移动的元素个数(length - index - 1),若个数为0,则表示移除的是数组的最后一个元素,复制elements数组,复制长度为length-1,然后设置数组,进入步骤③;否则,进入步骤②

    ② 先复制index索引前的元素,再复制index索引后的元素,然后设置数组。

    ③ 释放锁,返回旧值。

CopyOnWriteArrayList 的缺点:

  • 由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致young gc或者full gc
  • 不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个set操作后,读取到数据可能还是旧的

补:ConcurrentLinkedQueue

ConcurrentLinkedQueue的数据结构与LinkedBlockingQueue的数据结构相同,都是使用的链表结构。ConcurrentLinkedQueue的数据结构如下:

1654413333052

说明: ConcurrentLinkedQueue采用的链表结构,并且包含有一个头节点和一个尾结点。

全程无锁,仅使用CAS

  1. 构造函数

    • 无参构造:

      该构造函数用于创建一个最初为空的 ConcurrentLinkedQueue,头节点与尾结点指向同一个结点,该结点的item域为null,next域也为null

      public ConcurrentLinkedQueue() {
          // 初始化头节点与尾结点
          head = tail = new Node<E>(null);
      }
      
    • 有参构造:

      该构造函数用于创建一个最初包含给定 collection 元素的 ConcurrentLinkedQueue,按照此 collection 迭代器的遍历顺序来添加元素

      public ConcurrentLinkedQueue(Collection<? extends E> c) {
          Node<E> h = null, t = null;
          for (E e : c) { // 遍历c集合
              // 保证元素不为空
              checkNotNull(e);
              // 新生一个结点
              Node<E> newNode = new Node<E>(e);
              if (h == null) // 头节点为null
                  // 赋值头节点与尾结点
                  h = t = newNode;
              else {
                  // 直接头节点的next域
                  t.lazySetNext(newNode);
                  // 重新赋值头节点
                  t = newNode;
              }
          }
          if (h == null) // 头节点为null
              // 新生头节点与尾结点
              h = t = new Node<E>(null);
          // 赋值头节点
          head = h;
          // 赋值尾结点
          tail = t;
      }
      
  2. offer()

  3. poll()

  4. remove()

  5. size()

HOPS(延迟更新的策略)的设计:

通过上面对offer和poll方法的分析,我们发现tail和head是延迟更新的,两者更新触发时机为:

  • tail更新触发时机:当 tail 指向的下一个结点不为null时,完成插入后,通过 casTail() 进行tail更新;当tail指向的结点下一结点为null时,只插入结点不更新tail。
  • head更新触发时机:当head指向的结点的 item 域为null时,完成删除操作后,会通过 updateHead() 进行更新head;否则,只删除结点,不更新head。

补:FutureTask

FutureTask实现了RunnableFuture接口,则RunnableFuture接口继承了Runnable接口和Future接口,所以FutureTask既能当做一个Runnable直接被Thread执行,也能作为Future用来得到Callable的计算结果。

FutureTask 的线程安全由CAS来保证

FutureTask 为 Future 提供了基础实现,如获取任务执行结果(get)和取消任务(cancel)等。如果任务尚未完成,获取任务执行结果时将会阻塞。一旦执行结束,任务就不能被重启或取消(除非使用runAndReset执行计算)。

Future接口

public interface Future<V> {
    boolean cancel(boolean mayInterruptIfRunning);
    boolean isCancelled();
    boolean isDone();
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}
  • cancel() :取消异步任务的执行。
  • isCancel():判断任务是否被取消,如果任务在结束(正常执行结束或者执行异常结束)前被取消则返回true,否则返回false
  • isDone() :判断任务是否已经完成,如果完成则返回true,否则返回false。需要注意的是:任务执行过程中发生异常、任务被取消也属于任务已完成,也会返回true。
  • get() :获取任务执行结果,如果任务还没完成则会阻塞等待直到任务执行完成。
  • get(long timeout, TImeunit unit):带超时时间的get(),如果阻塞等待过程中超时则会抛出TimeoutException异常。

7种状态:

  • NEW:表示是个新的任务或者还没被执行完的任务。这是初始状态。

  • COMPLETING:任务已经执行完成或者执行任务的时候发生异常,但是任务执行结果或者异常原因还没有保存到outcome字段(outcome字段用来保存任务执行结果,如果发生异常,则用来保存异常原因)的时候,状态会从NEW变更到COMPLETING。但是这个状态会时间会比较短,属于中间状态。

  • NORMAL:任务已经执行完成并且任务执行结果已经保存到outcome字段,状态会从COMPLETING转换到NORMAL。这是一个最终态。

  • EXCEPTIONAL:任务执行发生异常并且异常原因已经保存到outcome字段中后,状态会从COMPLETING转换到EXCEPTIONAL。这是一个最终态。

  • CANCELLED:任务还没开始执行或者已经开始执行但是还没有执行完成的时候,用户调用了cancel(false)方法取消任务且不中断任务执行线程,这个时候状态会从NEW转化为CANCELLED状态。这是一个最终态。

  • INTERRUPTING: 任务还没开始执行或者已经执行但是还没有执行完成的时候,用户调用了cancel(true)方法取消任务并且要中断任务执行线程但是还没有中断任务执行线程之前,状态会从NEW转化为INTERRUPTING。这是一个中间状态。

  • INTERRUPTED:调用interrupt()中断任务执行线程之后状态会从INTERRUPTING转换到INTERRUPTED。这是一个最终态。 有一点需要注意的是,所有值大于COMPLETING的状态都表示任务已经执行完成(任务正常执行完成,任务执行异常或者任务被取消)

1654430732676

posted @ 2022-11-28 17:38  柯文先生  阅读(29)  评论(0)    收藏  举报