阿里P6之三JUC多线程及并发包

个人专题目录


3 JUC多线程及并发包

3.1 谈谈你对volatile的理解

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

保证可见性

不保证原子性

禁止指令重排

3.1.2 JMM你谈谈

JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念 并不真实存在,它描述的是一组规则或规范通过规范定制了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式.

JMM关于同步规定:

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

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

  3. 加锁解锁是同一把锁

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

1558676129678.png

可见性

通过前面对JMM的介绍,我们知道

各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存操作后再写回主内存中的.

这就可能存在一个线程AAA修改了共享变量X的值还未写回主内存中时 ,另外一个线程BBB又对内存中的一个共享变量X进行操作,但此时A线程工作内存中的共享比那里X对线程B来说并不不可见.这种工作内存与主内存同步延迟现象就造成了可见性问题.

原子性

number++在多线程下是非线程安全的,如何不加synchronized解决?

/**
 * @author xubh
 * @date 2019-05-24
 * @mondify
 * @copyright
 */
@Slf4j
public class Test01 {
    public volatile int n;

    public void add() {
        /*
         * n++被拆分成了3个指令:
         * 执行getfield拿到原始n;
         * 执行iadd进行加1操作;
         * 执行putfield写把累加后的值写回
         */
        n++;
    }
}

// access flags 0x1
  public add()V
   L0
    LINENUMBER 20 L0
    ALOAD 0
    DUP
    GETFIELD Test01.n : I
    ICONST_1
    IADD
    PUTFIELD Test01.n : I
   L1
    LINENUMBER 21 L1
    RETURN
   L2
    LOCALVARIABLE this LTest01; L0 L2 0
    MAXSTACK = 3
    MAXLOCALS = 1

VolatileDemo代码演示可见性+原子性代码

有序性

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

源代码-->编译器优化的重排-->指令并行的重排-->内存系统的重排-->最终执行的命令

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

处理器在进行重排序时必须要考虑指令之间的数据依赖性

多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。

重排1
public void mySort() {
        int x = 11;//语句1
        int y = 12;//语句2
        x = x + 5;//语句3
        y = x * x;//语句4
}
1234
2134
1324
问题:
请问语句4 可以重排后变成第一条码?
存在数据的依赖性 没办法排到第一个
重排2

int a ,b ,x,y=0;

线程1 线程2
x=a; y=b;
b=1; a=2;
x=0 y=0

如果编译器对这段代码进行执行重排优化后,可能出现下列情况:

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

这也就说明在多线程环境下,由于编译器优化重排的存在,两个线程使用的变量能否保持一致是无法确定的.

案例
/**
 * @author xubh
 * @date 2019-05-24
 * @mondify
 * @copyright
 */
public class ReSortSeqDemo {
    int a = 0;
    boolean flag = false;

    public void method01() {
        a = 1;//语句1
        flag = true;//语句2
    }

    public void method02() {
        if (flag) {
            a = a + 5;//语句3
            System.out.println("****retValue:" + a);
        }
    }
}
禁止指令重排小总结

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

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

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

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

由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

1558678861354.png

线程安全性获得保证

    工作内存与主内存同步延迟现象导致的可见性问题,可以使用synchronized或volatile关键字解决,它们都可以使`一个线程修改后的变量立即对其它线程可见`。

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

3.1.3 你在哪些地方用到过volatile?

单例模式DCL代码

public class SingletonDemo {
    private static volatile SingletonDemo instance = null;

    private SingletonDemo() {
        System.out.println(Thread.currentThread().getName() + "\t 构造方法");
    }

    /**
     * 双重检测机制
     *
     * @return
     */
    public static SingletonDemo getInstance() {
        if (instance == null) {
            synchronized (SingletonDemo.class) {
                if (instance == null) {
                    instance = new SingletonDemo();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        for (int i = 1; i <= 10; i++) {
            new Thread(() -> {
                SingletonDemo.getInstance();
            }, String.valueOf(i)).start();
        }
    }

}

单例模式volatile分析

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

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

instance=new SingletonDem(); 可以分为以下步骤(伪代码)

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

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

instance=memory;//3.设置instance的指向刚分配的内存地址,此时instance!=null

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

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

instance=memory;//3.设置instance的指向刚分配的内存地址,此时instance!=null 但对象还没有初始化完.
instance(memory);//2.初始化对象
但是指令重排只会保证串行语义的执行一致性(单线程) 并不会关心多线程间的语义一致性

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

3.2 CAS你知道吗

3.2.1 比较并交换

/**
 * 1.什么是CAS ? ===> compareAndSet
 * 比较并交换
 *
 * @author xubh
 * @date 2019-05-24
 * @mondify
 * @copyright
 */
public class CASDemo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(5);
        System.out.println(atomicInteger.compareAndSet(5, 2019) + "\t current" + atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(5, 2014) + "\t current" + atomicInteger.get());
    }
}

3.2.2 CAS底层原理?如果知道,谈谈你对UnSafe的理解

atomicInteger.getAndIncrement();

/**
 * Atomically increments by one the current value.
 *
 * @return the previous value
 */
public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

UnSafe

 * @since 1.5
 * @author Doug Lea
*/
public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

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

    private volatile int value;
  1. UnSafe

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

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

  1. 变量ValueOffset,便是该变量在内存中的偏移地址,因为UnSafe就是根据内存偏移地址获取数据的
/**
  * Atomically increments by one the current value.
  *
  * @return the previous value
  */
 public final int getAndIncrement() {
     return unsafe.getAndAddInt(this, valueOffset, 1);
 }
  1. 变量value和volatile修饰,保证了多线程之间的可见性.

CAS是什么

    CAS的全称为Compare-And-Swap,`它是一条CPU并发原语`。它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。

    CAS并发原语体现在JAVA语言中就是sum.misc.Unsafe类中的各个方法。调用Unsafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于`硬件`的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,`并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所有的数据不一致问题`。
unSafe.getAndIncrement
var1 AtomicInteger对象本身.
var2 该对象值的引用地址
var4 需要变动的数值
var5 是用过var1 var2找出内存中真实的值
用该对象当前的值与var5比较
如果相同,更新var5的值并且返回true
如果不同,继续取值然后比较,直到更新完成

/**
  * Atomically increments by one the current value.
  *
  * @return the previous value
  */
 public final int getAndIncrement() {
     return unsafe.getAndAddInt(this, valueOffset, 1);
 }
 
//unsafe.getAndAddInt
public final int getAndAddInt(Object var1, long var2, int var4){
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
}

假设线程A和线程B两个线程同时执行getAndAddInt操作(分别在不同的CPU上):

  1. AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存.

  2. 线程A通过getIntVolatile(var1,var2) 拿到value值3,这时线程A被挂起.

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

  4. 这是线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的数值和内存中的数字4不一致,说明该值已经被其他线程抢先一步修改了,那A线程修改失败,只能重新来一遍了.

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

底层汇编
1558682168859.png
简单版小总结

CAS(CompareAndSwap)

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

CAS应用

CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B。

当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

3.2.3 CAS缺点

循环时间长开销很大

我们可以看到getAndAddInt方法执行时,有个do while
//unsafe.getAndAddInt
public final int getAndAddInt(Object var1, long var2, int var4){
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
}

如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会能CPU带来很大的开锁。

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

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,

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

3.3 原子类AtomicInteger的ABA问题谈谈?原子更新引用知道吗

3.3.1 ABA问题的产生

CAS会导致“ABA问题”

CAS算法实现一个重要前提需要取出内存上某时刻的数据并在当下时刻比较并替换,那么在这个时间差内会导致数据的变化。

比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存上取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置数据变成A,这时候线程one进行CAS操作发现内存中仍然是A, 然后线程one操作成功。

尽管线程one的CAS操作成功,但是不代表这个过程是没有问题的。

3.3.2 原子引用

@Getter
@Setter
@AllArgsConstructor
@ToString
class User {
    private String name;
    private int age;
}

public class AtomicReferenceDemo {
    public static void main(String[] args) {
        User zs = new User("zs", 22);
        User ls = new User("ls", 22);
        AtomicReference userAtomicReference = new AtomicReference<>();
        userAtomicReference.set(zs);
        System.out.println(userAtomicReference.compareAndSet(zs, ls) + "\t" + userAtomicReference.get().toString());
        System.out.println(userAtomicReference.compareAndSet(zs, ls) + "\t" + userAtomicReference.get().toString());
    }
}

true    User(name=ls, age=22)
false   User(name=ls, age=22)

3.3.3 时间戳原子引用

AtomicStampedReference ABADemo

public class ABADemo {
    private static AtomicReference atomicReference = new AtomicReference<>(100);
    private static AtomicStampedReference stampedReference = new AtomicStampedReference<>(100, 1);

    public static void main(String[] args) {
        System.out.println("===以下是ABA问题的产生===");
        new Thread(() -> {
            atomicReference.compareAndSet(100, 101);
            atomicReference.compareAndSet(101, 100);
        }, "t1").start();

        new Thread(() -> {
            //先暂停1秒 保证完成ABA
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
        }, "t2").start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("===以下是ABA问题的解决===");

        new Thread(() -> {
            int stamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t 第1次版本号" + stamp + "\t值是" + stampedReference.getReference());
            //暂停1秒钟t3线程
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            stampedReference.compareAndSet(100, 101, stampedReference.getStamp(), stampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "\t 第2次版本号" + stampedReference.getStamp() + "\t值是" + stampedReference.getReference());
            stampedReference.compareAndSet(101, 100, stampedReference.getStamp(), stampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "\t 第3次版本号" + stampedReference.getStamp() + "\t值是" + stampedReference.getReference());
        }, "t3").start();

        new Thread(() -> {
            int stamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t 第1次版本号" + stamp + "\t值是" + stampedReference.getReference());
            //保证线程3完成1次ABA
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean result = stampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
            System.out.println(Thread.currentThread().getName() + "\t 修改成功否" + result + "\t最新版本号" + stampedReference.getStamp());
            System.out.println("最新的值\t" + stampedReference.getReference());
        }, "t4").start();
    }
}

3.4 我们知道ArrayList是线程不安全,请编写一个不安全的案例并给出解决方案

3.4.1 解决方案1-ContainerNotSafeDemo

public class ContainerNotSafeDemo {
    /**
     * 写时复制 copyOnWrite 容器即写时复制的容器 往容器添加元素的时候,不直接往当前容器object[]添加,而是先将当前容器object[]进行
     * copy 复制出一个新的object[] newElements 然后向新容器object[] newElements 里面添加元素 添加元素后,
     * 再将原容器的引用指向新的容器 setArray(newElements);
     * 这样的好处是可以对copyOnWrite容器进行并发的读,而不需要加锁 因为当前容器不会添加任何容器.所以copyOnwrite容器也是一种
     * 读写分离的思想,读和写不同的容器.
     * 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);
     * newElements[len] = e;
     * setArray(newElements);
     * return true;
     * } finally {
     * lock.unlock();
     * }
     * }
     */
    public static void main(String[] args) {
        List list = new CopyOnWriteArrayList<>();
        for (int i = 1; i <= 30; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(1, 8));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
        /*
         * 1.故障现象
         *  java.util.ConcurrentModificationException
         * 2.导致原因
         *    并发争抢修改导致
         * 3.解决方案
         *  3.1 new Vector<>()
         *  3.2 Collections.synchronizedList(new ArrayList<>());
         *  3.3 new CopyOnWriteArrayList<>();
         *
         *
         * 4.优化建议
         */
    }
}

3.4.2 限制不可以使用vector和Collections工具类解决方案2

List线程copyOnWriteArrayList

set线程CopyOnwriteHashSet

map线程ConcurrentHashMap

3.5 公平锁/非公平锁/可重入锁/递归锁/自旋锁谈谈你的理解?请手写一个自旋锁

3.5.1 公平锁和非公平锁

是什么

公平锁

是指多个线程按照申请锁的顺序来获取锁类似排队打饭 先来后到

非公平锁

是指在多线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取到锁,在高并发的情况下,有可能造成优先级反转或者饥饿现象

两者的区别

公平锁/非公平锁

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

关于两者区别:

公平锁:Threads acquire a fair lock in the order in which they requested it

公平锁,就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己

非公平锁:a nonfair lock permits barging:threads requesting a lock can jump ahead of the queue of waiting threads if the lock happens to be avaiable when it is requested.

非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。

Java ReentrantLock而言,
通过构造函数指定该锁是否是公平锁 默认是非公平锁 非公平锁的优点在于吞吐量必公平锁大.

对于synchronized而言 也是一种非公平锁.

3.5.2 可重入锁(又名递归锁)

是什么

指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁

也即是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块

ReentrantLock/synchronized就是一个典型的可重入锁

可重入锁最大的作用就是避免死锁

ReenterLockDemo

class Phone {
    public synchronized void sendSms() throws Exception {
        System.out.println(Thread.currentThread().getName() + "\tsendSms");
        sendEmail();
    }

    public synchronized void sendEmail() throws Exception {
        System.out.println(Thread.currentThread().getName() + "\tsendEmail");
    }

}

/**
 * 可重入锁(也叫做递归锁)
 * 指的是同一先生外层函数获得锁后,内层敌对函数任然能获取该锁的代码
 * 在同一线程外外层方法获取锁的时候,在进入内层方法会自动获取锁
 * 也就是说,线程可以进入任何一个它已经标记的锁所同步的代码块
 **/
class ReenterLockDemo {
    /**
     * t1 sendSms
     * t1 sendEmail
     * t2 sendSms
     * t2 sendEmail
     *
     * @param args
     */
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(() -> {
            try {
                phone.sendSms();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "t1").start();
        new Thread(() -> {
            try {
                phone.sendSms();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "t2").start();
    }
}
class Phone implements Runnable {
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        get();
    }

    private void get() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\tget");
            set();
        } finally {
            lock.unlock();
        }
    }

    private void set() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\tset");
        } finally {
            lock.unlock();
        }
    }
}

public class ReenterLockDemo {
    /**
     * Thread-0 get
     * Thread-0 set
     * Thread-1 get
     * Thread-1 set
     */
    public static void main(String[] args) {
        Phone phone = new Phone();
        Thread t3 = new Thread(phone);
        Thread t4 = new Thread(phone);
        t3.start();
        t4.start();

    }
}

3.5.3 自旋锁(spinlock)

是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消夏,缺点是循环会消耗CPU

//unsafe.getAndAddInt
public final int getAndAddInt(Object var1, long var2, int var4){
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
}

/**
 * 题目:实现一个自旋锁
 * 自旋锁好处:循环比较获取直到成功为止,没有类似wait的阻塞。
 * 通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒钟,B随后进行发现
 * 当前有线程持有锁,不是null,所以只能通过自旋等待,直到A释放锁后B随后捡到。
 *
 * @author xubh
 * @date 2019-05-24
 * @mondify
 * @copyright
 */
public class SpinLockDemo {

    //原子引用线程
    AtomicReference atomicReference = new AtomicReference<>();

    public void myLock() {
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + "\t come in");
        while (!atomicReference.compareAndSet(null, thread)) {
        }
    }

    public void myUnlock() {
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread, null);
        System.out.println(Thread.currentThread().getName() + "\t invoked myUnlock");

    }

    public static void main(String[] args) {
        SpinLockDemo spinLockDemo = new SpinLockDemo();
        new Thread(() -> {
            spinLockDemo.myLock();
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLockDemo.myUnlock();
        }, "AA").start();

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

        new Thread(() -> {
            spinLockDemo.myLock();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLockDemo.myUnlock();
        }, "BB").start();
    }
}

3.5.4 独占锁(写)/共享锁(读)/互斥锁

独占锁:指该锁一次只能被一个线程所持有。对ReentrantLock和Synchronized而言都是独占锁

共享锁:指该锁可被多个线程所持有。

对ReentrantReadWriteLock其读锁共享锁,其写锁是独占锁。

读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。

ReadWriteLockDemo

class MyCaChe {
    /**
     * 保证可见性
     */
    private volatile Map map = new HashMap<>();
    private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();

    /**
     * 写
     *
     * @param key
     * @param value
     */
    public void put(String key, Object value) {
        reentrantReadWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t正在写入" + key);
            //模拟网络延时
            try {
                TimeUnit.MICROSECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "\t正在完成");
        } finally {
            reentrantReadWriteLock.writeLock().unlock();
        }
    }

    /**
     * 读
     *
     * @param key
     */
    public void get(String key) {
        reentrantReadWriteLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t正在读取");
            //模拟网络延时
            try {
                TimeUnit.MICROSECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Object result = map.get(key);
            System.out.println(Thread.currentThread().getName() + "\t正在完成" + result);
        } finally {
            reentrantReadWriteLock.readLock().unlock();
        }
    }

    public void clearCaChe() {
        map.clear();
    }

}

/**
 * 多个线程同时操作 一个资源类没有任何问题 所以为了满足并发量
 * 读取共享资源应该可以同时进行
 * 但是
 * 如果有一个线程想去写共享资源来  就不应该有其他线程可以对资源进行读或写
 * 小总结:
 * 读 读能共存
 * 读 写不能共存
 * 写 写不能共存
 * 写操作 原子+独占 整个过程必须是一个完成的统一整体 中间不允许被分割 被打断
 **/
public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCaChe myCaChe = new MyCaChe();
        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++) {
            int finalI = i;
            new Thread(() -> {
                myCaChe.get(finalI + "");
            }, String.valueOf(i)).start();
        }
    }
}

3.6 CountDownLatch/CyclicBarrier/Semaphore使用过吗?

3.6.1 CountDownLatch

让一些线程阻塞直到另外一些完成后才被唤醒

CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,调用线程会被阻塞.其他线程调用countDown方法计数器减1(调用countDown方法时线程不会阻塞),当计数器的值变为0,因调用await方法被阻塞的线程会被唤醒,继续执行

内部采用共享锁来实现

  • 与CyclicBarrier区别
    • CountDownLatch的作用是允许1或N个线程等待其他线程完成执行;而CyclicBarrier则是允许N个线程相互等待
    • CountDownLatch的计数器无法被重置;CyclicBarrier的计数器可以被重置后使用,因此它被称为是循环的barrier

CountDownLatchDemo

public class CountDownLatchDemo {
    public static void main(String[] args) throws Exception {
        closeDoor();

    }

    /**
     * 关门案例
     *
     * @throws InterruptedException
     */
    private static void closeDoor() throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "\t" + "上完自习");
                countDownLatch.countDown();
            }, String.valueOf(i)).start();
        }
        countDownLatch.await();
        System.out.println(Thread.currentThread().getName() + "\t班长锁门离开教室");
    }

}
public enum CountryEnum {
    ONE(1, "齐"),
    TWO(2, "楚"),
    THREE(3, "燕"),
    FOUR(4, "赵"),
    FIVE(5, "魏"),
    SIX(6, "韩");

    CountryEnum(Integer code, String name) {
        this.code = code;
        this.name = name;
    }

    @Getter
    private Integer code;
    @Getter
    private String name;

    public static CountryEnum forEach(int index) {
        CountryEnum[] countryEnums = CountryEnum.values();
        for (CountryEnum countryEnum : countryEnums) {
            if (index == countryEnum.getCode()) {
                return countryEnum;
            }
        }
        return null;
    }
}
public class CountDownLatchDemo {
    public static void main(String[] args) throws Exception {
        sixCountry();
    }

    /**
     * 秦灭六国 一统华夏
     *
     * @throws InterruptedException
     */
    private static void sixCountry() throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "\t" + "国,灭亡");
                countDownLatch.countDown();
            }, CountryEnum.forEach(i).getName()).start();
        }
        countDownLatch.await();
        System.out.println("秦统一");
    }

}

3.6.2 CyclicBarrier

CyclicBarrier的字面意思是可循环(Cyclic) 使用的屏障(barrier).它要做的事情是,让一组线程到达一个屏障(也可以叫做同步点)时被阻塞,知道最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CyclicBarrier的await()方法.

底层采用ReentrantLock + Condition实现

CyclicBarrierDemo

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
            System.out.println("召唤神龙");
        });

        for (int i = 1; i <= 7; i++) {
            final int temp = i;
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "\t 收集到第" + temp + "颗龙珠");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }, String.valueOf(i)).start();
        }
    }
}

3.6.3 Semaphore

信号量的主要用户两个目的,一个是用于多个共享资源的相互排斥使用,另一个用于并发资源数的控制.

通常用于限制可以访问某些资源(物理或逻辑的)的线程数目,内部采用共享锁实现

从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动.

信号量Semaphore是一个非负整数(>=1)。当一个线程想要访问某个共享资源时,它必须要先获取Semaphore,当Semaphore >0时,获取该资源并使Semaphore – 1。如果Semaphore值 = 0,则表示全部的共享资源已经被其他线程全部占用,线程必须要等待其他线程释放资源。当线程释放资源时,Semaphore则+1

SemaphoreDemo

public class SemaphoreDemo {
    public static void main(String[] args) {
        //模拟3个停车位
        Semaphore semaphore = new Semaphore(3);
        //模拟6部汽车
        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                try {
                    //抢到资源
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + "\t抢到车位");
                    try {
                        TimeUnit.SECONDS.sleep(3);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "\t 停3秒离开车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //释放资源
                    semaphore.release();
                }
            }, String.valueOf(i)).start();
        }
    }
}

3.6.4 Exchanger

可以在对中对元素进行配对和交换的线程的同步点

允许在并发任务之间交换数据。具体来说,Exchanger类允许在两个线程之间定义同步点。当两个线程都到达同步点时,他们交换数据结构,因此第一个线程的数据结构进入到第二个线程中,第二个线程的数据结构进入到第一个线程中

3.7 阻塞队列知道吗?

3.7.1 队列+阻塞队列

阻塞队列,顾名思义,首先它是一个队列,而一个阻塞队列在数据结构中所起的作用大致如图所示:

1558686496677.png

线程1往阻塞队列中添加元素二线程2从队列中移除元素

当阻塞队列是空时,从队列中获取元素的操作将会被阻塞.

当阻塞队列是满时,往队列中添加元素的操作将会被阻塞.

同样
试图往已满的阻塞队列中添加新圆度的线程同样也会被阻塞,知道其他线程从队列中移除一个或者多个元素或者全清空队列后使队列重新变得空闲起来并后续新增.

3.7.2 为什么用?有什么好处?

在多线程领域:所谓阻塞,在某些情况下会挂起线程(即线程阻塞),一旦条件满足,被挂起的线程又会被自动唤醒

为什么需要使用BlockingQueue

好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为BlockingQueue都一手给你包办好了

在concurrent包 发布以前,在多线程环境下,我们每个程序员都必须自己去控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度.

3.7.3 BlockingQueue的核心方法

1558686774076.png
抛出异常 当阻塞队列满时,再往队列里面add插入元素会抛IllegalStateException: Queue full
当阻塞队列空时,再往队列Remove元素时候回抛出NoSuchElementException
特殊值 插入方法,成功返回true 失败返回false
移除方法,成功返回元素,队列里面没有就返回null
一直阻塞 当阻塞队列满时,生产者继续往队列里面put元素,队列会一直阻塞直到put数据or响应中断退出
当阻塞队列空时,消费者试图从队列take元素,队列会一直阻塞消费者线程直到队列可用.
超时退出 当阻塞队列满时,队列会阻塞生产者线程一定时间,超过后限时后生产者线程就会退出

3.7.4 架构梳理+种类分析

架构介绍

1558686919608.png

种类分析

ArrayBlockingQueue: 由数组结构组成的有界阻塞队列.
  • ArrayBlockingQueue有界且固定,在构造函数时确认大小,确认后不支持改变
  • 在多线程环境下不保证“公平性”
  • 实现:ReentrantLock+Condition
LinkedBlockingDeque: 由链表结构组成的有界(但大小默认值Integer>MAX_VALUE)阻塞队列.
PriorityBlockingQueue:支持优先级排序的无界阻塞队列.
  • 默认情况下元素采用自然顺序升序排序,可以通过指定Comparator来对元素进行排序
    • 二叉堆,添加操作则是不断“上冒”,而删除操作则是不断“下掉”
    • 实现:ReentrantLock + Condition+二叉堆
DelayQueue: 使用优先级队列实现的延迟无界阻塞队列.
  • 应用场景:缓存:清掉缓存中超时的缓存数据,任务超时处理
  • 实现:ReentrantLock + Condition+根据Delay时间排序的优先级队列:PriorityQueue
SynchronousQueue:不存储元素的阻塞队列,也即是单个元素的队列.
理论

SynchronousQueue没有容量,应用在交换工作,生产者的线程和消费者的线程同步以传递某些信息、事件或者任务

与其他BlcokingQueue不同,SynchronousQueue是一个不存储元素的BlcokingQueue

每个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然.

SynchronousQueueDemo
public class SynchronousQueueDemo {
    public static void main(String[] args) {
        BlockingQueue blockingQueue = new SynchronousQueue<>();
        new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + "\t put 1");
                blockingQueue.put("1");
                System.out.println(Thread.currentThread().getName() + "\t put 2");
                blockingQueue.put("2");
                System.out.println(Thread.currentThread().getName() + "\t put 3");
                blockingQueue.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "AAA").start();

        new Thread(() -> {
            try {
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "\t" + blockingQueue.take());
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "\t" + blockingQueue.take());
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "\t" + blockingQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "BBB").start();
    }
}
LinkedTransferQueue:由链表结构组成的无界阻塞队列.
  • 相当于ConcurrentLinkedQueue、SynchronousQueue (公平模式下)、无界的LinkedBlockingQueues等的超集
  • 预占模式:有就直接拿走,没有就占着这个位置直到拿到或者超时或者中断
LinkedBlockingDeque:由链表结构组成的双向阻塞队列.
  • 容量可选,在初始化时可以设置容量防止其过度膨胀,如果不设置,默认容量大小为Integer.MAX_VALUE

3.7.5 用在哪里

生产者消费者模式

传统版 ProdConsumerTraditionDemo
/**
 * 共享资源类
 */
class ShareData {
    private int num = 0;
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void increment() throws Exception {
        lock.lock();
        try {
            //判断
            while (num != 0) {
                //等待 不生产
                condition.await();
            }
            //干活
            num++;
            System.out.println(Thread.currentThread().getName() + "\t" + num);
            //通知唤醒
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public void deIncrement() throws Exception {
        lock.lock();
        try {
            //判断
            while (num == 0) {
                //等待 不生产
                condition.await();
            }
            //干活
            num--;
            System.out.println(Thread.currentThread().getName() + "\t" + num);
            //通知唤醒
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }
}

/**
 * Description
 * 一个初始值为0的变量 两个线程交替操作 一个加1 一个减1来5轮
 **/
public class ProdConsumerTraditionDemo {
    public static void main(String[] args) {
        ShareData shareData = new ShareData();
        new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                try {
                    shareData.increment();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "AA").start();
        new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                try {
                    shareData.deIncrement();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "BB").start();
    }
}
阻塞队列版 ProdConsumerBlockQueueDemo
class MyResource {
    /**
     * 默认开启 进行生产消费的交互
     */
    private volatile boolean flag = true;
    /**
     * 默认值是0
     */
    private AtomicInteger atomicInteger = new AtomicInteger();

    private BlockingQueue blockingQueue = null;

    public MyResource(BlockingQueue blockingQueue) {
        this.blockingQueue = blockingQueue;
        System.out.println(blockingQueue.getClass().getName());
    }

    public void myProd() throws Exception {
        String data = null;
        boolean returnValue;
        while (flag) {
            data = atomicInteger.incrementAndGet() + "";
            returnValue = blockingQueue.offer(data, 2L, TimeUnit.SECONDS);
            if (returnValue) {
                System.out.println(Thread.currentThread().getName() + "\t 插入队列数据" + data + "成功");
            } else {
                System.out.println(Thread.currentThread().getName() + "\t 插入队列数据" + data + "失败");
            }
            TimeUnit.SECONDS.sleep(1);
        }
        System.out.println(Thread.currentThread().getName() + "\t 停止 表示 flag" + flag);
    }

    public void myConsumer() throws Exception {
        String result = null;
        while (flag) {
            result = blockingQueue.poll(2L, TimeUnit.SECONDS);
            if (null == result || "".equalsIgnoreCase(result)) {
                flag = false;
                System.out.println(Thread.currentThread().getName() + "\t" + "超过2m没有取到 消费退出");
                System.out.println();
                System.out.println();
                return;
            }
            System.out.println(Thread.currentThread().getName() + "消费队列" + result + "成功");

        }
    }

    public void stop() throws Exception {
        flag = false;
    }
}

/**
 * Description
 * volatile/CAS/atomicInteger/BlockQueue/线程交互/原子引用
 *
 **/
public class ProdConsumerBlockQueueDemo {
    public static void main(String[] args) throws Exception {
        MyResource myResource = new MyResource(new ArrayBlockingQueue<>(10));
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t生产线程启动");
            try {
                myResource.myProd();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "Prod").start();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t消费线程启动");
            try {
                myResource.myConsumer();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "consumer").start();
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println();
        System.out.println();
        System.out.println();
        System.out.println("时间到,停止活动");
        myResource.stop();
    }
}

线程池

消息中间件

3.8 线程池用过吗?ThreadPoolExecutor谈谈你的理解?

3.8.1 为什么使用线程池,优势

线程池做的工作主要是控制运行的线程的数量,处理过程中将任务加入队列,然后在线程创建后启动这些任务,如果先生超过了最大数量,超出的数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行.

他的主要特点为:线程复用:控制最大并发数:管理线程.

第一:降低资源消耗.通过重复利用自己创建的线程降低线程创建和销毁造成的消耗.

第二: 提高响应速度.当任务到达时,任务可以不需要等到线程和粗昂就爱你就能立即执行.

第三: 提高线程的可管理性.线程是稀缺资源,如果无限的创阿金,不仅会消耗资源,还会较低系统的稳定性,使用线程池可以进行统一分配,调优和监控.

3.8.2 线程池如何使用?

架构实现

Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类.

1558687659905.png

编码实现

Executors.newCachedThreadPool();
java8新出

Executors.newWorkStealingPool(int);

java8新增,使用目前机器上可以的处理器作为他的并行级别

Executors.newFixedThreadPool(int)
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue(),
                                      threadFactory);
    }

执行一个长期的任务,性能好很多

主要特点如下:

  1. 创建一个定长线程池,可控制线程的最大并发数,超出的线程会在队列中等待.

  2. newFixedThreadPool创建的线程池corePoolSize和MaxmumPoolSize是相等的,它使用的的LinkedBlockingQueue

Executors.newSingleThreadExecutor()

一个任务一个线程执行的任务场景

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

主要特点如下:

  1. 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务都按照指定顺序执行.
  2. newSingleThreadExecutor将corePoolSize和MaxmumPoolSize都设置为1,它使用的的LinkedBlockingQueue
Executors.newCachedThreadPool()

适用:执行很多短期异步的小程序或者负载较轻的服务器

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue());
    }

主要特点如下:

  1. 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则创建新线程.
  2. newCachedThreadPool将corePoolSize设置为0 MaxmumPoolSize设置为Integer.MAX_VALUE,它使用的是SynchronousQUeue,也就是说来了任务就创建线程运行,如果线程空闲超过60秒,就销毁线程
ScheduledThreadPoolExecutor

适用:继承自ThreadPoolExecutor,给定的延迟之后运行任务,或者定期执行任务

public ScheduledThreadPoolExecutor(int corePoolSize,
                                       ThreadFactory threadFactory) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue(), threadFactory);
    }

主要特点如下:

  1. 内部使用DelayQueue来实现 ,会把调度的任务放入DelayQueue中。DelayQueue内部封装PriorityQueue,这个PriorityQueue会对队列中的ScheduledFutureTask进行排序

3.8.3 线程池几个重要参数介绍?

7大参数

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
corePoolSize:线程池中的常驻核心线程数
  1. 在创建了线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务,近似理解为今日当值线程
  2. 当线程池中的线程数目达到corePoolSize后,就会把到达的任务放入到缓存队列当中.
maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值大于等于1
keepAliveTime:多余的空闲线程存活时间,当空间时间达到keepAliveTime值时,多余的线程会被销毁直到只剩下corePoolSize个线程为止

默认情况下:
只有当线程池中的线程数大于corePoolSize时keepAliveTime才会起作用,知道线程中的线程数不大于corepoolSIze,

unit:keepAliveTime的单位
workQueue:任务队列,被提交但尚未被执行的任务.
threadFactory:表示生成线程池中工作线程的线程工厂,用户创建新线程,一般用默认即可
handler:拒绝策略,表示当线程队列满了并且工作线程大于等于线程池的最大显示数(maxnumPoolSize)时如何来拒绝.

3.8.4 说说线程池的底层工作原理?

1558688787182.png
1558688855917.png

3.9 线程池用过吗?生产上你是如何设置合理参数

线程池的拒绝策略请你谈谈

JDK内置的拒绝策略

AbortPolicy(默认):直接抛出RejectedException异常阻止系统正常运行
CallerRunPolicy:"调用者运行"一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者线程,从而降低新任务的流量。
DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务
DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常.如果允许任务丢失,这是最好的拒绝策略

以上内置策略均实现了RejectExecutionHandler接口

3.9.2 你在工作中单一的/固定数的/可变你的三种创建线程池的方法,你用哪个多?

一个都不用,我们生产上只能使用自定义的

Executors中JDK给你提供了为什么不用?

3.9.3 你在工作中是如何创建线程池的,是否自定义过线程池使用

public class MyThreadPoolDemo {
    public static void main(String[] args) {
        ExecutorService threadPool = new ThreadPoolExecutor(
                2,
                5,
                1L,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque(3),
                Executors.defaultThreadFactory(),
                //默认抛出异常
                //new ThreadPoolExecutor.AbortPolicy()
                //回退调用者
                //new ThreadPoolExecutor.CallerRunsPolicy()
                //处理不来的不处理
                //new ThreadPoolExecutor.DiscardOldestPolicy()
                new ThreadPoolExecutor.DiscardPolicy()
        );
        //模拟10个用户来办理业务 没有用户就是来自外部的请求线程.
        try {
            for (int i = 1; i <= 10; i++) {
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "\t 办理业务");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
        //threadPoolInit();
    }

    private static void threadPoolInit() {
        /**
         * 一池5个处理线程
         */
        //ExecutorService threadPool= Executors.newFixedThreadPool(5);
        /**
         * 一池一线程
         */
        //ExecutorService threadPool= Executors.newSingleThreadExecutor();
        /**
         * 一池N线程
         */
        ExecutorService threadPool = Executors.newCachedThreadPool();
        //模拟10个用户来办理业务 没有用户就是来自外部的请求线程.
        try {
            for (int i = 1; i <= 20; i++) {
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "\t 办理业务");
                });
                try {
                    TimeUnit.MICROSECONDS.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }
}

3.9.4 合理配置线程池你是如何考虑的?

CPU密集型

System.out.println(Runtime.getRuntime().availableProcessors());查看CPU核数
CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。
CPU密集任务只有真正的多核CPU上才能得到加速-通过多线程
而在单核CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那些。
CPU密集型任务配置尽可能少的线程数量:
一般公式:CPU核数+1个线程的线程池

IO密集型

由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如CPU核数*2
IO密集型,即该任务需要大量的IO,即大量的阻塞。
在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。
所以IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费的阻塞时间 。

IO密集型时,大部分线程都阻塞,故需要多配置线程数:
参考公式:CPU核数/1-阻塞系数       阻塞系统在0.8~0.9之间

比如8核CPU:8/1-0.9=80个线程数

3.10 死锁编码及定位分析

3.10.1 是什么

1558689718012.png

产生死锁的主要原因

系统资源不足
进程运行推进的顺序不合适
资源分配不当

3.10.2 代码

class HoldThread implements Runnable {

    private String lockA;
    private String lockB;

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

    @Override
    public void run() {
        synchronized (lockA) {
            System.out.println(Thread.currentThread().getName() + "\t 自己持有锁" + lockA + "尝试获得" + lockB);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lockB) {
                System.out.println(Thread.currentThread().getName() + "\t 自己持有锁" + lockB + "尝试获得" + lockA);
            }
        }
    }
}

/**
 * 死锁是指两个或者以上的进程在执行过程中,
 * 因争夺资源而造成的一种相互等待的现象,
 * 若无外力干涉那他们都将无法推进下去
 **/
public class DeadLockDemo {
    public static void main(String[] args) {
        String lockA = "lockA";
        String lockB = "lockB";
        new Thread(new HoldThread(lockA, lockB), "threadAAA").start();
        new Thread(new HoldThread(lockB, lockA), "threadBBB").start();
    }
}

3.10.3 解决

jps命令定位进程编号

jstack找到死锁查看

你可能感兴趣的:(阿里P6之三JUC多线程及并发包)