JUC 多线程及高并发笔记

JUC多线程及高并发

JUC 多线程及高并发

在一下代码中,之前没有注意到“/t”和“\t”的区别,一直错用,正确写法应为“\t”,代码中已经纠正,运行结果可能还带有“/t”字样为了保证各位的正常阅读,在此提醒一下。

一、谈谈你对volatile的理解总结

volatile可以称为乞丐版的synchronized。volatile有着保证可见性,不保证原子性,禁止指令重排的特点。和JMM(java内存模型)的不同点是volatile不保证原子性。

什么是JMM?

JMM本身是一个抽象的概念,并不真实存在的。是一组规则和规范。(相当于十二生肖中属龙。),JMM三大特性:原子性、可见性、有序性。

1.可见性

在jvm中程序的实体是线程,当线程创建的时候JVM会为其创建一个工作内存,工作内存是每一个线程的私有数据区域。在java中变量都定义在主内存中,如果线程要修改主内存中的数据,需要先把数据拷贝到自己的私有数据区域中进行修改然后再写主内存。在不同的线程访问内存时普通状态下,在修改内存数据后其他线程不知道。在volatile关键字定以后,内存以及其他线程会知道。

1.1 案例思路

假设int number=0,在没有添加volatile的情况下观察number在线程修改后和添加volatile的情况下线程修改后的number的变化情况。

public void add(){
  //其字节码为三条指令;
  //1.getfield  拿到原始值
  //2.iadd  进行加一操作
  //3.putfield  把值写回内存
  n++;
}
/**
 * 1.验证volatile的可见性
 * 1.1假设 int number=0,在没有添加volatile的情况下number变量没有可见性
 * <p>
 * 1.2添加了volatile可以解决可见性问题。
 * <p>
 */
class MyData {
    volatile int number = 0;

    public void addTo60() {
        this.number = 60;
    }
}
 //可以保证可见性,及时通知其他线程主内存的值已经被修改
    private static void seeOkByVolatile() {
        MyData myData = new MyData();
        System.out.println(Thread.currentThread().getName() + "\t number value" + myData.number);
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t com in");
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myData.addTo60();
            System.out.println(Thread.currentThread().getName() + "\t update number value" + myData.number);
        }, "AAA").start();
        while (myData.number == 0) {

        }
        System.out.println(Thread.currentThread().getName() + "\t mission is over ,number value" + myData.number);
    }

下图为添加volatile后的结果,如果不添加volatile,main线程最终结果为0.
在这里插入图片描述

2.不保证原子性

什么是原子性?

原子性指的是不可分割,当某个线程在做某个任务时,中间不可被加塞或者被分割,要么整体成功,要么整体失败。

2.1 volatile不保证原子性的理论解释?

假设3个线程访问主内存中的变量,同时访问谁抢到算谁的。如上文中n++的java字节码所示,第一步:getfiled三个线程都拿到了原始值===》第二步:iadd三个线程都在自己的工作内存中进行了加一操作===》第三步:putfield把值写回内存,假设第一个线程已经把值写回内存了值为1,第2个和第3个进程并不会把值在内存中进行加一操作只会写覆盖。所以volatile不能保证原子性。

2.2 解决方法

1.在方法上加上sychronized关键字

2.使用JUC下的atomic===>调用getAndIncrement方法

class MyData {
  int number=0
    //解决方法1.可以在以下方法中加入synchronized
    //2.使用AromicInteger
   public void addPlus() {
        number++;
    }
    AtomicInteger atomicInteger = new AtomicInteger();
    public void addAtomic() {
        atomicInteger.getAndIncrement();
    }
}
  //volatile不保证原子性
    private static void atomicByVolatile() {
        MyData myData = new MyData();
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                for (int n = 0; n < 1000; n++) {
                    myData.addPlus();
                    myData.addAtomic();
                }
            }, String.valueOf(i)).start();
        }
        //等待20个线程全部计算完成后,用main线程看是否正确。
        while (Thread.activeCount() > 2) {
            Thread.yield();//
        }
        System.out.println(Thread.currentThread().getName() + "\t int finally number value" + myData.number);
        System.out.println(Thread.currentThread().getName() +"\t atomicInteger finally value"+myData.atomicInteger);
    }

运行结果如下:
在这里插入图片描述

3.禁止指令重排(了解)

计算机在执行程序时,为了提高性能编译器和处理器常常对指令进行重排。

指令重排引发的乱序只发生在多线程中,单线程不会发生。

4.你在哪些地方用到了volatile

单例模式,DCL(双端检索机制),该机制并不一定安全,原因是有指令重排的存在。

public class SingletonDemo {
  //双端检索机制中安全隐如果不加volatile关键字有可能造成指令重排
    private static volatile SingletonDemo instance=null;
    private SingletonDemo(){
        System.out.println(Thread.currentThread().getName()+"\t我是构造方法SingletonDemo()");
    }
    //双端检索机制DCL
    public static SingletonDemo getInstance(){
        if (instance==null){
            synchronized (SingletonDemo.class){
                if (instance==null){
                    instance=new SingletonDemo();//在不加volatile时会有指令重排的风险,并不安全。
                }
            }
        }
        return instance;
    }
    public static void main(String[] args){
//        System.out.println(SingletonDemo.getInstance()==SingletonDemo.getInstance());
//        System.out.println(SingletonDemo.getInstance()==SingletonDemo.getInstance());
//        System.out.println(SingletonDemo.getInstance()==SingletonDemo.getInstance());

        for (int i=0;i<10;i++){
            new Thread(()->{
                SingletonDemo.getInstance();
            },String.valueOf(i)).start();
        }
    }
}

双端检索机制不安全的地方在哪?如上代码块,何为双端检索机制?仅仅使用if(instance==null)不能保证原子性,测试中会打印出多个线程调用方法。

在这里插入图片描述

所以需要在if语句中加上sync进行同步,所谓会出现指令重排的问题是在instance=new SingletonDemo()中,

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

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

3.instance=memory//设置instance指向刚分配的内存地址

如上操作2.3.因为没有数据依赖性可能会发生指令重排。

总结

1.工作内存与主内存同步延迟现象导致的可见性问题

可以使用synchronized或者volatile解决,他们都可以是一个线程修改后的变量对另一个线程可见。

2.对于指令重排导致的可见性有序性问题

可以使用volatile解决

二、CAS

1.什么是cas?

cas: compareAndSwep比较并交换,他是一条CPU并发原语。(有native修饰的都是直接可以操作底层变量的)

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
2.CAS的底层实现原理
import java.util.concurrent.atomic.AtomicInteger;

/**
 * CAS compareAndSet比较并交换
 */
public class CasDemo {
    public static void main(String[] args){
        AtomicInteger atomicInteger=new AtomicInteger(5);
        System.out.println(atomicInteger.compareAndSet(5,2020)+"\t value"+atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(5,2019)+"\t value"+atomicInteger.get());
        atomicInteger.getAndIncrement();

    }
}

由以上程序可以得出结果 true \t 2020 false \t 2020;因为这是单线程程序不存在并发,当第一个atomicInteger.compareAndSet在主内存中获取到值5之后并进行了修改,第二个在主内存中获取到的值是2020进行比较后发现和期望值不一样,所以返回false。

public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

compareAndSet方法中也是调用了unsafe类中的compareAndSwapInt方法。说到这unsafe类是一个什么样的存在,unsafe类是rt.jar包下的sun.msic包下的一个类,这个类中的方法可以直接操作内存中的特定数据。其内部方法可以向C语言中的指针一样操作内存。(native修饰的方法)

强调CAS是一种系统原语,原语属于操作系统的用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU原子指令,不会造成所谓的数据不一致。

2.1在第一节中不保证原子性小节提到的atomicInteger.getAndIncrement()是什么原理谈一谈理解?

getAndIncrement()方法中调用了unsafe类下的getAndAddInt()方法,而getAndAddInt方法调用了campareAndSwpInt方法,这是cas是CPU原语,原语的执行必须是连续的不能被中断,不会造成数据的不一致。所以,getAndIncrement()方法在高并发中能够保证原子性。

3.CAS的缺点
3.1 循环时间长开销大

就比如getAndAddInt方法。var1是对象本身,var2是ValueOfSet,var5是期望值也就是当期望值和内存中获取的值相等时才会修改,var5+var4是新值。如果期望值和从内存中获得的值不一样那么就会一直执行,占用了很大的cpu。

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); 
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;
    }

2.只能保证一个共享变量的原子操作。

3.2ABA问题

什么是aba问题简单来说就是“狸猫换太子”,CAS算法实现的一个重要前提是 需要取出内存中其时刻的数据,并在当下时刻比较并替换,在这个时间差中会导致数据的变化。

在这里插入图片描述

如上图所示,T1T2同时获取主内存中的变量,T1的响应时间为10秒,T2的响应时间为2秒,在T1还没有进行run操作时T2将值修改之后在修改,但是T1并不知道,还傻乎乎的判断为true。

public class ABADemo {
    static AtomicReference<Integer> reference=new AtomicReference<>(100);
    static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100,1);
    public static void main(String[] args) {
        System.out.println("=========ABA问题的产生=========");
        new Thread(()->{
            reference.compareAndSet(100,101);
            System.out.println(Thread.currentThread().getName()+"\t进行了修改,值为"+reference.get());
            reference.compareAndSet(101,100);
            System.out.println(Thread.currentThread().getName()+"\t进行了修改,值为"+reference.get());
        },"T1").start();
        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"\t进行了修改,修改成功否?"+reference.compareAndSet(100,2020)+
                    "\t值为"+reference.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);
            //这里等待1秒的目的是为了让T4线程也能够获取到这个没有进行更改前的数据
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            stampedReference.compareAndSet(100,101,stamp,++stamp);
            System.out.println(Thread.currentThread().getName()+"\t的第2次版本号"+stamp);
            stampedReference.compareAndSet(101,100,stamp,++stamp);
            System.out.println(Thread.currentThread().getName()+"\t的第3次版本号"+stamp);
        },"T3").start();
        new Thread(()->{
            int stamp=stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t的第1次版本号"+stamp);
            //这里的等待3秒是为了能够让T3线程执行一次ABA操作
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean result=stampedReference.compareAndSet(100,2020,stamp,++stamp);
            System.out.println(Thread.currentThread().getName()+"\t修改成功否?"+result+"\t当前罪新实际版本号"+stampedReference.getStamp());
            System.out.println(Thread.currentThread().getName()+"\t当前实际最新值"+stampedReference.getReference());

        },"T4").start();

    }
}

解决ABA问题的方法:如代码中所示,AtomicStampReference就是解决ABA问题的关键,它的解决思路是加上版本号,类似于加上时间戳。下图为运行后的结果。

在这里插入图片描述

三、集合类的不安全问题

1.ArrayList安全么?

ArrayList是不安全的,当多个线程执行ArrayList的add方法时会有并发修改异常出现,ConcurrentModificationException异常
在这里插入图片描述

1.1异常原因?

当线程正在执行写操作时,其他线程抢夺资源,导致了数据的不一致。

1.2解决方案:

1.new Vector();

2.Collections.synchronizedList(new ArrayList())

3.CopyOnWriteArrayList()//写时复制

不多废话直接上代码

/**
*1. new Voctor()
*2.Collections.synchronized(new ArrayList)  Collections工具类下的方法,同步方法
*3.CopyOnWriterArrayList写时复制
*/
private static void listNotSafe() {
        List<String> list= new CopyOnWriteArrayList<>();
        for (int i=1;i<=30;i++){
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0,8));
                System.out.println(list);
            },String.valueOf(i)).start();
        }
    }
1.2.1何为写时复制?

CopyOnWriteArrayList容器,即写时复制,往一个容器中添加元素时,不直接往容器中添加,而是现将容器中的Object[]复制出来,形成一个新的new Object[],然后往新的数据中添加元素,随后再将这个新的元素的引用,指向新的容器,setArray(new Element) 。

这样的做的优点是,容器可以并发的读,这是一种读写分离的思想。

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;//在新的的Object[]中添加元素
            setArray(newElements);//重新写回
            return true;
        } finally {
            lock.unlock();
        }
    }
2.Set安全么?

同样set也是会遇到并发修改异常。原因同上。

2.1解决方法

Set线程安全解决方法

1.Collections.synchronized(new HashSet<>())

2.Set set= new CopyOnWriteArraySet();//同样java.util.concurrent包下也有关于Set的写时复制方法。原理同上。不再一一赘述

 /**
     * Set线程安全解决方法
     * 1.Collections.synchronized(new HashSet<>())
     * 2.Set<String> set= new CopyOnWriteArraySet();
     */
    private static void setNotSafe() {
        Set<String> set= new CopyOnWriteArraySet();
        for (int i=1;i<=30;i++){
            new Thread(()->{
                set.add(UUID.randomUUID().toString().substring(0,8));
                System.out.println(set);
            },String.valueOf(i)).start();
        }
    }
2.2 HashSet的底层?

HashSet的底层就是HashMap。那为什么HashMap.put(K,V),而HashSet.add(K)只添加一个值,嘛也别说了直接撸源码。

 public HashSet() {
        map = new HashMap<>();
    }
public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
private static final Object PRESENT = new Object();

根据以上代码可以看出,HashSet的add方法就是掉用了HashMap中的put方法只不过,v是事先定义好的一个恒定的值。

3.Map线程不安全

在map中当多个线程同时只执行put操作时,同样会发出concurrentModifacationException异常。异常原因同上。

3.1解决方法

1.new ConcurrentHashMap<>();在java.util.concurrent包下存在一个专门为了解决并发修改异常的HashMap对象,put方法使用了synchronized进行了同步。

2.Collections.synchronizedMap(new HashMap<>())

 private static void mapNotSafe() {
        Map<String,String> map= Collections.synchronizedMap(new HashMap<>());//new ConcurrentHashMap<>();
        for (int i=1;i<=30;i++){
            new Thread(()->{
                map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,8));
                System.out.println(map);
            },String.valueOf(i)).start();
        }
    }

四、Java中的锁

1.公平锁非公平锁

公平锁:是指多个线程按照申请锁的顺序来获取锁。

生活中的案例类似于排队打饭

非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后者申请的线程比先申请的线程优先获得锁,在高并发的情况下,有可能会造成优先级翻转和饥饿现象。如果失败,在排队类似公平锁

生活中的案例类似于 排队打发的时候有人插队,前边的排队的同学不愿意和插队的人一般见识就让他插队了,然后后边同学看见后也来插队了,这就是优先级翻转和排队的同学一直打不上饭很饿,就是饥饿现象。如果打饭的师傅说你们都去后边排队去,这时候就类似于公平锁,这些个同学就都去排队了

java.util.concurrent.locks包中的ReentrantLock的创建可以指定构造函数的boolean类型来获得公平锁或者非公平锁。默认是非公平锁。

ReentrantLock和synchronized都是非公平锁,非公平锁的优点,吞吐量比公平锁大。

2.可重入锁(递归锁)

什么是可重入锁?同一线程外层函数获得锁之后,内层的递归函数仍然能够获得该锁的代码,就是说同一个线程在外层方法获取锁的时候进入内层方法会自动获取锁。

生活中的案例类似于我们的家,当我们回到家上厕所时,开开家门然后就能够直接去卫生间上厕所了,虽然卫生间也有锁但是没人的时候还是不会去锁的。


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

class Phone implements Runnable {
    public synchronized void sendSMS() throws Exception {
        System.out.println(Thread.currentThread().getName() + "\t invoked sendSMS()");
        sendEmail();
    }

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

    Lock lock = new ReentrantLock();

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

    public void get() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"\t invoked get()");
            set();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void set() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"\t~~~~~~~~ invoked set()");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

/**
 * 可重入锁
 */

public class ReenterLockDemo {
    public static void main(String[] args) throws Exception {
        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();

        TimeUnit.SECONDS.sleep(1);
        System.out.println();
        System.out.println();
        System.out.println();

        Thread t3=new Thread(phone,"t3");
        Thread t4=new Thread(phone,"t4");

        t3.start();
        t4.start();
    }
}

上述代码证明ReentrantLock和synchronized都是可重入锁。

3.自旋锁

其实自旋锁在CAS中见到过,对就是getAndAddInt方法中就是通过自旋的方式避免线程阻塞。自旋锁是指获取锁的线程不会立即阻塞,而是采用循环的方式尝试去获取锁,这样的好处是减少了线程上下文切换的消耗,缺点是循环会很大程度的消耗CPU。
在这里插入图片描述

生活中的例子,比如在公共洗手间中只有一个坑,AB来到了洗手间中,B进来时A已经在蹲坑了,这样B就出去抽了一根烟然后回来看了一下正好A出来了,然后B进去蹲坑了,然后B出来了。


import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

/**
 * 清手写一个自旋锁
 */
public class SpinLockDemo {
    //原子引用线程
    AtomicReference<Thread> atomicReference=new AtomicReference<>();

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

        }
    }
    //解锁
    public void unLock(){
        Thread thread=Thread.currentThread();
        atomicReference.compareAndSet(thread,null);
        System.out.println(thread.getName()+"\t invoked myUnLock");
    }

    public static void main(String[] args) {
        SpinLockDemo spinLockDemo=new SpinLockDemo();
        //AA线程先加锁并且在运行5s钟后解锁
        new Thread(()->{
            spinLockDemo.myLock();
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLockDemo.unLock();
        },"AA").start();
        //加时间确保AA线程先加锁
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //当BB线程进来是此时AA线程已经在运行,BB线程自旋等待,随时看看锁是否被解开,当锁被解开时BB进入
        new Thread(()->{
            spinLockDemo.myLock();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLockDemo.unLock();
        },"BB").start();
    }

}

运行结果如下:
在这里插入图片描述

4.独占锁(写锁)/共享锁(读锁)

对于ReenterantLock和sychronized而言就是独占锁,也就是所谓写锁。

独占锁:指该锁在某一时刻只能被一个线程所拥有的。

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

ReenterntartReadWriteLock是java.util.current.lock包下的一个对象,这个对象中,其读锁是共享锁,写锁是独占锁。多个线程同时读一个资源类没有任何问题,为了满足并发量,读取资源应该同时进行。但是如果有一个线程想去写共享资源。就不应该再有其他线程可以对该资源进行读或写。

生活中的案例类似于考试成绩张榜公示,同学们都可以共享的读,但是修改只能是老师进行修改。


import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;

class MyCache {
    private volatile Map<String, Object> cache = new HashMap<>();
    private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

    public void put(String key, Object value) {

        rwLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t 正在写入。。。" + key);
            try {
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            cache.put(key, value);
            System.out.println(Thread.currentThread().getName() + "\t 写入完成");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    public void get(String key) {

        rwLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t 正在读取。。。");
            try {
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Object result = cache.get(key);
            System.out.println(Thread.currentThread().getName() + "\t 读取完成" + result);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rwLock.readLock().unlock();
        }


    }
}

/**
 * 多个线程同时读一个资源类没哟任何问题,为了满足并发量,读取共享资源应该同时进行,但是一个线程想去写共享资源,就不应该再有其他线程可以对该资源进行读或写。
 * 小总结
 *   1.读-读能够共存
 *   2.读-写不能共存
 *   3.写写不能共存
 *
 * 写操作独占不能分割  原子+独占
 * 整个过程必须是一个完整的统一体,中间不允许被打断,写锁的时候不能被打断
 */

public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache cache = new MyCache();

        for (int i = 1; i <= 5; i++) {
            final int tempInt = i;
            new Thread(() -> {
                cache.put(tempInt + "", tempInt + "");
            }, String.valueOf(i)).start();
        }
        for (int i = 1; i <= 5; i++) {
            final int tempInt = i;
            new Thread(() -> {
                cache.get(tempInt + "");
            }, String.valueOf(i)).start();
        }
    }
}

代码中如果不加ReentertratReadWriteLock运行结果如下图所示,线程在写的同时,别的线程也在读,读出来的数据有可能是没写入之前的数据。

在这里插入图片描述
当加入了ReenteratReadWriteLock把写操作锁死,不允许中断,结果如下

在这里插入图片描述

总结:

在ReentrantReadWriteLock中读-读可以共存,读-写不可以共存,写-写不可以共存。

总结

ReentrantLock和Synchronized是非公平锁,可重入锁和写锁。RentrantReadWriteLock锁中有两个锁一个读锁readLock()可以并发执行,一个写锁writeLock()某一时刻只能一个线程执行。学会用while语句和cas手写自旋锁。

五、CountDownLatch/CyclicBarrier/Semaphore

1.CountDownLatch(倒计时)

juc下的一个一个类,用于计数使用,其中awit()方法是当线程全部执行完毕后再执行下边的线程。coutDown方法指每次执行完一个操作就减1,举例说明就类似于火箭发射倒计时,当各项准备工作都走完了火箭才能够发射。

import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo {
    public static void main(String[] args) throws Exception{
        CountDownLatch countDownLatch=new CountDownLatch(6);//6表示准备工作个数
        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName()+"\t 正常");
                countDownLatch.countDown();//每走一个减个1
            }, String.valueOf(i)).start();
        }
        countDownLatch.await();
        System.out.println(Thread.currentThread().getName()+"\t*******火箭发射");
    }

运行结果如下图所示

在这里插入图片描述

2.CyclicBarrier(循环屏障)

CyclicBarrier字面意思为循环屏障,Cyclic:循环,Barrier:屏障。它存在的意义是让一组线程到达一个屏障(同步点)时被阻塞,直到最后一个线程到达之后,阻塞的屏障才会放开。所有被拦截的线程才会干活。线程进入屏障通过awit()方法。

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

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 tInt=i;
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName()+"\t 收集到"+tInt+"星球");
                try {
                    cyclicBarrier.await();//先到的被阻塞
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }, String.valueOf(i)).start();
        }
    }
}

运行结果如下:

在这里插入图片描述

3.Semaphore(信号量)

信号量主要用于两个目的,一个是用于多个共享资源的互斥作用,另一个是用于并发资源的控制。可以用生活中抢车位的案例来解释。Semaphore是可以复用的。其中,semaphore.acquire()方法是抢占的意思。semaphore.release()为释放当前车位。

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

public class SemaphoreDemo {
    public static void main(String[] args) {
        Semaphore semaphore=new Semaphore(3);
        for (int i = 1; i <= 5; 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()+"停3秒后,离开车位");
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                }
            }, String.valueOf(i)).start();
        }
    }
}

运行结果如下:

在这里插入图片描述

六、阻塞队列

1.阻塞队列理论

阻塞队列,首先它是一个队列,而一个阻塞队列在数据结构中所起的作用大致如下图所示
在这里插入图片描述
当阻塞队列为空时,从队列里获得元素的操作将会被阻塞,当阻塞队列为满时,往队列中添加元素操作将会被阻塞。

2.接口和实现类

ArrayBlockingQueue:由数组结构组成的有界队列。

LinkedBlockQueue:由链表构成的有界阻塞队列。

SynchronousQueue:不存储元素的阻塞队列

3.实现方法
方法类型抛出异常特殊值(t/f)阻塞超时
插入add(e)offer(e)put(e)offer(e,time,unit)
移除remove()poll()take(e)poll(e,unit)
检查element()peek()不可用不可用

抛出异常列: 当队列已满,继续往阻塞队列中add元素时,会抛出异常IllegalStateException: Queue full; 当队列为空时,继续从队列中remove()元素,会抛出NoSuchElementException异常,可自行调用main放法进行测试

   private static void Exception() {
        BlockingQueue<String> blockingQueue=new ArrayBlockingQueue<>(3);
        System.out.println(blockingQueue.add("a"));
        System.out.println(blockingQueue.add("b"));
        System.out.println(blockingQueue.add("c"));
        //System.out.println(blockingQueue.add("c"));
        System.out.println("队首元素" + blockingQueue.element());
        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());
        //System.out.println(blockingQueue.remove());
    }

两个异常如下图所示:

在这里插入图片描述
在这里插入图片描述
特殊值列:当队列已满,继续往阻塞队列中offer元素时,结果为false; 当队列为空时,继续从队列中poll元素,结果为null。可自行调用main放法进行测试。

    private static void Special(BlockingQueue<String> blockingQueue) {
       BlockingQueue<String> blockingQueue=new ArrayBlockingQueue<>(3);
        System.out.println(blockingQueue.offer("a"));
        System.out.println(blockingQueue.offer("b"));
        System.out.println(blockingQueue.offer("c"));
        System.out.println(blockingQueue.offer("d"));
        System.out.println(blockingQueue.peek());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
    }

运行结果如下图所示:
在这里插入图片描述
阻塞列:当队列满时,生产者线程继续put,队列会一直阻塞,直到生产线程成功put数据或者响应中断退出;当队列为空时,take元素会一直阻塞消费线程直到消费者线程可用。

    private static void PutTake(BlockingQueue<String> blockingQueue) throws InterruptedException{
        BlockingQueue<String> blockingQueue=new ArrayBlockingQueue<>(3);
        blockingQueue.put("a");
        blockingQueue.put("a");
        blockingQueue.put("a");
        //blockingQueue.put("a");
        System.out.println("===================");
        blockingQueue.take();
        blockingQueue.take();
        blockingQueue.take();
        //blockingQueue.take();
    }

超时列:阻塞队列满时,阻塞队列会阻塞生产者线程一定时间,超时后自动退出。

   private static void OverTime(BlockingQueue<String> blockingQueue) throws InterruptedException{
        BlockingQueue<String> blockingQueue=new ArrayBlockingQueue<>(3);
        System.out.println(blockingQueue.offer("a", 2L, TimeUnit.SECONDS));
        System.out.println(blockingQueue.offer("a", 2L, TimeUnit.SECONDS));
        System.out.println(blockingQueue.offer("a", 2L, TimeUnit.SECONDS));
        System.out.println(blockingQueue.offer("a", 2L, TimeUnit.SECONDS));
    }
4.同步队列SynchronousQueue

是一个内部只能包含一个元素的队列,插入元素到队列的线程阻塞,直到另一个线程从队列中获取队列中的存储元素。同样如果线程尝试获取元素并且当前队列中不存在任何元素时,线程阻塞,直到线程将元素插入到队列中。(专属定制版,生产一个消费一个)

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

/**
 * 专属定制版,产生一个消费一个
 */
public class SynchronousQueueDemo {
    public static void main(String[] args) {
        BlockingQueue blockingQueue=new SynchronousQueue<>();
        new Thread(()->{
            try {
                System.out.println(Thread.currentThread().getName()+"\t put a");
                blockingQueue.put("a");
                System.out.println(Thread.currentThread().getName() + "\t put b");
                blockingQueue.put("b");
                System.out.println(Thread.currentThread().getName() + "\t put c");
                blockingQueue.put("c");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"AAA").start();
        new Thread(()->{
            try {
                try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
                System.out.println(Thread.currentThread().getName() + "\t get" + blockingQueue.take());
                try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
                System.out.println(Thread.currentThread().getName() + "\t get" + blockingQueue.take());
                try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
                System.out.println(Thread.currentThread().getName() + "\t get" + blockingQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"BBB").start();
    }
}

运行结果:
在这里插入图片描述

5.阻塞队列用在哪里?
5.1 生产者消费者模式

题目要求:一个初始值为0,两个线程对其交替操作。1个加1,一个减1。

解题思路:通过两个案例来解决上题,一个通过ReentrantLock解决,一个运用以上所学知识解决。

5.1.1 lock和sync的区别

在做题目之前,大家需要搞懂lock和sync有什么区别?总结为一下几点

1.sync是**java中的关键字,**其底层是moitorenter(进入)(底层通过moitorenter对象来完成wait和notify等方法,也依赖于monitor对象只有在同步块或方法中调用wait/notify等方法)。monitorexit(离开)

ReentrantLock是具体类(juc.locks.lock)包下的类,是api层面的锁

2.sync不需要用户手动释放锁,当sync代码执行完毕后,系统会自动让线程释放锁。而ReentrantLock则需要用户手动释放锁

3.等待是否中断?sync是不可中断的,除非抛出异常或正常运行完成。而ReentrantLock可以中断中断方法为设置超时时间,或者调用lock.interrupt()方法。

4.加锁是否公平?上面学到sync是非公平锁,ReentrantLock默认是非公平锁而也可以定义为公平锁。

5.唤醒条件:sync没有什么唤醒条件要么notify(随机唤醒)要么notifyAll(全部唤醒);而ReentrantLock可以通过Condition精确唤醒。

题目 多线程之间按顺序调用,实现A->B->C三个线程按顺序启动,要求如下。

AA打印5次,BB打印10次,CC打印15次, 紧接着, AA打印5次,BB打印10次,CC打印15次 。。。。 来10轮

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

/**
 * 题目:多线程之间按顺序调用,实现A->B->C三个线程按顺序启动,要求如下
 * AA打印5次,BB打印10次,CC打印15次
 * 紧接着
 * AA打印5次,BB打印10次,CC打印15次
 * 。。。。
 * 来10轮
 */
class ShareResource{
    private int num = 1;//A:1 B:2 C:3
    private Lock lock=new ReentrantLock();
    private Condition c1=lock.newCondition();
    private Condition c2=lock.newCondition();
    private Condition c3=lock.newCondition();
    public void print5(){
        lock.lock();
        try {
            //先判断,避免虚假唤醒
            while (num != 1){
                c1.await();
            }
            //干活
            for (int i=1;i<=5;i++){
                System.out.println(Thread.currentThread().getName()+"\t"+i);
            }
            //修改标志位,通知
            num=2;
            c2.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void print10(){
        lock.lock();
        try {
            //先判断,避免虚假唤醒
            while (num != 2){
                c2.await();
            }
            //干活
            for (int i=1;i<=10;i++){
                System.out.println(Thread.currentThread().getName()+"\t"+i);
            }
            //修改标志位,通知
            num=3;
            c3.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void print15(){
        lock.lock();
        try {
            //先判断,避免虚假唤醒
            while (num != 3){
                c3.await();
            }
            //干活
            for (int i=1;i<=15;i++){
                System.out.println(Thread.currentThread().getName()+"\t"+i);
            }
            //修改标志位,通知
            num=1;
            c1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
public class SyncAndReentrantLockDemo {
    public static void main(String[] args) {
        ShareResource shareResource=new ShareResource();
        new Thread(()->{
            for (int i=1;i<=10;i++){
                shareResource.print5();
            }
        },"A").start();
        new Thread(()->{
            for (int i=1;i<=10;i++){
                shareResource.print10();
            }
        },"B").start();
        new Thread(()->{
            for (int i=1;i<=10;i++){
                shareResource.print15();
            }
        },"C").start();
    }
}

以上题目通过Condition的方式精确唤醒线程。通过以上几点总结出lock的优势为等待可以中断,加锁比较灵活,可以精确唤醒线程。

5.2 传统版的生产-消费模式

题目要求:一个初始值为0,两个线程对其交替操作。1个加1,一个减1。

解题思路: 1.线程,操作,资源类。2.判断,干活,通知。3.防止虚假唤醒

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

/**
 * 题目:一个初始值为0,两个线程对其交替操作。1个加1,一个减1。
 * 解题思路
 *  1. 线程     操作    资源类
 *  2. 判断     干活    通知
 *  3. 防止虚假唤醒
 */
class ShareData{
    public int num=0;
    Lock lock=new ReentrantLock();
    Condition condition=lock.newCondition();
    //加法
    public void increment() throws Exception{
        lock.lock();
        try {
            //1.判断,这里的判断是判断线程是否需要等待,因为是交替加1减1
            while (num!=0){
                condition.await();
            }
            //2. 干活
            num++;
            System.out.println(Thread.currentThread().getName()+"\t"+num);
            //3. 唤醒通知
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }
    //加法
    public void decrement() throws Exception{
        lock.lock();
        try {
            ///这里的判断是判断线程是否等待
            while (num==0){
                condition.await();
            }
            num--;
            System.out.println(Thread.currentThread().getName()+"\t"+num);
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }
}

public class ProdConsumer_TraditionDemo {
    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();
                }
            }
        },"AAA").start();
        new Thread(()->{
            for (int i=1;i<=5;i++){
                try {
                    shareData.decrement();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        },"BBB").start();
    }
}

在这里插入图片描述
传统模式下,我们需要手动加锁解锁,判断什么时候等待,什么时候需要唤醒线程。在理解以上代码时,判断是否等待,我一直不能理解。因为题目要求是交替加1减1所以当num不等于0时,生产线程需要等待。为什么用while()而不用if(),防止虚假唤醒。

5.3阻塞队列版生产-消费模式

使用以上学习到的方法来解决生产消费模式volatile/CAS/AtomicInteger/BlockingQueue/线程通信,以生产蛋糕为例。


import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 题目:一个初始值为0,两个线程对其交替操作。1个加1,一个减1。
 * 解题思路
 * volatile/AtomicInteger/CAS/BlockQueue
 */
class MyResourse {
    private volatile boolean FLAG = true;//默认开启,生产+消费,volatile可见性
    private AtomicInteger atomicInteger = new AtomicInteger();//原子引用
    BlockingQueue<String> blockingQueue = null;//通过构造方法来定义阻塞队列
    public MyResourse(BlockingQueue<String> blockingQueue) {
        this.blockingQueue = blockingQueue;
        System.out.println(blockingQueue.getClass().getName());
    }
    public void myProd() throws Exception{
        String data= null;
        boolean retValue;//阻塞队里中offer数据的返回值是布尔值
        while (FLAG){
            data=atomicInteger.incrementAndGet()+"";//incrementAndGet相当于++i
            retValue=blockingQueue.offer(data,2L, TimeUnit.SECONDS);
          //判断是否插入成功
            if (retValue){
                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=false,生产动作结束");
    }
    public void myConsumer() throws Exception{
        String result=null;
        while (FLAG){
            result=blockingQueue.poll(2L,TimeUnit.SECONDS);//poll方法返回值是取走的数据
          //判断如果取走的数据是null那么直接修改FLAG
            if (null==result || result.equalsIgnoreCase("")){
                FLAG=false;
                System.out.println(Thread.currentThread().getName()+"\t 超过两秒钟没有取到蛋糕,消费退出");
                System.out.println();
                System.out.println();
                return;
            }
            System.out.println(Thread.currentThread().getName() + "\t 消费队列蛋糕" + result + "成功");
        }
    }
    public void stop(){
        this.FLAG=false;
    }

}

public class ProdConsumer_BlockQueueDemo {
    public static void main(String[] args) {
        MyResourse myResourse=new MyResourse(new ArrayBlockingQueue<>(10));
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"\t生产线程启动");
            try {
                myResourse.myProd();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"Prod").start();
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"\t消费线程启动");
            System.out.println();
            System.out.println();
            System.out.println();
            try {
                myResourse.myConsumer();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"Consumer").start();
        try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("5秒钟时间到,大老板main线程叫停,活动结束");
        myResourse.stop();
    }
}

运行结果:

在这里插入图片描述

多线程领域,所谓阻塞就是指在某些情况下会挂起线程,一旦满足条件,被挂起的线程会自动被唤醒。为什么需要BlockingQueue? 优点是我们不需要像传统模式下ReentrantLock那样加锁解锁,关心什么时候阻塞线程什么是唤醒线程,这一切BlockingQueue都一手包办了。

七、线程池

1.callable接口

callable接口是juc下的接口,callable接口是一个泛型接口,只有一个call()方法,实现callable重写call方法,实现Callable和实现Runnable类似,但是功能强大具体表现在:

1.可以在任务结束后,提供一个返回值,Runnable接口不行。

2.call方法可以抛出异常,Runnable中的run不行。

3.可以通过运行Callable接口得到future对象监听目标线程调用call方法的结果得到返回值(future.get()调用后会阻塞,直到获取返回值)

package demo0411;

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

class MyThread implements Callable<Integer>{

    @Override
    public Integer call() throws Exception {
        System.out.println(Thread.currentThread().getName()+"*********come in");
        try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
        return 1024;
    }
}

public class CallableDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {

        FutureTask<Integer> futureTask=new FutureTask<>(new MyThread());//多个线程抢futureTask,只有一个能抢到
       // FutureTask<Integer> futureTask2=new FutureTask<>(new MyThread());

        new Thread(futureTask,"AA").start();
        new Thread(futureTask,"BB").start();

        //int result2=futureTask.get();//如果futureTask放在前面会导致需要将callable接口中的活干完在干下边的活

        System.out.println(Thread.currentThread().getName()+"******");
        int result1=100;

        while (!futureTask.isDone()){

        }

        int result2=futureTask.get();//要求获得callable接口的计算结果,如果没有计算完成就去强求,会导致堵塞,值得计算完成。

        System.out.println(result1+result2);
    }
}

2.线程池的优势

java在95年就有多线程(一个小丑丢多个球),现在随着技术的发展多线程实际是(多个小丑都丢一个球)。

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

主要特点:线程的复用;控制最大并发数;管理线程。

2.1降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

2.2提高响应速度,当任务到达时,任务可以不需要等到线程创建就立即执行。

2.3提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源还会降低稳定性,使用线程池可以进行统一分配,调优和监控。

3.线程池3个常用方式

Executors ThreadPoolExecutors

Executors.newFixedThreadPool()//一个固定长度,长期执行的任务。

Executors.newSingleThreadExecutors()//一次只能执行一个任务,底层队列是SynchronousQueue。

Executors.newCachedThreadPool()//适用于执行多个短期异步的小程序,负载较轻的服务器。

    private static void threadPoolInit() {
        //System.out.println(Runtime.getRuntime().availableProcessors());//看底层CPU是多少
        //ExecutorService threadPool= Executors.newFixedThreadPool(5);//一池处理5个线程,相当于银行有5个窗口办理业务
        //ExecutorService threadPool= Executors.newSingleThreadExecutor();//一池一线程
        ExecutorService threadPool = Executors.newCachedThreadPool();//一池N线程
        //模拟10个用户来办理业务,每个用户就是一个来自外部的请求线程。
        try {
            for (int i = 0; i < 10; i++) {
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "\t 办理业务");
                });
                try {
                    TimeUnit.MILLISECONDS.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }
4.线程池的七大参数

1.corePoolSize:线程池中常驻核心线程数**(银行窗口的今日当值窗口)**

2.maximumPoolSize:线程池中能够容纳的最大线程数**(银行柜台的最大容纳量)**

3.keepActiveTime:多余的空闲线程存活时间,当线程数量超过corePoolSize时,当空闲时间达到了keepActiveTime就会被销毁(银行中今天来的人很多,不得不开启其他窗口,但是开启后一定时间左右,客户的访问量下降,这几个窗口就关闭了)

4.unit:keepActiveTime的单位

5.workQueue:任务队列**(排队等候区)**

6.threadFactory:表示生成线程池的线程工厂,用于创建线程,一般用默认即可**(银行网点的logo/制服)**

7.handler:拒绝策略,当阻塞队列已满,继续有任务请求时,会拒绝处理或报异常丢包等操作。(银行中办理业务窗口全满,候客区全满,不在允许任务进来)

5.线程池底层原理

在这里插入图片描述

1.当活跃线程数<线程池常驻核心线程数,创建线程执行任务addWork()(今天银行网点来的人小于两个,然后由柜台直接办理业务)

2.当活跃线程数>线程池中常驻核心数,将多余活跃线程放入阻塞队列中排队等待。(银行网点中,来的人对于近日当值柜台的数量,在等候区等待)

3.当阻塞队列已满,并未达到maximumPoolSize是创建线程执行任务。(银行经理看今天来的人有点多,把其他人叫回来加班)

4.当达到maximumPoolSize后,再次有任务请求进来,拒绝接受任务**(银行经理看今天来的人太多了不让进来了)**

在这里插入图片描述

execute方法源码对应上边1234步骤:

  int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {//************************************1
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {//***************************2
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))//***************************************3
            reject(command);//*******************************************************4
6.线程池的拒绝策略

验证代码在第7小节中。

corePoolSize:2;maximumPoolSize:5;workQueue:new LinkedBlockingQueue(3)

AbortPolicy:直接抛出RejectedExeception异常,阻止系统正常运行。

在这里插入图片描述

CallerRunsPolicy:“调用者运行”,一种调节机制,该策略不会抛弃任务,也不抛出异常,而是回退给调用者。

在这里插入图片描述

DiscardPolicy:直接丢弃任务,不予任何处理。

在这里插入图片描述

DiscardOldestPolicy:抛弃队伍中等待最久的任务,然后把当前任务加入队列中尝试再次提交。

在这里插入图片描述

7.线程池的创建方式

问题:你在工作中newFixedThreadPool、newSingThreadExecutor、newCachedThreadPool这三种方式你用哪个用的多?(超级大坑)

正确回答,哪个都不用,都是自己手写线程池。

newFixedThreadPool

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

newSingThreadExecutor

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

newCachedThreadPool

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

LinkedBlockingQueue

   public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

首先了解一下Integer.MAX_VALUE有多大

在这里插入图片描述

相信看到这里的朋友应该知道为什么不能用以上三种方式创建线程池了。

阿里巴巴开发手册中提到,线程池不允许使用Executor的方式创建,要用ThreadPoolExecutor的方式创建。

Executor创建的弊端

1.FixedThreadPool/SingleThreadPool

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

2.CachedThreadPool/ScheduledThreadPool

允许的最大线程数量为Integer.MAX,可能会创建大量线程,导致OOM。

手写线程池并测试

    private static void threadPool() {
        ExecutorService threadPool = new ThreadPoolExecutor(
                2,
                5,
                1L,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardPolicy());
        //AbortPolicy抛出异常/CallerRunsPolicy调用者机制,回退给调用者/DiscardOldestPolicy抛弃队伍中等待最久的任务
        //DiscardOldestPolicy将等待最久的丢弃/DiscardPolicy直接丢弃任务,不予任何处理
        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();
        }
    }

通过以上配置可以分析该线程池的最大容量加上允许等待数量为8,超过8将会触发拒绝策略。

8.如何合理配置线程池

corePoolSize

线程池中最重要的参数,配置corePoolSize分为CPU密集型和IO密集型。

CPU密集型:也叫计算密集型,CPU密集型也叫计算密集型,指的是系统的硬盘、内存性能相对CPU要好很多,此时,系统运作大部分的状况是CPU Loading 100%,CPU要读/写I/O(硬盘/内存),I/O在很短的时间就可以完成,而CPU还有许多运算要处理,CPU Loading 很高。在多重程序系统中,大部分时间用来做计算、逻辑判断等CPU动作的程序称之CPU bound。例如一个计算圆周率至小数点一千位以下的程序,在执行的过程当中绝大部分时间用在三角函数和开根号的计算,便是属于CPU bound的程序。CPU bound的程序一般而言CPU占用率相当高。这可能是因为任务本身不太需要访问I/O设备,也可能是因为程序是多线程实现因此屏蔽掉了等待I/O的时间。

IO密集型:指的是系统的CPU性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是CPU在等I/O (硬盘/内存) 的读/写操作,此时CPU Loading并不高。I/O bound的程序一般在达到性能极限时,CPU占用率仍然较低。这可能是因为任务本身需要大量I/O操作,而pipeline做得不是很好,没有充分利用处理器能力。

1.分析线程池处理的程序时CPU密集型还是IO密集型。

2.查看自己CPU的核数。

System.out.println(Runtime.getRuntime().availableProcessors());

3.CPU密集型:corePoolSize=核数+1;

IO密集型:

1.由于IO密集型线程并不一直执行任务,则应配置尽可能多的线程corePoolSize=核数*2;

2.IO有大量阻塞,参考公式:阻塞系数 0.8~0.9参考系数后 corePoolSize=CPU核数/(1-0.9);

其他参数举例说明:

如何来设置呢?

需要根据几个值来决定

tasks :每秒的任务数,假设为500~1000

taskcost:每个任务花费时间,假设为0.1s

responsetime:系统允许容忍的最大响应时间,假设为1s

做几个计算

corePoolSize = 每秒需要多少个线程处理?

threadcount = tasks/(1/taskcost) = tasks*taskcout = (500 ~ 1000)*0.1 = 50~100 个线程。

corePoolSize设置应该大于50。

根据8020原则,如果80%的每秒任务数小于800,那么corePoolSize设置为80即可。

queueCapacity = (coreSizePool/taskcost)*responsetime

计算可得 queueCapacity = 80/0.1*1 = 80。意思是队列里的线程可以等待1s,超过了的需要新开线程来执行。

切记不能设置为Integer.MAX_VALUE,这样队列会很大,线程数只会保持在corePoolSize大小,当任务陡增时,不能新开线程来执行,响应时间会随之陡增。

maxPoolSize = (max(tasks)- queueCapacity)/(1/taskcost)

最大线程数 =(最大任务数-队列容量)/每个线程每秒处理能力

计算可得 maxPoolSize = (1000-80)/10 = 92

rejectedExecutionHandler:根据具体情况来决定,任务不重要可丢弃,任务重要则要利用一些缓冲机制来处理。

八、死锁编码定位及分析

1.什么是死锁?

死锁是指两个或者两个或者两个以上的进程在执行过程中,因真多资源造成的一种相互等待的现象,若无外力干涉,那他们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就会降低,否则就会因为争夺有限的资源而陷入死锁。
在这里插入图片描述

2.手写一个死锁
import java.util.concurrent.TimeUnit;

class HoldLockThread implements Runnable{

    private String lockA;
    private String lockB;

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

    @Override
    public void run() {
        synchronized (lockA){
            System.out.println(Thread.currentThread().getName()+"\t 自己持有"+lockA+"\t 尝试获得:" +lockB);
            try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
            synchronized (lockB){
                System.out.println(Thread.currentThread().getName()+"\t 自己持有"+lockB+"\t 尝试获得:" +lockA);
            }
        }
    }
}
/**
 * 死锁是指两个或者两个以上的进程在执行过程中,
 * 因争夺资源而造成的一种互相等待的现象,
 * 若无外力干涉那他们都将无法推进下去。
 */
public class DeadLockDemo {
    public static void main(String[] args) {
        String lockA="lockA";
        String lockB="lockB";
        new Thread(new HoldLockThread(lockA,lockB),"AAA").start();
        new Thread(new HoldLockThread(lockB,lockA),"BBB").start();
    }
}
3.死锁定位分析
jps -l//定位端口
jstack  端口号//找到死锁查看

在这里插入图片描述

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值