JUC高并发学习笔记

1、什么是JUC

Java中,线程是一个重点,JUCjava.util.concurrent工具包的简称,这是一个处理线程的工具包,JDK 1.5开始出现。

1.1 进程与线程

  • 进程(Process)是系统进行资源分配和调度的基本单位,是操作系统结构的基础,进程是线程的容器

  • 线程(thread) 是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

小结:进程是系统资源分配的最小单位,线程是程序执行的最小单位。

1.2 wait()/sleep()的区别

  • sleep()Thread的静态方法,而wait()Object的方法,任何对象实例都能调用
  • sleep()不会释放锁,wait()会释放锁,但调用它的前提是当前线程占有锁,即代码要在synchronized
  • 它们都可以被interrupted方法中断

1.3 并发与并行

串行与并行
  • 串行表示所有任务都一一按先后顺序进行,串行是一次只能取得一个任务,然后执行这个任务

  • 并行意味着可以同时取得多个任务,并同时去执行所取得的这些任务。并行模式相当于将长长的一条队列,划分成了多条短队列,所以并行缩短了任务队列的长度。并行的效率从代码层次上强依赖于多进程/多线程代码,从硬件角度上则依赖于多核CPU。

并发

并发(concurrent)指的是多个程序可以同时运行的现象,并发描述的是多进程同时运行的现象

要解决大并发问题,通常是将大任务分解成多个小任务,由于操作系统对进程的调度是随机的,所以切分成多个小任务后,可能会从任一小任务处执行,这可能会出现一些现象:

  • 可能出现一个小任务执行了多次,还没开始下个任务的情况。这时一般会采用队列或类似的数据结构来存放各个小任务的成果
  • 可能出现还没准备好第一步就执行第二步的可能。这时,一般采用多路复用或异步的方式,比如只有准备好产生了事件通知才执行某个任务
  • 可以多进程/多线程的方式并行执行这些小任务。也可以单进程/单线程执行这些小任务,这时很可能要配合多路复用才能达到较高的效率

并发:同一时刻多个线程在访问同一个资源,多个线程对一个点
并行:多项工作一起执行,之后再汇总

并发产生的条件:

  • 多线程
  • 共享变量
  • 原子操作

数据一致性:

  • 原子性
  • 一致性
  • 有序性:指令重排

1.4 用户线程和守护线程

用户线程:平时用到的普通线程,自定义线程
守护线程:运行在后台,是一种特殊的线程,比如垃圾回收
当主线程结束后,如果用户线程还在运行,那么JVM存活,如果没有用户线程,都是守护线程,则JVM结束

public static void main(String[] args) {
    Thread aa = new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().isDaemon());
        while (true) {		

        }
    }, "aa");
    //设置守护线程
    aa.setDaemon(true);
    aa.start();
    System.out.println(Thread.currentThread().getName()+" over");
}

注:守护线程随着用户线程的结束而结束。可以在有较长耗时操作时,设置为守护线程进行。

2、Lock接口

2.1 synchronized

synchronizedJava中的关键字,是一种同步锁,它修饰的对象有以下几种:

  • 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象
  • 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象
  • 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象
  • 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象

虽然可以使用synchronized来定义方法,但synchronized并不属于方法定义的一部分,因此,synchronized关键字不能被继承

如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步,而必须显式地在子类的这个方法中加上synchronized关键字才可以。

当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。

如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里取得锁的线程释放锁只会有两种情况:

  • 获取锁的线程执行完了该代码块,然后线程释放对锁的占有
  • 线程执行发生异常,此时JVM会让线程自动释放锁

那么如果这个取得锁的线程由于要等待IO或者其他原因(比如调用sleep()方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到

多线程编程步骤:

  • 第一步,创建资源类,在资源类创建属性和操作方法(多个线程抢占资源)

  • 第二步,创建多个线程,调用资源类的操作方法

场景:3个窗口卖30张票

//第一步  创建资源类,定义属性和和操作方法
class Ticket {
    //票数
    private int number = 30;
    //操作方法:卖票
    public synchronized void sale() {
        //判断:是否有票
        if(number > 0) {
            System.out.println(Thread.currentThread().getName()+" : 卖出:"+(number--)+" 剩下:"+number);
        }
    }
}

public class SaleTicket {
    //第二步 创建多个线程,调用资源类的操作方法
    public static void main(String[] args) {
        // 创建共享资源Ticket对象
        Ticket ticket = new Ticket();
        // 创建三个线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                //调用卖票方法
                for (int i = 0; i < 40; i++) {
                    ticket.sale();
                }
            }
        },"AA").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                //调用卖票方法
                for (int i = 0; i < 40; i++) {
                    ticket.sale();
                }
            }
        },"BB").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                //调用卖票方法
                for (int i = 0; i < 40; i++) {
                    ticket.sale();
                }
            }
        },"CC").start();
    }
}

2.2 什么是Lock

Lock提供了比synchronized更多的功能,Lock与的Synchronized区别:

  • Lock不是Java语言内置的,synchronizedJava语言的关键字,因此是内置特性
  • Lock是一个类,通过这个类可以实现同步访问
  • Locksynchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象
// Lock是个接口
public interface Lock { 
    void lock(); 
    void lockInterruptibly() throws InterruptedException; 
    boolean tryLock(); 
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException; 
    void unlock(); 
    Condition newCondition(); 
}

Lock是个接口,实现类有:ReentrantLock、ReentrantReadWriteLock.ReadLock、ReentranReadWriteLock.WriteLock

lock()方法

lock()方法是平常使用得最多的一个方法,作用就是用来获取锁。如果锁已被其他线程获取,则进行等待。

采用Lock必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。通常使用Lock来进行同步的话,是以下面这种形式去使用的:

Lock lock = new ReentrantLock(); 
lock.lock(); 
try{ 
    //处理任务 
}catch(Exception ex){

}finally{
    lock.unlock(); //释放锁 
}
Condition

等待/通知模式的方法

  • 关键字synchronizedwait()/notify()这两个方法一起使用可以实现等待/通知模式

  • Lock锁的newContition()方法返回Condition对象,Condition类也可以实现等待/通知模式

    notify()通知时,JVM会随机唤醒某个等待的线程,使用Condition类可以进行选择性通知,Condition比较常用的两个方法:

    • await()会使当前线程等待,同时会释放锁,当其他线程调用signal()时,线程会重新获得锁并继续执行
    • signal()用于唤醒一个等待的线程

注意:

在调用Conditionawait()/signal()方法前,也需要线程持有相关的Lock锁,调用await()后线程会释放这个锁,在signal()调用后会从当前Condition对象的等待队列中,唤醒一个线程,唤醒的线程尝试获得锁, 一旦获得锁成功就继续执行

2.3 ReentrantLock

ReentrantLock,是可重入锁,ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法,下面通过实例具体看一下如何使用。

场景:3个窗口卖30张票
//第一步  创建资源类,定义属性和和操作方法
class Ticket {
    // 票数量
    private int number = 30;

    // 创建可重入锁
    private final ReentrantLock lock = new ReentrantLock(true);
    // 卖票方法
    public void sale() {
        // 上锁
        lock.lock();
        try {
            // 判断是否有票
            if(number > 0) {
                System.out.println(Thread.currentThread().getName()+" :卖出"+(number--)+" 剩余:"+number);
            }
        } finally {
            // 解锁
            lock.unlock();
        }
    }
}

public class SaleTicket {
    // 第二步 创建多个线程,调用资源类的操作方法
    // 创建三个线程
    public static void main(String[] args) {

        // 创建一个共享资源
        Ticket ticket = new Ticket();

        new Thread(()-> {
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        },"AA").start();

        new Thread(()-> {
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        },"BB").start();

        new Thread(()-> {
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        },"CC").start();
    }
}

2.4 ReadWriteLock

ReadWriteLock也是一个接口,在它里面只定义了两个方法:

public interface ReadWriteLock { 
	Lock readLock(); // 获取读锁
   	Lock writeLock(); // 获取写锁
}

一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。

ReentrantReadWriteLock实现了ReadWriteLock接口,ReentrantReadWriteLock里面提供了很多丰富的方法,不过最主要的有两个方法:readLock()writeLock()用来获取读锁和写锁。

假如有多个线程要同时进行读操作的话,先看一下synchronized达到的效果:(同时只能有一个线程读)

public class Test { 

    public static void main(String[] args) { 
        final Test test = new Test(); 
        
        new Thread(){ 
            public void run() {
                test.get(Thread.currentThread()); 
            }; 
        }.start(); 
        
        new Thread(){ 
            public void run() { 
                test.get(Thread.currentThread()); 
            }; 
        }.start(); 
    } 
    
    public synchronized void get(Thread thread) { 
        long start = System.currentTimeMillis(); 
        while(System.currentTimeMillis() - start <= 1) { 
            System.out.println(thread.getName()+"正在进行读操作"); 
        } 
        System.out.println(thread.getName()+"读操作完毕"); 
    } 
}

而改成用读写锁的话:

public class Test {
    
	private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); 
    
    public static void main(String[] args) { 
        final Test test = new Test(); 
        new Thread(){ 
            public void run() { 
                test.get(Thread.currentThread()); 
            }; 
        }.start(); 
        
        new Thread(){ 
            public void run() { 
                test.get(Thread.currentThread()); 
            }; 
        }.start(); 
    } 
    
    public void get(Thread thread) { 
        rwl.readLock().lock(); // 获取读锁
        try { 
            long start = System.currentTimeMillis();
            while(System.currentTimeMillis() - start <= 1) {
                System.out.println(thread.getName()+"正在进行读操作"); 
            } 
            System.out.println(thread.getName()+"读操作完毕"); 
        } finally { 
            rwl.readLock().unlock(); // 释放读锁
        } 
    } 
}

说明thread1thread2在同时进行读操作,这样就大大提升了读操作的效率。

注:

  • 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁
  • 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。

2.5 小结(☆☆☆)

Locksynchronized有以下几点不同:

  • 原始构成:

    Lock是一个接口,而synchronizedJava中的关键字,synchronized是内置的语言实现

    Lock是具体类java.util.concurrent.locks.Lockapi层面的锁

  • 使用方法

    synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;

    Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁

  • 等待是否可中断

    Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断

  • 加锁是否严

    通过Lock可以知道有没有成功获取锁,而synchronized却无法办到

    synchronized非公平锁
    ReentrantLock两者都可以,默认非公平锁,构造方法可以传入boolean值,true为公平锁,false为非公平锁

  • 锁绑定多个条件Condition

    synchronized没有

    Reentrantlock用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程

    Lock可以提高多个线程进行读操作的效率

    在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized

3、线程间通信

线程间通信的模型有两种:共享内存和消息传递,以下方式都是基本这两种模型来实现的。

场景:两个线程,一个线程对当前数值加1,另一个线程对当前数值减1,要求用线程间通信

3.1 synchronized方案

// 第一步 创建资源类,定义属性和操作方法
class Share {
    // 初始值
    private int number = 0;
    
    // +1的方法
    public synchronized void incr() throws InterruptedException {
        // 第二步 判断 干活 通知
        while(number != 0) { //判断number值是否是0,如果不是0,等待
            this.wait(); // 在哪里睡,就在哪里醒,wait会释放锁
        }
        // 如果number值是0,就+1操作
        number++;
        System.out.println(Thread.currentThread().getName()+" :: "+number);
        // 通知其他线程
        this.notifyAll();
    }

    // -1的方法
    public synchronized void decr() throws InterruptedException {
        // 判断
        while(number != 1) {
            this.wait(); // 在哪里睡,就在哪里醒,wait会释放锁
        }
        // 干活
        number--;
        System.out.println(Thread.currentThread().getName()+" :: "+number);
        // 通知其他线程
        this.notifyAll();
    }
}

public class ThreadDemo1 {
    //第三步 创建多个线程,调用资源类的操作方法
    public static void main(String[] args) {
        // 创建共享资源
        Share share = new Share();
        
        new Thread(()->{
            for (int i = 1; i <=10; i++) {
                try {
                    share.incr(); //+1
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"AA").start();

        new Thread(()->{
            for (int i = 1; i <=10; i++) {
                try {
                    share.decr(); //-1
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"BB").start();

        new Thread(()->{
            for (int i = 1; i <=10; i++) {
                try {
                    share.incr(); //+1
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"CC").start();

        new Thread(()->{
            for (int i = 1; i <=10; i++) {
                try {
                    share.decr(); //-1
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"DD").start();
    }
}

注:如果在判断number值是否是0的地方用if,而不是while,会产生虚假唤醒问题。

如果只有两个线程的的话,一个加线程,一个减线程,if不会有问题,但如果是多个加线程和多个减线程,使用if判断就会有问题,因为线程唤醒的时候,在哪里睡,就在哪里醒,就不会进行再次条件判断,所以结果会产生问题。

3.2 Lock方案

//第一步 创建资源类,定义属性和操作方法
class Share {
    private int number = 0;

    //创建Lock
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    //+1
    public void incr() throws InterruptedException {
        //上锁
        lock.lock();
        try {
            //判断
            while (number != 0) {
                condition.await();
            }
            //干活
            number++;
            System.out.println(Thread.currentThread().getName()+" :: "+number);
            //通知
            condition.signalAll();
        }finally {
            //解锁
            lock.unlock();
        }
    }

    //-1
    public void decr() throws InterruptedException {
        lock.lock();
        try {
            while(number != 1) {
                condition.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName()+" :: "+number);
            condition.signalAll();
        }finally {
            lock.unlock();
        }
    }
}

public class ThreadDemo2 {

    public static void main(String[] args) {
        Share share = new Share();
        // 线程AA
        new Thread(()->{
            for (int i = 1; i <=10; i++) {
                try {
                    share.incr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"AA").start();
        
        // 线程BB
        new Thread(()->{
            for (int i = 1; i <=10; i++) {
                try {
                    share.decr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"BB").start();

        // 线程CC
        new Thread(()->{
            for (int i = 1; i <=10; i++) {
                try {
                    share.incr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"CC").start();
        
        // 线程DD
        new Thread(()->{
            for (int i = 1; i <=10; i++) {
                try {
                    share.decr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"DD").start();
    }

}

3.3 线程间定制化通信

问题: A线程打印5次A,B线程打印10次B,C线程打印15次C,按照此顺序循环10轮

// 第一步 创建资源类
class ShareResource {
    // 定义标志位
    private int flag = 1;  // 1 AA     2 BB     3 CC

    // 创建Lock锁
    private Lock lock = new ReentrantLock();

    // 创建三个condition
    private Condition c1 = lock.newCondition();
    private Condition c2 = lock.newCondition();
    private Condition c3 = lock.newCondition();

    // 打印5次,参数第几轮
    public void print5(int loop) throws InterruptedException {
        //上锁
        lock.lock();
        try {
            //判断
            while(flag != 1) {
                //等待
                c1.await();
            }
            //干活
            for (int i = 1; i <=loop; i++) {
                System.out.println(Thread.currentThread().getName()+" :: "+i+" :轮数:"+loop);
            }
            //通知
            flag = 2; //修改标志位 2
            c2.signal(); //通知BB线程
        }finally {
            //释放锁
            lock.unlock();
        }
    }

    //打印10次,参数第几轮
    public void print10(int loop) throws InterruptedException {
        lock.lock();
        try {
            while(flag != 2) {
                c2.await();
            }
            for (int i = 1; i <=loop; i++) {
                System.out.println(Thread.currentThread().getName()+" :: "+i+" :轮数:"+loop);
            }
            //修改标志位
            flag = 3;
            //通知CC线程
            c3.signal();
        }finally {
            lock.unlock();
        }
    }

    //打印15次,参数第几轮
    public void print15(int loop) throws InterruptedException {
        lock.lock();
        try {
            while(flag != 3) {
                c3.await();
            }
            for (int i = 1; i <=loop; i++) {
                System.out.println(Thread.currentThread().getName()+" :: "+i+" :轮数:"+loop);
            }
            //修改标志位
            flag = 1;
            //通知AA线程
            c1.signal();
        }finally {
            lock.unlock();
        }
    }
}

public class ThreadDemo {
    public static void main(String[] args) {
        ShareResource shareResource = new ShareResource();
        new Thread(()->{
            for (int i = 1; i <=10; i++) {
                try {
                    shareResource.print5(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"AA").start();

        new Thread(()->{
            for (int i = 1; i <=10; i++) {
                try {
                    shareResource.print10(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"BB").start();

        new Thread(()->{
            for (int i = 1; i <=10; i++) {
                try {
                    shareResource.print15(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"CC").start();
    }
}

3.4 多线程编程步骤

第一步,创建资源类,在资源类创建属性和操作方法

第二步,在资源类操作方法

  • 判断
  • 干活
  • 通知

第三步,创建多个线程,调用资源类的操作方法

第四步,防止虚假唤醒问题(用while循环,代替if判断)

4、集合的线程安全

4.1 ArrayList 线程不安全

以下三种方法都可以解决ArrayList线程不安全问题:

  • new Vector<>()
  • Collections.synchronizedList(new ArrayList<>())
  • CopyOnWriteArrayList<>():写时复制技术

写时复制技术:

CopyOnWrite容器即写时复制的容器。往一个容器添加元素的时候,不直接往当前容器Object []添加,而是先将当前容器Object []进行copy,复制出一个新的容器Object [] newElements,然后往新的容器Object [] newElements里添加元素,添加完元素之后,再将原容器的引用指向新的容器setArray(newElements),这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素,所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

4.2 HashSet 线程不安全

可以使用 CopyOnWriteArraySet来解决线程不安全问题

4.3 HashMap 线程不安全

可以使用 ConcurrentHashMap来解决线程不安全问题

5、多线程锁

5.1 synchronized锁

synchronized实现同步的基础是:Java中的每一个对象都可以作为锁,具体表现为以下三种形式。

  • 对于普通同步方法,锁是当前实例对象;

  • 对于静态同步方法,锁是当前类的Class对象;

  • 对于同步方法块,锁是synchronized括号里配置的对象;

5.2 公平锁和非公平锁

private final ReentrantLock lock = new ReentrantLock(false);  // 非公平锁
private final ReentrantLock lock = new ReentrantLock(true);  // 公平锁

非公平锁:会导致线程饿死,只有一个线程在执行,其他线程空着,但效率高;

公平锁:每个线程都能得到执行,需要排队,效率相对低;

5.3 可重入锁(递归锁)

可重入锁:指的是同一个线程外层函数获得锁之后,内层递归函数仍然能获取改锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。也就是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块。

  • 也叫作递归锁
  • ReentrantLock/Synchronized
  • 可以避免死锁

synchronized(隐式)和Lock(显示)都是可重入锁。

// synchronized 可重入锁
Object o = new Object();
new Thread(()->{
    synchronized(o) {
        System.out.println(Thread.currentThread().getName()+" 外层");
        synchronized (o) {
            System.out.println(Thread.currentThread().getName()+" 中层");
            synchronized (o) {
                System.out.println(Thread.currentThread().getName()+" 内层");
            }
        }
    }
},"t1").start();

// Lock可重入锁
Lock lock = new ReentrantLock();
new Thread(()->{
	try {
		//上锁
		lock.lock();
		System.out.println(Thread.currentThread().getName()+" 外层");
		try {
			//上锁
			lock.lock();
			System.out.println(Thread.currentThread().getName()+" 内层");
		}finally {
			//释放锁
			lock.unlock();
		}
	}finally {
		//释放做
		lock.unlock();
	}
},"t1").start();

注:Lock可重入锁,必须上锁和解锁一一对应,要不然锁不会释放。

5.4 死锁

public class DeadLock {

    //创建两个对象
    static Object a = new Object();
    static Object b = new Object();

    public static void main(String[] args) {
        new Thread(()->{
            synchronized (a) {
                System.out.println(Thread.currentThread().getName()+" 持有锁a,试图获取锁b");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (b) {
                    System.out.println(Thread.currentThread().getName()+" 获取锁b");
                }
            }
        },"A").start();

        new Thread(()->{
            synchronized (b) {
                System.out.println(Thread.currentThread().getName()+" 持有锁b,试图获取锁a");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (a) {
                    System.out.println(Thread.currentThread().getName()+" 获取锁a");
                }
            }
        },"B").start();
    }
}

自旋锁:Unsafe+CAS

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

独占锁(写锁)/共享锁(读锁)/互斥锁:ReentrantLock/Synchronized都是独占锁

6、Callable接口

6.1 创建线程的方式

创建线程有两种方法:

  • 通过创建Thread
  • 通过使用Runnable创建线程

通过Runnable创建线程缺少的一项功能是,当线程终止时,即run()完成时,我们无法使线程返回结果。

为了支持此功能,Java中提供了Callable接口,下面介绍创建线程的第三种方案,Callable接口

  • 为了实现Runnable,需要实现不返回任何内容的run()方法,而对于Callable,需要实现在完成时返回结果的call()方法
  • call()方法可以引发异常,而run()则不能
  • 为实现Callable而必须重写call()方法
  • 不能直接替换runnable,因为Thread类的构造方法根本没有Callable
// 创建新类MyThread实现runnable接口 
class MyThread implements Runnable{ 
    @Override public void run() { 
    
    } 
} 

// 新类MyThread2实现callable接口 
class MyThread2 implements Callable<Integer>{ 
    @Override public Integer call() throws Exception {
        return 200; 
    } 
}

注:Runnable接口有实现类FutureTask,FutureTask构造可以传递Callable

6.2 Future接口

call()方法完成时,结果必须存储在主线程已知的对象中,以便主线程可以知道该线程返回的结果。为此,可以使用Future对象。

Future视为保存结果的对象,它可能暂时不保存结果,但将来会保存(一旦Callable返回)。Future基本上是主线程可以跟踪进度以及其他线程的结果的一种方式。要实现此接口,必须重写5个方法,这里列出了重要的方法:

// 如果尚未启动,它将停止任务。如果已启动,则仅在mayInterrupt为true时才会中断任务。
public boolean cancel(boolean mayInterrupt)// 用于停止任务。

// 如果任务完成,它将立即返回结果,否则将等待任务完成,然后返回结果。
public Object get()throws InterruptedExceptionExecutionException // 用于获取任务的结果。

public boolean isDone()//如果任务完成,则返回true,否则返回false

可以看到CallableFuture做两件事,CallableRunnable类似,因为它封装了要在另一个线程上运行的任务,而Future用于存储从另一个线程获得的结果。实际上,Future也可以与Runnable一起使用。

要创建线程,需要Runnable,为了获得结果,需要Future

6.3 FutureTask

Java库有具体的FutureTask类型,该类型实现RunnableFuture,并方便地将两种功能组合在一起。可以通过为其构造函数提供Callable来创建FutureTask,然后,将FutureTask对象提供给Thread的构造函数以创建Thread对象,因此,间接地使用Callable创建线程。

核心原理:
在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给Future对象在后台完成

  • 当主线程将来需要时,就可以通过Future对象获得后台作业的计算结果或者执行状态

  • 一般FutureTask多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。

  • 仅在计算完成时才能检索结果,如果计算尚未完成,则阻塞 get()方法

  • 一旦计算完成,就不能再重新开始或取消计算

  • get()获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常

  • get()只计算一次,因此get()方法放到最后

6.4 使用Callable和Future

比较两个接口

//实现Runnable接口
class MyThread1 implements Runnable {
    @Override
    public void run() {

    }
}

//实现Callable接口
class MyThread2 implements Callable {

    @Override
    public Integer call() throws Exception {
        System.out.println(Thread.currentThread().getName()+" come in callable");
        return 200;
    }
}

public class Demo1 {
    public static void main(String[] args) throws Exception {
        // Runnable接口创建线程
        new Thread(new MyThread1(),"AA").start();

        //FutureTask
        FutureTask<Integer> futureTask1 = new FutureTask<>(new MyThread2());

        //lam表达式
        FutureTask<Integer> futureTask2 = new FutureTask<>(()->{
            System.out.println(Thread.currentThread().getName()+" come in callable");
            return 1024;
        });

        //创建一个线程
        new Thread(futureTask2,"lucy").start();
        new Thread(futureTask1,"mary").start();

//        while(!futureTask2.isDone()) {
//            System.out.println("wait.....");
//        }
        // 调用FutureTask的get方法
        System.out.println(futureTask2.get());
        System.out.println(futureTask1.get());
        System.out.println(Thread.currentThread().getName()+" come over");
    }
}

7、JUC三大辅助类

JUC中提供了三种常用的辅助类,通过这些辅助类可以很好的解决线程数量过多时Lock锁的频繁操作。这三种辅助类为:

  • CountDownLatch:减少计数
  • CyclicBarrier:循环栅栏
  • Semaphore:信号灯

7.1 CountDownLatch

CountDownLatch类可以设置一个计数器,然后通过countDown()方法来进行减1的操作,使用await()方法等待计数器不大于0,然后继续执行await()方法之后的语句。

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

场景:6个同学陆续离开教室后值班同学才可以关门

public class CountDownLatchDemo {
    //6个同学陆续离开教室之后,班长锁门
    public static void main(String[] args) throws InterruptedException {

        //创建CountDownLatch对象,设置初始值
        CountDownLatch countDownLatch = new CountDownLatch(6);

        //6个同学陆续离开教室之后
        for (int i = 1; i <=6; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+" 号同学离开了教室");
                //计数  -1
                countDownLatch.countDown();
            },String.valueOf(i)).start();
        }

        //等待
        countDownLatch.await();
        System.out.println(Thread.currentThread().getName()+" 班长锁门走人了");
    }
}

7.2 CyclicBarrier

在使用中CyclicBarrier的构造方法第一个参数是目标障碍数,每次执行CyclicBarrier一次障碍数会加一,如果达到了目标障碍数,才会执行cyclicBarrier.await()之后的语句。

场景:集齐7颗龙珠就可以召唤神龙

public class CyclicBarrierDemo {

    //创建固定值
    private static final int NUMBER = 7;

    public static void main(String[] args) {
        // 创建CyclicBarrier
        CyclicBarrier cyclicBarrier =
                new CyclicBarrier(NUMBER,()->{
                    System.out.println("*****集齐7颗龙珠就可以召唤神龙");
                });
        //集齐七颗龙珠过程
        for (int i = 1; i <=7; i++) {
            new Thread(()->{
                try {
                    System.out.println(Thread.currentThread().getName()+" 星龙被收集到了");
                    // 等待
                    cyclicBarrier.await();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}

7.3 Semaphore

Semaphore的构造方法中传入的第一个参数是最大信号量(可以看成最大线程池),每个信号量初始化为一个最多只能分发一个许可证。使用acquire()方法获得许可证,release()方法释放许可。

场景:抢车位,6部汽车3个停车位

public class SemaphoreDemo {
    public static void main(String[] args) {
        //创建Semaphore,设置许可数量
        Semaphore semaphore = new Semaphore(3);

        //模拟6辆汽车
        for (int i = 1; i <=6; i++) {
            new Thread(()->{
                try {
                    //抢占
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName()+" 抢到了车位");
                    //设置随机停车时间
                    TimeUnit.SECONDS.sleep(new Random().nextInt(5));
                    System.out.println(Thread.currentThread().getName()+" ------离开了车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //释放
                    semaphore.release();
                }
            },String.valueOf(i)).start();
        }
    }
}

8、读写锁:ReentrantReadWriteLock

8.1 读写锁

现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源,但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了。

针对这种场景,Java的并发包提供了读写锁 ReentrantReadWriteLock,它表示两个锁,一个是读操作相关的锁,称为共享锁,一个是写相关的锁,称为排他锁。

线程进入读锁的前提条件:

  • 没有其他线程的写锁
  • 没有写请求,或者有写请求,但调用线程和持有锁的线程是同一个(可重入锁)。

线程进入写锁的前提条件:

  • 没有其他线程的读锁
  • 没有其他线程的写锁

而读写锁有以下三个重要的特性:

  • 公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平
  • 重进入:读锁和写锁都支持线程重进入
  • 锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁

读写锁:一个自由可以被多个读线程访问,或者可以被一个写线程访问,但不能同时存在读写线程,读写互斥,读读共享。

场景:使用ReentrantReadWriteLock对一个HashMap进行读和写操作

//资源类
class MyCache {
    //创建map集合
    private volatile Map<String,Object> map = new HashMap<>();

    //创建读写锁对象
    private ReadWriteLock rwLock = new ReentrantReadWriteLock();

    //放数据
    public void put(String key,Object value) {
        //添加写锁
        rwLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName()+" 正在写操作"+key);
            //暂停一会
            TimeUnit.MICROSECONDS.sleep(300);
            //放数据
            map.put(key,value);
            System.out.println(Thread.currentThread().getName()+" 写完了"+key);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //释放写锁
            rwLock.writeLock().unlock();
        }
    }

    //取数据
    public Object get(String key) {
        //添加读锁
        rwLock.readLock().lock();
        Object result = null;
        try {
            System.out.println(Thread.currentThread().getName()+" 正在读取操作"+key);
            //暂停一会
            TimeUnit.MICROSECONDS.sleep(300);
            result = map.get(key);
            System.out.println(Thread.currentThread().getName()+" 取完了"+key);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //释放读锁
            rwLock.readLock().unlock();
        }
        return result;
    }
}

public class ReadWriteLockDemo {
    public static void main(String[] args) throws InterruptedException {
        MyCache myCache = new MyCache();
        // 创建线程放数据
        for (int i = 1; i <=5; i++) {
            final int num = i;
            new Thread(()->{
                myCache.put(num+"",num+"");
            },String.valueOf(i)).start();
        }

        TimeUnit.MICROSECONDS.sleep(300);

        //创建线程取数据
        for (int i = 1; i <=5; i++) {
            final int num = i;
            new Thread(()->{
                myCache.get(num+"");
            },String.valueOf(i)).start();
        }
    }
}

8.2 小结

  • 在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。
  • 在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。
    原因:当线程获取读锁的时候,可能有其他线程同时也在持有读锁,因此不能把获取读锁的线程“升级”为写锁;而对于获得写锁的线程,它一定独占了读写锁,因此可以继续让它获取读锁,当它同时获取了写锁和读锁后,还可以先释放写锁继续持有读锁,这样一个写锁就“降级”为了读锁。

9、阻塞队列

9.1 BlockingQueue

BlockingQueue(阻塞队列),顾名思义,首先它是一个队列,通过一个共享的队列,可以使得数据由队列的一端输入,从另外一端输出。

JUC高并发学习笔记_第1张图片

  • 当队列是空的,从队列中获取元素的操作将会被阻塞
  • 当队列是满的,从队列中添加元素的操作将会被阻塞
  • 试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素
  • 试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列变得空闲起来并后续新增

JUC高并发学习笔记_第2张图片
JUC高并发学习笔记_第3张图片

常用的队列主要有以下两种:

  • 先进先出(FIFO):先插入的队列的元素也最先出队列
  • 后进先出(LIFO):后插入队列的元素最先出队列,这种队列优先处理最近发生的事件(栈)

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

为什么需要BlockingQueue?

多线程环境中,通过队列可以很容易实现数据共享,比如经典的“生产者”和“消费者”模型中,通过队列可以很便利地实现两者之间的数据共享。假设我们有若干生产者线程,另外又有若干个消费者线程。如果生产者线程需要把准备好的数据共享给消费者线程,利用队列的方式来传递数据,就可以很方便地解决他们之间的数据共享问题。但如果生产者和消费者在某个时间段内,万一发生数据处理速度不匹配的情况呢?理想情况下,如果生产者产出数据的速度大于消费者消费的速度,并且当生产出来的数据累积到一定程度的时候,那么生产者必须暂停等待一下(阻塞生产者线程),以便等待消费者线程把累积的数据处理完毕,反之亦然。

  • 当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列
  • 当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒

9.2 常见的BlockingQueue

9.2.1 ArrayBlockingQueue(常用)

基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。

ArrayBlockingQueue在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行,这点尤其不同于LinkedBlockingQueue

ArrayBlockingQueueLinkedBlockingQueue间还有一个明显的不同之处在于,前者在插入或删除元素时不会产生或销毁任何额外的对象实例,而后者则会生成一个额外的Node对象。这在长时间内需要高效并发地处理大批量数据的系统中,其对于GC的影响还是存在一定的区别。而在创建ArrayBlockingQueue时,我们还可以控制对象的内部锁是否采用公平锁,默认采用非公平锁

一句话总结:由数组结构组成的有界阻塞队列。

9.2.2 LinkedBlockingQueue(常用)

基于链表的阻塞队列,其内部也维持着一个数据缓冲队列,该队列由一个链表构成,当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。

LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。

一句话总结:由链表结构组成的有界(但大小默认值为Integer.MAX_VALUE)阻塞队列。

9.2.3 DelayQueue

DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。
一句话总结:使用优先级队列实现的延迟无界阻塞队列。

9.2.4 PriorityBlockingQueue

基于优先级的阻塞队列,优先级的判断通过构造函数传入的Compator对象来决定,但需要注意的是PriorityBlockingQueue不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。

因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。

在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁

一句话总结:支持优先级排序的无界阻塞队列。

9.2.5 SynchronousQueue

一种无缓冲的等待队列,类似于无中介的直接交易,相对于有缓冲的BlockingQueue来说,少了一个中间经销商的环节(缓冲区),如果有经销商,生产者直接把产品批发给经销商,而无需在意经销商最终会将这些产品卖给那些消费者,由于经销商可以库存一部分商品,因此相对于直接交易模式,总体来说采用中间经销商的模式会吞吐量高一些(可以批量买卖);但另一方面,又因为经销商的引入,使得产品从生产者到消费者中间增加了额外的交易环节,单个产品的及时响应性能可能会降低。

声明一个SynchronousQueue有两种不同方式,它们之间有着不太一样的行为,公平模式和非公平模式的区别:

  • 公平模式:SynchronousQueue会采用公平锁,并配合一个FIFO队列来阻塞多余的生产者和消费者,从而体系整体的公平策略

  • 非公平模式(SynchronousQueue默认):SynchronousQueue采用非公平锁,同时配合一个LIFO队列来管理多余的生产者和消费者,而后一种模式,如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理

一句话总结: 不存储元素的阻塞队列,也即单个元素的队列。

10、ThreadPool线程池

Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类。
JUC高并发学习笔记_第4张图片

10.2 线程池的种类与创建

10.2.1 newCachedThreadPool(常用)

作用:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程

特点:

  • 一池可扩容线程

  • 线程池中数量没有固定,可达到最大值(Interger. MAX_VALUE

  • 线程池中的线程可进行缓存重复利用和回收(回收默认时间为1分钟)

  • 当线程池中,没有可用线程,会重新创建一个线程

创建方式:

/** 
* 可缓存线程池 
* @return 
*/ 
public static ExecutorService newCachedThreadPool(){ 
    /** 
    * corePoolSize 线程池的核心线程数 
    * maximumPoolSize 能容纳的最大线程数 
    * keepAliveTime 空闲线程存活时间 
    * unit 存活的时间单位 
    * workQueue 存放提交但未执行任务的队列 
    * threadFactory 创建线程的工厂类:可以省略 
    * handler 等待队列满后的拒绝策略:可以省略 
    */ 
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, 
                                  new SynchronousQueue<>(), 
                                  Executors.defaultThreadFactory(),
                                  new ThreadPoolExecutor.AbortPolicy()); 
}

场景:适用于创建一个可无限扩大的线程池,服务器负载压力较轻,执行时间较短,任务多的场景

10.2.2 newFixedThreadPool(常用)

作用:创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。

特征:

  • 一池N线程

  • 线程池中的线程处于一定的量,可以很好的控制线程的并发量

  • 线程可以重复被使用,在显示关闭之前,都将一直存在

  • 超出一定量的线程被提交时候需在队列中等待

创建方式:

/** 
* 固定长度线程池 
* @return 
*/
public static ExecutorService newFixedThreadPool(){ 
    /** 
    * corePoolSize 线程池的核心线程数 
    * maximumPoolSize 能容纳的最大线程数 
    * keepAliveTime 空闲线程存活时间 
    * unit 存活的时间单位 
    * workQueue 存放提交但未执行任务的队列 
    * threadFactory 创建线程的工厂类:可以省略 
    * handler 等待队列满后的拒绝策略:可以省略 
    */ 
    return new ThreadPoolExecutor(10, 10, 0L, TimeUnit.SECONDS, 
                                  new LinkedBlockingQueue<>(), 
                                  Executors.defaultThreadFactory(),
                                  new ThreadPoolExecutor.AbortPolicy()); 
}

场景:适用于可以预测线程数量的业务中,或者服务器负载较重,对线程数有严格限制的场景

10.2.3 newSingleThreadExecutor(常用)

作用:创建一个使用单个 worker线程的 Executor,以无界队列方式来运行该线程。

特征:

  • 一池一线程

  • 线程池中最多执行1个线程,之后提交的线程活动将会排在队列中以此执行

创建方式:

/** 
* 单一线程池 
* @return 
*/ 
public static ExecutorService newSingleThreadExecutor(){
    /** 
    * corePoolSize 线程池的核心线程数 
    * maximumPoolSize 能容纳的最大线程数 
    * keepAliveTime 空闲线程存活时间 
    * unit 存活的时间单位 
    * workQueue 存放提交但未执行任务的队列 
    * threadFactory 创建线程的工厂类:可以省略 
    * handler 等待队列满后的拒绝策略:可以省略 
    */ 
    return new ThreadPoolExecutor(1, 1, 0L, TimeUnit.SECONDS, 
                                  new LinkedBlockingQueue<>(), 
                                  Executors.defaultThreadFactory(), 
                                  new ThreadPoolExecutor.AbortPolicy());
}

场景:适用于需要保证顺序执行各个任务,并且在任意时间点,不会同时有多个线程的场景

10.2.4 newScheduleThreadPool(了解)

作用:线程池支持定时以及周期性执行任务,创建一个corePoolSize为传入参数,最大线程数为整形的最大数的线程池

特征

  • 线程池中具有指定数量的线程,即便是空线程也将保留
  • 可定时或者延迟执行线程活动

创建方式:

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory) { 
    
    return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory); 
}

场景:适用于需要多个后台线程执行周期任务的场景

10.2.5 newWorkStealingPool

jdk1.8提供的线程池,底层使用的是ForkJoinPool实现,创建一个拥有多个任务队列的线程池,可以减少连接数,创建当前可用cpu核数的线程来并行执行任务

创建方式:

public static ExecutorService newWorkStealingPool(int parallelism) { 
    /** 
    * parallelism:并行级别,通常默认为JVM可用的处理器个数 
    * factory:用于创建ForkJoinPool中使用的线程。 
    * handler:用于处理工作线程未处理的异常,默认为null 
    * asyncMode:用于控制WorkQueue的工作模式:队列---反队列
    */ 
    return new ForkJoinPool(parallelism, 
                            ForkJoinPool.defaultForkJoinWorkerThreadFactory, 
                            null, 
                            true); 
}

场景:适用于大耗时,可并行执行的场景

10.3 线程池参数说明

10.3.1 线程池七个参数
  • corePoolSize:线程池的核心线程数
  • maximumPoolSize:能容纳的最大线程数
  • keepAliveTime:多余的空闲线程存活时间。(当前线程池梳理超过corePoolSize时,当空间时间达到keepAliveTime值时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止)
  • unit:存活的时间单位
  • workQueue:存放提交但未执行任务的队列
  • threadFactory:创建线程的工厂类
  • handler:等待队列满后的拒绝策略

线程池中,有三个重要的参数,决定影响了拒绝策略:

  • corePoolSize- 核心线程数,也即最小的线程数

  • maximumPoolSize- 最大线程数

  • workQueue- 阻塞队列

当提交任务数大于 corePoolSize的时候,会优先将任务放到 workQueue阻塞队列中。当阻塞队列饱和后,会扩充线程池中线程数,直到达到 maximumPoolSize最大线程数配置。此时,再多余的任务,则会触发线程池的拒绝策略了

总结起来,也就是一句话,当提交的任务数大于(workQueue.size() + maximumPoolSize ),就会触发线程池的拒绝策略。

10.3.2 拒绝策略(☆☆☆)

JDK内置的拒绝策略:

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

10.4 线程池入门案例

//演示线程池三种常用分类
public class ThreadPoolDemo1 {
    public static void main(String[] args) {
        //一池五线程
        ExecutorService threadPool1 = Executors.newFixedThreadPool(5); //5个窗口

        //一池一线程
        ExecutorService threadPool2 = Executors.newSingleThreadExecutor(); //一个窗口

        //一池可扩容线程
        ExecutorService threadPool3 = Executors.newCachedThreadPool();
        //10个顾客请求
        try {
            for (int i = 1; i <=10; i++) {
                //执行
                threadPool3.execute(()->{
                    System.out.println(Thread.currentThread().getName()+" 办理业务");
                });
            }
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            //关闭
            threadPool3.shutdown();
        }
    }
}

10.5 线程池底层工作原理(☆☆☆)

JUC高并发学习笔记_第5张图片

  • 在创建了线程池后,线程池中的线程数为零,等待提交过来的任务请求。
  • 当调用execute()方法添加一个请求任务时,线程池会做出如下判断:
    • 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
    • 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
    • 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
    • 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
  • 当一个线程完成任务时,它会从队列中取下一个任务来执行
  • 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:
    • 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
    • 所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。

10.6 注意事项(☆☆☆)

  • 项目中创建多线程时,使用常见的三种线程池创建方式,单一、可变、定长都有一定问题,原因是FixedThreadPoolSingleThreadExecutor底层都是用LinkedBlockingQueue实现的,这个队列最大长度为Integer.MAX_VALUE,容易导致OOM。所以实际生产一般自己通过ThreadPoolExecutor的7个参数,自定义线程池

为什么不允许使用Executors的方式手动创建线程池?阿里巴巴Java开发手册有说明,如下图
JUC高并发学习笔记_第6张图片

//自定义线程池创建
public class ThreadPoolDemo2 {
    public static void main(String[] args) {
        ExecutorService threadPool = new ThreadPoolExecutor(
                2, 5, 2L, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()
        );

        //10个顾客请求
        try {
            for (int i = 1; i <=10; i++) {
                //执行
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+" 办理业务");
                });
            }
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            //关闭
            threadPool.shutdown();
        }
    }
}

你可能感兴趣的:(并发编程,java,juc,高并发编程)