JUC并发编程之十万个为什么

835 阅读48分钟

多线程基础

什么是JUC?

java.util里的一个工具包

java可以开启线程吗?

不可以.实际上开启线程的是本地方法.

并发和并行的区别是什么?

  1. 并行: 多核CPU同时执行多个任务
  2. 并发: 多个线程同时执行多个任务

怎么获取cpu的核心数?

Runtime.getRuntime().availableProcessors();

为什么需要并发编程?

CPU/内存/IO设备的速度有极大差异,为了合理利用CPU的高性能,操作系统增加了进程/线程.

为了合理利用CPU的性能,带来了哪些问题?

  1. 为了均衡CPU与内存的速度差异,CPU增加了缓存,带来了: 可见性问题.
  2. 为了均衡CPU与I/O设备的速度差异,操作系统增加了进程/线程,以及时分复用CPU.带来了: 原子性问题.
  3. 为了使缓存能够更加合理的利用,编译程序又花了指令执行次序,带来了: 有序性问题.

线程有几个状态?

  1. NEW: 新生
  2. RUNNABLE: 运行
  3. BLOCKED: 阻塞
  4. WAITING: 等待
  5. TIMED_WAITING: 超时等待
  6. TERMINATED: 终止

wait和sleep有什么区别?

  1. 所属的类: wait是Object类的,sleep是Thread类的
  2. 释放锁: wait释放锁,sleep不释放锁
  3. 使用范围: wait必须在同步代码块中,sleep可以在任何地方
  4. 是否捕获异常: wait不需要捕获异常,sleep必须捕获异常

synchronize怎么保证...?

以下代码,加了synchronize锁之后就是有序执行的.

public class SaleTicketDemo01 {
    public static void main(String[] args) {
         Ticket ticket = new Ticket();
         //三个线程都去售票,超过总票数,确保卖完
        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();
    }
}
class Ticket {
    private int number = 50;
    public synchronized void sale() {//卖票 同步锁
        if (number > 0) {
            System.out.println(Thread.currentThread().getName() + ",还剩余" + (number--) + "张票");
        }
    }
}

结果:严格按照顺序来售票.

A,还剩余50张票
A,还剩余49张票
A,还剩余48张票
A,还剩余47张票
A,还剩余46张票
......

如果不加synchronize就是乱序的.

为什么上面的例子不加锁就是乱序的?

因为上面的打印语句不是原子的.所以有可能已经执行了number--,但是没有立即执行打印,被其他线程抢先了.如果仔细观察乱序的结果,会发现大体是顺序的,出现乱序都是在切换线程的时候.

什么是公平锁和非公平锁?

公平锁: 按照先来后到执行. 非公平锁: 乱序的 代码:

public class MyFairLock {
    private  ReentrantLock lock = new ReentrantLock(true);//true 表示 ReentrantLock 的公平锁
    public   void testFail(){
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName() +"获得了锁");
        }finally {
            lock.unlock();
        }
    }
    public static void main(String[] args) {
        MyFairLock fairLock = new MyFairLock();
        Runnable runnable = () -> {
            System.out.println(Thread.currentThread().getName()+"启动");//表示先后顺序
            fairLock.testFail();//进入锁
        };
        for(int i=0;i<100;i++){
            new Thread(runnable).start();
        }
    }
}

结果:

Thread-0启动
Thread-0获得了锁
Thread-3启动
Thread-2启动
Thread-1启动
Thread-6启动
Thread-4启动
Thread-3获得了锁
Thread-5启动
Thread-7启动
Thread-13启动
Thread-2获得了锁
......

如果上面的例子改为非公平锁,结果就是启动锁和获得锁的顺序是乱序的

lock锁和sychronized有什么区别?

  1. 类型: synchronize是java关键字,lock是java一个类
  2. 能够判断是否已经获得了锁: ?????
  3. 自动释放锁: synchronized会自动释放锁,lock不会,需要手动释放

锁是什么?

谁持有锁?

如何判断锁的是谁?

怎么写一个生产者-消费者模式来使用资源?

代码

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 {
        while(number != 0 ){//使用while,虚假唤醒之后会再去判断条件
            this.wait();//如果没有消费完,就等待
        }
        number++;//执行具体业务
        System.out.println(Thread.currentThread().getName()+">>>>>>>>>"+number);
        this.notifyAll();//通知其他线程,我+1完毕
    }
    //-1
    public synchronized void decrement() throws InterruptedException {
        while(number == 0){
            this.wait();//如果消费完了就等待
        }
        number--;//执行业务
        System.out.println(Thread.currentThread().getName()+">>>>>>>>>"+number);
        //通知其他线程,我-1完毕
        this.notifyAll();
    }
}

执行结果:

A>>>>>>>>>1
B>>>>>>>>>0
A>>>>>>>>>1
B>>>>>>>>>0
......

生产者消费者的模板: 等待/执行业务/唤醒.即使不止两个线程,结果也是类似的,生产者生产后,等待消费者消费. 注意: 在等待的时候,不能使用if做判断,否则可能出现虚假唤醒.

什么是虚假唤醒?

多处理器的系统下,发出wait的程序有可能在没有notify的情形下苏醒继续执行.底层wait函数在设计之初为了不减慢条件变量操作的效率,没有保证每次唤醒都由notify触发,而把这个任务交由上层应用去实现,需要定义一个循环去判断是否条件真能满足程序继续运行的需求.所以判断的时候,不能使用if来判断,而是应该由while来判断,这样每次都会进行判断条件,防止wait虚假唤醒.

怎么解决虚假唤醒?

将一次判断改为多次判断.

如果使用lock怎么写生产者-消费者的使用资源?

测试代码的逻辑还是一样.区别在于等待和唤醒的逻辑变了.不再使用this.wait()this.notifyAll(),而是使用condition.await()condition.signalAll() 生产者和消费者的代码如下:

//lock.newCondition().await();//等待
//ock.newCondition().signalAll();//唤醒
class Data2 {//数字 资源类
    private int number = 0;
    Lock lock = new ReentrantLock();//新建一把锁
    Condition condition = lock.newCondition();//新建一个条件
    //+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 (Exception 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 (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

执行结果:

A>>>>>>>>>1
B>>>>>>>>>0
A>>>>>>>>>1
D>>>>>>>>>0
C>>>>>>>>>1
B>>>>>>>>>0
......

怎么写一个有多个生产者-多个消费者模式来使用资源?

使用ReentrantLockCondition.ReentrantLock可以定义多个Condition,并且可以指定满足哪些条件的线程等待,也可以指定哪些线程唤醒. 省略测试代码,代码如下:

class Data3{//资源类
    private Lock lock = new ReentrantLock();
    private Condition condition1 = lock.newCondition();//条件1
    private Condition condition2 = lock.newCondition();//条件2
    private Condition condition3 = lock.newCondition();//条件3
    private int number = 1;//1A 2B 3C
    public void printA(){
        lock.lock();
        try {
            //判断等待->执行->唤醒
            while (number != 1){//非1:等待
                condition1.await();
            }
            System.out.println(Thread.currentThread().getName()+">>>>>>>A");
            number = 2;//唤醒指定的人B
            condition2.signal();//唤醒B
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    public void printB(){
        lock.lock();
        try {
            while (number != 2){//不等于2就等待
                condition2.await();
            }
            System.out.println(Thread.currentThread().getName()+">>>>>>>B");
            number = 3;//唤醒执行的人C
            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()+">>>>>>>C");
            number = 1;//唤醒A
            condition1.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

结果:

T1>>>>>>>A
T2>>>>>>>B
T3>>>>>>>C
T1>>>>>>>A
T2>>>>>>>B
T3>>>>>>>C
......

notify()notifyAll()的区别是什么?

使用notify()的时候,只有一个等待线程会被唤醒,但是不能保证是哪个,notifyAll()会唤醒所有的线程.

condition比object的wait()notify()有什么优势?

锁的问题可以简化为:谁持有了什么锁?锁的所有者都是线程,持有的锁分为对象和类.wait()就是让线程进入等待状态(调用wait方法的对象必须是锁的持有对象.),notify()就是唤醒某一个线程,notifyAll()是唤醒所有线程.问题是:不管是让线程等待还是唤醒,该方法与当前线程没有建立联系.而condition对象可以新建多个,每一个同步代码块里都放一个,这样,可以等待await()或者唤醒signal()指定的同步代码块里的线程. 使用condition后可以让线程按照指定的顺序执行.

什么是8锁现象?

标准情况下谁先执行?

代码:

public class Test1 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(()->{
            phone.sendSms();
        },"A").start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            phone.call();
        },"B").start();
    }
}
class Phone{
    //synchronize锁的对象是方法的调用者!
    //两个方法用的是同一把锁,谁先拿到谁先执行
    public synchronized void sendSms(){
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    public synchronized void call(){
        System.out.println("打电话");
    }
}

结果: 延迟4秒后先发短信,后打电话.

发短信
打电话

结论: 谁先拿到锁,谁先执行.

两个对象,两把锁,谁先执行?

代码如下:

public class Test2 {
    public static void main(String[] args) {
        //两个对象,两把锁
        Phone2 phone = new Phone2();
        Phone2 phone2 = new Phone2();
        new Thread(()->{
            phone.sendSms();
        },"A").start();
        //等待1秒
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            phone2.call();
        },"B").start();
    }
}
class Phone2{
    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");
    }
}

结果: 等待1s后先执行打电话,再等待3s执行发短信.

打电话
发短信

结论:当使用两把锁的时候,互不影响.

两个对象,调用两个静态加锁方法,先打电话还是先发短信?

public class Test3 {
    public static void main(String[] args) {
        //两个对象的class类模板只有一个,static锁的是class
        Phone3 phone = new Phone3();
        Phone3 phone2 = new Phone3();
        new Thread(()->{
            phone.sendSms();
        },"A").start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            phone2.call();
        },"B").start();
    }
}
class Phone3{
    //synchronize锁的对象是方法的调用者!
    //类一加载就有了!锁的是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("打电话");
    }
}

结果: 等待4s后,先发短信再打电话

发短信
打电话

结论: 静态加锁的方法,锁的是类,因此还是先持有锁的先执行.

两个对象,分别调用静态加锁方法和普通加锁方法,先打电话还是先发短信?

public class Test4 {
    public static void main(String[] args) {
        //两个对象的class类模板只有一个,static锁的是class
        Phone4 phone = new Phone4();
        Phone4 phone2 = new Phone4();
        new Thread(()->{
            phone.sendSms();
        },"A").start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            phone2.call();
        },"B").start();
    }
}
class Phone4{
    //静态的同步方法
    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("打电话");
    }
}

结果: 1s后先打电话,再3s后发短信.

打电话
发短信

结论: 一个锁的是类,一个锁的是类的对象.两把锁互不影响.

为什么说list类不安全?

以下代码:

public static void main(String[] args) {
    List<String> list = new ArrayList<>();
    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();
    }
}

执行结果:

[null, 9c18d, d25bf, 0b19c, 8f23b, 1f7d3]
[null, 9c18d, d25bf, 0b19c, 8f23b, 1f7d3, 74ac0, 5d23a, 86246, 476b8]Exception in thread "4" 
Exception in thread "1" Exception in thread "6" [null, 9c18d, d25bf, 0b19c, 8f23b, 1f7d3]
Exception in thread "5" [null, 9c18d, d25bf, 0b19c, 8f23b, 1f7d3, 74ac0, 5d23a, 86246]
[null, 9c18d, d25bf, 0b19c, 8f23b, 1f7d3, 74ac0, 5d23a]
[null, 9c18d, d25bf, 0b19c, 8f23b, 1f7d3, 74ac0]
java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
......

总结: 报错了.ConcurrentModificationException:并发修改异常.

怎么解决list类的线程不安全?

  1. 使用线程安全的Vector类
  2. 使用工具包Collections下的:List<String> list = Collections.synchronizedList(new ArrayList<>());
  3. 使用JUC包下的:CopyOnWriteArrayList: List<String> list = new CopyOnWriteArrayList<>();

CopyOnWriteArrayList的原理是什么?

add方法源码:

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

原理:

  1. 获取锁(保证多线程的安全访问),获取当前的Object数组,获取Object数组的长度为length,进入步骤②。
  2. 根据Object数组复制一个长度为length+1的Object数组为newElements(此时,newElements[length]为null),进入下一步骤。
  3. 将下标为length的数组元素newElements[length]设置为元素e,再设置当前Object[]为newElements,释放锁,返回。这样就完成了元素的添加。

CopyOnWriteArrayList比Vector的优势在哪?

Vector是增删改查方法都加了synchronized,保证同步,但是每个方法执行的时候都要去获得锁,性能就会大大下降, 而CopyOnWriteArrayList 只是在增删改上加锁,但是读不加锁,在读方面的性能就好于Vector,CopyOnWriteArrayList支持读多写少的并发情况.

CopyOnWriteArrayList为什么叫CopyOnWriteArrayList

读写分离,写入的时候先复制一份旧的,再将值塞到新的里面,最后将引用指向新的list.

怎么解决set类的线程不安全?

使用JUC包下的CopyOnWriteArraySet: Set<String> set = new CopyOnWriteArraySet<>(); 使用Set<String> set = Collections.synchronizedSet(new HashSet<>());

HashSet的底层是什么?

HashMap.set里面的值对应HashMap里面的键.

什么是哈希表解决冲突的开放地址法和链表法?

  1. 开放地址法: 当发生地址冲突时,按照某种方法继续探测哈希表中的其他存储单元,直到找到空位置为止。
  2. 它是在出现冲突的地方存储一个链表(jdk1.8之后采用链表+红黑树),所有的同义词记录都存在其中

HashMap中hashCode()方法的作用?

HashMap

hashCode()方法决定了对象会被放到哪个bucket里

HashMap中equals()方法的作用?

当多个对象的哈希值冲突时,equals()方法决定了这些对象是否是“同一个对象”.

为什么HashMap的容量为2的整数次幂?

n为2的整数倍,那么n-1就是一个奇数,奇数的二进制最后一位肯定为1.为1的好处就是(n-1) & hash的值后一位为0或者为1,如果n不是2的整数幂,那么(n-1) & hash的运算结果后一位始终为0,这样下标结果肯定为偶数. 导致所有的数据都只能存放在偶数位置。

什么是|=?

类似于+=,拆开就是a = a | b,计算规则:两个二进制对应位同时为0时,结果为0,否则为1.

什么是&=?

两个二进制对应位同时为1时结果为1,否则为0.

什么是^=?

两个二进制对应位相同时为0,否则为1.

<< >> >>>分别是是什么运算?

  1. <<,左移,低位补0
  2. >>,右移,如果该数是正数,高位补0,若为负数,高位补1.
  3. >>>,无符号右移,不管该数正负,高位补0.

给定一个正整型数值,如何获取该数下一个2的n次幂值?

思路: 先把这个数从最高位开始,每一位都变成1.然后再加1. HashMap的源代码如下:

private static final int tableSizeFor(int c) {
    int n = c - 1;//先-1,防止进位
    n |= n >>> 1;//右移1位,然后按位或,那么最高位和次高位都是1.
    n |= n >>> 2;//右移2位,然后按位或,那么前四位都是1
    n |= n >>> 4;//前8位都是1
    n |= n >>> 8;//前16位都是1
    n |= n >>> 16;//前32位都是1.因为int最高只有32位,因此从最高位开始所有个位数都是1了
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;//如果没有超过最大空间,那么+1,就成了下一个2的n次幂.
}

HashMap的容量大小为什么是2的整数次幂?

  1. hash方法: 将key的object转换为一个整数的方法
  2. indexFor方法: 将hash求的整数转换为链表数组的下标 JDK 1.7源码如下:
static int indexFor(int h, int length) {
    return h & (length-1);
}

解释: 参数h就是hash方法得到的整数,length是hashMap的容量,为2的n次幂 length-1就是得到一个比容量少一位的各位都是1的二进制数,再与整数h按位与,其实就是去除比容量最高位大的部分,保留比容量最高位小的部分,就是取余.所以这个操作就相当于是高效的取余操作.即X % 2^n = X & (2^n – 1). 示例: 10&7=2

001010
&
000111
=
000010

总结: JDK工程师为了提高取模的效率,使用位运算代替了取模运算,这就要求Map的容量一定得是2的整数次幂.

HashMap数据结构以及2的整数次幂探究

ConcurrentHashMap的原理是什么?

参考: 别再问我ConcurrentHashMap了
参考: 《面试官》系列-ConcurrentHashMap & Hashtable(文末送书)

Callable相比Runnable有什么特点?

  1. callable有返回值
  2. callable可以抛出异常
  3. 需要重写的方法是call()方法

两个线程去执行callable,会执行几次?

1次.会缓存结果.

怎么使用callable?

将callable对象放到futureTask中,因为futureTask同时实现了Runnable接口,因此,可以直接放在Thread中执行. 代码如下:

public class CallableTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //怎么启动callable?
        //1. 新建一个callable对象
        MyThread myThread = new MyThread();
        //2. 将callable对象转换为futureTask对象.因为futureTask对象既实现了callable接口,又实现了runnable接口
        FutureTask futureTask = new FutureTask(myThread);
        //将futureTask放到Thread对象里
        new Thread(futureTask,"A").start();
        new Thread(futureTask,"B").start();//结果会被缓存,提高效率
        //可能产生阻塞,把它放到最后去获取
        //或者使用异步通讯来处理
        Integer result = (Integer) futureTask.get();
        System.out.println(result);
    }
}
//继承callable
class  MyThread implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        Thread.sleep(2000);
        System.out.println("进入call>>>>>>>>>>>>");
        return 1024;
    }
}

CountDownLatch的作用是什么?

减法计数器.作用是等到线程都执行完了再继续往下执行.

CountDownLatch的原理是什么?

CyclicBarrier的作用是什么?

类似于加法计数器.线程执行到某处后开始等待,计数器增加到指定值后,可以执行特定的操作.线程可以重复等待. 代码如下:

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        //集齐7颗龙珠召唤神龙
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
            System.out.println(Thread.currentThread().getName()+"召唤神龙成功");
        });
        for(int i=1;i<=7;i++){
            //lamba表达式能操作到i吗?
            int finalI = i;
            new Thread(()->{
                try {
                    System.out.println(Thread.currentThread().getName()+"收集了"+finalI+"颗龙珠");
                    cyclicBarrier.await();
                    System.out.println(Thread.currentThread().getName()+"收集了"+finalI+"颗龙猪");
                    cyclicBarrier.await();
                    System.out.println(Thread.currentThread().getName()+",龙猪集齐,天下无敌");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

执行结果:

Thread-1收集了2颗龙珠
Thread-3收集了4颗龙珠
Thread-0收集了1颗龙珠
Thread-2收集了3颗龙珠
Thread-4收集了5颗龙珠
Thread-5收集了6颗龙珠
Thread-6收集了7颗龙珠
Thread-6召唤神龙成功
Thread-6收集了7颗龙猪
Thread-1收集了2颗龙猪
Thread-4收集了5颗龙猪
Thread-2收集了3颗龙猪
Thread-5收集了6颗龙猪
Thread-0收集了1颗龙猪
Thread-3收集了4颗龙猪
Thread-3召唤神龙成功
Thread-3,龙猪集齐,天下无敌
Thread-1,龙猪集齐,天下无敌
Thread-4,龙猪集齐,天下无敌
Thread-5,龙猪集齐,天下无敌
Thread-0,龙猪集齐,天下无敌
Thread-6,龙猪集齐,天下无敌
Thread-2,龙猪集齐,天下无敌

countdownLatch和cyclicBarrier差异总结如下:

  1. countdownLatch是减法,cyclicBarrier是加法
  2. countdownLatch需要代码手动实现减法计算,cyclicBarrier线程到达await()方法自动执行加法计数
  3. cyclicBarrier可以指定线程到达await()后执行的操作,countdownLatch不行.

CyclicBarrier的原理是什么?

Semaphore的作用是什么?

semophore类似于一把资源锁,与synchronize或者lock类似,但是可以允许大于一个线程进入锁. 代码如下:

public class SemophoreDemo {
    public static void main(String[] args) {
        //线程数量:停车位
        Semaphore semaphore = new Semaphore(3);
        for(int i=1;i<=6;i++){
            new Thread(()->{
                //acquire()得到
                try {
                    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();
        }
    }
}

执行结果:

2抢到车位
1抢到车位
3抢到车位
2离开车位
1离开车位
3离开车位
6抢到车位
4抢到车位
5抢到车位
4离开车位
5离开车位
6离开车位

Semaphore的原理是什么?

什么是读写锁?

读-读共存,读-写不共存,写-写不共存.

读写锁怎么使用?

  1. 先定义一把锁
  2. 在使用的时候可以选择读锁或者写锁.
  3. 手动lock()unlock() 代码如下:
public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache2 myCache = new MyCache2();
        //写入
        for (int i = 1; i < 10; i++) {
            int finalI = i;
            new Thread(() -> {
                myCache.put(finalI + "", finalI + "");
            }, String.valueOf(i)).start();
        }
        //读取
        for (int i = 1; i < 10; i++) {
            int finalI = i;
            new Thread(() -> {
                myCache.get(finalI + "");
            }, String.valueOf(i)).start();
        }
        //读取2
        for (int i = 1; i < 10; i++) {
            int finalI = i;
            new Thread(() -> {
                myCache.get2(finalI + "");
            }, String.valueOf(i)).start();
        }
    }
}
/**
 * 自定义缓存,加锁的
 */
class MyCache2 {
    private volatile Map<String, Object> map = new HashMap<>();
    //读写锁,更加细粒度的操作
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    //存,写.写入的时候只希望同时只有一个线程可以写
    public void put(String key, Object value) {
        readWriteLock.writeLock().lock();
        try {
            TimeUnit.SECONDS.sleep(1);
            System.out.println(Thread.currentThread().getName() + "写入" + key);
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "写入完毕");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }
    //取,读
    public Object get(String key) {
        readWriteLock.readLock().lock();
        try {
            TimeUnit.SECONDS.sleep(1);
            System.out.println(Thread.currentThread().getName() + "读取" + key);
            Object o = map.get(key);
            System.out.println(Thread.currentThread().getName() + "读取完毕" + o);
            return o;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
        }
        return null;
    }
    //取,读
    public Object get2(String key) {
        readWriteLock.readLock().lock();
        try {
            TimeUnit.SECONDS.sleep(1);
            System.out.println(Thread.currentThread().getName() + "读取===" + key);
            Object o = map.get(key);
            System.out.println(Thread.currentThread().getName() + "读取===完毕" + o);
            return o;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
        }
        return null;
    }
}

结果:

1写入1
1写入完毕
3写入3
3写入完毕
4写入4
4写入完毕
2写入2
2写入完毕
5写入5
5写入完毕
6写入6
6写入完毕
7写入7
7写入完毕
8写入8
8写入完毕
9写入9
9写入完毕
6读取6
6读取完毕6
6读取===6
6读取===完毕6
9读取===9
9读取===完毕9
5读取===5
5读取===完毕5
2读取===2
2读取===完毕2
8读取===8
8读取===完毕8
3读取3
7读取===7
7读取===完毕7
7读取7
7读取完毕7
3读取===3
3读取===完毕3
1读取===1
1读取===完毕1
4读取===4
4读取===完毕4
9读取9
9读取完毕9
5读取5
5读取完毕5
4读取4
4读取完毕4
1读取1
1读取完毕1
8读取8
8读取完毕8
2读取2
2读取完毕2
3读取完毕3

总结: 写入的时候会占有锁,因此每隔1秒输出一次结果,输出完后,读取的时候,所有结果一起输出.说明读-写不能共存,读读可以共存.

什么是共享锁和独占锁?

使用了读写锁后,可以同时读吗?可以读写共存吗?可以同时写吗?

可以同时读,不能同时读写,不能同时写.

什么是阻塞队列?

  1. 队列满了,就阻塞入队的操作
  2. 队列空了,就阻塞出队的操作

什么情况下会使用阻塞队列?

  1. 生产者-消费者
  2. 线程池

什么是AbstractQueue?

ArrayBlockingQueue抛出异常的方法是什么?

add(增)|remove(删)|element(查看队首元素)

ArrayBlockingQueue不抛出异常的方法是什么?

offer|poll|peek

ArrayBlockingQueue一直阻塞的方法是什么?

put|take

ArrayBlockingQueue超时退出的阻塞方法是什么?

offer("d", 2, TimeUnit.SECONDS)|poll(2,TimeUnit.SECONDS)

什么是SychronizeQueue?

同步队列.是一个不存储元素(存一个元素就必须取出来才能继续存.),取出元素直接交给消费者. 代码如下:

public class SychronizeQueueDemo {
    public static void main(String[] args) {
        SynchronousQueue<String> queue = new SynchronousQueue<>();
        new Thread(()->{
            try {
                System.out.println(Thread.currentThread().getName() + "put 1");
                queue.put("1");
                System.out.println(Thread.currentThread().getName() + "put 2");
                queue.put("2");
                System.out.println(Thread.currentThread().getName() + "put 3");
                queue.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"T1").start();
        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(1);
                System.out.println(Thread.currentThread().getName() + "take" + queue.take());
                TimeUnit.SECONDS.sleep(2);
                System.out.println(Thread.currentThread().getName() + "take" + queue.take());
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName()  + "take" + queue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"T2").start();
    }
}

结果如下:

T1put 1
T2take1
T1put 2
T2take2
T1put 3
T2take3

总结: 运行后先展示put,1s后先take再put,2s后先take再put,3s后take.说明: 如果没有take,put操作就会阻塞等待,实际上没有put,take操作也会阻塞等待.

SychronizeQueue在多线程下是否安全?

什么是池化技术?

就是提前保存一些资源,以备不时之需.

  1. 线程池: 先启动若干数量的线程,并让这些线程都处于睡眠状态,当客户端有一个新的请求时,就会唤醒线程池中的某一个睡眠线程,让它来处理客户端的这个请求,当处理完这个请求,线程又进入睡眠状态.
  2. 内存池: 预先分配足够大的内存,形成一个初步的"内存池",释放内存是,不是真正的释放或者删除,而是把内存放回内存池的过程.把内存放入内存池的同事,要把标志位置为空闲,最后应用程序结束时,要把内存池销毁.主要工作就是把内存池中的每一块内存释放.
  3. 数据库连接池: 数据库连接是一种关键的有限的昂贵的资源,数据库连接池就是在应用程序启动时简历足够的数据库连接,并将这些链接组成一个连接池.有应用程序动态的池中的连接进行申请|动态增加|减少池中的连接数.

有哪些池?

  1. 线程池
  2. 内存吃
  3. 数据库连接池
  4. 对象池

线程池有哪些好处?

  1. 降低资源的消耗
  2. 提高响应速度
  3. 方便管理

线程池的3大方法是什么?

  1. Executors.newSingleThreadExecutor();//生成单个线程池
  2. Executors.newFixedThreadPool(5);//生成一个固定大小的线程池
  3. Executors.newCachedThreadPool();//生成一个可伸缩的线程池

FixedThreadPool和SingleThreadPool的隐患是什么?

允许的请求队列长度为Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM.

CachedThreadPool和ScheduleThreadPool的隐患是什么?

允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM.

线程池的7大参数是什么?

  1. int corePoolSize//核心线程池大小,核心线程会一直存在
  2. int maximumPoolSize//最大核心线程池大小,线程池中允许的最大线程数量
  3. long keepAliveTime//超时没人调用就会释放,线程数量超过核心线程池大小,并且超过这个时间,线程会被结束.
  4. TimeUnit unit//超时单位
  5. BlockingQueue<Runnable> workQueue//阻塞队列,任务会先被存放在这个队列中,直到被线程执行.
  6. ThreadFactory threadFactory//线程工厂:创建线程的,一般不用动.创建线程时调用
  7. RejectedExecutionHandler handle//拒绝策略,任务数量达到队列的最大容量时的执行策略. 代码如下:
public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {

线程池的4种拒绝策略是什么?

  1. new ThreadPoolExecutor.DiscardOldestPolicy()//丢弃最早的未处理的任务,然后重试.不抛出异常
  2. new ThreadPoolExecutor.DiscardPolicy//静静的丢弃任务,不抛出异常
  3. new ThreadPoolExecutor.AbortPolicy//丢弃任务,并抛出异常:RejectedExecutionException
  4. new ThreadPoolExecutor.CallerRunsPolicy//直接在调用该方法的线程里执行任务.(和没调线程池一样),不抛出异常

什么是CPU密集型任务?

进行大量的运算的任务,如:计算圆周率|对视频进行高清解码等,这种计算密集型任务虽然可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低.所以要高效的利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数.

什么是IO密集型任务?

涉及到网络|磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成.对于IO密集型任务,任务越多,CPU效率越高.常见的大部分任务都是IO密集型任务,如Web应用.

最大线程到底该如何定义?

怎么自定义线程池?

代码如下:

public class Demo1 {
    public static void main(String[] args) {
//        ExecutorService executorService = Executors.newSingleThreadExecutor();//单个线程
//        ExecutorService executorService = Executors.newFixedThreadPool(5);//创建一个固定大小的线程池
//        ExecutorService executorService = Executors.newCachedThreadPool();//可伸缩的,遇强则强,遇弱则弱
        //最大线程到底该如何定义
        // 1. CPU密集型,几核就定义为几
        // 2. IO密集型,判断程序中十分耗IO的线程
        ExecutorService executorService = new ThreadPoolExecutor(2,
                5,
                3,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
//                new ThreadPoolExecutor.AbortPolicy());//拒绝并报错
//                new ThreadPoolExecutor.CallerRunsPolicy());//哪来的回哪去
//                new ThreadPoolExecutor.DiscardPolicy());//队列满了,丢掉任务不会抛出异常
                new ThreadPoolExecutor.DiscardOldestPolicy());//队列满了,尝试和最早的竞争,不会抛出异常
        try {
            for (int i = 0; i < 9; i++) {
                executorService.execute(()->{
                    System.out.println(Thread.currentThread().getName()+" ok");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //线程池用完,程序结束,关闭线程池
            executorService.shutdown();
        }
    }
}

执行结果:

pool-1-thread-1 ok
pool-1-thread-4 ok
pool-1-thread-4 ok
pool-1-thread-3 ok
pool-1-thread-2 ok
pool-1-thread-4 ok
pool-1-thread-1 ok
pool-1-thread-5 ok

分析: 超过线程池核心线程数,启用最大5个线程取执行任务.

新时代的程序员必须掌握的四种编程风格是什么?

  1. lambda表达式
  2. 链式编程
  3. 函数式接口
  4. Stream流式计算

四大函数式接口是哪四大?

  1. Supplier
  2. Consumer
  3. Predicate
  4. Function

什么是函数式接口?

有且仅有一个抽象方法.

函数式接口有什么用?

什么是Stream流式计算?

流仅仅代表着数据流,并没有数据结构,它的来源可以是Collection|array|io等.流的作用是提供了一种操作大数据接口,让数据操作更容易和更快.它具有过滤/映射以及减少遍历数等方法.它的方法分为: 中间方法和终端方法.中间方法返回的永远是Stream.

Stream有哪些方法?

  1. Stream<T> filter(Predicate<? super T> predicate):中间方法,返回符合条件的流
  2. Stream<R> map(Function<? super T, ? extends R> mapper):对流中的元素应用方法,并返回新的流
  3. Stream<T> distinct();: 返回包含不同元素的流.对于有序的流,不同元素中保留的总是第一次出现的元素.
  4. Stream<T> sorted();:自然排序
  5. Stream<T> sorted(Comparator<? super T> comparator);: 使用传入的比较器排序.
  6. Stream<T> peek(Consumer<? super T> action);
  7. Stream<T> limit(long maxSize);: 将流中的元素截断为不超过maxSize长度.
  8. Stream<T> skip(long n);: 丢掉第一个元素,然后返回流.如果元素只有一个,将会返回一个空的流.
  9. void forEach(Consumer<? super T> action);: 对流中的每一个元素执行操作
  10. Object[] toArray();: 返回一个包含流中元素的数组
  11. T reduce(T identity, BinaryOperator<T> accumulator);:
  12. <R, A> R collect(Collector<? super T, A, R> collector);:
  13. Optional<T> min(Comparator<? super T> comparator);:

怎么用流式计算?

什么是Optional?

Optional是一个包装类.简单说就是把NULL包装了一层,防止对NULL操作报空指针异常.

Optional有哪些方法?

  1. 获取Optional实例的方法: 1.1. public static <T> Optional<T> empty(): 获取一个Optional空实例 1.2. public static <T> Optional<T> of(T var0): 获取一个Optional实例,var0为null会报空指针 1.3. public static <T> Optional<T> ofNullable(T var0): 获取一个Optional实例.var0为null会返回一个Optional空实例.
  2. 判断Optional是否存在? 2.1. public boolean isPresent(): 返回Optional实例中的value是否为NULL 2.2. public void ifPresent(Consumer<? super T> var1): value不为NULL则执行var1
  3. 判断是否相等 3.1. public boolean equals(Object var1): optional实例中的value为NULL也不会有异常,而是会正常进行比较.

Optional怎么使用?

代码如下:

public class OptionalTest {
    static List<User> list = new ArrayList<>();
    public static void main(String[] args) {
        User user1 = new User("1","guohao",11);
        User user2 = new User("2","liqin",10);
        list.add(user1);
        list.add(user2);
        Optional<User> user3 = Optional.ofNullable(getUserById("1"));
        user3.ifPresent(u-> System.out.println(u.getName()));
        Optional<User> user4 = Optional.ofNullable(getUserById("3"));
        user4.ifPresent(u-> System.out.println(u.getName()));
    }
    public static User getUserById(String id){
        return list.stream().filter(u -> u.getId() == id).findAny().orElse(new User());
    }
}

总结: Optional可以有效减少空指针的判断.与Stream流搭配使用非常简洁. 参考链接: Java高级(三):Optional的巧用 参考链接: JAVA8之妙用Optional解决判断Null为空的问题

什么是Function函数式接口?

Function<T, R>: V代表输入参数,R代表返回的结果.该函数式接口的作用等同于y=f(x).

  1. R apply(T t);//将Function对象应用到输入的参数上,然后返回计算结果。
  2. Function<T, V> andThen(Function<? super R, ? extends V> after)//返回一个先执行当前函数对象apply方法再执行after函数对象apply方法的函数对象。return (T t) -> after.apply(apply(t));
  3. Function<V, R> compose(Function<? super V, ? extends T> before)//返回一个先执行before函数对象apply方法再执行当前函数对象apply方法的函数对象.return (V v) -> apply(before.apply(v)); 代码如下:
public class Demo01 {
    public static void main(String[] args) {
        Function<Integer, Integer> name = e -> e * 2;
        Function<Integer, Integer> square = e -> e * e;
        int value = name.andThen(square).apply(3);//36
        System.out.println("andThen value=" + value);
        int value2 = name.compose(square).apply(3);//18
        System.out.println("compose value2=" + value2);
        //返回一个执行了apply()方法之后只会返回输入参数的函数对象
        Object identity = Function.identity().apply("huohuo");
        System.out.println(identity);
    }
}

结果:

andThen value=36
compose value2=18
huohuo

总结: andThen()会先执行apply,再将结果填入调用者的apply方法执行.compose()会先调用compose()中的function方法,然后将结果填入apply方法

什么是Predicate接口?

有入参,返回true或者false的接口.

  1. boolean test(T t);
  2. Predicate<T> and(Predicate<? super T> other)//return (t) -> test(t) && other.test(t);
  3. Predicate<T> negate()//return (t) -> !test(t);
  4. Predicate<T> or(Predicate<? super T> other)//return (t) -> test(t) || other.test(t);

什么是Consumer接口?

方法只有入参,没有返回.

  1. void accept(T t);//执行方法
  2. Consumer<T> andThen(Consumer<? super T> after)//return (T t) -> { accept(t); after.accept(t); };先执行accept方法,再执行andThen()中的方法 代码如下:
public class Demo03 {
    public static void main(String[] args) {
        testAndThen();
    }
    public static void testAndThen(){
        Consumer<Integer> consumer1 = x -> System.out.println("first x : " + x);
        Consumer<Integer> consumer2 = x -> {
            System.out.println("second x : " + x);
        };
        Consumer<Integer> consumer3 = x -> System.out.println("third x : " + x);
        consumer1.andThen(consumer2).andThen(consumer3).accept(1);
    }
}

输出结果:

first x : 1
second x : 1
third x : 1

总结: 1. acceptandThen联合使用的时候,accept的入参会作为共同的入参. 2. 先执行accept,然后按照先后顺序执行andThen.

什么是Supplier接口?

方法只有返回,没有入参.只有一个get方法 T get(); 代码示例:

Supplier<String> supplier = ()->{return "hehe";};
System.out.println(supplier.get());

代码如下:

public static void main(String[] args) {
    Predicate<String> predicate = str->{return str==null || str.isEmpty();};
    System.out.println(predicate.test("123"));
}

join(long millis)怎么用?

join的作用是最多等待多少秒直到调用join的线程死亡.当线程终止的时候调用notifyAll来唤醒其他线程.

什么是双冒号运算符?

双冒号(::)运算符在java 8中被用作方法引用(method reference),方法引用是与lambda表达式相关的一个重要特性,它提供了一种不执行方法的方法.lambda是匿名的调用方法,而双冒号就是使用名字调用一个已经存在的方法.

  1. 静态方法的引用: classname::methodname,例如:Person::getAge
  2. 对象的实例方法引用: instancename::methodname,例如:System.out::println
  3. 对象的超类方法引用: super::methodname
  4. 类构造器引用: classname:new,例如:Arraylist::new
  5. 数组构造器引用: typename[]::new,例如:String[]::new

什么是ForkJoin?

本质是一个用于并行执行任务的框架,能够把一个大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务的计算结果.ForkJoin框架是Jdk 1.7引入的新特性,同ThreadPoolExecutor一样,实现了Executor和ExecutorService接口.它使用了一个无限队列来保存需要执行的任务,而线程的数量则是通过构造函数传入,如果没有传入,默认为当前计算机的可用CPU数量.

什么是工作窃取?

我们把大人物分割为互不依赖的子任务,为了减少线程间的竞争,把子任务放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应.但是有的线程会把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理,于是它就去其他线程的队列里窃取一个任务来执行.为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿去任务,而窃取任务的线程永远从双端队列的尾部拿任务执行.

怎么使用ForkJoin?

  1. 新建一个类,继承RecursiveTask类.
  2. 重写compute方法,方法里写合并分支计算的规则.使用fork来讲任务加入线程队列.
  3. 使用ForkJoinPool,使用合并分支计算. 代码如下:
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 {
            //分支合并计算
            long middle = (end + start)/2;//中间值
            ForkJoinDemo task1 = new ForkJoinDemo(start,middle);
            task1.fork();//拆分任务,把任务压入线程队列
            ForkJoinDemo task2 = new ForkJoinDemo(middle+1,end);
            task2.fork();//拆分任务,把任务压入线程队列
            return task1.join()+task2.join();
        }
    }
}
public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
//        test1();//6460
        test2();//4491
//        test3();//205
    }
    //普通程序员
    public static void test1(){
        long start = System.currentTimeMillis();
        Long sum = 0L;
        for(Long i=1L;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(1, 10_0000_0000);
        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(0, 10_0000_0000).parallel().reduce(0, Long::sum);
        long end = System.currentTimeMillis();
        System.out.println("sum = "+sum+" 时间:"+(end-start));
    }
}

总结: 使用ForkJoin可以提高运算效率,使用并行流计算,可以大大提高运行效率.

RecursiveAction和RecursiveTask有什么区别?

都是递归的方式来执行任务.

  1. RecursiveAction没有返回值,实现Runnable接口
  2. RecursiveTask有返回值,实现Callable接口.

什么是CompletableFuture?

JDK 1.8新增的类.拓展了Future,可以简化异步编程,并提供函数式编程的能力,可以通过回调的方式处理计算结果,也提供了转换和组合CompletableFuture方法.

为什么增加CompletableFuture类?

JDK 1.5新增了Future接口,提供了异步执行任务的能力.但是对于结果的获取却很不方便,只能通过阻塞或者轮询的方式得到任务的结果,阻塞的方式显然和异步编程的初衷违背,轮询的方式又会耗费CPU资源. CompletableFuture类,对于结果的阻塞和轮询,依然可以通过CompletionStage和Future接口方式支持.也可以在一部计算后指定回调函数.例如 CompletableFuture.supplyAsync(this::sendMsg) .thenAccept(this::notify);

怎么使用CompletableFuture实现异步回调任务?

以下代码,演示如何进行如下:

public class Main {
    public static void main(String[] args) throws Exception {
        // 创建异步执行任务:
        CompletableFuture<Double> cf = CompletableFuture.supplyAsync(Main::fetchPrice);
        // 如果执行成功:
        cf.thenAccept((result) -> {
            System.out.println("price: " + result);
        });
        // 如果执行异常:
        cf.exceptionally((e) -> {
            e.printStackTrace();
            return null;
        });
        System.out.println(">>>>>>>>>");
        // 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
        Thread.sleep(100);
        System.out.println("<<<<<<<<<");
    }
    static Double fetchPrice() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
        if (Math.random() < 0.3) {
            throw new RuntimeException("fetch price failed!");
        }
        return 5 + Math.random() * 20;
    }
}

执行结果:

>>>>>>>>>
price: 20.583016768717513
<<<<<<<<<

代码2如下:

public class Main2 {
    public static void main(String[] args) throws Exception {
        // 第一个任务:
        CompletableFuture<String> cfQuery = CompletableFuture.supplyAsync(() -> {
            return queryCode("中国石油");
        });
        // cfQuery成功后继续执行下一个任务:
        CompletableFuture<Double> cfFetch = cfQuery.thenApplyAsync((code) -> {
            return fetchPrice(code);
        });
        // cfFetch成功后打印结果:
        cfFetch.thenAccept((result) -> {
            System.out.println("price: " + result);
        });
        // 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
        Thread.sleep(2000);
    }
    static String queryCode(String name) {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
        return "601857";
    }
    static Double fetchPrice(String code) {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
        return 5 + Math.random() * 20;
    }
}

结果:

price: 21.408665476262335

多个CompletableFuture串行处理代码如下:

public class Main3 {
    public static void main(String[] args) throws Exception {
        // 两个CompletableFuture执行异步查询:
        CompletableFuture<String> cfQueryFromSina = CompletableFuture.supplyAsync(() -> {
            return queryCode("中国石油", "https://finance.sina.com.cn/code/");
        });
        CompletableFuture<String> cfQueryFrom163 = CompletableFuture.supplyAsync(() -> {
            return queryCode("中国石油", "https://money.163.com/code/");
        });
        // 用anyOf合并为一个新的CompletableFuture:
        CompletableFuture<Object> cfQuery = CompletableFuture.anyOf(cfQueryFromSina, cfQueryFrom163);
        // 两个CompletableFuture执行异步查询:
        CompletableFuture<Double> cfFetchFromSina = cfQuery.thenApplyAsync((code) -> {
            return fetchPrice((String) code, "https://finance.sina.com.cn/price/");
        });
        CompletableFuture<Double> cfFetchFrom163 = cfQuery.thenApplyAsync((code) -> {
            return fetchPrice((String) code, "https://money.163.com/price/");
        });
        // 用anyOf合并为一个新的CompletableFuture:
        CompletableFuture<Object> cfFetch = CompletableFuture.anyOf(cfFetchFromSina, cfFetchFrom163);
        // 最终结果:
        cfFetch.thenAccept((result) -> {
            System.out.println("price: " + result);
        });
        // 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
        Thread.sleep(200);
    }
    static String queryCode(String name, String url) {
        System.out.println("query code from " + url + "...");
        try {
            Thread.sleep((long) (Math.random() * 100));
        } catch (InterruptedException e) {
        }
        return "601857";
    }
    static Double fetchPrice(String code, String url) {
        System.out.println("query price from " + url + "...");
        try {
            Thread.sleep((long) (Math.random() * 100));
        } catch (InterruptedException e) {
        }
        return 5 + Math.random() * 20;
    }
}

结果如下

query code from https://finance.sina.com.cn/code/...
query code from https://money.163.com/code/...
query price from https://finance.sina.com.cn/price/...
query price from https://money.163.com/price/...
price: 21.516033760587096

总结: CompletableFuture可以异步处理任务,并且可以实现异步完成自动调用回调函数. thenAccept()处理正常结果; exceptional()处理异常结果; thenApplyAsync()用于串行化另一个CompletableFuture; anyOf()allOf()用于并行化多个CompletableFuture。

什么是volatile?

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

volatile的作用是什么?

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排序

什么是JMM?

java内存模型,是一个概念.

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

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

什么是JMM的8种操作?

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

JMM8种操作

JMM8种指令的规则是什么?

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

怎么验证volatile的可见性?

不加volatile,线程就不知道主线程中的值已经改变了.代码如下:

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

为什么说volatile不保证原子性?

因为volatile没有加锁.代码如下:

public class VDemo02 {
    private static volatile int num = 0 ;//加了volatile,还是不能保证结果的正确性
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(20);
        for (int i = 0; i <20 ; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    add();
                }
                countDownLatch.countDown();
            }).start();
        }
        countDownLatch.await();
        System.out.println(num);//理论上结果为20000
    }
    public  static void add(){
        num ++;
    }
}

实际结果:

19436

总结: volatile不能保证非原子性的操作结果正确.类似于num++的操作,可以使用原子类来确保结果正确.

什么是指令重排?

操作系统在保证单线程执行结果正确的前提下,为了提高执行效率会对没有数据依赖的指令进行重排.指令重排有: 源代码->编译器优化重排->指令并行重排->内存系统重排->执行 示例:

boolean contextReady = false;
//在线程A中执行:
context = loadContext();
contextReady = true;
//在线程B中执行:
while( ! contextReady ){ 
   sleep(200);
}
doAfterContextReady (context);

指令重排后可能变成:

boolean contextReady = false;
//在线程A中执行:
contextReady = true;
context = loadContext();
//在线程B中执行:
while( ! contextReady ){ 
   sleep(200);
}
doAfterContextReady (context);

这个时候,很可能context对象还没有加载完成,变量contextReady 已经为true,线程B直接跳出了循环等待,开始执行doAfterContextReady 方法,结果自然会出现错误。

什么是内存屏障?

内存屏障使CPU或者编译器对屏障指令之前和之后发出的内存操作执行一个排序约束,意味着屏障之前发布的操作保证在屏障之后发布的操作之前执行. 简单理解就是各指令本来应该是顺序执行,但是为了提高效率,可能会调换执行顺序,内存屏障就是隔开前后的指令代码,不允许跨过这道屏障进行交换. volatile关键字修饰的变量,会在变量的内存操作的前后都加一道内存屏障.

怎么使用双重检测的单例模式?

public class LazyMan {
    //一定要使用volatile,防止还未初始化的对象逸出
    private volatile static LazyMan lazyMan;
    private LazyMan(){
        System.out.println(Thread.currentThread().getName()+" OK");
    }
    //双重检测模式
    public static LazyMan getInstance(){
        if(lazyMan == null){//如果已经初始化,直接返回
            synchronized (LazyMan.class){
                if(lazyMan == null){
                    /**
                     * 1. 分配内存空间
                     * 2. 执行构造方法,初始化对象
                     * 3. 把这个对象指向这个空间
                     */
                    lazyMan = new LazyMan();//不是原子性操作,必须加volatile,防止重排序
                }
            }
        }
        return lazyMan;
    }
    //使用反射的方式破坏单例模式
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        LazyMan lazyMan = LazyMan.getInstance();
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        LazyMan lazyMan1 = declaredConstructor.newInstance();
        System.out.println(lazyMan);
        System.out.println(lazyMan1);
    }
}

使用双重检测的懒汉式单例模式,是否安全?

不安全,可能会被反射破环. 各种单例模式的实现比较如下:

单例模式的比较

enum是什么?

Enum实质上是一个类,继承了java.lang.Enum<E>.可以用作常量类.

怎么使用Enum实现单例模式?

在类中使用Enum实现单例模式代码如下:

public class User {
    //私有化构造函数
    private User(){ }
    //定义一个静态枚举类
    static enum SingletonEnum{
        //创建一个枚举对象,该对象天生为单例
        INSTANCE;
        private User user;
        //私有化枚举的构造函数
        private SingletonEnum(){
            user=new User();
        }
        public User getInstnce(){
            return user;
        }
    }
    //对外暴露一个获取User对象的静态方法
    public static User getInstance(){
        return SingletonEnum.INSTANCE.getInstnce();
    }
}
 class Test1 {
    public static void main(String [] args){
        System.out.println(User.getInstance());
        System.out.println(User.getInstance());
        System.out.println(User.getInstance()==User.getInstance());
    }
}

测试结果如下:

com.guohao.pc.single.User@1540e19d
com.guohao.pc.single.User@1540e19d
true

Enum实现的单例模式确实是线程安全并且防止反射机制破坏.缺点就是无法实现懒加载.

怎么使用Enum?

使用Enum作为常量的方式如下:

public enum Color {
    RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4);
    // 成员变量
    private String name;
    private int index;
    // 构造方法
    private Color(String name, int index) {
        this.name = name;
        this.index = index;
    }
    // 普通方法
    public static String getName(int index) {
        for (Color c : Color.values()) {
            if (c.getIndex() == index) {
                return c.name;
            }
        }
        return null;
    }
    // get set 方法
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getIndex() {
        return index;
    }
    public void setIndex(int index) {
        this.index = index;
    }
}

参考链接: java enum的用法详解

枚举类有没有无参构造方法?

没有,只有一个有两个参数的构造方法.例如,查看枚举类的源码,私有构造方法如下:

//枚举的构造方法,只能由编译器调用
protected Enum(String name, int ordinal) {
    this.name = name;
    this.ordinal = ordinal;
}

参考链接: 深入理解Java枚举类型(enum)

什么是DCL?

双重检查加锁(double checked locking).

public class Singleton {
    private volatile static Singleton uniqueSingleton;
    private Singleton() {
    }
    public Singleton getInstance() {
        if (null == uniqueSingleton) {
            synchronized (Singleton.class) {
                if (null == uniqueSingleton) {
                    uniqueSingleton = new Singleton();
                }
            }
        }
        return uniqueSingleton;
    }
}

什么是unsafe类?

Unsafe是Java中一个底层类.包含很多基础操作,比如数组操作,对象操作,内存操作,CAS操作,线程(park)操作,栅栏(fence)操作.JUC包和一些第三方框架都使用了Unsafe来保证并发安全. 这个类属于sun.*API中的类,并且它不是J2SE真正的部分,因此没有任何官方文档,也没有代码文档. Unsafe类设计只提供给JVM信任的启动类加载器使用,是一个典型的单例模式类.非启动类加载器直接调用Unsafe.getUnsafe()方法会抛出SecurityException.

  1. 内存管理,包括分配内存,释放内存等
  2. 非常规的对象实例化.使用allocateInstance()方法可以直接生成对象实例,且无需调用构造方法和其他初始化方法.
  3. 操作类,对象,变量.
  4. 数组操作.包括获取数组第一个元素的偏移地址,获取数组中元素的增量地址等方法.
  5. 多线程同步,包括锁机制,CAS操作等.
  6. 挂起和恢复.包括park,unpark等方法.
  7. 内存屏障.这部分包括了loadFence,storeFence等方法. 参考链接: 说一说Java的Unsafe类
public static Unsafe getUnsafe() {
 Class cc = sun.reflect.Reflection.getCallerClass(2);
 if (cc.getClassLoader() != null)
  throw new SecurityException("Unsafe");
 return theUnsafe;
}

什么是CAS?

Compare and Swap.比较并交换.是一种无锁算法.

compareAndSet的原理是什么?

比较当前工作内存中的值和主内存中的值,如果这两个值是相等的,说明没有其他线程改变了主内存的值,那么执行操作,如果不是就一直循环.

CAS的缺点是什么?

  1. 循环会耗时,CPU开销较大
  2. 一次性只能保证一个共享变量的原子性
  3. ABA问题

什么是ABA问题?

就是一个变量的值从A变成了B,又从B改成了A.在实际场景下会出现问题.比如: 往栈里添加元素,如果栈顶是A就往栈里添加元素,实际有可能栈里元素发生了改变,但栈顶还是A.

怎么解决ABA问题?

不仅比较工作内存和主内存中的值,还比较它们的版本号.可以使用AtomicStampedReference类. 代码示例:

public class CASDemo {
    private static AtomicStampedReference<Long> stampedReference = new AtomicStampedReference<>(20L,1);
    public static void main(String[] args) {
        new Thread(()->{
            int stamp = stampedReference.getStamp();//取到版本号
            System.out.println("B:"+stamp);
            try {
                TimeUnit.SECONDS.sleep(2);//睡眠2秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("B:"+stampedReference.compareAndSet(20L, 30L, stamp, stampedReference.getStamp() + 1));
            System.out.println("B:"+stampedReference.getStamp());
        },"B:").start();
        new Thread(()->{
            int stamp = stampedReference.getStamp();//取到版本号
            System.out.println("A:"+stamp);
            try {
                TimeUnit.SECONDS.sleep(1);//睡眠1秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("A:"+stampedReference.compareAndSet(20L, 50L, stampedReference.getStamp(), stampedReference.getStamp() + 1));
            System.out.println("A:"+stampedReference.getStamp());
            System.out.println("A:"+stampedReference.compareAndSet(50L, 20L, stampedReference.getStamp(), stampedReference.getStamp() + 1));
            System.out.println("A:"+stampedReference.getStamp());
        },"A:").start();
    }
}

执行结果:

B:1
A:1
A:true
A:2
A:true
A:3
B:false
B:3

总结: 使用了AtomicStampedReference后,你是值没有变,只是版本变了,依然无法执行set.至于是不是要使用AtomicStampedReference,要根据业务场景来.很多时候,如果发现值或者版本改变了之后,需要进行重试,这时候需要将变量的原子操作包裹在while循环里进行重试.

Integer包装类有什么坑?

-128-127之间,可以使用==进行判断,超过这个范围应该使用equals进行判断.

什么是公平锁?

按照先来后到的顺序执行加锁和解锁操作.

什么是非公平锁?

效率优先,加锁过程可以插队.

什么是可重入锁?

拿到外面的锁就自动获得里面的锁. 代码示例:

public class Demo01 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(()->{
            try {
                phone.call();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A:").start();
        new Thread(()->{
            try {
                phone.sendSms();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"B:").start();
    }
}
class Phone{
    public synchronized void call() throws InterruptedException {
        System.out.println(Thread.currentThread().getName()+" call");
        TimeUnit.SECONDS.sleep(2);//睡眠2秒
        sendSms();//自动获得sendSms的锁
    }
    public synchronized void sendSms(){
        System.out.println(Thread.currentThread().getName()+" sendSms");
    }
}

执行结果:

A: call
A: sendSms
B: sendSms

锁必须配对吗?

必须配对使用.

什么是自旋锁?

当一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取锁才会退出循环。 自旋锁减少了线程切换上下文的时间,但是如果锁的等待时间较长,就会造成CPU的浪费。

怎么实现自旋锁?

加锁的时候,将变量的值置为currentThread,解锁的时候将变量的值置为null.加锁和解锁的时候采用CAS即可.

//自旋锁
public class SpinLockDemo {
    private AtomicReference<Thread> reference = new AtomicReference<>(null);
    //加锁
    public void lock(){
        //如果线程不为空,就一直自旋
        while(!reference.compareAndSet(null,Thread.currentThread())){
        };
    }
    //解锁
    public void unlock(){
        //如果已经锁定了当前线程,将锁定的线程置为空
        reference.compareAndSet(Thread.currentThread(),null);
    }
}

但是上述方式加的锁不能实现可重入.可以增加一个计数器来实现可重入.

public class ReentrantSpinLock {
    private AtomicReference cas = new AtomicReference();
    private int count;
    public void lock() {//加锁
        Thread current = Thread.currentThread();
        if (current == cas.get()) { // 如果当前线程已经获取到了锁,线程数增加一,然后返回
            count++;
            return;
        }
        // 如果没获取到锁,则通过CAS自旋
        while (!cas.compareAndSet(null, current)) {
            // DO nothing
        }
    }
    public void unlock() {//解锁
        Thread cur = Thread.currentThread();
        if (cur == cas.get()) {
            if (count > 0) {// 如果大于0,表示当前线程多次获取了该锁,释放锁通过count减一来模拟
                count--;
            } else {// 如果count==0,可以将锁释放,这样就能保证获取锁的次数与释放锁的次数是一致的了。
                cas.compareAndSet(cur, null);
            }
        }
    }
}

总结: 使用一个计数器即可实现锁的可重入.ReentrantLock的底层也是这样实现的,获取锁的时候,如果state为0,state+1,设置获取锁的线程为当前线程.否则执行tryAcquire(),再执行一次获取锁.先跟上面一样判断state是否为0,如果不为0那么看锁的线程是否是当前线程,如果是当前线程,state+1,否则获取锁失败,执行addWaiter(),向等待队列添加一个独占的节点.

锁怎么使用?是定义在方法里还是定义在方法外?

锁同样锁的是对象,如果放在方法里面,每次取到的都是不同的对象,那么无法有效的上锁.所以必须定义在方法的外面.

什么是死锁?

两个线程持有锁的同时,想获取对方的锁.

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()+"持有"+lockA+",想get"+lockB);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lockB){
                System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>");
            }
        }
    }
}
T1持有lockA,想getlockB
T2持有lockB,想getlockA

怎么使用工具排查死锁?

  1. 使用jps -l命令定位进程号
  2. 使用jstack 进程号找到死锁.