JUC - 并发编程

第一章、基础

一、基础入门

1.传统线程使用-卖票业务

传统的项目使用线程举例:卖票业务

代码如下:

/**
 * @author byChen
 * @date 2022/8/11
 */
@RestController
@RequestMapping("/thread1")
@AllArgsConstructor
public class ThreadDemo1 {
    /**
     * 为了方便,在此处定义实体类
     * 根据返回状态,来判断没有票了,就不会重复来调用
     */
    class Ticket {
        private int num = 40;

        public synchronized boolean saleTicket() {
            if (num > 0) {
                System.out.println(Thread.currentThread().getName() + " 线程卖出第" + (num--) + ",剩余 " + num + "张");
                return true;
            } else {
                System.out.println(Thread.currentThread().getName() + " 线程到达,无余票了");
                return false;
            }
        }
    }

    @GetMapping("/test1")
    public void test() {
        Ticket ticket = new Ticket();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 40; i++) {
                    boolean b = ticket.saleTicket();
                    //判断余量
                    if (!b) {
                        break;
                    }
                }
            }
        }, "A").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 40; i++) {
                    boolean b = ticket.saleTicket();
                    //判断余量
                    if (!b) {
                        break;
                    }
                }
            }
        }, "B").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 40; i++) {
                    boolean b = ticket.saleTicket();
                    //判断余量
                    if (!b) {
                        break;
                    }
                }
            }
        }, "C").start();

    }
}

访问结果:

A 线程卖出第40,剩余 39张
A 线程卖出第39,剩余 38张

。。。中间省略。。。。

C 线程卖出第3,剩余 2张
C 线程卖出第2,剩余 1张
A 线程卖出第1,剩余 0张
A 线程到达,无余票了
C 线程到达,无余票了
B 线程到达,无余票了

2.修改,可重复锁 - ReentrantLock

上面的关键字 synchronized 虽然可以给资源加锁,但是锁的范围过于巨大,整个类中的代码都会被加锁。这是不太合理的。

由此可以使用另外一种锁,只在关键部分的代码进行锁的处理。这种细粒度化的锁才是最合适的。

线程 操作 资源类

ReentrantLock

官方示例代码:
在这里插入图片描述

代码修改为:

/**
 * @author byChen
 * @date 2022/8/11
 */
@RestController
@RequestMapping("/thread1")
@AllArgsConstructor
public class ThreadDemo1 {
    /**
     * 为了方便,在此处定义实体类
     * 根据返回状态,来判断没有票了,就不会重复来调用
     */
    class Ticket {
        private int num = 40;
        //增加细粒度锁,可重复锁
        private Lock lock = new ReentrantLock();

        public boolean saleTicket() {
            boolean flag = true;
            //锁【lock.lock】必须紧跟try代码块,且unlock要放到finally第一行
            lock.lock();
            try {
                if (num > 0) {
                    System.out.println(Thread.currentThread().getName() + " 线程卖出第" + (num--) + ",剩余 " + num + "张");
                    flag = true;
                } else {
                    System.out.println(Thread.currentThread().getName() + " 线程到达,无余票了");
                    flag = false;
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                //finally 体内必须第一行就释放锁
                lock.unlock();
            }
            return flag;
        }
    }

    @GetMapping("/test1")
    public void test() {
        Ticket ticket = new Ticket();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 40; i++) {
                    boolean b = ticket.saleTicket();
                    //判断余量
                    if (!b) {
                        break;
                    }
                }
            }
        }, "A").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 40; i++) {
                    boolean b = ticket.saleTicket();
                    //判断余量
                    if (!b) {
                        break;
                    }
                }
            }
        }, "B").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 40; i++) {
                    boolean b = ticket.saleTicket();
                    //判断余量
                    if (!b) {
                        break;
                    }
                }
            }
        }, "C").start();

    }
}

访问结果,与之前传统写法一样:

A 线程卖出第40,剩余 39张
A 线程卖出第39,剩余 38张

。。。中间省略。。。。

C 线程卖出第3,剩余 2张
C 线程卖出第2,剩余 1张
A 线程卖出第1,剩余 0张
A 线程到达,无余票了
C 线程到达,无余票了
B 线程到达,无余票了

代码优化:
代码过于臃肿,可以使用lamba表达式进行代码简洁
在这里插入图片描述
如果不计算返回值的话,上面的是最简洁的,计算的话就下面这样。

    @GetMapping("/test1")
    public void test() {
        Ticket ticket = new Ticket();

        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                boolean b = ticket.saleTicket();
                if (!b) {
                    break;
                }
            }
        }, "A").start();

        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                boolean b = ticket.saleTicket();
                if (!b) {
                    break;
                }
            }
        }, "B").start();

        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                boolean b = ticket.saleTicket();
                if (!b) {
                    break;
                }
            }
        }, "C").start();

    }

但是这样不适应内部逻辑很复杂的代码,因此看情况使用

3.线程的几种状态

线程调用 .start() 方法后,线程进入就绪状态,并没有直接启动,而是等 cpu 来调用才会启用。

查看源码枚举:

   public enum State {
        /**
         * 新建
         * 尚未启动的线程的线程状态。
         */
        NEW,

        /**
         * 就绪
         * 可运行线程的线程状态。
         * 处于可运行状态的线程正在 Java 虚拟机中执行,
         * 但它可能正在等待来自操作系统的其他资源,例如处理器
         */
        RUNNABLE,

        /**
         * 阻塞
         * 线程阻塞等待监视器锁的线程状态。
         * 处于阻塞状态的线程正在等待监视器锁进入同步块方法或调用 {.wait()方法 } 后重新进入同步块方法。
         */
        BLOCKED,

        /**
         * 未指定时间的 等待
         * 等待线程的线程状态
         * 由于调用以下方法之一,线程处于等待状态:
         * {@link Object#wait() Object.wait}
         * {@link #join() Thread.join}
         * {@link LockSupport#park() LockSupport.park}
         * 线程会进入对象的等待池中
         * 处于等待状态的线程正在等待另一个线程执行特定操作 (唤醒操作notify())
         */
        WAITING,

        /**
         * 指定时间的 等待
         *具有指定等待时间的等待线程的线程状态。
       
         * 
         */
        TIMED_WAITING,

        /**
         * 已终止线程的线程状态。线程已完成执行。
         */
        TERMINATED;
    }

引申知识点:

    /**
     * notify()和notifyAll()有什么区别?
     * 首先 要明确锁池跟等待池概念
     * 等待池:线程调用了某个对象的wait()方法后,就会在释放掉该对象的锁后,进入等待池
     *  锁池:当多个线程去争夺某个对象的锁时,这些线程又都到锁池中
     * notify()和notifyAll()就是将等待池中的线程唤醒,让其进入到锁池中去竞争对象的锁,
     * notify()是唤醒一个线程  notifyAll()是唤醒所有线程, 唤醒一个线程又可能导致死锁,唤醒所有则不会
     * 因为唤醒所有,一个线程挂掉会有其他线程补上,唤醒一个,要是其挂掉就死锁了
     */
    /**
     * 为什么wait, notify 和 notifyAll这些方法不在thread类里面?
     *
     *      因为JAVA提供的锁是对象级(资源类)的而不是线程级的,因此把这些方法定义在定义在Object类中,这样就是锁属于对象,
     *      当线程需要这些对象时,直接调用对象中的wait()方法;
     *      如果wait()方法定义在Thread类中,线程正在等待的是哪个资源的锁就不明显了。
     *      简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在对象类中
     *
     */

4.线程间通信

① 单生产者/单消费者 通信模式

判断/干活/通知

案例: 两个线程,轮流对值进行 加 减 操作,十轮之后,值不变
分析: 要求一个线程对值操作后,要通知另外一个来对值操作,然后互相通知;这就需要线程之间横向通信协助;

代码:


/**
 * 线程间通信
 *
 * @author byChen
 * @date 2022/8/11
 */
@RestController
@RequestMapping("/thread2")
@AllArgsConstructor
public class ThreadDemo2 {
    /**
     * 为了方便,在此处定义实体类
     * 实现 : 循环10轮,加减一,得出结果为 0
     */
    class NumController {
        private int num = 0;
        //增加细粒度锁,可重复锁
        private final Lock lock = new ReentrantLock();

        /**
         * 加一
         */
        public synchronized void add() throws InterruptedException {
            //1.判断,不等于0 就不加一
            if (num != 0) {
                //wait 是object类的方法,因此这里是类调用wait方法,使得线程释放锁,进入线程池等待
                this.wait();
            }
            //2.干活
            num++;
            System.out.println("线程:" + Thread.currentThread().getName() + "进行 【加一】 作业:操作后当前值:" + num);
            //3.通知 notifyAll也是object类的方法,这里当前线程处理完,会唤醒所有在该类的等待池中的线程,一同进入锁池去争抢线程权
            this.notifyAll();
        }

        /**
         * 减一
         */
        public synchronized void cut() throws InterruptedException {
            //1.判断
            if (num == 0) {
                this.wait();
            }
            //2.干活
            num--;
            System.out.println("线程:" + Thread.currentThread().getName() + "进行 【减一】 作业:操作后当前值:" + num);
            //3.通知
            this.notifyAll();
        }
    }

    /**
     * 实现两个线程轮番对数据进行加减,保证结束后值不变
     */
    @GetMapping("/test1")
    public void test1() {
        NumController numController = new NumController();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    numController.add();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();

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

}

结果截图:
在这里插入图片描述
有条不紊,轮番处理

补充知识点:

1.明确锁池跟等待池概念
	等待池:线程调用了某个对象的wait()方法后,就会在释放掉该对象的锁后,进入等待池,等待某个线程去唤醒它
	锁池:当多个线程去争夺某个对象的锁时,这些线程又都到锁池中,去竞争锁资源。
	
    notify()和notifyAll()就是将等待池中的线程唤醒,让其进入到锁池中去竞争对象的锁
2.wait()方法,notify()和notifyAll(),都是属于Object类中的方法
	JAVA提供的锁是对象级的而不是线程级的,因此把这些方法定义在定义在Object类中,这样就是锁属于资源类对象的;
	当线程需要针对这些对象进行等待时,直接在线程中让对象调用wait()方法,这样线程就进入该对象的等待池中;
	这样该线程等待的是那个资源类的锁就很清晰。
	同样的,因为notify()和notifyAll()也都是object类中的方法,因此使用资源类来唤醒,就很清晰的知道要唤醒的是那个资源类的等待池中的线程了
② 多生产者/多消费者 通信模式

如果单纯的增加线程,就会出现混乱的情况,

        //增加两个线程,分别进行加减
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    numController.add();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "C").start();

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

结果:

线程:A进行 【加一】 作业:操作后当前值:1
线程:B进行 【减一】 作业:操作后当前值:0
线程:C进行 【加一】 作业:操作后当前值:1
线程:A进行 【加一】 作业:操作后当前值:2
线程:D进行 【减一】 作业:操作后当前值:1
线程:D进行 【减一】 作业:操作后当前值:0
线程:A进行 【加一】 作业:操作后当前值:1
线程:D进行 【减一】 作业:操作后当前值:0
线程:B进行 【减一】 作业:操作后当前值:-1
线程:B进行 【减一】 作业:操作后当前值:-2
线程:B进行 【减一】 作业:操作后当前值:-3
线程:B进行 【减一】 作业:操作后当前值:-4
线程:B进行 【减一】 作业:操作后当前值:-5
线程:B进行 【减一】 作业:操作后当前值:-6
线程:B进行 【减一】 作业:操作后当前值:-7
线程:C进行 【加一】 作业:操作后当前值:-6
线程:D进行 【减一】 作业:操作后当前值:-7

出现了多加、多减的情况;

这种情况就属于是多线程的虚假唤醒

虚假唤醒情况 - 分析与解决

注意,上述问题的根本原因,就是多线程之间相互通信,因为使用了if 来做判断,因此造成虚假唤醒。

多线程通信唤醒,禁止用 if 来做判断;而是要改为使用 while

Object 类的wait 方法 源码中指明,使用wait 的时候,判断必须应该使用 while

在这里插入图片描述

为什么?:
在这里插入图片描述
注:不是线程抢占,只是正常的调用 .wait() s交出线程控制权

也就是if 只会 在线程进入的时候进行条件的判断,判断之后,不管线程是直接执行了方法体,还是执行之前被阻塞然后再次唤醒,它都不会再次进行判断条件,而是直接执行,这种就导致了 超加 或者 超减 的情况;

而while 判断是每次线程进入,或者唤醒,都会再次去判断条件是否满足。
这样就保证不管线程是否在执行方法体之前被阻塞而导致 num 值被修改,因为再次判断条件的存在,都不会产生错误执行的情况。

tips:加锁之后使用if才会出现上述情况,正常的逻辑代码不会,因为使用了synchronized 关键字,一进接口就相当于加锁。如果使用 Lock 那么只会在 。lock之后才会有 if 跟 while 的区别。 

因此 “虚假唤醒exception” 其实更应该叫 “唤醒之后未再次判断exception”

代买修改为 用while 做判断

    /**
     * 为了方便,在此处定义实体类
     * 实现 : 循环10轮,加减一,得出结果为 0
     */
    class NumController {
        private int num = 0;
        //增加细粒度锁,可重复锁
        private final Lock lock = new ReentrantLock();

        /**
         * 加一
         */
        public synchronized void add() throws InterruptedException {
            //1.判断,不等于0 就不加一 (使用while进行判断)
            while (num != 0) {

                //wait 是object类的方法,因此这里是类调用wait方法,使得线程释放锁,进入线程池等待
                this.wait();
            }
            //2.干活
            num++;
            System.out.println("线程:" + Thread.currentThread().getName() + "进行 【加一】 作业:操作后当前值:" + num);
            //3.通知 notifyAll也是object类的方法,这里当前线程处理完,会唤醒所有在该类的等待池中的线程,一同进入锁池去争抢线程权
            this.notifyAll();
        }

        /**
         * 减一
         */
        public synchronized void cut() throws InterruptedException {
            //1.判断 (使用while进行判断)
            while (num == 0) {
                this.wait();
            }
            //2.干活
            num--;
            System.out.println("线程:" + Thread.currentThread().getName() + "进行 【减一】 作业:操作后当前值:" + num);
            //3.通知
            this.notifyAll();
        }
    }

结果就正常了:

线程:A进行 【加一】 作业:操作后当前值:1
线程:B进行 【减一】 作业:操作后当前值:0
线程:A进行 【加一】 作业:操作后当前值:1
。。。中间省略。。。
线程:D进行 【减一】 作业:操作后当前值:0
线程:C进行 【加一】 作业:操作后当前值:1
线程:D进行 【减一】 作业:操作后当前值:0

③ 新方法:使用lock 的生产者消费者模式

synchronized 作为锁的话,那根据条件进行 等待、唤醒,就使用的 wait notify 方法

使用 Lock 作为锁,那应该使用什么方法呢?

看源码提示:
源码指向使用 Condtion 接口来进行条件判断
在这里插入图片描述


   class BoundedBuffer<E> {
     final Lock lock = new ReentrantLock();
     final Condition notFull  = lock.newCondition(); 
     final Condition notEmpty = lock.newCondition(); 
  
     final Object[] items = new Object[100];
     int putptr, takeptr, count;
  
     public void put(E x) throws InterruptedException {
       lock.lock();
       try {
         while (count == items.length)
           notFull.await();
         items[putptr] = x;
         if (++putptr == items.length) putptr = 0;
         ++count;
         notEmpty.signal();
       } finally {
         lock.unlock();
       }
     }
  
     public E take() throws InterruptedException {
       lock.lock();
       try {
         while (count == 0)
           notEmpty.await();
         E x = (E) items[takeptr];
         if (++takeptr == items.length) takeptr = 0;
         --count;
         notFull.signal();
         return x;
       } finally {
         lock.unlock();
       }
     }
   }

总结来说,新旧替换对应 如图:
在这里插入图片描述
替换后代码:


/**
 * 线程间通信 使用新方法  Lock + Condition
 *
 * @author byChen
 * @date 2022/8/11
 */
@RestController
@RequestMapping("/thread3")
@AllArgsConstructor
public class ThreadDemo3 {
    /**
     * 为了方便,在此处定义实体类
     * 实现 : 循环10轮,加减一,得出结果为 0
     */
    class NumController {
        private int num = 0;
        //增加细粒度锁,可重复锁
        private final Lock lock = new ReentrantLock();
        //使用 Lock 搭配使用
        private Condition condition = lock.newCondition();

        /**
         * 加一
         */
        public void add() throws InterruptedException {
            lock.lock();
            try {
                //1.判断,不等于0 就不加一
                while (num != 0) {
                    //wait 方法被替换
//                    this.wait();
                    condition.await();
                }
                //2.干活
                num++;
                System.out.println("线程:" + Thread.currentThread().getName() + "进行 【加一】 作业:操作后当前值:" + num);
                //3.通知 notifyAll 也被替换
//                this.notifyAll();
                condition.signalAll();
            } finally {
                lock.unlock();
            }
        }

        /**
         * 减一
         */
        public void cut() throws InterruptedException {
            lock.lock();
            try {
                //1.判断
                while (num == 0) {
//                    this.wait();
                    condition.await();
                }
                //2.干活
                num--;
                System.out.println("线程:" + Thread.currentThread().getName() + "进行 【减一】 作业:操作后当前值:" + num);
                //3.通知
//                this.notifyAll();
                condition.signalAll();
            } finally {
                lock.unlock();
            }

        }
    }

    /**
     * 实现两个线程轮番对数据进行加减,保证结束后值不变
     */
    @GetMapping("/test1")
    public void test1() {
        NumController numController = new NumController();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    numController.add();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();

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

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

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

}

结果:

线程:A进行 【加一】 作业:操作后当前值:1
线程:B进行 【减一】 作业:操作后当前值:0
。。。省略。。。
线程:D进行 【减一】 作业:操作后当前值:0
线程:C进行 【加一】 作业:操作后当前值:1
线程:D进行 【减一】 作业:操作后当前值:0

疑问?

使用这种 Condition 不是就没有办法体现 当前线程是在等待哪个资源的锁或者在哪个资源类的等待池中被唤醒了吗?

回答:
先贴一段,Condition的官方源码解释:

 * {@code Condition} factors out the {@code Object} monitor
 * methods ({@link Object#wait() wait}, {@link Object#notify notify}
 * and {@link Object#notifyAll notifyAll}) into distinct objects to
 * give the effect of having multiple wait-sets per object, by
 * combining them with the use of arbitrary {@link Lock} implementations.
 * Where a {@code Lock} replaces the use of {@code synchronized} methods
 * and statements, a {@code Condition} replaces the use of the Object
 * monitor methods.

以下为翻译
ConditionObject 的监控方法(wait、notify 和 ObjectnotifyAll notifyAll)分解为不同的对象,
以产生多重等待的效果-sets 每个对象,通过将它们与任意 Lock 实现的使用结合起来。 
Lock 代替了 synchronized 方法和语句的使用,Condition 代替了 Object 监视器方法的使用。

也就是说,提出疑问是因为陷入了误区;
Condition 不是thread 类的方法,也不属于Object类,它只是JUC一个工具类,它只是将Object类的 wait notify notifyAll 方法进行分解组合,实际上是对Object那几个监控方法的一次封装增强处理;
因此,使用Condition实际上还是使用的Object的方法,只是做了增强处理。

5.Lock 使用的优点好处

① 精确唤醒,保证线程顺序

1.需要标志类,来进行顺序的判断。
2.每个线程都需要自己的线程判断类,来对线程进行精确等待或者唤醒

线程进来,先判断当前标志位是不是属于自己的标志位。
① 如果不是,就进行wait等待,
② 如果是,就执行自己的代码体,执行完;就将标志位修改为下一个线程的标志位,然后使用该线程的判断类来进行精确唤醒
③ 当下一个线程进来后重复上述步骤,最后一个唤醒第一个,达成循环。

代码如下:

/**
 * 线程间通信 使用新方法  Lock + Condition
 * 进行精准顺序唤醒
 * 需要由顺序,就需要增加标志位 flag
 *
 * @author byChen
 * @date 2022/8/11
 */
@RestController
@RequestMapping("/thread4")
@AllArgsConstructor
public class ThreadDemo4 {
    /**
     * 为了方便,在此处定义实体类
     */
    class OutTurn {
        private int sort = 1; // 1 A线程, 2 B线程 ,3 C线程
        private final Lock lock = new ReentrantLock();
        //因为是多线程 有顺序的依次执行,所以就得每个线程配一个判断类
        //线程只找自己的Condition来判断是等待(wait)还是被唤醒(signal)
        private final Condition condition1 = lock.newCondition();
        private final Condition condition2 = lock.newCondition();
        private final Condition condition3 = lock.newCondition();

        /**
         * 打印方法
         * 实现三个线程之间按顺序调用,实现 A-B-C
         * 三个线程启动,要求如下
         * A输出5次,B输出8次,C输出10次
         * 接着
         * A输出5次,B输出8次,C输出10次
         * 循环3次
         * @param flag 当前执行得是哪个线程 1A线程 2B线程 3C线程
         */
        public void print(int flag) throws InterruptedException {
            //这里只是单纯的逻辑判断,未加锁因此就不涉及锁,因此可以使用if
            if (flag == 1) {
                lock.lock();
                try {
                    //判断 (因为前面使用了lock方法,因此这里只能用 while)
                    while (sort != 1) {
                        condition1.await();
                    }
                    //干活
                    for (int i = 1; i <= 5; i++) {
                        System.out.println("线程名:【" + Thread.currentThread().getName() + "】打印" + i + "次");
                    }
                    //通知,修改标志位+使用下一个线程的判断类来唤醒
                    sort = 2; //修改标志位
                    condition2.signal();//使用线程2的判断类来进行精确唤醒
                } finally {
                    lock.unlock();
                }
            } else if (flag == 2) {
                lock.lock();
                try {
                    //判断
                    while (sort != 2) {
                        condition2.await();
                    }
                    //干活
                    for (int i = 1; i <= 8; i++) {
                        System.out.println("线程名:【" + Thread.currentThread().getName() + "】打印" + i + "次");
                    }
                    //通知
                    sort = 3;
                    condition3.signal();
                } finally {
                    lock.unlock();
                }
            } else {
                lock.lock();
                try {
                    //判断
                    while (sort != 3) {
                        condition3.await();
                    }
                    //干活
                    for (int i = 1; i <= 10; i++) {
                        System.out.println("线程名:【" + Thread.currentThread().getName() + "】打印" + i + "次");
                    }
                    //通知
                    sort = 1;
                    condition1.signal();
                } finally {
                    lock.unlock();
                }
            }
        }
    }

    /**
     * 实现三个线程之间按顺序调用,实现 A-B-C
     * 三个线程启动,要求如下
     * A输出5次,B输出8次,C输出10次
     * 接着
     * A输出5次,B输出8次,C输出10次
     * 循环3次
     */
    @GetMapping("/test1")
    public void test1() {
        OutTurn outTurn = new OutTurn();
        //每个线程都执行3次,每个线程执行的时候都会进行线程判断
        new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                try {
                    outTurn.print(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }, "A").start();

        new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                try {
                    outTurn.print(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();

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


    }

}

输出结果,正常按顺序输出了:

线程名:【A】打印1次
。。。
线程名:【A】打印5次
线程名:【B】打印1次
。。。
线程名:【B】打印8次
线程名:【C】打印1次
。。。
线程名:【C】打印10-------------------------------
线程名:【A】打印1次
。。。
线程名:【A】打印5次
线程名:【B】打印1次
。。。
线程名:【B】打印8次
线程名:【C】打印1次
。。。
线程名:【C】打印10-------------------------------
线程名:【A】打印1次
。。。
线程名:【A】打印5次
线程名:【B】打印1次
。。。
线程名:【B】打印8次
线程名:【C】打印1次
。。。
线程名:【C】打印9次
线程名:【C】打印10

6.总结

1.线程 操作 资源类(根本)

2.判断 干活 通知 (线程间通讯)

3.避免虚假唤醒,锁后使用 while 不能使用 if

4.线程需要顺序,增加每个线程自己的判断类,增加标志位判断当前是谁的顺序

二、进阶理论知识

1.多线程 8 锁

① 标准访问,单纯的锁

资源类:

    /**
     * 为了方便,在此处定义实体类
     * 实现 : 手机类,有多个方法,判断方法执行顺序
     */
    class Phone {
        /**
         * 发邮件
         */
        public synchronized void sendEmail() {
            System.out.println("线程:【" + Thread.currentThread().getName() + "】发送邮件");
        }

        /**
         * 发短信
         */
        public synchronized void sendSMS() {
            System.out.println("线程:【" + Thread.currentThread().getName() + "】发短息");
        }
    }

线程操作:

    /**
     * 测试哪个方法先执行
     */
    @GetMapping("/test1")
    public void test1() {
        Phone phone = new Phone();

        new Thread(() -> {
            phone.sendEmail();
        }, "A线程").start();

        new Thread(() -> {
            phone.sendSMS();
        }, "B线程").start();
    }

结果:
在这里插入图片描述
发起4次请求,每次线程执行顺序都不固定,具体看CPU对线程的调度。

因为这样随机选取线程先执行,无法对具体哪个方法是先执行的做出判断,因此我们手动把线程睡眠,保证线程先后的顺序。
修改后的线程执行:

    /**
     * 修改以下,保证线程A 先执行
     */
    @GetMapping("/test1")
    public void test1() throws InterruptedException {
        Phone phone = new Phone();

        new Thread(() -> {
            phone.sendEmail();
        }, "A线程").start();
        Thread.sleep(200);
        new Thread(() -> {
            phone.sendSMS();
        }, "B线程").start();
    }

在这里插入图片描述
下面的情况都基于线程A 先执行,线程B 后执行的基础上。
能够清楚的看明白,资源类中的不同方法,因为不同操作而产生的先后执行顺序问题。

② 手动让其中一个方法 sleep,看执行顺序

1.资源类:

    /**
     * 为了方便,在此处定义实体类
     * 实现 : 手机类,有多个方法,判断方法执行顺序
     */
    class Phone {
        /**
         * 发邮件
         */
        public synchronized void sendEmail() {
            //让方法一执行到之后,睡眠2秒钟,看看是否会越过此方法去执行另外一个方法
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程:【" + Thread.currentThread().getName() + "】发送邮件");
        }

        /**
         * 发短信
         */
        public synchronized void sendSMS() {
            System.out.println("线程:【" + Thread.currentThread().getName() + "】发短息");
        }
    }

2.线程执行类不变

3.执行结果:
在这里插入图片描述
可以看见,在发邮件方法睡眠之后,另外的线程无法去执行该同一个资源类中的另外的锁方法。而是等发邮件方法执行完,才会执行。

因为在同一个对象里如果有多个synchronized方法或synchronized代码块,某一个时刻内,只要有一个线程去调用了其中一个synchronized方法,其它调用该资源类中其他锁方法的线程只能等待。
换句话说,某一个时刻内,只能有一个线程去访问这些synchronized方法,在本例中即使是线程A休眠了2秒,因为是它先调用资源类,所以线程B会等待线程A执行完才会执行

③ 增加一个普通方法,看执行顺序

1.资源类

    /**
     * 为了方便,在此处定义实体类
     * 实现 : 手机类,有多个方法,判断方法执行顺序
     */
    class Phone {
        /**
         * 发邮件
         */
        public synchronized void sendEmail() {
            //让方法一执行到之后,睡眠2秒钟,看看是否会越过此方法去执行另外一个方法
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程:【" + Thread.currentThread().getName() + "】发送邮件");
        }

        /**
         * 发短信
         */
        public synchronized void sendSMS() {
            System.out.println("线程:【" + Thread.currentThread().getName() + "】发短息");
        }

        /**
         * 普通方法,不参与锁竞争
         */
        public void sayHello() {
            System.out.println("线程:【" + Thread.currentThread().getName() + "】say Hello ");
        }
    }

2.线程操作

    /**
     * 让普通方法也被执行
     * 普通方法会在执行到它的时候立刻执行
     * 其余两个锁方法,会根据上一个锁情况进行执行
     */
    @GetMapping("/test1")
    public void test1() throws InterruptedException {
        Phone phone = new Phone();

        new Thread(() -> {
            phone.sendEmail();
        }, "A线程").start();
        //依旧睡眠
        Thread.sleep(200);
        new Thread(() -> {
            phone.sendSMS();
        }, "B线程").start();
        new Thread(() -> {
            phone.sayHello();
        }, "C线程").start();
    }

3.执行结果
在这里插入图片描述

普通方法不加锁,会在主线程执行到的时候直接执行。

④ 调用两个不同资源类实例的两个锁方法,看执行顺序

1.资源类

未做改变

2.线程操作
多 new 一个实例资源类


    /**
     * 让线程去分别执行不同的实例的锁方法
     * 因为 同一个资源类中,同一时刻只能执行其中一个锁方法
     * 但是这是两个不同的资源类,因此不会被阻塞
     */
    @GetMapping("/test1")
    public void test1() throws InterruptedException {
        //创建两个资源类对象
        Phone phone = new Phone();
        Phone phone2 = new Phone();

        //调用资源类 ①
        new Thread(() -> {
            phone.sendEmail();
        }, "A线程").start();

        //调用资源类 ②
        new Thread(() -> {
            phone2.sendSMS();
        }, "B线程").start();
    }

3.执行结果
在这里插入图片描述
先发了短信,后发了邮件

同一时刻只能执行其中一个锁方法,是在同一个资源类内部的限定前提下生效的,本例是创建两个不同的资源类分别交给两个线程调用。
因此当线程调用资源类①发邮件方法睡眠时,阻塞的只是资源类①的发短信方法,而不会阻塞资源类②的发短信方法,因此发短信执行在邮件前面。

⑤ 静态锁方法,使用相同的资源类实例,看执行顺序

1.资源类

/**
 * 为了方便,在此处定义实体类
 * 实现 : 手机类,有多个方法,判断方法执行顺序
 */
class Phone {
    /**
     * 发邮件
     */
    public static synchronized void sendEmail() {
        //让方法一执行到之后,睡眠2秒钟,看看是否会越过此方法去执行另外一个方法
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程:【" + Thread.currentThread().getName() + "】发送邮件");
    }

    /**
     * 发短信
     */
    public static synchronized void sendSMS() {
        System.out.println("线程:【" + Thread.currentThread().getName() + "】发短息");
    }

    /**
     * 普通方法,不参与锁竞争
     */
    public void sayHello() {
        System.out.println("线程:【" + Thread.currentThread().getName() + "】say Hello ");
    }
}

2.线程操作

    /**
     * 将资源类中的锁方法变成静态方法,
     * 因为static修饰的方法在JVM中只会有一份,不管实例多少个对象,调用的都是同一个方法
     * 自然不管创建多少资源类实例,不管多少线程,都是相当于在一个资源类中,
     * 当然也会遵循 “同一时刻只能执行一个资源类中的一个锁方法,其余普通方法正常执行,锁方法等待”的原则
     */
    @GetMapping("/test1")
    public static void test1() throws InterruptedException {
        //使用相同的资源类对象
        Phone phone = new Phone();

        //调用资源类 ①
        new Thread(() -> {
            phone.sendEmail();
        }, "A线程").start();

        //调用资源类 ②
        new Thread(() -> {
            phone.sendSMS();
        }, "B线程").start();
    }

3.执行结果
请求两次,都是先邮件,后短信
在这里插入图片描述
⑤类与⑥类一起做总结

⑥ 静态锁方法,使用不同的资源类实例,看执行顺序

1.资源类

/**
 * 为了方便,在此处定义实体类
 * 实现 : 手机类,有多个方法,判断方法执行顺序
 */
class Phone {
    /**
     * 发邮件
     */
    public static synchronized void sendEmail() {
        //让方法一执行到之后,睡眠2秒钟,看看是否会越过此方法去执行另外一个方法
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程:【" + Thread.currentThread().getName() + "】发送邮件");
    }

    /**
     * 发短信
     */
    public static synchronized void sendSMS() {
        System.out.println("线程:【" + Thread.currentThread().getName() + "】发短息");
    }

    /**
     * 普通方法,不参与锁竞争
     */
    public void sayHello() {
        System.out.println("线程:【" + Thread.currentThread().getName() + "】say Hello ");
    }
}

2.线程操作


    /**
     * 将资源类中的锁方法变成静态方法,
     * 因为static修饰的方法在JVM中只会有一份,不管实例多少个对象,调用的都是同一个方法
     * 自然不管创建多少资源类实例,不管多少线程,都是相当于在一个资源类中,
     * 当然也会遵循 “同一时刻只能执行一个资源类中的一个锁方法,其余普通方法正常执行,锁方法等待”的原则
     */
    @GetMapping("/test1")
    public static void test1() throws InterruptedException {
        //使用两个不同的资源类对象
        Phone phone = new Phone();
        Phone phone2 = new Phone();
        //调用资源类 ①
        new Thread(() -> {
            phone.sendEmail();
        }, "A线程").start();

        //调用资源类 ②
        new Thread(() -> {
            phone2.sendSMS();
        }, "B线程").start();
    }

3.执行结果
发送三次请求,跟使用相同资源类实例的结果一样。

在这里插入图片描述

被static修饰的方法在JVM中只会有一份,不管实例多少个对象,调用的都是同一个方法,自然不管创建多少资源类实例,不管多少线程,都是相当于在一个资源类中;当然也会遵循 “同一时刻只能执行一个资源类中的一个锁方法,其余普通方法正常执行,锁方法等待” 的原则。

⑦ 一个锁方法,一个静态锁方法,一个实例,看执行顺序

1.资源类


/**
 * 为了方便,在此处定义实体类
 * 实现 : 手机类,有多个方法,判断方法执行顺序
 */
class Phone {
    /**
     * 发邮件 静态锁方法
     */
    public static synchronized void sendEmail() {
        //让方法一执行到之后,睡眠2秒钟,看看是否会越过此方法去执行另外一个方法
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程:【" + Thread.currentThread().getName() + "】发送邮件");
    }

    /**
     * 发短信 普通锁方法
     */
    public synchronized void sendSMS() {
        System.out.println("线程:【" + Thread.currentThread().getName() + "】发短息");
    }

    /**
     * 普通方法,不参与锁竞争
     */
    public void sayHello() {
        System.out.println("线程:【" + Thread.currentThread().getName() + "】say Hello ");
    }
}

2.线程操作

    /**
     * 因为 静态锁方法,锁的是当前的整个类
     * 而   普通锁方法,锁的只是当前的实例类
     * 可以理解为两个方法锁的不是同一个类,自然不会遵循
     * “同一时刻只能执行一个资源类中的一个锁方法,其余普通方法正常执行,锁方法等待” 原则
     */
    @GetMapping("/test1")
    public static void test1() throws InterruptedException {
        //使用相同的资源类对象
        Phone phone = new Phone();
        //调用资源类 ①
        new Thread(() -> {
            phone.sendEmail();
        }, "A线程").start();

        //调用资源类 ②
        new Thread(() -> {
            phone.sendSMS();
        }, "B线程").start();
    }

3.执行结果
请求两次,都是短信后邮件
在这里插入图片描述
原理同⑧,放一块总结

⑧一个锁方法,一个静态锁方法,两个不同实例

1.资源类

/**
 * 为了方便,在此处定义实体类
 * 实现 : 手机类,有多个方法,判断方法执行顺序
 */
class Phone {
    /**
     * 发邮件 静态锁方法
     */
    public static synchronized void sendEmail() {
        //让方法一执行到之后,睡眠2秒钟,看看是否会越过此方法去执行另外一个方法
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程:【" + Thread.currentThread().getName() + "】发送邮件");
    }

    /**
     * 发短信 普通锁方法
     */
    public synchronized void sendSMS() {
        System.out.println("线程:【" + Thread.currentThread().getName() + "】发短息");
    }

    /**
     * 普通方法,不参与锁竞争
     */
    public void sayHello() {
        System.out.println("线程:【" + Thread.currentThread().getName() + "】say Hello ");
    }
}

2.线程操作

    /**
     * 因为 静态锁方法,锁的是当前的整个类
     * 而   普通锁方法,锁的只是当前的实例类
     * 可以理解为两个方法锁的不是同一个类,自然不会遵循
     * “同一时刻只能执行一个资源类中的一个锁方法,其余普通方法正常执行,锁方法等待” 原则
     */
    @GetMapping("/test1")
    public static void test1() throws InterruptedException {
        //使用两个不同的资源类对象
        Phone phone = new Phone();
        Phone phone2 = new Phone();
        //调用资源类 ①
        new Thread(() -> {
            phone.sendEmail();
        }, "A线程").start();

        //调用资源类 ②
        new Thread(() -> {
            phone2.sendSMS();
        }, "B线程").start();
    }

3.执行结果
结果与 第⑦ 例相同
在这里插入图片描述

因为 静态锁方法,锁的是当前的整个类
而 普通锁方法,锁的只是当前的实例类
可以理解为两个方法锁的不是同一个类,自然不会遵循“同一时刻只能执行一个资源类中的一个锁方法,其余普通方法正常执行,锁方法等待” 原则

tips 线程睡眠新写法
        try {
            //秒为单位
            TimeUnit.SECONDS.sleep(4);
            //小时为单位
            TimeUnit.HOURS.sleep(1);
            //分钟为单位
            TimeUnit.MINUTES.sleep(3);
            // 。。。还有纳秒、微秒、毫秒等
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
总结

1.普通方法不参与锁
2.同一时刻只能执行一个资源类中的一个锁方法,其余普通方法正常执行,锁方法等待
3.静态锁方法锁的是整个类,普通锁方法锁的是当前实例对象,他们相当于两个不同的类,不遵循 2

2.集合类的线程不安全 解析

① list 不安全
不安全举例:
    @GetMapping("/test1")
    public static void test1() throws InterruptedException {
        List<String> list=new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0,8));
                System.out.println(list);
            }).start();
        }
    }

结果:

第一次
[a63b45a5]
[a63b45a5, 7a300328]
[a63b45a5, 7a300328, 52d11cc0]
第二次
[f24f30b6, 304e3618]
[f24f30b6, 304e3618, 46b3d7f1]
[f24f30b6, 304e3618, 46b3d7f1, 8d5b56b7, ef9d077d]
[f24f30b6, 304e3618, 46b3d7f1, 8d5b56b7]
[f24f30b6, 304e3618]
当把线程扩大到 30 个,甚至报错 并发修改异常
Exception in thread "Thread-38" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at java.util.AbstractCollection.toString(AbstractCollection.java:461)
	at java.lang.String.valueOf(String.java:2994)
	at java.io.PrintStream.println(PrintStream.java:821)
	at com.spring.controller.NoSafeDemo.lambda$test1$0(NoSafeDemo.java:29)
	at java.lang.Thread.run(Thread.java:748)

出现这种情况是因为,多线程同时对list进行 写入 跟 读取 的操作,无法分辨那些先那些后,有可能读取到别的线程未提交的数据。

解决方案

① 将 ArrayList 更换成 Vector,因为 Vector 是线程安全的。

点击两者的add 方法源码,ArrayList 不带锁,Vector 带锁;

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

    @GetMapping("/test1")
    public static void test1() throws InterruptedException {
//        List<String> list=new ArrayList<>();
        //Vector 是线程安全的
        List<String> list = new Vector<>();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(list);
            }).start();
        }
    }
[37b8128e, c2aa2c99, 93ad7516, a70994df, 864ef4ed]
[37b8128e, c2aa2c99, 93ad7516, a70994df, 864ef4ed]
[37b8128e, c2aa2c99, 93ad7516, a70994df, 864ef4ed]
[37b8128e, c2aa2c99, 93ad7516, a70994df, 864ef4ed]
[37b8128e, c2aa2c99, 93ad7516, a70994df, 864ef4ed]

Vector 线程安全,性能不高
ArrayList 线程不安全,性能很高

② 使用工具类将ArrayList转化为线程安全的

Collections.synchronizedList(new ArrayList<>())

    public static void main(String[] args) {
//        List<String> list = new ArrayList<>(); //不安全
//        List<String> list = new Vector<>();  //安全
        //使用工具类将ArrayList转化为线程安全的
        List<String> list = Collections.synchronizedList(new ArrayList<>());//使用工具类将ArrayList转化为线程安全的
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }

③ 最推荐使用的方法 CopyOnWriteArrayList

它能够保证写入数据一致性,也能够保证高性能

    public static void main(String[] args) {
//        List<String> list = new ArrayList<>(); //不安全
//        List<String> list = new Vector<>();  //安全
//        List<String> list = Collections.synchronizedList(new ArrayList<>());//安全
        List<String> list = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }

CopyOnWriteArrayList 原理

·CopyOnWriteArrayList 初始化的时候只是一个容器,当线程只发生读取操作的时候,读的都是这个容器中的数据,是一致的,安全的;
·当有线程进行写操作的时候,CopyOnWriteArrayList 底层会先copy出一个新的容器副本,然后将需要写入的数据放入新容器副本中,添加完成后将新容器的地址赋值给旧容器地址,完成添加数据;
·在添加数据到容器地址替换这段时间期间,如果要读取数据,还是从旧的容器中读取,以保证数据一致性,并且因为可以在写的时候(写新容器副本)供给其他线程去读(读旧容器),因此性能也是很高的。
·所以它才叫 CopyOnWriteArrayList ( “写入时拷贝 Array集合类”)

add方法 源码解析:

    public boolean add(E e) {
    	//进入先加锁,保证只有一个线程可以进行写入,(读操作在旧容器,不影响)
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
        	//获取当前数组
            Object[] elements = getArray();
            int len = elements.length;
            //复制出来一个长度比旧容器+1的新数组容器
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //把add的数据放在数组最后,添加进数组
            newElements[len] = e;
            //将新数组赋值给旧数组,完成新旧容器替换
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }
② set 不安全

Set集合是线程不安全的,这里不再演示。

引申: HashSet的底层是如何实现的呢?

看源码:

无参构造
在这里插入图片描述
add 方法


    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();

    /**
     *如果指定的元素尚不存在,则将其添加到此集合中。
     更正式地说,如果此集合不包含元素 e则将指定的元素 e添加到此集合
     如果该集合已包含该元素,则调用将保持该集合不变并返回false。
     *
     * @param e element to be added to this set
     * @return <tt>true</tt> if this set did not already contain the specified
     * element
     */
    public boolean add(E e) {
    	//底层是HashMap,需要键值对,而键就是放入的值,值就是一个常量
        return map.put(e, PRESENT)==null;
    }

解决方案
    public static void main(String[] args) {
        //解决方案与list基本一致
       // Set<Object> set = Collections.synchronizedSet(new HashSet<>());
        Set<Object> set = new CopyOnWriteArraySet();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                set.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(set);
            }, String.valueOf(i)).start();
        }
    }

这里重点分析 HashSet 与 HsahMap 的区别与关联

HashMap分析在下节

③ map 不安全

首先看HashMap的构造方法源码:

    /**
     * 构造一个具有默认初始容量 (16) 和默认负载因子 (0.75) 的空 HashMap。
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // 所有其他字段默认
    }

什么是 初始容量 和 负载因子 ?
· 初始容量:容量是哈希表中桶的数量,初始容量只是哈希表在创建时的容量,(可以初略理解为map里面能够放多少数据)
· 负载因子:标志了容器达到何种程序才需要进行扩容,当哈希表中的条目数超出了加载因子与当前容量的乘积时(16*0.75=12),则要对该哈希表进行扩容、rehash操作(即重建内部数据结构),扩容后的哈希表将具有两倍的原容量(变为32)

为什么负载因子默认值是 0.75?
· 加载因子过高,例如为1,这样会减少空间开销,提高空间利用率,但同时会增加查询时间的成本
· 加载因子过低,例如为0.5,虽然可以减少查询时间,但是空间利用率很低,同时提高了rehash操作的次数
选择0.75作为默认的加载因子,完全是时间和空间成本上寻求折中的选择.

然后是线程不安全的解决方案

解决方案

注意这里的是 ConcurrentHashMap

    public static void main(String[] args) {
        //解决方案与list基本一致
//        Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
        Map<String, String> map =new ConcurrentHashMap<>();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0, 8));
                System.out.println(map);
            }, String.valueOf(i)).start();
        }
    }

3.获得多线程的方式

① 实现 Callable 接口

如何使用?

在这里插入图片描述
Thread构造方法没有参数为 Callable的,因此,我们需要一个工具,来包装一下 Callable类

FutureTask
源码解释:

    /**
     * FutureTask可用于包装 Callable 或 Runnable 对象。
     * 因为  FutureTask 实现了  Runnable 接口,所以可以将  FutureTask 提交给  Executor 执行。
     *
     * 此 FutureTask 的  get 方法可获取返回的结果类型
     */

使用“

/**
 * 实现callable接口
 */
class MyThread implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println("这是callable接口");
        return "1024";
    }
}
     public static void main(String[] args) throws ExecutionException, InterruptedException {
        //使用 FutureTask类包装 资源类
        FutureTask futureTask = new FutureTask(new MyThread());
        //放入,并交由现场执行
        new Thread(futureTask).start();
        //获取返回值
        Object o = futureTask.get();
        System.out.println("返回值:" + o);
        System.out.println("===主线程===");
    }

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

原理

一个可取消的异步计算。
·FutureTask提供了对Future的基本实现,可以调用方法去开始和取消一个计算,可以查询计算是否完成并且获取计算结果。只有当计算完成时才能获取到计算结果,一旦计算完成,计算将不能被重启或者被取消,除非调用runAndReset方法。
·FutureTask还实现了Runnable接口,因此FutureTask交由Executor执行,也可以直接用线程调用执行(futureTask.run())。

》根据FutureTask的run方法执行的时机,FutureTask可以处于以下三种执行状态:

  • 未启动

在FutureTask.run()还没执行之前,FutureTask处于未启动状态。当创建一个FutureTask对象,并且run()方法未执行之前,FutureTask处于未启动状态

  • 已启动

FutureTask对象的run方法启动并执行的过程中,FutureTask处于已启动状态

  • 已完成

FutureTask正常执行结束,或者FutureTask执行被取消(FutureTask对象cancel方法),或者FutureTask对象run方法执行抛出异常而导致中断而结束,FutureTask都处于已完成状态。

在这里插入图片描述

FutureTask的get方法 - 获取返回值

·FutureTask对象的get方法,如果在FutureTask处于”未启动“或者”已启动“的状态时使用,会导致其他线程阻塞,其他线程会等待FutureTask所在的线程处理完,有返回值之后才会继续执行。
·FutureTask对象的get方法,如果在FutureTask处于”已完成“的状态时使用,会立即放回调用结果或者抛出异常。

代码实例:

class MyThread implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println("这是callable接口");
        //睡眠4秒
        TimeUnit.SECONDS.sleep(4);
        return "1024";
    }
}
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //使用 FutureTask类包装 资源类
        FutureTask futureTask = new FutureTask(new MyThread());
        //放入,并交由线程执行
        new Thread(futureTask).start();
        //在这里处于已启动状态,直接获取返回值,导致主线程阻塞
        System.out.println("返回值:" + futureTask.get());
        System.out.println("主线程继续");
    }

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

FutureTask的cancel方法 - 取消线程执行

·1.FutureTask对象的cancel方法,如果在FutureTask处于”未启动“的状态时使用,会导致导致该分支线程永远不会被执行:
·2.如果在FutureTask处于”未启动“的状态时,
FutureTask对象的cancel(true)方法,将以中断执行此任务的线程的方式来试图停止此任务
FutureTask对象的cancel(false)方法,将不会对正在进行的任务产生任何影响
·3.如果在FutureTask处于”已完成“的状态时,调用FutureTask对象cancel方法将返回false;

代码示例:

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //使用 FutureTask类包装 资源类
        FutureTask futureTask = new FutureTask(new MyThread());
        futureTask.cancel(true);//放在此处,该线程就不会执行,而直接执行主线程,再去获取 。get方法,会报错
        //放入,并交由线程执行
        new Thread(futureTask).start();
        futureTask.cancel(true);//放在此处,会以中断执行此任务的线程的方式来试图停止此任务
        System.out.println("返回值:" + futureTask.get());
        futureTask.cancel(true);//放在此处,只会返回false
        
        System.out.println("主线程继续");
    }

在这里插入图片描述

多线程多次调用

当将该类交给多个线程去执行,也是只会执行一遍,返回一个结果。

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //使用 FutureTask类包装 资源类
        FutureTask futureTask = new FutureTask(new MyThread());

        //放入,分别交由两个线程执行
        new Thread(futureTask, "A").start();
        new Thread(futureTask, "B").start();

        System.out.println("主线程继续");

        System.out.println("返回结果:" + futureTask.get());
    }

在这里插入图片描述

总结

1.因为get方法会导致其余线程阻塞,因此应当将get方法放在尽可能靠后的位置,等待其他线程执行完再请求返回结果
2.多个线程调用,只会第一次去执行,再次调用就不执行,直接返回第一次的结果

② 实现Runnable接口

适用于类已经继承了其他的类,无法再继承thread类的时候

资源类

/**
 * 实现Runnable接口来实现线程
 */
public class MyThread1 implements Runnable{
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("实现Runnable接口 分支线程:" + i);
        }
    }
}

线程操作

    public static void main(String[] args) {
        /**
         * Runable方法实现线程,需要将线程类作为参数,放入实现new出来的Thread参数中
         */
        MyThread1 myThread1 = new MyThread1();
        Thread thread = new Thread(myThread1);
        /**
         * 使用Thread类的 .start()方法进行启动线程
         * 接着Thread类的.run()方法中会调用创建类的 .run()方法,进而开辟线程
         */
        thread.start();
        System.out.println("===正常线程执行===");
    }
③ 继承Thread类

Thread类本质上是像上面方法一样实现Runnable的,Thread类其实是实现了Runnable接口的一个实例,代表一个线程的实例。
它适用于那些还没有继承其他类的类来创建线程,实现起来更方便,只需要将需要开启线程的类继承Thread类,然后重写 .run() 方法,最后使用该对象来调用 .start()方法即可开启线程。

资源类

/**
 * 继承Thread类实现线程
 */
public class MyThread2 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("线程分支:"+i);
        }
    }
}

线程操作

public class ThreadCon {
    public static void main(String[] args) {
        /**
         * 直接new 对象,对象调用 .start() 方法即可
         */
        MyThread2 myThread2 = new MyThread2();
        myThread2.start();
        System.out.println("===主线程===");
    }

4.JUC常用辅助类

① CountDownLatch - 计数器

保证一个线程会在另外一组线程执行完之后才执行;或者当前线程组执行完成之后才继续主线程

案例

保证全部线程出门之后才关门

1.不使用辅助类处理:

public class CountDownLatchDemo {
    public static void main(String[] args) {
        System.out.println("===开始整个线程===");
        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                System.out.println("线程 【" + Thread.currentThread().getName() + "】,出门");
            }, String.valueOf(i)).start();
        }
        System.out.println("关门");
    }
}
===开始整个线程===
线程 【1,出门
线程 【3,出门
线程 【2,出门
关门
线程 【4,出门
线程 【5,出门
线程 【6,出门

不符合

2.使用辅助类

public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("===开始整个线程===");
        //开启线程计数
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                System.out.println("线程 【" + Thread.currentThread().getName() + "】,出门");
                //每执行完一个线程,计数减一
                countDownLatch.countDown();
            }, String.valueOf(i)).start();
        }
        //计数器为0之前,都等待;等待计数器为零 ,然后在往下执行;
        countDownLatch.await();
        System.out.println("关门");
    }
}
===开始整个线程===
线程 【1,出门
线程 【3,出门
线程 【2,出门
线程 【5,出门
线程 【4,出门
线程 【6,出门
关门

符合预期
原理
在这里插入图片描述

② CyclicBarrier - 循环栅栏

原理

从字面上的意思可以知道,这个类的中文意思是“循环栅栏”。大概的意思就是一个可循环利用的屏障;
它的作用就是会让所有线程都等待完成后才会继续下一步行动(或者才会一块进入下一个栅栏)。

步骤

1.如果线程资源类拥有多段任务,那么将多个线程放在一个 循环栅栏中管理,
2.这样当线程到达栅栏处,会停下等待其他线程,
3.等待最后一个线程到达,并完成特定的动作后,解除当前栅栏
4.全部线程赶往下一个栅栏,并再次循环上述步骤;

如何使用

/**
*  parties 是参与线程的个数
*  第二个构造方法有一个 Runnable 参数,这个参数的意思是最后一个到达线程要做的任务
*/
public CyclicBarrier(int parties, Runnable barrierAction)
/* 
*  线程调用 await() 表示自己已经到达栅栏
* BrokenBarrierException 表示栅栏已经被破坏,破坏的原因可能是其中一个线程 await() 时被中断或者超时
*/
public int await() throws InterruptedException, BrokenBarrierException

代码如下:

1.资源类

    static class MyThread implements Runnable {
        /**
         * 利用有参构造,实现依赖注入
         */
        CyclicBarrier cyclicBarrier;

        public MyThread(CyclicBarrier cyclicBarrier) {
            this.cyclicBarrier = cyclicBarrier;
        }

        /**
         * 重写线程方法
         */
        @Override
        public void run() {

            try {
                System.out.println("线程 【" + Thread.currentThread().getName() + "】 =到达栅栏 1= ");
                cyclicBarrier.await();//线程调用 await() 表示自己已经到达栅栏
                System.out.println("线程 【" + Thread.currentThread().getName() + "】 =越过栅栏 1= ");

                //为验证可以多次循环 阻拦,这里设置第二道阻塞
                TimeUnit.SECONDS.sleep(2);//到达前睡眠一会,以求展示更加直观
                System.out.println("线程 【" + Thread.currentThread().getName() + "】 ==到达栅栏 2== ");
                cyclicBarrier.await();//线程调用 await() 表示自己已经到达栅栏
                System.out.println("线程 【" + Thread.currentThread().getName() + "】 ==越过栅栏 2== ");
            } catch (Exception e) { //BrokenBarrierException 表示栅栏已经被破坏,破坏的原因可能是其中一个线程 await() 时被中断或者超时
                e.printStackTrace();
            }
        }
    }

2.线程操作

    public static void main(String[] args) {
        //设置栅栏,参数为 线程组内一共几个线程+最后一个线程到达要做的事情
        CyclicBarrier cyclicBarrier = new CyclicBarrier(5, new Runnable() {
            /**
             * 
             */
            @Override
            public void run() {
                System.out.println("线程 【" + Thread.currentThread().getName() + "】 最后到达,完成最后到达要做的任务,并解除当前栅栏");
            }
        });

        //开启多线程
        for (int i = 1; i <= 5; i++) {
            new Thread(new MyThread(cyclicBarrier)).start();
        }
    }

使用场景

可以用于多线程计算数据,最后合并计算结果的场景

与 CountDownLatch 的区别

1.CountDownLatch 是一次性的,CyclicBarrier 是可循环利用的
2.CountDownLatch 参与的线程的职责是不一样的,有的在进行倒计时,有的在等待倒计时结束。CyclicBarrier 参与的线程职责是一样的

③ Semaphore - 资源余量信号量

Semaphore 通常我们叫它信号量, 可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。

步骤:

1.定义资源允许访问的全部量
2.开启多线程去访问,并判断余量
3.尝试去获取允许令牌,
获取到-进入
获取不到-阻塞等待
4.消费完资源,解除对令牌的占用,唤醒一个阻塞线程

常用方法:

acquire()  
获取一个令牌,在获取到令牌、或者被其他线程调用中断之前线程一直处于阻塞状态。

acquire(int permits)  
获取一个令牌,在获取到令牌、或者被其他线程调用中断、或超时之前线程一直处于阻塞状态。

tryAcquire()
尝试获得令牌,返回获取令牌成功或失败,不阻塞线程。

release()
释放一个令牌,唤醒一个获取令牌不成功的阻塞线程。

availablePermits()
返回可用的令牌数量。

drainPermits()
清空令牌把可用令牌数置为0,返回清空令牌的数量。

示例代码:
极端一点,设置允许量为 1

     public static void main(String[] args) {
        //1.定义资源允许访问的全部量
        Semaphore semaphore = new Semaphore(1);
        //2.开启多线程去访问
        for (int i = 1; i <= 3; i++) {
            new Thread(()->{
                //3.检查余量
                int lave = semaphore.availablePermits();
                System.out.println("【"+Thread.currentThread().getName()+"】来到限制牌前,当前余量:"+lave);
                try {
                    //4.尝试去获取允许令牌,获取到-进入  获取不到-阻塞等待
                    semaphore.acquire();//尝试获取进入的令牌,在获取到令牌、或者被其他线程调用中断之前线程一直处于阻塞状态
                    System.out.println("【"+Thread.currentThread().getName()+"】成功进入停车场");
                    TimeUnit.SECONDS.sleep(3);//模拟停车时间
                    System.out.println("【"+Thread.currentThread().getName()+"】离开停车场");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    //5.消费完资源,解除对令牌的占用,唤醒一个阻塞线程
                    semaphore.release();//释放一个令牌,唤醒一个获取令牌不成功的阻塞线程。
                }
            },i+"号车").start();
        }
    }

运行结果:

1号车】来到限制牌前,当前余量:1当前等待车辆数:03号车】来到限制牌前,当前余量:1当前等待车辆数:02号车】来到限制牌前,当前余量:1当前等待车辆数:01号车】成功进入停车场
【1号车】离开停车场
【3号车】成功进入停车场
【3号车】离开停车场
【2号车】成功进入停车场
【2号车】离开停车场

原理
在这里插入图片描述

5.读写锁 - ReadWriteLock

Lock锁是一个线程进入资源之后,无论做读操作或者写操作,都不允许其他线程进入
但是其实读操作并不影响数据一致性
因此 读写锁 是
如果当前线程进行读操作,那么其他线程可以进行读,不能写
如果当前线程进行写操作,那么其他线程不允许任何读或者写操作
特性如下:
1.读-读 能够共存
2.读-写 不能共存
3.写-写 不能共存

未加锁展示

代码实例-不加锁版

资源类


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

    /**
     * 写入操作
     *
     * @param key
     * @param value
     */
    public void put(String key, String value) {
        //开始写入
        System.out.println("线程 【" + Thread.currentThread().getName() + "】开始写入数据");
        //模拟网络波动
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        map.put(key, value);
        //写入完成
        System.out.println("线程 【" + Thread.currentThread().getName() + "】写入完成");
    }

    /**
     * 读取操作
     *
     * @param key
     */
    public void get(String key) {
        //开始读取
        System.out.println("线程 【" + Thread.currentThread().getName() + "】开始读取数据");
        //模拟网络波动
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        map.get(key);
        //读取完成
        System.out.println("线程 【" + Thread.currentThread().getName() + "】读取完成");
    }
}

线程操作


public class ReadWriteLockDemo {
    
    public static void main(String[] args) {
        MyCache myCache = new MyCache();
        //模拟写入
        for (int i = 0; i < 5; i++) {
            final int temp = i;
            new Thread(() -> {
                myCache.put(String.valueOf(temp), "value");
            }, String.valueOf(i)).start();
        }
        //模拟读取
        for (int i = 0; i < 5; i++) {
            final int temp = i;
            new Thread(() -> {
                myCache.get(String.valueOf(temp));
            }, String.valueOf(i)).start();
        }
    }
}

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

加锁版展示

资源类


class MyCache {
    private volatile Map<String, Object> map = new HashMap<>();
    //创建锁
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    /**
     * 写入操作
     *
     * @param key
     * @param value
     */
    public void put(String key, String value) {
        //写入操作使用写入锁
        readWriteLock.writeLock().lock();
        try {
            //开始写入
            System.out.println("线程 【" + Thread.currentThread().getName() + "】开始写入数据");
            //模拟网络波动
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            map.put(key, value);
            //写入完成
            System.out.println("线程 【" + Thread.currentThread().getName() + "】写入完成");
        } finally {
            //解除写入锁
            readWriteLock.writeLock().unlock();
        }

    }

    /**
     * 读取操作
     *
     * @param key
     */
    public void get(String key) {
        readWriteLock.readLock().lock();
        try {
            //开始读取
            System.out.println("线程 【" + Thread.currentThread().getName() + "】开始读取数据");
            //模拟网络波动
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            map.get(key);
            //读取完成
            System.out.println("线程 【" + Thread.currentThread().getName() + "】读取完成");
        } finally {
            readWriteLock.readLock().unlock();
        }

    }
}

线程操作


public class ReadWriteLockDemo {

    public static void main(String[] args) {
        MyCache myCache = new MyCache();
        //模拟写入
        for (int i = 0; i < 5; i++) {
            final int temp = i;
            new Thread(() -> {
                myCache.put(String.valueOf(temp), "value");
            }, String.valueOf(i)).start();
        }
        //模拟读取
        for (int i = 0; i < 5; i++) {
            final int temp = i;
            new Thread(() -> {
                myCache.get(String.valueOf(temp));
            }, String.valueOf(i)).start();
        }
    }
}

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

三、线程池

1.阻塞队列 - BlockingQueue

在这里插入图片描述

在这里插入图片描述
阻塞队列相当于把 wait+notify 这些操作给封装包装起来了

(ArrayBlockingQueue)
插入方法

向队列中添加元素

① add(e) 插入时,如果队列满了,会抛出异常
② offer(e) 插入时,如果队列满了,会返回false,不满,返回true
③ put (e) 插入时,如果队列满了,会一直阻塞等待
④ offer(e,time,unit) 插入时,如果队列满了,会阻塞等待,直到到达超时时间

代码实例

    public static void main(String[] args) throws InterruptedException {
        //创建一个阻塞队列,队列中只容纳 3 个元素
        ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
//        blockingQueue.add("1"); //返回 true
//        blockingQueue.add("2");//返回 true
//        blockingQueue.add("3");//返回 true
//        blockingQueue.add("4"); //报错 IllegalStateException: Queue full

//        blockingQueue.offer("1"); //true
//        blockingQueue.offer("2"); //true
//        blockingQueue.offer("3"); //true
//        blockingQueue.offer("4");//false

//        blockingQueue.put("1");
//        blockingQueue.put("2");
//        blockingQueue.put("3");
//        blockingQueue.put("4"); //会一直等待,直到有元素出去,他插入

        blockingQueue.offer("1",2,TimeUnit.SECONDS);
        blockingQueue.offer("2",2,TimeUnit.SECONDS);
        blockingQueue.offer("3",2,TimeUnit.SECONDS);
        blockingQueue.offer("4",2,TimeUnit.SECONDS);; //会一直等待,直到达到超时时间,就直接放弃,返回
    }
移除方法

从队首移除数据元素

① remove( ) 移除时,如果队列是空的,会抛出异常
② poll( ) 移除时,如果队列是空的,会返回null,不空,返回移除的数据元素
③ take ( ) 移除时,如果队列是空的,会一直阻塞等待
④ poll(time,unit) 移除时,如果队列是空的,会阻塞等待,直到到达超时时间

代码实例

public class BlockQueueDemo {
    public static void main(String[] args) throws InterruptedException {
        //创建一个阻塞队列,队列中只容纳 3 个元素
        ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);

        //从队首移除数据元素
//        blockingQueue.remove(); //报错  NoSuchElementException

//        System.out.println(blockingQueue.poll());//返回null

//        blockingQueue.take();//会一直阻塞

        blockingQueue.poll(3,TimeUnit.SECONDS);//会阻塞等待,直到到达超时时间
    }
}
检查方法

获取队首的元素数据

element() 检查时,队列中没有数据,报错 NoSuchElementException
peek() 检查时,队列中没有数据,返回null,有数据就返回队首

代码实例

public class BlockQueueDemo {
    public static void main(String[] args) throws InterruptedException {
        //创建一个阻塞队列,队列中只容纳 3 个元素
        ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);


//        System.out.println(blockingQueue.element());//报错 NoSuchElementException

        System.out.println(blockingQueue.peek());//null
    }
}

2.线程池

线程池的优势

在这里插入图片描述

非手动创建线程池的三种方法
① Executors.newFixedThreadPool(int)一池N线程

作用:创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。
如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。

代码:

        ExecutorService executorService = Executors.newFixedThreadPool(3);
        //使用线程池中的线程进行操作资源类
        for (int i = 0; i < 20; i++) {
            executorService.execute(() -> {
                System.out.println(Thread.currentThread().getName() + " 执行");
            });
        }
        //归还线程池
        executorService.shutdown();

特征:
• 线程池中的线程处于一定的量,可以很好的控制线程的并发量;
• 线程可以重复被使用,在显示关闭之前,都将一直存在;
• 超出一定量的线程被提交时候需在队列中等待;

② Executors.newSingleThreadPool()一个任务一个任务的执行,一池一线程

作用:创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。(注意,如果因为在关闭前的执行期间出现失败而终止了此单个线程,那么如果需要,一个新线程将代替它执行后续的任务)。可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。与其他等效的newFixedThreadPool 不同,可保证无需重新配置此方法所返回的执行程序即可使用其他的线程。

代码:

        //线程池中只有一个核心线程
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        //使用线程池中的线程进行操作资源类
        for (int i = 0; i < 20; i++) {
            executorService.execute(() -> {
                System.out.println(Thread.currentThread().getName() + " 执行");
            });
        }
        //归还线程池
        executorService.shutdown();

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

③ Executors.newCachedThreadPool()线程池根据需求创建线程,可扩容,遇强则强

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

代码:

        //线程池中不指定线程,需要多少创建多少
        ExecutorService executorService = Executors.newCachedThreadPool();
        //使用线程池中的线程进行操作资源类
        for (int i = 0; i < 20; i++) {
            executorService.execute(() -> {
                System.out.println(Thread.currentThread().getName() + " 执行");
            });
        }
        //归还线程池
        executorService.shutdown();

特征:

• 线程池中数量没有固定,可达到最大值(Interger. MAX_VALUE)
• 线程池中的线程可进行缓存重复利用和回收(回收默认时间为 1 分钟)
• 当线程池中,没有可用线程,会重新创建一个线程

源码

其实三种方法,最底层的实现都是同一个方法:new ThreadPoolExecutor()

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

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

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

使用手动创建线程池的方式,能够更好的理解线程池的工作原理

下面先研究手动创建的七大参数的含义

手动创建线程池 - 七大参数
    @GetMapping("/test1")
    public void test() {
        //创建一个阻塞队列,队列中只容纳 3 个元素
        ArrayBlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<>(4);
        //手动创建线程池,七大参数
        /**
         * @param corePoolSize 核心线程池大小,可以理解为线程池中的常驻核心线程数量
         * @param maximumPoolSize 最大核心线程池大小,可以理解为线程池中能够容纳的最大核心线程数量
         *
         * @param keepAliveTime 多余的空闲线程(指的是阻塞队列满之后,挪用的最大线程数的那部分线程)的存活时间,
         *                      可以理解为当前池中线程数量超出了corePoolSize时,
         *                      当空闲时间达到keepAliveTime时,多余的线程会被销毁直到只剩下corePoolSize个数量的线程为止
         * @param unit          超时时间keepAliveTime的单位
         *
         * @param workQueue 任务阻塞队列,举例说明:核心线程池大小是3个,当任务请求线程有5个时,
         *                  超出核心线程池大小的2个会进入到BlockingQueue阻塞队列中等待,
         *                  设置阻塞队列的容量,如果在阻塞队列中等待的请求超过了阻塞队列的容量,
         *                  那么就会在线程数最大承载量的范围内创建新的线程,如果超过最大承载量,就走拒绝策略
         *
         *                 举例说明:核心线程池大小是3个,阻塞队列容量是4,最大线程数是6
         *                  ① 当任务请求线程有5个时:
         *                      核心线程处理3个,超出的会先进入阻塞队列,阻塞队列里放置2个等待处理,所有请求线程处理完毕
         *                  ② 当任务请求线程是8个时
         *                      核心线程处理3个,超出的会先进入阻塞队列,阻塞队列里放置4个等待处理,剩余一个会先扩容动用最大承载量的线程
         *                      最大承载量-核心线程数量=6-3=3,也就是还有3个备用线程,备用线程再处理1个,所有请求线程处理完毕
         *                  ③ 当任务请求线程是11个时
         *                      核心线程处理3个,超出的会先进入阻塞队列,阻塞队列里放置4个等待处理,剩余一个会先扩容动用最大承载量的线程
         *                      最大承载量-核心线程数量=6-3=3,也就是还有3个备用线程,备用线程再处理3个,还剩一个,此刻已经没有备用的,
         *                      就会去走 拒绝策略。
         *
         * @param threadFactory 线程工厂,用于创建线程的,一般只是为了设置线程名称,可以使用hutool包的工具类
         *                      “ new ThreadFactoryBuilder().setNamePrefix("MyThread-").build() ”
         *                      
         * @param handler 拒绝策略,当线程请求超过了线程数最大承载(最大核心线程池大小+阻塞队列容量),超出部分执行拒绝策略
         *          *   拒绝策略
         *                  ① AbortPolicy: 丢弃任务,并抛出拒绝执行RejectedExecutionException 异常信息。线程池默认的拒绝策略。
         *                      必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行;
         *                  ②CallerRunsPolicy: 当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,
         *                      不允许失败。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大;
         *                  ③DiscardPolicy: 直接丢弃,其他啥都没有;
         *                  ④DiscardOldestPolicy: 当触发拒绝策略,只要线程池没有关闭的话丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入
         */
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                3,
                6,
                3,
                TimeUnit.SECONDS,
                blockingQueue,
                new ThreadFactoryBuilder().setNamePrefix("MyThread-").build(),
                new ThreadPoolExecutor.AbortPolicy()
        );
        //使用线程池中的线程进行操作资源类
        for (int i = 0; i < 20; i++) {
            threadPoolExecutor.execute(() -> {
                System.out.println(Thread.currentThread().getName() + " 执行");
            });
        }
        //归还线程池
        threadPoolExecutor.shutdown();
    }

1. corePoolSize : 核心线程池大小,可以理解为线程池中的常驻核心线程数量

2. maximumPoolSize : 最大核心线程池大小,可以理解为线程池中能够容纳的最大核心线程数量

3. keepAliveTime : 多余的空闲线程(指的是阻塞队列满之后,挪用的最大线程数的那部分线程)的存活时间,
* 可以理解为当前池中线程数量超出了corePoolSize时,当超出的那部分线程空闲了,且当空闲时间达到keepAliveTime时,多余的线程会被销毁直到只剩下corePoolSize个数量的线程为止
*
4. unit : 超时时间keepAliveTime的单位

5. workQueue : 任务阻塞队列,举例说明:核心线程池大小是3个,当任务请求线程有5个时,
* 超出核心线程池大小的2个会进入到BlockingQueue阻塞队列中等待,
* 设置阻塞队列的容量,如果在阻塞队列中等待的请求超过了阻塞队列的容量,
* 那么就会在线程数最大承载量的范围内创建新的线程,如果超过最大承载量,就走拒绝策略

workQueue 举例

         *            举例说明:核心线程池大小是3个,阻塞队列容量是4,最大线程数是6
         *              ① 当任务请求线程有5个时:
         *                 核心线程处理3个,超出的会先进入阻塞队列,阻塞队列里放置2个等待处理,所有请求线程处理完毕
         *              ② 当任务请求线程是8个时
         *                 核心线程处理3个,超出的会先进入阻塞队列,阻塞队列里放置4个等待处理,剩余一个会先扩容动用最大承载量的线程
         *                 最大承载量-核心线程数量=6-3=3,也就是还有3个备用线程,备用线程再处理1个,所有请求线程处理完毕
         *              ③ 当任务请求线程是11个时
         *                 核心线程处理3个,超出的会先进入阻塞队列,阻塞队列里放置4个等待处理,剩余一个会先扩容动用最大承载量的线程
         *                 最大承载量-核心线程数量=6-3=3,也就是还有3个备用线程,备用线程再处理3个,还剩一个,此刻已经没有备用的,
         *                 就会去走 拒绝策略。

6. threadFactory : 线程工厂,用于创建线程的,一般只是为了设置线程名称,可以使用hutool包的工具类
“ new ThreadFactoryBuilder().setNamePrefix(“MyThread-”).build() ”

7. handler : 拒绝策略,当线程请求超过了线程数最大承载(最大核心线程池大小+阻塞队列容量),超出部分执行拒绝策略

拒绝策略
① AbortPolicy: 丢弃任务,并抛出拒绝执行RejectedExecutionException 异常信息。线程池默认的拒绝策略。
(银行满了,再来人就会抛出异常)必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行;

②CallerRunsPolicy: 当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务,一般并发比较小,性能要求不高(银行满了,让主线程执行)。
不会照成失败。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大;

③DiscardPolicy: 直接丢弃,其他啥都没有(丢掉任务,不报错);

④DiscardOldestPolicy: 当触发拒绝策略,只要线程池没有关闭的话丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入(银行满了,让最后进来的人和最开始进银行的人进行线程竞争)
底层工作原理

基于上面的七大参数,可以很好的总结出工作流程原理:

在这里插入图片描述

![在这里插入图片描述]

如何合理的设置线程池最大线程数

CPU密集型+IO密集型

如果是cpu密集型,那就比cpu核数多一到两个即可

        //获取当前系统的cpu核心数
        System.out.println("当前系统cpu核数:"+Runtime.getRuntime().availableProcessors());

如果是io密集型,设置为为2倍CPU核数

线程池注意事项

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

2.创建线程池推荐适用 ThreadPoolExecutor 及其 7 个参数手动创建;
corePoolSize 线程池的核心线程数
maximumPoolSize 能容纳的最大线程数
keepAliveTime 空闲线程存活时间
unit 存活的时间单位
workQueue 存放提交但未执行任务的队列
threadFactory 创建线程的工厂类
handler 等待队列满后的拒绝策略

3.分而治之线程池 - ForkJoinPool

背景

通常在计算机中,每个任务都是交由每个线程来处理的,当一个非常耗时的任务交由一个线程来完成,而其他线程处于空闲状态时就显得分配不太合理。
ForkJoinPool的优势在于,可以充分利用多cpu,多核cpu的优势,把一个任务拆分成多个“小任务”,把多个“小任务”放到多个处理器核心上并行执行;当多个“小任务”执行完成之后,再将这些执行结果合并起来汇总返回即可。

如何使用

流程图:
转载于

① 构造函数
        /**
         * @param parallelism 由几个线程来拆分任务,如果不填则默认为CPU核数创建线程数
         * @param ForkJoinWorkerThreadFactory 创建工作线程的工厂实现
         * @param UncaughtExceptionHandler 线程因未知异常而终止的回调处理
         * @param asyncMode 是否异步,默认false
         */
    public ForkJoinPool(int parallelism,
                        ForkJoinWorkerThreadFactory factory,
                        UncaughtExceptionHandler handler,
                        boolean asyncMode)
② 两种任务继承方式 - RecursiveTask 有回调结果

资源任务类

/**
 * 这里继承RecursiveTask,有回调结果
 */
class MyTask extends RecursiveTask<Integer> {
    private final Integer start;
    private final Integer end;

    /**
     * 依赖注入
     *
     * @param start
     * @param end
     */
    public MyTask(Integer start, Integer end) {
        this.start = start;
        this.end = end;
    }

    /**
     * 计算逻辑
     *
     * @return 计算出来的结果
     */
    @Override
    protected Integer compute() {
        int sum;
        //如果当前计算量小于5,不再拆分
        if ((end - start) <= 2) {
            //IntStream.rangeClosed(start, end) 方法,得到从 start,一直到end,递增1 的数组
            // 比如 IntStream.rangeClosed(1, 4) = 1,2,3,4
            System.out.println(Thread.currentThread().getName() + " 线程正在计算 " + start + " 加到 " + end);
            return IntStream.rangeClosed(start, end).sum();
        } else {
            //任务分割
            int middle = (start + end) / 2;
            //将任务二等分,二等分之后的任务如果没有达到 end - start < 2 的条件,还会继续拆分
            MyTask myTask = new MyTask(start, middle);
            MyTask myTask1 = new MyTask(middle + 1, end);
            //分别执行,也会分别继续拆分
            myTask.fork();
            myTask1.fork();
            //等待计算结束,全部合并返回
            return myTask.join() + myTask1.join();
        }
    }
}

线程操作

/**
 * ForkJoinPool 线程池
 */
@RestController
@RequestMapping("/forkDemo")
@AllArgsConstructor
public class ForkJoinPoolDemo {
    /**
     *
     */
    @GetMapping("/test1")
    public void test() {
        //创建 forkJoinPool 线程池
        /**
         * @param parallelism 由几个线程来拆分任务,如果不填则默认为CPU核数创建线程数
         * @param ForkJoinWorkerThreadFactory 创建工作线程的工厂实现
         * @param UncaughtExceptionHandler 线程因未知异常而终止的回调处理
         * @param asyncMode 是否异步,默认false
         */
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        //提交任务以执行
        ForkJoinTask<Integer> submit = forkJoinPool.submit(new MyTask(0, 10));

        try {
            //等待计算完成,获取结果
            Integer integer = submit.get();
            System.out.println("计算结果为:" + integer);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

执行结果:

ForkJoinPool-1-worker-9 线程正在计算 0 加到 2
ForkJoinPool-1-worker-11 线程正在计算 3 加到 5
ForkJoinPool-1-worker-13 线程正在计算 9 加到 10
ForkJoinPool-1-worker-4 线程正在计算 6 加到 8
计算结果为:55

可以看出,每个线程都会承担一部分的计算量,最后汇总,分而治之

如果使用原本的方法,则单线程压力过大,而其余线程空闲,是不合理的。
原本代码比较:

class MyTask implements Runnable {
    private final Integer start;
    private final Integer end;

    /**
     * 依赖注入
     *
     * @param start
     * @param end
     */
    public MyTask(Integer start, Integer end) {
        this.start = start;
        this.end = end;
    }
    @Override
    public void run() {
        //IntStream.rangeClosed(start, end) 方法,得到从 start,一直到end,递增1 的数组
        // 比如 IntStream.rangeClosed(1, 4) = 1,2,3,4
        System.out.println(Thread.currentThread().getName() + " 线程正在计算 " + start + " 加到 " + end);
        int sum = IntStream.rangeClosed(start, end).sum();
        System.out.println("结果为:"+sum);
    }
}

/**
 * ForkJoinPool 线程池
 */
@RestController
@RequestMapping("/forkDemo")
@AllArgsConstructor
public class ForkJoinPoolDemo {
    /**
     *
     */
    @GetMapping("/test1")
    public void test() {
        new Thread(new MyTask(0,10)).start();
    }
}

结果:

Thread-9 线程正在计算 0 加到 10
结果为:55
② 两种任务方式 - RecursiveAction 无回调结果

资源任务类

/**
 * RecursiveAction,无回调结果
 */
class MyNoCallTask extends RecursiveAction {
    private final int start1;
    private final int end1;
    private AtomicInteger SUM; //需要一个公共变量,来接受结果

    /**
     * 依赖注入
     *
     * @param start
     * @param end
     */
    public MyNoCallTask(Integer start, Integer end, AtomicInteger SUM) {
        this.start1 = start;
        this.end1 = end;
        this.SUM = SUM;
    }

    /**
     * 计算逻辑
     */
    @Override
    protected void compute() {
        //如果当前计算量小于5,不再拆分
        if ((end1 - start1) <= 2) {
            //IntStream.rangeClosed(start, end) 方法,得到从 start,一直到end,递增1 的数组
            // 比如 IntStream.rangeClosed(1, 4) = 1,2,3,4
            System.out.println(Thread.currentThread().getName() + " 线程正在计算 " + start1 + " 加到 " + end1);
            //addAndGet() 方法,将原值加上参数的值,保证原子性
            SUM.addAndGet(IntStream.rangeClosed(start1, end1).sum());
        } else {
            //任务分割
            int middle = (start1 + end1) / 2;
            //将任务二等分,二等分之后的任务如果没有达到 end - start < 2 的条件,还会继续拆分
            MyNoCallTask myNoCallTask = new MyNoCallTask(start1, middle,SUM);
            MyNoCallTask myNoCallTask1 = new MyNoCallTask(middle + 1, end1,SUM);
            //分别执行,也会分别继续拆分
            myNoCallTask.fork();
            myNoCallTask1.fork();
            //因为没有返回值,因此不需要合并返回
        }
    }
}

线程操作

    @GetMapping("/test11")
    public void test() throws InterruptedException {
        AtomicInteger SUM = new AtomicInteger(0);
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        //提交任务以执行,把原子全局变量也赋值
        forkJoinPool.submit(new MyNoCallTask(0, 10,SUM));
        //等待3秒,等待线程计算完
        forkJoinPool.awaitTermination(3, TimeUnit.SECONDS);

        System.out.println("计算结果为:" + SUM);
    }

执行结果:

ForkJoinPool-1-worker-9 线程正在计算 9 加到 10
ForkJoinPool-1-worker-11 线程正在计算 6 加到 8
ForkJoinPool-1-worker-2 线程正在计算 3 加到 5
ForkJoinPool-1-worker-4 线程正在计算 0 加到 2
计算结果为:55

4.异步回调线程

① 无返回值
    /**
     * 没有返回值的异步回调
     */
    @GetMapping("/test1")
    public void test() throws ExecutionException, InterruptedException {
        CompletableFuture<Void> voidCompletableFuture = CompletableFuture.runAsync(() -> {
            System.out.println(Thread.currentThread().getName() + " 没有返回,模拟执行更新操作");
        });
        Void aVoid = voidCompletableFuture.get();
        System.out.println(aVoid);
    }

结果:

ForkJoinPool.commonPool-worker-3 没有返回,模拟执行更新操作
null

② 有返回值
  
    /**
     * 存在返回值的异步回调
     */
    @GetMapping("/test2")
    public void test2() throws ExecutionException, InterruptedException {
        //开始异步执行无返回值的线程
        CompletableFuture<String> stringCompletableFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + " 执行无返回值任务");
//            int s = 10 / 0;
            return "正确返回的值:1"; //任务无异常,就会输出本条,有异常本条不输出
        });
        //针对任务运行情况做操作
        String s = stringCompletableFuture.whenComplete((t, u) -> {
            // 无论是否有异常,都会走这里
            System.out.println("正常返回的结果:" + t);
            System.out.println("过程产生的异常:" + u);
        }).exceptionally((f) -> {
            //有异常,就会进入这里
            return "异常返回结果:0";//任务有异常,就会输出本条
        }).get();
        //
        System.out.println("=打印=" + s);
    }

结果:

无异常:

ForkJoinPool.commonPool-worker-3 执行无返回值任务
正常返回的结果:正确返回的值:1
过程产生的异常:null
=打印=正确返回的值:1

有异常:

ForkJoinPool.commonPool-worker-3 执行无返回值任务
正常返回的结果:null
过程产生的异常:java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
=打印=异常返回结果:0

参考视频

第二章、进阶

进阶部分

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值