尚硅谷JUC笔记

JUC

第一集:JUC介绍

1、面试常见问题类别

  • 面向对象、高级语法(1、抽象类和接口有什么区别?2、Java中获得多线程有几种方法?)
  • Java集合类(1、java.util.*中的ArrayList、Vector、LinkedList、TreeMap、HashSet、HashMap、ConcurrentHashMap、CopyOnWriteHashMap、Queue等?2、谈谈你对HashMap的理解,什么是Hash算法,什么是Hash碰撞?)
  • Java多线程(1、Java中获得多线程的方法有几种?)
  • Java IO 和 NIO

2、JUC是什么?

在这里插入图片描述

简单来说JUC就是java.util.concurrent并发包

3、回顾进程和线程

1).进程和线程是什么?

答:进程是计算机分配资源的基本单位,它是一个具有独立功能的程序,例如QQ运行起来就是一个大的进程,这可以在任务管理器中看到;线程是程序执行的最小单位,一个进程里面可以有很多个线程,这些线程共享进程中的资源。

2).进程和线程举例?

答:例如QQ运行起来就是一个大的进程,然后里面有天气预报,我们可以同时进行文字聊天和视频,这些都是小的线程

3).线程状态?

答: JVM中的线程状态

4).wait和sleep的区别?

答:wait睡眠的时候会放开手中的锁,而sleep睡眠的时候会带着手中的锁,wait和sleep都可以设置睡眠时间,那他们的线程进入的是TIMED_WAITING状态,如果wait不设置睡眠时间,那需要使用其他方法使用共享对象.notify()或者共享对象.notifyAll()方法唤醒,并且进入的是WAITING状态

5).并发和并行分别是什么?

答:并发:同一时刻多个线程访问同一个资源,例如:例子:小米9限量抢购、春运抢票、电商秒杀…
并行:同一时刻多个线程同时访问不同资源

第二集:卖票复习

1、多线程编程的企业级套路+模板

1).最重要的三句话
  1. 在高内聚低耦合的前提下,线程 操作(资源类中对外暴露的方法) 资源类
  2. 先判断,然后干活,最后通知
  3. 在多线程交互中,必须要防止多线程的虚假唤醒,也即判断只能用while,不能用if
2).第一句话的例子

题目:

三个售票员 卖出 30张票

使用synchronized编写代码:

// 资源类
class Ticket {
    // 共享资源
    private int number = 30;
    
    // 操作
    public synchronized void saleTicket() {
        if (number > 0) {
            System.out.println(Thread.currentThread().getName() + "卖出了第" + number-- + "张票");
        }
    }
}

public class Test {
    public static void main(String[] args) {
        // 创建资源类
        Ticket ticket = new Ticket();
        // 创建线程
        new Thread(() -> {for (int i = 0; i < 40; i++) ticket.saleTicket();}, "A").start();
        new Thread(() -> {for (int i = 0; i < 40; i++) ticket.saleTicket();}, "B").start();
        new Thread(() -> {for (int i = 0; i < 40; i++) ticket.saleTicket();}, "C").start();
    }
}

使用synchronized编写代码的解释:

由于结果太长,所以就不给大家看了,我们创建了Ticket资源类,里面放着共享资源number,并且资源类中包含着操作saleTicket(),然后我们在main()方法中创建资源类对象,然后创建线程A、B、C同时去利用Ticket资源类对象调用saleTicket(),当然了saleTicket()方法上加的有synchronized关键字,由于A、B、C线程操作的都是同一个Ticket资源类对象,所以synchronized关键字锁住的就是该对象,因此不会出现多卖票的问题

使用Lock锁编写代码:

// 资源类
class Ticket {
    // 共享资源
    private int number = 30;
    // 创建锁
    private final Lock lock = new ReentrantLock();

    // 操作
    public void saleTicket() {
        // 加锁
        lock.lock();
        try {
            // 业务代码
            if (number > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出了第" + number-- + "张票");
            }
        } finally {
            // 解锁
            lock.unlock();
        }
    }
}

public class Test {
    public static void main(String[] args) {
        // 创建资源类
        Ticket ticket = new Ticket();
        // 创建线程
        new Thread(() -> {for (int i = 0; i < 40; i++) ticket.saleTicket();}, "A").start();
        new Thread(() -> {for (int i = 0; i < 40; i++) ticket.saleTicket();}, "B").start();
        new Thread(() -> {for (int i = 0; i < 40; i++) ticket.saleTicket();}, "C").start();
    }
}

使用Lock锁编写代码的解释:

加锁之后锁的不是某个对象,而是锁的这段代码,也就是说同一时刻有且只能有一个线程来执行加锁的这段代码,不过记着finally中解锁,使用finally的目的是为了保证在业务代码发生异常的时候依然可以解锁成功。其实这种做法和synchronized(this){***}还是有很多相似之处的,但是synchronized同步锁还是需要锁着线程共享的东西,比如前面写的this代表共享对象ticket,只要锁着的是多个线程共享的东西就可以了

3).第二句话、第三句话的例子

题目:

现在有四个线程去操作一个初始值为0的变量,要求实现其中两个线程对该变量加1,另外两个线程对该变量减1,要求变量的值只能是0和1,并且要进行交替展示,每个线程来10轮操作

使用synchronized、wait、notifyAll来解决问题的代码:

// 资源类
class Resource {
    private int number = 0;
    // 加1操作
    @SneakyThrows // 不用抛出异常
    public synchronized void increase() {
        // 判断
        while (number == 1) {
            this.wait();
        }
        // 干活
        number++;
        System.out.println(Thread.currentThread().getName() + ":" + number);
        // 通知
        this.notifyAll();
    }
    // 减1操作
    @SneakyThrows
    public synchronized void decrease() {
        // 判断
        while (number == 0) {
            this.wait();
        }
        // 干活
        number--;
        System.out.println(Thread.currentThread().getName() + ":" + number);
        // 通知
        this.notifyAll();
    }
}

public class Test {
    public static void main(String[] args) {
        // 创建资源类对象
        Resource resource = new Resource();
        // A线程和B线程加
        new Thread(() -> {for (int i = 0; i < 10; i++)resource.increase();}, "A").start();
        new Thread(() -> {for (int i = 0; i < 10; i++)resource.increase();}, "B").start();
        // C线程和D线程加
        new Thread(() -> {for (int i = 0; i < 10; i++)resource.decrease();}, "C").start();
        new Thread(() -> {for (int i = 0; i < 10; i++)resource.decrease();}, "D").start();
    }
}

使用synchronized、wait、notifyAll来解决问题的代码解释:

4个线程中两个加1两个减1,那肯定要在加1操作方法和减1操作方法中判断是否要进行加1或者减1,如果不加1或者减1的话那就要把同步锁让出来给其他线程,因此我们需要判断是否应该执行这个操作,如果不符合条件的话那就需要使用wait()方法睡眠,如果符合条件那就干活,干完活后唤醒其他线程,其他线程在开始争抢cpu时间片,至于为什么使用while而不用if判断呢?我来给你解释,现在假设A线程执行第一次判断发现可以加,那number就变成了1,之后唤醒其他线程,假设当A线程释放锁之后B线程抢到了锁,然后他一判断不能做蛋糕,那就执行wait()方法,假设当B线程释放锁之后A线程抢到了锁,然后他一判断还是不能做蛋糕,那就执行wait()方法,假设A线程释放锁之后C线程获得了锁,她属于顾客,然后她取走了一个蛋糕,那number就变成了0,之后唤醒其他线程,假设当C线程释放锁之后A线程获得了锁,然后A线程生产蛋糕,number就变成了1,之后唤醒其他线程,假设当A线程释放锁之后B线程抢到了锁,如果使用的是if判断,由于之前B线程执行了wait()方法,那它会从wait()方法之后执行,那么B先生会继续做蛋糕,这就违背了题目要求,所以不能使用if,而是应该使用while,假设还是上面的情况,即当A线程释放锁之后B线程抢到了锁,如果使用的是while判断,那就会在经过一次判断,一经判断发现number是1,那就不能做蛋糕,还是只能执行wait()方法,所以使用while判断可以完美解决虚假唤醒问题

使用Lock、await、signalAll来解决问题的代码:

// 资源类
class Resource {
    private int number = 0;
    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();

    // 加1操作
    @SneakyThrows // 不用抛出异常
    public void increase() {
        // 加锁
        lock.lock();
        try {
            // 判断
            while (number == 1) {
                condition.await();
            }
            // 干活
            number++;
            System.out.println(Thread.currentThread().getName() + ":" + number);
            // 通知
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    // 减1操作
    @SneakyThrows
    public void decrease() {
        // 加锁
        lock.lock();
        try {
            // 判断
            while (number == 0) {
                condition.await();
            }
            // 干活
            number--;
            System.out.println(Thread.currentThread().getName() + ":" + number);
            // 通知
            condition.signalAll();
        } finally {
            lock.unlock();

        }
    }
}

public class Test {
    public static void main(String[] args) {
        // 创建资源类对象
        Resource resource = new Resource();
        // A线程和B线程加
        new Thread(() -> {for (int i = 0; i < 10; i++)resource.increase();}, "A").start();
        new Thread(() -> {for (int i = 0; i < 10; i++)resource.increase();}, "B").start();
        // C线程和D线程加
        new Thread(() -> {for (int i = 0; i < 10; i++)resource.decrease();}, "C").start();
        new Thread(() -> {for (int i = 0; i < 10; i++)resource.decrease();}, "D").start();
    }
}

使用Lock、await、signalAll来解决问题的代码的解释:

之前用wait()和notifyAll()来做的,现在用private final Condition condition = lock.newCondition();创建出来的condition调用await()和signalAll()来做,作用都是一样的,只不过之前的synchronized、wait、notifyAll铁三角换成了Lock、await、signalAll铁三角

4).精准通知顺序操作(为什么要用Lock取代synchronized的原因)

题目:

多线程之间严格按照顺序调用,实现A线程—》B线程—》C线程,当三个线程启动之后,要求如下:

AA打印5次,BB打印10次,CC打印15次
接着
AA打印5次,BB打印10次,CC打印15次
……一共来10轮

代码:

// 资源类
class Resource {
    private int number = 1;
    private final Lock lock = new ReentrantLock();
    // condition1、condition2、condition3分别对应线程AA、BB、CC
    private final Condition condition1 = lock.newCondition();
    private final Condition condition2 = lock.newCondition();
    private final Condition condition3 = lock.newCondition();

    @SneakyThrows // 不用抛出异常
    public void print(int size) {
        lock.lock();
        try {
            // 判断
            if (size == 5) {
                while (number != 1) {
                    condition1.await();
                }
            }
            if (size == 10) {
                while (number != 2) {
                    condition2.await();
                }
            }
            if (size == 15) {
                while (number != 3) {
                    condition3.await();
                }
            }
            // 干活
            for (int i = 1; i <= size; i++) {
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
            // 精准通知
            if (size == 5) {
                number = 2;
                condition2.signal();
            }
            if (size == 10) {
                number = 3;
                condition3.signal();
            }
            if (size == 15) {
                number = 1;
                condition1.signal();
            }
        } finally {
            lock.unlock();
        }
    }
}

public class Test {
    public static void main(String[] args) {
        // 创建资源类对象
        Resource resource = new Resource();
        // 创建多个线程
        new Thread(()->{for (int i = 0; i < 10;i++) resource.print(5); },"AA").start();
        new Thread(()->{for (int i = 0; i < 10;i++) resource.print(10); },"BB").start();
        new Thread(()->{for (int i = 0; i < 10;i++) resource.print(15); },"CC").start();
    }
}

解释:

完美实现效果,可以创建多个Condition类进行操作,实现精准通知,如果使用synchronized铁三角就无法实现相同的效果

5).lambda表达式写法

一句话:

复制小括号,写死右箭头,落地大括号

解释:

首先只有函数式接口中的方法才能使用Lambda表达式,而函数式接口就是方法中有且仅有一个方法需要被实现,但是里面还可以有default修饰的方法或者static修饰的静态方法是不影响的,毕竟它们不需要被实现,当然如果接口符合函数式接口的限制条件,并且该接口想成为函数式接口,最好在接口名称上面加上@FunctionalInterface注解,一方面可以表明本接口是函数式接口,另外一方面还可以验证当前接口是否符合函数式接口的约束条件,例如Runnable接口就是函数式接口;复制小括号就是复制那个被实现的方法中的(),例如Runnable接口中的public abstract void run();中的(),写死右箭头就是在小括号后面写上->,落地大括号就是最后写一个{};最后说明lambda表达式的作用就是简写匿名内部类

例子:

// 使用匿名内部类实现接口
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("我是一个线程");
    }
}).start();
// 使用Lambda表达式改写匿名内部类
new Thread(() -> {System.out.println("我是一个线程"); }).start();}

代码解释:

在这里插入图片描述
当然只有一条语句的时候,大括号可以省略,另外还有一些其他的规则,例如小括号中有参数的时候可以省略参数类型,如果只有一个参数不仅可以省略参数类型,还可以省略包裹参数的小括号,还有如果只有一条代码的话还可以省略大括号,如果这行代码是return返回值代码不仅可以省略大括号,还可以省略return关键字,直接在->后面写上返回值就可以了,更多细节请看:Lambda表达式详解

第三集:8锁的现象

在这里插入图片描述
解释:

其实做对这些问题需要了解这么一些事情,首先对象锁和类锁是不一样的,如果一个方法上加了synchronized,那使用的是对象锁,如果一个方法上加了static和synchronized,那使用的是类锁,然后对象锁锁的是一个对象,如果同一个类中的多个方法上加的仅仅有synchronized,并且同一时刻多个线程使用某对象去运行同步方法,但是只会有一个线程运行同步方法,其他线程都要等待;如果同一个类中的多个方法上加的有static和synchronized,那获得的锁就是类锁,类锁也叫做静态同步锁,当然类锁只关注同一个类中同时加的有static和synchronized的那些方法,另外我还想说对象锁和类锁是不同的,他们互不影响,他们是两个锁,没有从属关系,一个线程获得类锁的同时,另外一个线程可以获得对象锁,如果上述所说的类中的方法上没有加synchronized,也没有加static,那就不需要获得对象锁或者类锁,多少个线程同时去调用这些方法都是没有错的;另外还需要知道sleep()方法代表执行该方法的线程是带着锁睡的,醒来的时候线程依然带着锁

第四集:不安全的ArrayList

1、ArrayList不安全吗?

问题:

请举例说明ArrayList不是线程安全的?

代码:

public class Test {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        for (int i = 1; i < 100; i++) {
            new Thread(() -> {
                // 写入
                list.add(UUID.randomUUID().toString().substring(0, 8));
                // 读出
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }
}

说明:

会出现java.util.ConcurrentModificationException,该异常俗称并发修改异常,即多个线程需要写入,同时还有多个线程还要读出,所以会出现该异常

2、ArrayList不安全怎么办?

方案1:使用Vector类(不采用)

举例:

// 除了这一行之外,其他代码的和上面“1、ArrayList不安全吗?”中的测试代码相同
List<String> list = new Vector<>();

说明:

加了synchronized,读取效率太低

方案2:使用Collections.synchronizedList(new ArrayList<>())(适合低并发小数据量的时候使用)

举例:

// 除了这一行之外,其他代码的和上面“1、ArrayList不安全吗?”中的测试代码相同
List<String> list = Collections.synchronizedList(new ArrayList<>());

说明:

可以把线程不安全的ArrayList对象变成线程安全的对象,其实就是对ArrayList中的每个方法上加synchronized

在这里插入图片描述
现在来解释一下里面的原理,我们点进synchronizedList()方法,代码如下:

public static <T> List<T> synchronizedList(List<T> list) {
    return (list instanceof RandomAccess ?
            new SynchronizedRandomAccessList<>(list) :
            new SynchronizedList<>(list));
}

由于listnew ArrayList<>(),所以instanceof判断结果是false,所以执行new SynchronizedList<>(list),我们点击SynchronizedList进去看一下,代码如下:

static class SynchronizedCollection<E> implements Collection<E>, Serializable {
	private static final long serialVersionUID = 3053995032091335093L;

	final Collection<E> c;  // Backing Collection
	final Object mutex;     // Object on which to synchronize
	………………
}

static class SynchronizedList<E>
extends SynchronizedCollection<E>
implements List<E> {
	private static final long serialVersionUID = -7754090372962971524L;

	// list就是new ArrayList<>()
	final List<E> list;

	SynchronizedList(List<E> list) {
		super(list);
		this.list = list;
	}
	// 使用synchronized同步代码块来保证原子性操作
	public void add(int index, E element) {
		synchronized (mutex) {list.add(index, element);}
	}
}

可以看到代码中用到了synchronized同步代码块,因此会保证操作的原子性

方案3:使用CopyOnWriteArrayList代替ArrayList(适合多线程高并发大数据量的时候使用)

举例:

// 除了这一行之外,其他代码的和上面“1、ArrayList不安全吗?”中的测试代码相同
List<String> list = new CopyOnWriteArrayList<>();

说明:

这是写时复制思想, 首先看add()方法中有可重入锁,这个目的是防止多个线程争抢写的权力,然后下面红框中的内容是将原件复制出来一份,然后在复印件上写,之后通过setArray()方法让原件地址指向复印件,这样可以让所有人读原件,而我只修改复印件,所以读和写不会出现冲突,因此通过加锁和写时复制思想可以很好保证了多线程情况下所有线程都可以读,但是只有一个线程在写,因此不会出现并发修改异常,如下图:

在这里插入图片描述
以上是说自己的解释,以下是老师的解释:在这里插入图片描述

第五集:不安全的HashSet

1、HashSet不安全吗?

问题:

请举例说明HashSet不是线程安全的?

代码:

public class Test {
    public static void main(String[] args) {
        Set<String> set = new HashSet<>();
        for (int i = 1; i < 100; i++) {
            new Thread(() -> {
                // 写入
                set.add(UUID.randomUUID().toString().substring(0, 8));
                // 读出
                System.out.println(set);
            }, String.valueOf(i)).start();
        }
    }
}

说明:

会出现java.util.ConcurrentModificationException,该异常俗称并发修改异常,即多个线程需要写入,同时还有多个线程还要读出,所以会出现该异常
另外还需要说明的一点是HashSet的底层是HashMap,可以看源码:

在这里插入图片描述
然而HashMap的底层是Hash表,Hash表是数组加单向链表的组成结构

2、HashSet不安全怎么办?

方案1:使用Collections.synchronizedSet(new HashSet<>())(适合低并发小数据量的时候使用)

举例:

// 除了这一行之外,其他代码的和上面“1、HashSet不安全吗?”中的测试代码相同
Set<String> set = Collections.synchronizedSet(new HashSet<>());

说明:

可以把线程不安全的HashSet对象变成线程安全的对象,其实就是对HashSet中的每个方法上加synchronized

在这里插入图片描述

方案2:使用CopyOnWriteArraySet代替HashSet(适合多线程高并发大数据量的时候使用)

举例:

// 除了这一行之外,其他代码的和上面“1、HashSet不安全吗?”中的测试代码相同
Set<String> set = new CopyOnWriteArraySet<>();

说明:

首先CopyOnWriteArraySetadd()方法:

在这里插入图片描述
然后点击addIfAbsent()就到了CopyOnWriteArrayList类中:

在这里插入图片描述
之后我们点击addIfAbsent()方法后发现CopyOnWriteArrayList类中的该方法内部代码:

在这里插入图片描述

第六集:不安全的HashMap

1、HashMap不安全吗?

问题:

请举例说明HashMap不是线程安全的?

代码:

public class Test {
    public static void main(String[] args) {
        Map<String, String> map = new HashMap<>();
        for (int i = 1; i < 100; i++) {
            new Thread(() -> {
                // 写入
                map.put(UUID.randomUUID().toString().substring(0, 8), UUID.randomUUID().toString().substring(0, 8));
                // 读出
                System.out.println(map);
            }, String.valueOf(i)).start();
        }
    }
}

说明:

会出现java.util.ConcurrentModificationException,该异常俗称并发修改异常,即多个线程需要写入,同时还有多个线程还要读出,所以会出现该异常

2、HashMap不安全怎么办?

方案1:使用HashTable(不采用)

举例:

// 除了这一行之外,其他代码的和上面“1、HashMap不安全吗?”中的测试代码相同
Map<String, String> map = new Hashtable<>();

说明:

加了synchronized,读取效率太低,一次锁整张Hash表,相当于这个表是表锁,只要当前线程在读或者写,其他线程都不能进行操作,虽然解决了并发问题,但是相当于将并行操作变成了串行操作,另外在进行复合操作的时候也不是线程安全的,比如添加操作是判断是否存在和添加的联合操作,我们不期待这个过程在别人打扰,但是两个子操作中间是有可能被别人打扰的,可能我判断的时候没有这个值,但是在我执行添加操作之前被别人添加上了,然后我再去添加的时候发现有人已经添加了,这不就是添了个寂寞吗,说明添加操作就不是线程安全的

方案2:使用Collections.synchronizedMap(new HashMap<>())(适合低并发小数据量的时候使用)

举例:

// 除了这一行之外,其他代码的和上面“1、HashMap不安全吗?”中的测试代码相同
Map<String, String> map = Collections.synchronizedMap(new HashMap<>());

说明:

可以把线程不安全的HashMap对象变成线程安全的对象,其实就是对HashMap中的每个方法上加synchronized
在这里插入图片描述

方案3:使用ConcurrentHashMap代替HashMap(适合多线程高并发大数据量的时候使用)

举例:

// 除了这一行之外,其他代码的和上面“1、HashMap不安全吗?”中的测试代码相同
Map<String, String> map = new ConcurrentHashMap<>();

说明:

在JDK1.7中,ConcurrentHashMap里面使用的是锁分段机制,默认ConcurrentLevel分段级别是16,也就是说默认有16个段Segment段,每个段上有一个锁,每个段后面都是一个哈希表,这样每次就有16个锁了,我们每次操作的是不同的锁,这样比HashTable中的1个锁要好多了,至少不是串行操作了

在jdk1.8中,ConcurrentHashMap取消了分段锁机制(锁分段机制也是一样的意思,都可以用,没有区别),底层使用CAS算法,CAS算法根本没有使用锁,它使用的是while循环判断,如果修改成功那就结束while循环,否则只要依然有CPU时间片,那就继续执行修改操作,直到执行成功为止,由于JVM为Unsafe类中的CAS方法写成汇编指令,虽然CAS中涉及到获取比较交换三个操作,但是这三个操作是原子操作,中途不会被打断,所以CAS操作是原子性的,并且没有锁,效率更高,由于一直在while循环中,所以会减少线程上下文切换的消耗,但是循环也会带来一些时间开销

在这里插入图片描述

第七集:Callable

代码:

class MyThread implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println("hello MyThread!");
        TimeUnit.SECONDS.sleep(4);
        return 1024;
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        FutureTask futureTask = new FutureTask(new MyThread());
        new Thread(futureTask, "A").start();
        System.out.println(futureTask.get());
        System.out.println("hello Test!");
    }
}

解释:

首先输出hello MyThread!,经过4秒中之后输出1024,然后输出hello Test!,说明get()方法会阻塞调用该方法的线程,比如本次阻塞的就是主线程,因此我们最好将get()方法往后放,等到主线程中的其他操作完成的时候在通过get()方法获取返回值,毕竟尽量少等会是最好的选择,其实我们都知道Thread类的构建方法中的两个参数其中的第一个参数是Runnable接口,如果我们想使用Runnable接口,那我们需要Callable接口和Runnable接口挂上钩,具体关联如下:

在这里插入图片描述
Callable接口和Runnable的不同:

  • 名称不同
  • 重写方法不同,Callable中是call()方法,而Runnable中是run()方法
  • 是否带泛型,其中Callable接口带有泛型,而Runnable接口不带有泛型
  • 是否有返回值,其中Callable接口有返回值,返回值类型和泛型类型一致
  • 构建方式不同,其中Callable接口用的是FutureTask类作为媒介来联系上Runnable接口

第八集:CountDownLatch

1、概念讲解

代码:

public class Test {
    public static void main(String[] args) throws Exception {
        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();
        }
        countDownLatch.await();
        System.out.println("所有学生走完,关门");
    }
}

结果:

1号学生离开教室
6号学生离开教室
5号学生离开教室
4号学生离开教室
3号学生离开教室
2号学生离开教室
所有学生走完,关门

解释:

CountDownLatch countDownLatch = new CountDownLatch(6);代表只有执行6次countDownLatch.countDown();之后总量变成0,执行一次countDownLatch.countDown();就少一个1,然后countDownLatch.await();表示在等待着6变成0的时候,只要变成了0,那么System.out.println("所有学生走完,关门");就会执行

2、公司使用案例

// 使用背景
// 敏感词系统需要添加/删除敏感词的时候,需要向我的系统发送消息,但是我的系统同时只能处理一个敏感词的添加/删除操作,所以这就是方法中synchronized关键字的来处
// 本次代码的作用是删除敏感词,我需要保证所有词条都处理完成,并且处理无误之后,才能删除敏感词记录数据,以及执行其他数据
// 并且这是在多线程下操作,因此要使用CountDownLatch来帮助,其中CountDownLatch对象的初始值就是需要处理的敏感词记录列表数量
// 每处理一条记录,就调用一次countDownLatch.countDown(),然后调用countDownLatch.await()进行等待,什么时候所有敏感词记录都处理完了,那就删除敏感词记录,并处理其他数据
// 虽然也可以使用Thread睡眠完成,但是在本地使用不太恰当,毕竟我们无法控制准确的睡眠时间,而添加/删除敏感词都是需要立即响应的操作,所以线程睡眠用在此处不恰当
@Override
public synchronized void removeSensitiveWord(String sensitiveWord) {
    try {
        final long startTime = System.currentTimeMillis();
        if (StringUtils.isBlank(sensitiveWord)) {
            logger.error("删除敏感词时候,敏感词不能为空");
            return;
        } else {
            sensitiveWord = sensitiveWord.trim();
        }
        SensitiveWord entity = new SensitiveWord();
        entity.setOriginalWord(sensitiveWord);
        entity = dao.loadByWhere(entity);
        // 总数量
        int totalNumber = 0;
        // 错误数量,用于确定是否删除敏感词记录
        final AtomicInteger errorNumber = new AtomicInteger(0);
        if (entity != null) {
            final SensitiveWord finalEntity = entity;
            final SensitiveWordKItemRelation relation = new SensitiveWordKItemRelation();
            relation.setSensitiveWordId(finalEntity.getId());
            totalNumber = (int)relationDao.countByWhere(relation);
            if (totalNumber > 0) {
                final List<SensitiveWord> list = dao.all();
                log.info("》》》敏感词名称:" + sensitiveWord + ";总数量:" + totalNumber);
                final CountDownLatch countDownLatch = new CountDownLatch(totalNumber);
                ThreadPoolExecutor poolHolder = ThreadPoolHolder.getThreadPoolExecutor();
                final String finalSensitiveWord = sensitiveWord;
                final int pageSize = 1000;
                int totalPage = totalNumber % pageSize == 0 ? totalNumber / pageSize : totalNumber / pageSize + 1;
                for (int currentPage = 1; currentPage <= totalPage; currentPage++) {
                    final int finalCurrentPage = currentPage;
                    poolHolder.execute(new Runnable() {
                        @Override
                        public void run() {
                            // 存储WikiHunter对象
                            List<WikiHunter> hunterList = new ArrayList<WikiHunter>(pageSize);
                            // 生成WikiHunter对象
                            PageDataList<SensitiveWordKItemRelation> pageDataList = relationDao.pageByWhere(finalCurrentPage, pageSize, relation);
                            List<SensitiveWordKItemRelation> dataList = pageDataList.getList();
                            for (SensitiveWordKItemRelation itemRelation : dataList) {
                                try {
                                    String kItemId = itemRelation.getkItemId();
                                    // 先从词条历史表中查找当前版本词条信息
                                    KItemHistory kItemHistory = new KItemHistory();
                                    kItemHistory.setkItemId(kItemId);
                                    kItemHistory.setStatus(AutidStatusEnums.PASS.getValue());
                                    kItemHistory.setFlag(true);
                                    final KItemHistory history = historyDao.loadByWhere(kItemHistory);

                                    // 如果词条历史表中不存在,那就从原始词条表中查找信息
                                    KItem kItem = null;
                                    if (history == null) {
                                        kItem = kItemDao.loadById(kItemId);
                                    }

                                    if (history == null && kItem == null) {
                                        continue;
                                    }

                                    // 判断是否可以提交词条信息到es,如果可以,那就让除当前敏感词之外的敏感词在处理一次之后就提交
                                    String title;
                                    if (history != null) {
                                        title = history.getTitle();
                                    } else {
                                        title = kItem.getTitle();
                                    }
                                    boolean flag = true;
                                    String againstWordId = null;
                                    for (SensitiveWord item : list) {
                                        if (!finalSensitiveWord.equals(item.getOriginalWord()) && title.contains(item.getOriginalWord())) {
                                            flag = false;
                                            againstWordId = item.getId();
                                            break;
                                        }
                                    }

                                    // 词条依然敏感,不能恢复
                                    if (!flag) {
                                        addSensitiveWordKItemRelation(againstWordId, kItemId);
                                        continue;
                                    }

                                    // 提交词条信息到es
                                    String substituteWord = null;
                                    if (history != null) {
                                        // 根据要求处理history对象
                                        for (SensitiveWord item : list) {
                                            if (!finalSensitiveWord.equals(item.getOriginalWord())) {
                                                // 替换词
                                                substituteWord = item.getSubstituteWord();
                                                // 进行该操作的目的是进行完全匹配,避免遗漏“足 浴”这种情况
                                                String regex = WikiUtil.getDesensitizationRegex(item.getOriginalWord());
                                                // 执行敏感词汇替换操作
                                                boolean replace = false;
                                                if (StringUtils.isNotBlank(history.getSearchContent())) {
                                                    String replaceStr = history.getSearchContent().replaceAll(regex, substituteWord);
                                                    if (!history.getSearchContent().equals(replaceStr)) {
                                                        replace = true;
                                                    }
                                                    history.setSearchContent(replaceStr);
                                                }
                                                if (StringUtils.isNotBlank(history.getSummary())) {
                                                    String replaceStr = history.getSummary().replaceAll(regex, substituteWord);
                                                    if (!history.getSummary().equals(replaceStr)) {
                                                        replace = true;
                                                    }
                                                    history.setSummary(replaceStr);
                                                }
                                                if (StringUtils.isNotBlank(history.getBasicInfo())) {
                                                    String replaceStr = history.getBasicInfo().replaceAll(regex, substituteWord);
                                                    if (!history.getBasicInfo().equals(replaceStr)) {
                                                        replace = true;
                                                    }
                                                    history.setBasicInfo(replaceStr);
                                                }
                                                if (StringUtils.isNotBlank(history.getCatalog())) {
                                                    String replaceStr = history.getCatalog().replaceAll(regex, substituteWord);
                                                    if (!history.getCatalog().equals(replaceStr)) {
                                                        replace = true;
                                                    }
                                                    history.setCatalog(replaceStr);
                                                }
                                                if (StringUtils.isNotBlank(history.getContent())) {
                                                    String replaceStr = history.getContent().replaceAll(regex, substituteWord);
                                                    if (!history.getContent().equals(replaceStr)) {
                                                        replace = true;
                                                    }
                                                    history.setContent(replaceStr);
                                                }
                                                // 记录item.getOriginalWord()和词条id到敏感词库中
                                                if (replace) {
                                                    // 记录词条id到敏感词库中
                                                    addSensitiveWordKItemRelation(item.getId(), kItemId);
                                                }
                                            }
                                        }
                                        // 添加WikiHunter对象到集合中
                                        WikiHunter hunter = elasticsearchService.getWikiHunterByHistory(history, false);
                                        hunterList.add(hunter);
                                        // 将词条信息入es
                                        // historyService.createIndexForKItemHistory(null, history, map);
                                    } else {
                                        WikiHunter hunter = wikiHunterTemplate.getWikiHunterByKItem(kItem);
                                        if (hunter != null) {
                                            // 词条脱敏
                                            for (SensitiveWord item : list) {
                                                if (!finalSensitiveWord.equals(item.getOriginalWord())) {
                                                    // 替换词
                                                    substituteWord = item.getSubstituteWord();
                                                    // 进行该操作的目的是进行完全匹配,避免遗漏“足 浴”这种情况
                                                    String regex = WikiUtil.getDesensitizationRegex(item.getOriginalWord());
                                                    // 处理WikiHunter对象的同时,还需要判断是否当前词条是否违反其他敏感词要求
                                                    boolean replace = false;
                                                    if (StringUtils.isNotBlank(hunter.getContent())) {
                                                        String replaceStr = hunter.getContent().replaceAll(regex, substituteWord);
                                                        if (!hunter.getContent().equals(replaceStr)) {
                                                            replace = true;
                                                        }
                                                        hunter.setContent(replaceStr);
                                                    }
                                                    if (StringUtils.isNotBlank(hunter.getSummary())) {
                                                        String replaceStr = hunter.getSummary().replaceAll(regex, substituteWord);
                                                        if (!hunter.getSummary().equals(replaceStr)) {
                                                            replace = true;
                                                        }
                                                        hunter.setSummary(replaceStr);
                                                    }
                                                    if (StringUtils.isNotBlank(hunter.getBasicInfo())) {
                                                        String replaceStr = hunter.getBasicInfo().replaceAll(regex, substituteWord);
                                                        if (!hunter.getBasicInfo().equals(replaceStr)) {
                                                            replace = true;
                                                        }
                                                        hunter.setBasicInfo(replaceStr);
                                                    }
                                                    if (StringUtils.isNotBlank(hunter.getCatalog())) {
                                                        String replaceStr = hunter.getCatalog().replaceAll(regex, substituteWord);
                                                        if (!hunter.getCatalog().equals(replaceStr)) {
                                                            replace = true;
                                                        }
                                                        hunter.setCatalog(replaceStr);
                                                    }
                                                    if (StringUtils.isNotBlank(hunter.getHtmlContent())) {
                                                        String replaceStr = hunter.getHtmlContent().replaceAll(regex, substituteWord);
                                                        if (!hunter.getHtmlContent().equals(replaceStr)) {
                                                            replace = true;
                                                        }
                                                        hunter.setHtmlContent(replaceStr);
                                                    }
                                                    // 记录item.getOriginalWord()和词条id到敏感词库中
                                                    if (replace) {
                                                        // 记录词条id到敏感词库中
                                                        addSensitiveWordKItemRelation(item.getId(), kItemId);
                                                    }
                                                }
                                            }
                                            // 添加WikiHunter对象到集合中
                                            hunterList.add(hunter);
                                        }
                                    }
                                } catch (Exception e) {
                                    log.info("删除敏感词过程中出现错误,敏感词id:"+itemRelation.getSensitiveWordId()+";词条id:"+itemRelation.getkItemId());
                                    e.printStackTrace();
                                    // 增加错误数
                                    errorNumber.getAndIncrement();
                                } finally {
                                    long currentTime = System.currentTimeMillis();
                                    log.info("敏感词:"+finalEntity.getOriginalWord()+";剩余数量:" + countDownLatch.getCount()+";耗时:" + (currentTime - startTime) / 1000 / 60 + "分钟,当前时间:"+ DateUtil.now(DateUtil.DATETIME_FORMAT));
                                    // 完成一次减1
                                    countDownLatch.countDown();
                                }
                            }
                            // 上传WikiHunter对象到es
                            batchUpdateWikiHunter(finalEntity.getId(), hunterList, false, errorNumber);
                            // 回收资源
                            hunterList = null;
                        }
                    });
                }
               	// 等待敏感词删除完成
                countDownLatch.await();

                // 删除敏感词记录、敏感词和词条关系记录
                SensitiveWordKItemRelation item = new SensitiveWordKItemRelation();
                item.setSensitiveWordId(finalEntity.getId());
                log.info("异常数量:"+errorNumber+";说明:如果异常数量大于0,该词条不会删除敏感词和敏感词及词条关系");
                if (errorNumber.get() == 0) {
                    // 删除敏感词记录
                    dao.deleteById(finalEntity.getId());
                    // 删除敏感词和词条关系记录
                    relationDao.deleteByWhere(item);
                }

                // 防止redis缓存中存在敏感词,所以执行清空操作
                redisService.removeAllHomePageRedisCache();
            }
        }
        long endTime = System.currentTimeMillis();
        log.info("删除敏感词:"+sensitiveWord+";敏感词总数:"+totalNumber+";耗费时间:"+(endTime - startTime) / 1000 + "秒");
    } catch (Exception e) {
        e.printStackTrace();
    }
}

第九集:CyclicBarrier

代码:

public class Test {
    public static void main(String[] args) throws Exception {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> System.out.println("7颗龙珠聚齐,开始召唤神龙!"));
        for (int i = 1; i <= 7; i++) {
            final int finalI = i;
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "号收集到第" + finalI + "颗龙珠");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }, String.valueOf(i)).start();
        }
    }
}

结果:

2号收集到第2颗龙珠
3号收集到第3颗龙珠
4号收集到第4颗龙珠
1号收集到第1颗龙珠
5号收集到第5颗龙珠
6号收集到第6颗龙珠
7号收集到第7颗龙珠
7颗龙珠聚齐,开始召唤神龙!

解释:

之前说的CountDownLatch是每次减去1,而本次是执行一次await()方法就加一个1,加到7,那就可以执行CyclicBarrier中的第二个参数中的线程了

第十集:Semaphore

1、概念讲解

代码:

public class Test {
    public static void main(String[] args) throws Exception {
        Semaphore semaphore = new Semaphore(3);
        for (int i = 1; i <= 6; i++) {
            new Thread(()->{
                try {
                    // 获得信号量
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName()+"号车主抢到了车位");
                    // 暂停一会线程,表示占据车位的过程
                    TimeUnit.SECONDS.sleep(3);
                    System.out.println(Thread.currentThread().getName()+"号车主离开了车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 释放信号量
                    semaphore.release();
                }
            },String.valueOf(i)).start();
        }
    }
}

结果:

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

解释:

一共准备了3个车位,也就是信号量构造方法中的3,然后有6个车主去抢车位,当3个车主抢到车位之后,另外几位车主只能等待,当有车主移出车位之后其他车主才可以抢车位,其中我们所说的车主就是一个一个的线程

2、本人使用案例

我写了一个服务端代码,作用是通过java代码操作exe进行ppt转pdf,然后在客户端那边同时开10个线程同时转换,发现只能成功转换2~6个,但是我不知道原因是什么,所以只能来解决这个问题,因此我通过Semaphore信号量控制同时转换的只能有3个,然后基本上可以让转换不报错了,代码如下:


import com.mingming.doconvert.util.FileUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.Semaphore;

import static org.springframework.util.FileCopyUtils.copy;

/**
 * 文档转换
 *
 * @author 明快de玄米61
 * @date 2022/11/18 0:06
 */
@RestController
public class ConvertController {

    // 信号量
    private volatile static Semaphore semaphore = new Semaphore(3);

	……………………………………

    /**
     * 文档转换接口
     * @author 明快de玄米61
     * @date   2022/11/22 10:53
     * @param  response 返回
     * @param  file 待转换文件
     * @param  targetType 目标格式
     **/
    @PostMapping("/convert/{targetType}")
    public void convert(HttpServletResponse response, @RequestParam("file") MultipartFile file, @PathVariable String targetType) throws Exception {
        ……………………………………

        // 文档转换
        // 原始文件
        File srcFile = null;
        // 目标文件
        File targetFile = null;
        try {
            // 获得信号量
            semaphore.acquire();
            
            // 转存原始文件
            ……………………………………

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
        	……………………………………
            // 释放信号量
            semaphore.release();
        }
    }
	……………………………………
}

第十一集:ReentrantReadWriteLock

代码:

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

    // 写入数据
    @SneakyThrows
    public void put(String key, Object value) {
        readWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName()+"号线程写入数据开始,key="+key);
            map.put(key, value);
            // 模拟程序执行过程,但是不会出现多次连续写
            TimeUnit.MICROSECONDS.sleep(100);
            System.out.println(Thread.currentThread().getName()+"号线程写入数据结束,key="+key);
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }

    // 读出数据
    @SneakyThrows
    public void get(String key) {
        readWriteLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName()+"号线程读出数据开始,key="+key);
            Object o = map.get(key);
            // 模拟程序执行过程,可以出现多次连续读
            TimeUnit.MICROSECONDS.sleep(100);
            System.out.println(Thread.currentThread().getName()+"号线程读出数据结束,key="+key);
        } finally {
            readWriteLock.readLock().unlock();
        }
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        MyCache myCache = new MyCache();
        for (int i = 1; i <= 5; i++) {
            int finalI = i;
            new Thread(()->{myCache.put(String.valueOf(finalI), String.valueOf(finalI));},String.valueOf(i)).start();
        }
        for (int i = 6; i <= 10; i++) {
            int finalI = i;
            new Thread(()->{myCache.get(String.valueOf(finalI-5));},String.valueOf(finalI)).start();
        }
    }
}

结果:

1号线程写入数据开始,key=1
1号线程写入数据结束,key=1
3号线程写入数据开始,key=3
3号线程写入数据结束,key=3
2号线程写入数据开始,key=2
2号线程写入数据结束,key=2
4号线程写入数据开始,key=4
4号线程写入数据结束,key=4
5号线程写入数据开始,key=5
5号线程写入数据结束,key=5
6号线程读出数据开始,key=1
7号线程读出数据开始,key=2
8号线程读出数据开始,key=3
10号线程读出数据开始,key=5
9号线程读出数据开始,key=4
7号线程读出数据结束,key=2
9号线程读出数据结束,key=4
8号线程读出数据结束,key=3
10号线程读出数据结束,key=5
6号线程读出数据结束,key=1

解释:

一个线程加上写锁,那其他的线程无论是获取写锁还是获取读锁都只能等待该线程释放写锁,而一个线程加上读锁,那其他的线程获取写锁需要等待,但是多个线程同时获取读锁;
简单来说就是:写的时候其他线程不能写不能读,而读的时候其他线程可以读但不能写

第十二集:BlockingQueue

代码:

public class Test {
    public static void main(String[] args) throws Exception {
        BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
        /*
        // 1、抛出异常(插入add(e)、移除remove()、检查element())
        // 往阻塞队列中放入元素
        System.out.println(blockingQueue.add("a")); // true
        System.out.println(blockingQueue.add("b")); // true
        System.out.println(blockingQueue.add("c")); // true
        System.out.println(blockingQueue.add("d")); // Exception in thread "main" java.lang.IllegalStateException: Queue full
        // 查看阻塞队列尾部中的元素
        System.out.println(blockingQueue.element()); // a
        // 从阻塞队列中取出元素
        System.out.println(blockingQueue.remove()); // a
        System.out.println(blockingQueue.remove()); // b
        System.out.println(blockingQueue.remove()); // c
        System.out.println(blockingQueue.remove()); // Exception in thread "main" java.util.NoSuchElementException
        */

        /*
        // 2、返回布尔值(插入offer(e)、移除poll()、检查peek())
        // 往阻塞队列中放入元素
        System.out.println(blockingQueue.offer("a")); // true
        System.out.println(blockingQueue.offer("b")); // true
        System.out.println(blockingQueue.offer("c")); // true
        System.out.println(blockingQueue.offer("d")); // false
        // 查看阻塞队列尾部中的元素
        System.out.println(blockingQueue.peek()); // a
        // 从阻塞队列中取出元素
        System.out.println(blockingQueue.poll()); // a
        System.out.println(blockingQueue.poll()); // b
        System.out.println(blockingQueue.poll()); // c
        System.out.println(blockingQueue.poll()); // null
        */

        /*
        // 3、阻塞(插入put(e)、移除take())
        // 往阻塞队列中放入元素
        blockingQueue.put("a"); // 不阻塞
        blockingQueue.put("b"); // 不阻塞
        blockingQueue.put("c"); // 不阻塞
        blockingQueue.put("d"); // 阻塞
        // 从阻塞队列中取出元素
        System.out.println(blockingQueue.take()); // a
        System.out.println(blockingQueue.take()); // b
        System.out.println(blockingQueue.take()); // c
        System.out.println(blockingQueue.take()); // 阻塞
        */

        /*
        // 4、超时(插入offer(e,time,unit)、移除poll(time,unit))
        // 往阻塞队列中放入元素
        System.out.println(blockingQueue.offer("a",1,TimeUnit.SECONDS)); // true
        System.out.println(blockingQueue.offer("b",1,TimeUnit.SECONDS)); // true
        System.out.println(blockingQueue.offer("c",1,TimeUnit.SECONDS)); // true
        System.out.println(blockingQueue.offer("d",1,TimeUnit.SECONDS)); // 等待1秒钟之后返回false
        // 从阻塞队列中取出元素
        System.out.println(blockingQueue.poll(1,TimeUnit.SECONDS)); // a
        System.out.println(blockingQueue.poll(1,TimeUnit.SECONDS)); // b
        System.out.println(blockingQueue.poll(1,TimeUnit.SECONDS)); // c
        System.out.println(blockingQueue.poll(1,TimeUnit.SECONDS)); // 等待1秒钟之后返回null
        */
    }
}

解释:

以上方法如下:
在这里插入图片描述

阻塞队列有7个实现类,比较重要的实现类有:ArrayBlockingQueue:由数组结构组成的有界阻塞队列LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列PriorityBlockingQueue:支持优先级排序的无界阻塞队列,其他几个实现类如下:

在这里插入图片描述

第十三集:线程池的三大方法

1、newFixedThreadPool(n)代码:

public class Test {
    public static void main(String[] args) throws Exception {
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        try {
            for (int i = 1; i <= 5; i++) {
                int finalI = i;
                threadPool.submit(() -> {
                    System.out.println(Thread.currentThread().getName() + "办理" + finalI + "号业务");
                });
            }
        } finally {
            threadPool.shutdown();
        }
    }
}

newFixedThreadPool(n)结果:

pool-1-thread-1办理1号业务
pool-1-thread-2办理2号业务
pool-1-thread-1办理4号业务
pool-1-thread-2办理5号业务
pool-1-thread-3办理3号业务

newFixedThreadPool(n)解释:

相当于三个员工办理5个业务,无论刮风下雨,3个员工始终在岗,底层代码:

在这里插入图片描述
另外Executor可以用来创建线程池,而ExecutorService是该接口的子接口,相比来说,子接口功能更加强大,因此我们使用子接口ExecutorService,另外submit()方法是可以有返回值的,而execute()方法没有返回值,不过他们都可以提交任务

2、newSingleThreadExecutor()代码:

使用

ExecutorService threadPool = Executors.newSingleThreadExecutor();

代替:

ExecutorService threadPool = Executors.newFixedThreadPool(3);

其他的代码和newFixedThreadPool(n)中的代码一致

newSingleThreadExecutor()结果:

pool-1-thread-1办理1号业务
pool-1-thread-1办理2号业务
pool-1-thread-1办理3号业务
pool-1-thread-1办理4号业务
pool-1-thread-1办理5号业务

newSingleThreadExecutor()解释:

相当于银行始终只有一个员工,底层代码:

在这里插入图片描述

3、newCachedThreadPool()代码:

使用

ExecutorService threadPool = Executors.newCachedThreadPool();

代替:

ExecutorService threadPool = Executors.newFixedThreadPool(3);

其他的代码和newFixedThreadPool(n)中的代码一致

newCachedThreadPool()结果:

pool-1-thread-2办理2号业务
pool-1-thread-3办理3号业务
pool-1-thread-4办理4号业务
pool-1-thread-1办理1号业务
pool-1-thread-5办理5号业务

newCachedThreadPool()解释:

银行的有Integer.MAX_VALUE个员工(Integer.MAX_VALUE即2147483647),但是没有默认在岗员工,只要有顾客需要,那这些员工都可以到岗,如果不需要这么多,他们自然不会到岗,底层代码:

在这里插入图片描述

第十四集:ThreadPoolExecutor的底层原理

1、底层原理

对于以上线程池的三大方法,在工作中我们一个也不用,他们都是具有缺陷的,在上面我也给大家展示了它的底层,他们都是new ThreadPoolExecutor(),其中ThreadPoolExecutor实现了AbstractExecutorService,而AbstractExecutorService是ExecutorService接口的子接口,因此我们可以来新建ThreadPoolExecutor对象,我们暂且不说我们此时看到的ThreadPoolExecutor类的构造方法中的5个参数,因为该类还有一个构造方法里面有7个参数,当使用线程池的时候,我们使用的是这7个参数的,我们首先看一下它长什么样子,然后在说明里面参数的含义:

在这里插入图片描述

现在来解释这7个参数的含义:
在这里插入图片描述
现在来更细致的解释这几个参数:

corePoolSize(核心线程数):假设该值是3的话,说明线程池中至少有3个线程始终保持活跃,相当于银行窗口的默认值守员工,无论银行有没有顾客,窗口最少有三个员工在值守

maximumPoolSize(最大线程数):假设该值是10的话,说明线程池中最多有10个线程同时保持活跃,相当于银行最多有10个窗口

keepAliveTime、unit(存活时间):假设keepAliveTime是2,unit是TimeUnit.HOURS,说明在2个小时之内还用不到大于corePoolSize数量的这些线程的话,那就会销毁这些多余的线程,相当于银行在2个小时之内都只用不到默认的3个窗口办公就行,那只留下3个员工值守就行,其他的员工可以回家休息了

workQueue(阻塞队列):如果核心线程池中的线程都被用了,那多余的任务f就需要先到阻塞队列中等待,相当于银行的窗口都被顾客用着,那在来的顾客只能去候客厅等着了

threadFactory(创建线程工厂):创建线程的工厂,毕竟核心线程数不够用了,那还需要创建线程,这就使用到了创建线程的工厂,相当于大堂经理一看人太多了需要增加窗口员工

handler(拒绝策略):如果任务太多时候的处理办法,一共有4种处理办法,当核心线程数中的线程都被用了,那其他任务先到阻塞队列中,然后创建线程的工厂创建新的线程,一直到达了最大线程数,但是人还是很多,最后阻塞队列也满了,这个时候就需要使用到拒绝策略来处理多余的那些任务

现在来使用通俗易懂的例子来解释这7个参数是怎么回事,假设银行每天默认有3个窗口在开放,无论有没有顾客都有这三个窗口都必须开放,这3个窗口就相当于corePoolSize;假设顾客超过了3个,那就需要先去候客厅排号等待,候客厅相当于workQueue;当候客厅已经满了,大堂经理一看这可不行,得赶快加窗口,然后他打电话让几个员工来加班,打开窗口就相当于threadFactory;然后顾客一点点的少了下来,只用3个窗口就够用了,又等了两个小时,还不需要那么多窗口,然后大堂经理就告诉加班的员工可以回去了,这2个小时就相当于keepAliveTime、unit;然后等了一会来了很多顾客,大堂经理一看这得把全部窗口打开,让员工都加班,这10个窗口相当于maximumPoolSize;这个时候候客厅人数得到了缓解,但随着时间的推移,人越来越多,窗口全部打开并且占满着,候客厅也完全满了,外边还有顾客等着进来,大堂经理一看这可不行,不能让顾客等那么久,然后他就去门口告诉那些还要进来的顾客们人已经满了,对他们进行拒绝,这就是handler;

接下来我将说一下maximumPoolSize怎么设置,老师说了需要考虑当前的程序是CPU密集型还是IO密集型,如果是CPU密集型,就需要改成当前电脑或者服务器核数+1或者+2,总结如下:
在这里插入图片描述

如果是IO密集型处理起来就比较麻烦,因为IO密集型任务线程并不是一直执行任务,所以应该配置尽可能多的线程,比如CPU核数*2,以上IO密集型配置方法是一个笼统的说法,我们还可以采用某大厂用过的方式,总结如下:
在这里插入图片描述

然后我说一下如何使用代码来获取当前电脑或者服务器的CPU核数:

int coreNum = Runtime.getRuntime().availableProcessors();

等会我编写代码演示这7个参数使用的时候会具体用到这个方法,不用着急

然后来说一下handler(拒绝策略),直接看图:

在这里插入图片描述
建议使用CallerRunsPolicy拒绝策略,防止数据丢失

接下来举出一个例子说明具体如何使用:

public class Test {
    public static void main(String[] args) throws Exception {
        ExecutorService threadPool = new ThreadPoolExecutor(
        		3,
                Runtime.getRuntime().availableProcessors() + 1,
                2,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(100),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy());
        try {
            for (int i = 1; i <= 100; i++) {
                int finalI = i;
                threadPool.submit(() -> {
                    System.out.println(Thread.currentThread().getName() + "办理" + finalI + "号业务");
                });
            }
        } finally {
            threadPool.shutdown();
        }
    }
}

其中 new LinkedBlockingDeque<>(100)是Executors中默认使用的,不过我设置了阻塞队列的大小而已,在第十三集:线程池的三大方法中写的我也截图了,现在我再来截个图:

在这里插入图片描述
Executors.defaultThreadFactory()、new ThreadPoolExecutor.AbortPolicy()来自于ThreadPoolExecutor.java中的构造方法中:

在这里插入图片描述

如果你想知道该线程池可以同时处理多少任务,你可以使用(maximumPoolSize+workQueue的大小)之和计算出来,如果同时处理的并发数大于该值,当使用new ThreadPoolExecutor.AbortPolicy()拒绝策略的时候就会出现java.util.concurrent.RejectedExecutionException,所以需要合理设计阻塞队列的大小,最大线程数量是可以计算出来的,那就相等于固定数值,主要根据并发数算出来阻塞队列的大小

2、公司使用案例

线程池工具类代码:

// 使用单例模式创建的线程池工具类
public class ThreadPoolHolder {
    /**
     * 静态内部类
     */
    private static class SingletonHolder {
        // CallerRunsPolicy:任务提交缓冲,不会丢失数据
        private static volatile ThreadPoolExecutor INSTANCE = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), 5 * Runtime.getRuntime().availableProcessors(), 0,
                TimeUnit.SECONDS, new SynchronousQueue<Runnable>(true),
                Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
    }

    /**
     * 私有化构造器
     */
    private ThreadPoolHolder() {}

    /**
     * 单例方法
     */
    public static ThreadPoolExecutor getThreadPoolExecutor() {
        return ThreadPoolHolder.SingletonHolder.INSTANCE;
    }
}

线程池工具类使用案例代码:

// 使用场景说明:
// 公司需要为2000万百科词条数据索引到Elasticsearch上,因此需要使用线程池来加速效率
// 下面代码首先获取全部词条数量,然后分页获取词条列表,之后通过线程池创建线程来处理词条列表
// 最终写了睡眠程序,避免主线程停止的时候导致工作线程(通过线程池创建)的工作线程也停止,保证所有数据都能处理完毕
@Override
public Map createIndex() {
    Map result = new HashMap();
    // 获取不能被创建索引的词条id集合
    final Set<String> delRecordSet = new HashSet<String>();
    List<KItemDelRecord> delRecordList = kItemDelRecordDao.all();
    if (delRecordList != null && delRecordList.size() > 0) {
        for (KItemDelRecord entity : delRecordList) {
            delRecordSet.add(entity.getkItemId());
        }
    }
    // 词条总量
    long count = kItemDao.countAllKItem();
    log.info("本次需要处理的原始词条数量(处理不代表建立索引):" + count);
    // 为原始词条数据建立索引
    // 计数功能
    final AtomicInteger serialNumber = new AtomicInteger();
    // 开始时间
    final long startTime = System.currentTimeMillis();
    // 页面容量
    final int pageSize = 100;
    // 全部页码
    int pageNum = (int) (count / pageSize);
    pageNum = count % pageSize > 0 ? pageNum + 1 : pageNum;
    // 获取线程池
    ThreadPoolExecutor poolHolder = ThreadPoolHolder.getThreadPoolExecutor();
    // 使用线程池分页处理数据
    for (int currentPage = 1; currentPage <= pageNum; currentPage++) {
        final int finalCurrentPage = currentPage;
        // 线程池执行
        poolHolder.execute(new Runnable() {
            @Override
            public void run() {
	            // 处理该分页的数据列表
                pageCreateIndexForOriginalKItem(finalCurrentPage, pageSize, delRecordSet, serialNumber, startTime);
            }
        });
    }
    long endTime = System.currentTimeMillis();
    // 日志打印
    log.info("词条数据已经处理完成,由于是多线程程序,可能还有线程正在运行,但是请不要关闭tomcat,其中建立索引的词条数量:" + serialNumber.get() + ";共耗时:" + (endTime - startTime) / 1000 / 60 + "分钟!!!!!!,当前时间:" + DateUtil.now(DateUtil.DEFAULT_DATE_FORMAT));
    log.info("词条数据已经处理完成,由于是多线程程序,可能还有线程正在运行,但是请不要关闭tomcat,其中建立索引的词条数量:" + serialNumber.get() + ";共耗时:" + (endTime - startTime) / 1000 / 60 + "分钟!!!!!!,当前时间:" + DateUtil.now(DateUtil.DEFAULT_DATE_FORMAT));
    log.info("词条数据已经处理完成,由于是多线程程序,可能还有线程正在运行,但是请不要关闭tomcat,其中建立索引的词条数量:" + serialNumber.get() + ";共耗时:" + (endTime - startTime) / 1000 / 60 + "分钟!!!!!!,当前时间:" + DateUtil.now(DateUtil.DEFAULT_DATE_FORMAT));

    // 保证程序绝对完成,执行到此处时,上述线程可能还没有完成,毕竟多线程需要执行时间,主线程完成,工作线程就会停止,所以不能让主线程停止,因此需要定时功能
    int minute = 0;
    for (int i = 1; i < 100; i++) {
        try {
            // 睡眠10分钟,保证数据处理完成之前程序不结束
            Thread.sleep(1000 * 60 * 10L);
            minute += 10;
            log.info("所有数据已经处理完成" + minute + "分钟了,由于是多线程程序,可能还有线程正在运行,但是请不要关闭tomcat,其中建立索引的词条数量:" + serialNumber.get() + ";共耗时:" + (endTime - startTime) / 1000 / 60 + "分钟!!!!!!,当前时间:" + DateUtil.now(DateUtil.DEFAULT_DATE_FORMAT));
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
    result.put("retCode", 0);
    result.put("message", "所有索引建立结束,处理词条总数:" + serialNumber.get() + ";共耗时:" + (endTime - startTime) / 1000 / 60 + "分钟");
    return result;
}

第十五集:四大函数式接口

总览:
在这里插入图片描述

代码:

public class Test {
    public static void main(String[] args) throws Exception {
        // Consumer:有输入没有返回值,泛型就是输入值的类型
        Consumer<String> consumer = t -> System.out.println(t);
        consumer.accept("hello world!");
        // Supplier:没有输入没有返回值,泛型就是返回值的类型
        Supplier<String> supplier = ()->{return "hello world!";};
        System.out.println(supplier.get());
        // Function:有输入有返回值,第一个泛型就是输入值的类型,第二个参数是返回值的类型
        Function<String,Integer> function = t->{return t.length();};
        System.out.println(function.apply("hello world!"));
        // Predicate:有输入有返回值,泛型就是输入值的类型,返回值是布尔类型
        Predicate<String> predicate = t->{return t.length()>0;};
        System.out.println(predicate.test("hello world!"));
    }
}

结果:

hello world!
hello world!
12
true

总结:

这四个函数式接口在流式计算中很常用

第十六集:流式编程

代码:

public class Test {
    public static void main(String[] args) throws Exception {
        User u1 = new User(11, "a", 23);
        User u2 = new User(12, "b", 24);
        User u3 = new User(13, "c", 22);
        User u4 = new User(14, "d", 28);
        User u5 = new User(16, "e", 26);
        List<User> list = Arrays.asList(u1, u2, u3, u4, u5);
        list.stream().filter(u -> u.getId() % 2 == 0 && u.getAge() > 24)
                .map(u -> u.getUserName().toUpperCase())
                .sorted((name1, name2) -> name2.compareTo(name1))
                .limit(1)
                .forEach(System.out::println);
    }

    @Data
    @AllArgsConstructor
    public static class User {
        private Integer id;
        private String userName;
        private Integer age;
    }
}

结果:

E

解释:

  • 流式计算为数组和集合服务,不为Map服务,对于集合来说,所有集合的父类Collection中有一个stream()方法,所以所有的集合对象都可以用stream方法,而数组可以通过Arrays.stream(数组名称)来把一个数组变成流,然后就可以进行流式计算;
  • stream方法的返回值是一个流,流后面的泛型可以是集合中的数据类型,例如List<User>中的User,根据default Stream<E> stream(){XXX}可知;也可以是数组中的类型,例如Integer[]中的Integer,根据public static <T> Stream<T> stream(T[] array){XXX}可知;
  • filter中用的是断定型接口Predicate,可以用来过滤,传入一个参数,但是返回一个布尔值,最终filter方法返回的是由Predicate断定型接口的输入值作为泛型的流,根据Stream<T> filter(Predicate<? super T> predicate)可知;
  • map中用的是函数型接口Function,可以用来映射,注意map()可不是集合,传入一个参数,可以返回一个任意类型的参数,最终map方法的返回值是一个由Function函数型接口的返回值作为泛型的流,根据<R> Stream<R> map(Function<? super T, ? extends R> mapper)可知;
  • sorted中用的是比较型接口Comparator,可以用来比较,需要传输两个相同类型的参数,通过比较得出返回值,例如o1.compareTo(o2)就是按照正序排列,而o2.compareTo(o1)就是按照倒序排列,返回值就是比较型接口输入值的类型作为泛型的流,根据Stream<T> sorted(Comparator<? super T> comparator)可知
  • limit中用的是一个long型的数字,返回的还是调用limit()方法的流类型
  • forEach中用的是消费型接口Consumer,可以用来输出,没有返回值,只需要传输一个参数就可以了,根据void forEach(Consumer<? super T> action)可知
  • collect(Collectors.toList())叫做收集,一般用在最后将流变成一个List集合
  • toArray()也叫做收集,一般用在最后将流变成一个数组,但是只能是Object类型的
  • 除此之外还有count()distinct()empty()generate(Supplier<T> s)mapToInt(ToIntFunction<? super T> mapper),但是有可能使用的不是我们平常使用的方法,例如:
    在这里插入图片描述

第十七集:ForkJoin例子

代码:

class MyTask extends RecursiveTask<Integer> {
    private static final Integer ADJUST_VALUE = 10;
    private int begin;
    private int end;
    private int result;

    public MyTask(int begin, int end) {
        this.begin = begin;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        if ((end - begin) <= ADJUST_VALUE) {
            for (int i = begin; i <= end; i++) {
                result = result + i;
            }
        } else {
            int middle = (begin + end) / 2;
            MyTask task01 = new MyTask(begin, middle);
            MyTask task02 = new MyTask(middle + 1, end);
            task01.fork();
            task02.fork();
            result = task01.join() + task02.join();
        }
        return result;
    }
}

/**
 *  * 分支合并例子
 *  * ForkJoinPool
 *  * ForkJoinTask
 *  * RecursiveTask
 *  
 */
public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyTask myTask = new MyTask(0, 100);
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinTask<Integer> forkJoinTask = forkJoinPool.submit(myTask);
        System.out.println(forkJoinTask.get());
        forkJoinPool.shutdown();
    }
}

结果:

5050

解释:

Fork:把一个复杂任务进行分拆,大事化小
Join:把分拆任务的结果进行合并
在这里插入图片描述

第十八集:CompletableFuture例子

代码:

public class Test {
    // 创建线程池,简单起见就随便创建一个,也不用那7个参数了
    public static final ExecutorService executor = Executors.newFixedThreadPool(3);

    public static void main(String[] args) throws Exception {
        // 注意:使用我们自己创建的线程池去做事情,所以1和3都不用,由于我们没有关闭线程池,所以程序不会停,不要怀疑

        // 1、没有返回值的异步回调(Void仅有的一个无参构造方法已经被私有化了,相当于没有返回值),不使用自己的线程池
        System.out.println("***1、没有返回值的异步回调(Void仅有的一个无参构造方法已经被私有化了),不使用自己的线程池***");
        CompletableFuture<Void> completableFuture1 = CompletableFuture.runAsync(() -> {
            System.out.println(Thread.currentThread().getName() + "》completableFuture1中的代码执行");
        });

        // 2、没有返回值的异步回调,使用自己的线程池
        System.out.println("***2、没有返回值的异步回调,使用自己的线程池***");
        CompletableFuture<Void> completableFuture2 = CompletableFuture.runAsync(() -> {
            System.out.println(Thread.currentThread().getName() + "》completableFuture2中的代码执行");
        }, executor);

        // 3、用于测试whenComplete()、exceptionally(),然后有返回值的异步回调,不使用自己的线程池
        System.out.println("***3、有返回值的异步回调,不使用自己的线程池***");
        CompletableFuture<Integer> completableFuture3 = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + "》completableFuture3中的代码执行");
            int i = 10 / 0;
            return 1024;
        });
        System.out.println(completableFuture3.whenComplete((result, exception) -> {
            System.out.println("whenComplete中的返回值 = " + result);
            System.out.println("whenComplete中的异常 = " + exception);
        }).exceptionally(exception -> {
            System.out.println("exceptionally中的异常 = " + exception.getMessage());
            // 假设出现异常就返回0
            return 0;
        }).get());

        // 4、用于测试whenComplete()、exceptionally(),然后有返回值的异步回调,使用自己的线程池
        System.out.println("***4、有返回值的异步回调,使用自己的线程池***");
        CompletableFuture<Integer> completableFuture4 = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + "》completableFuture4中的代码执行");
            int i = 10 / 0;
            return 1024;
        }, executor);

        // 如果completableFuture4中不出错,只会执行whenComplete()中的内容,而不会执行exceptionally()中的内容;
        // 如果completableFuture4中出错,不仅会执行,whenComplete()中的内容,而且会执行exceptionally()中的内容;
        // 无论是否出错,都会等着completableFuture4中的代码执行完成,才会执行下面的whenComplete()或者exceptionally()方法,
        // 如果completableFuture4中的代码没有执行完成,那会阻塞在这里,等待着代码执行完成
        System.out.println(completableFuture4.whenComplete((result, exception) -> {
            System.out.println("whenComplete中的返回值 = " + result);
            System.out.println("whenComplete中的异常 = " + exception);
        }).exceptionally(exception -> {
            System.out.println("exceptionally中的异常 = " + exception.getMessage());
            // 假设出现异常就返回0
            return 0;
        }).get());

        // 5、用于测试handle(),相比4的做法,本次直接在handle()中进行返回值处理以及异常处理了,
        //   不需要用到whenComplete和exceptionally两个方法了,直接通过get()方法获取的就是返回值
        System.out.println("***5、有返回值的异步回调,使用自己的线程池,相比4的做法,本次直接在handle()中进行返回值处理以及异常处理***");
        CompletableFuture<Integer> completableFuture5 = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + "》completableFuture5中的代码执行");
            int i = 10 / 0;
            return 1024;
        }, executor).handle((result, exception) -> {
            if (exception == null) {
                return result;
            }
            return 0;
        });
        System.out.println(completableFuture5.get());

        // 6、用于测试thenRunAsync(),不能接收上一步的返回值,不能返回结果,只有成功才能执行thenRunAsync()
        System.out.println("***6、用于测试thenRunAsync(),不能接收上一步的返回值,不能返回结果***");
        CompletableFuture<Void> completableFuture6 = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + "》completableFuture6中的代码执行");
            return 1024;
        }, executor).thenRunAsync(() -> {
            System.out.println("thenRunAsync成功执行了");
        }, executor);
        System.out.println(completableFuture6.get());

        // 7、用于测试thenAcceptAsync(),可以接收上一步的返回值,不能返回结果,只有成功才能执行thenAcceptAsync()
        System.out.println("***7、用于测试thenAcceptAsync(),可以接收上一步的返回值,不能返回结果,只有成功才能执行thenAcceptAsync()***");
        CompletableFuture<Void> completableFuture7 = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + "》completableFuture7中的代码执行");
            return 1024;
        }, executor).thenAcceptAsync(result -> {
            System.out.println("thenAcceptAsync接收到返回值=" + result);
        }, executor);
        System.out.println(completableFuture7.get());

        // 8、用于测试thenAcceptAsync(),可以接收上一步的返回值,不能返回结果,只有成功才能执行thenAcceptAsync()
        System.out.println("***8、用于测试thenAcceptAsync(),可以接收上一步的返回值,不能返回结果,只有成功才能执行thenAcceptAsync()***");
        CompletableFuture<Integer> completableFuture8 = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + "》completableFuture8中的代码执行");
            return 1024;
        }, executor).thenApplyAsync(result -> {
            System.out.println("thenAcceptAsync接收到返回值=" + result);
            return result + 1;
        }, executor);
        System.out.println(completableFuture8.get());

        CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + "》future1执行");
            return 1024;
        });
        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + "》future2执行");
            return "hello";
        });

        CompletableFuture<Integer> future3 = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + "》future3执行");
            return 2021;
        });
        // 9、测试两个组合任务,只有future1和future2完成之后才会执行该任务,thenCombineAsync方法可以接收future1和future2的返回值,另外该方法还可以返回一个值
        System.out.println("***9、测试两个组合任务,只有future1和future2完成之后才会执行该任务,thenCombineAsync方法可以接收future1和future2的返回值,另外该方法还可以返回一个值***");
        CompletableFuture<String> completableFuture9 = future1.thenCombineAsync(future2, (future1Result, future2Result) -> {
            System.out.println(Thread.currentThread().getName() + "》completableFuture9执行");
            return future1Result + "和" + future2Result;
        }, executor);
        System.out.println(completableFuture9.get());

        // 10、测试两个组合任务,只有future1和future2完成之后才会执行该任务,thenAcceptBothAsync方法可以接收future1和future2的返回值,但是不会返回值
        System.out.println("***10、测试两个组合任务,只有future1和future2完成之后才会执行该任务,thenAcceptBothAsync方法可以接收future1和future2的返回值,但是不会返回值***");
        CompletableFuture<Void> completableFuture10 = future1.thenAcceptBothAsync(future2, (future1Result, future2Result) -> {
            System.out.println(Thread.currentThread().getName() + "》completableFuture10执行");
        }, executor);

        // 11、测试两个组合任务,只有future1和future2完成之后才会执行该任务,runAfterBothAsync方法不接收future1和future2的返回值,并且不会返回值
        System.out.println("***11、测试两个组合任务,只有future1和future2完成之后才会执行该任务,runAfterBothAsync方法不接收future1和future2的返回值,并且不会返回值***");
        CompletableFuture<Void> completableFuture11 = future1.runAfterBothAsync(future2, () -> {
            System.out.println(Thread.currentThread().getName() + "》completableFuture11执行");
        }, executor);

        // 12、测试两个组合任务,只要future1和future3有一个完成之后就会执行该任务,applyToEitherAsync方法可以接收future1或者future3的返回值,并且会返回值,另外要求future1和future3返回值类型一致,当然你可以都用Object
        System.out.println("***12、测试两个组合任务,只要future1和future3有一个完成之后就会执行该任务,applyToEitherAsync方法可以接收future1或者future3的返回值,并且会返回值,另外要求future1和future3返回值类型一致***");
        CompletableFuture<Integer> completableFuture12 = future1.applyToEitherAsync(future3, futureResult -> {
            System.out.println(Thread.currentThread().getName() + "》completableFuture12执行");
            return futureResult;
        }, executor);
        System.out.println(completableFuture12.get());

        // 13、测试两个组合任务,只要future1和future3有一个完成之后就会执行该任务,acceptEitherAsync方法可以接收future1或者future3的返回值,但是不会返回值,另外要求future1和future3返回值类型一致,当然你可以都用Object
        System.out.println("***13、测试两个组合任务,只要future1和future3有一个完成之后就会执行该任务,acceptEitherAsync方法可以接收future1或者future3的返回值,但是不会返回值,另外要求future1和future3返回值类型一致***");
        CompletableFuture<Void> completableFuture13 = future1.acceptEitherAsync(future3, futureResult -> {
            System.out.println(Thread.currentThread().getName() + "》completableFuture13执行");
        }, executor);

        // 14、测试两个组合任务,只要future1和future2有一个完成之后就会执行该任务,runAfterEitherAsync方法不会接收future1或者future3的返回值,也不会返回值,不要求future1和future2返回值类型一致
        System.out.println("***14、测试两个组合任务,只要future1和future2有一个完成之后就会执行该任务,runAfterEitherAsync方法不会接收future1或者future3的返回值,也不会返回值,不要求future1和future2返回值类型一致***");
        CompletableFuture<Void> completableFuture14 = future1.runAfterEitherAsync(future2, () -> {
            System.out.println(Thread.currentThread().getName() + "》completableFuture14执行");
        }, executor);

        // 15、测试多个组合任务,只有全部任务完成才算完成
        System.out.println("***15、测试多个组合任务,只有全部任务完成才算完成***");
        CompletableFuture<Void> completableFuture15 = CompletableFuture.allOf(future1, future2, future3);
        completableFuture15.get();
        System.out.println("多组合任务完成,最终future1.get()=" + future1.get() + "、future2.get()=" + future2.get() + "、future3.get()=" + future3.get());

        // 16、测试多个组合任务,只要有一个完成就算完成
        System.out.println("***16、测试多个组合任务,只要有一个完成就算完成***");
        CompletableFuture<Object> completableFuture16 = CompletableFuture.anyOf(future1, future2, future3);
        completableFuture16.get();
        System.out.println("多组合任务完成,最终completableFuture16.get()=" + completableFuture16.get());
    }
}

结果:

***1、没有返回值的异步回调(Void仅有的一个无参构造方法已经被私有化了),不使用自己的线程池***
***2、没有返回值的异步回调,使用自己的线程池***
ForkJoinPool.commonPool-worker-1》completableFuture1中的代码执行
***3、有返回值的异步回调,不使用自己的线程池***
pool-1-thread-1》completableFuture2中的代码执行
ForkJoinPool.commonPool-worker-1》completableFuture3中的代码执行
whenComplete中的返回值 = null
whenComplete中的异常 = java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
exceptionally中的异常 = java.lang.ArithmeticException: / by zero
0
***4、有返回值的异步回调,使用自己的线程池***
pool-1-thread-2》completableFuture4中的代码执行
whenComplete中的返回值 = null
whenComplete中的异常 = java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
exceptionally中的异常 = java.lang.ArithmeticException: / by zero
0
***5、有返回值的异步回调,使用自己的线程池,相比4的做法,本次直接在handle()中进行返回值处理以及异常处理***
pool-1-thread-3》completableFuture5中的代码执行
0
***6、用于测试thenRunAsync(),不能接收上一步的返回值,不能返回结果***
pool-1-thread-1》completableFuture6中的代码执行
thenRunAsync成功执行了
null
***7、用于测试thenAcceptAsync(),可以接收上一步的返回值,不能返回结果,只有成功才能执行thenAcceptAsync()***
pool-1-thread-3》completableFuture7中的代码执行
thenAcceptAsync接收到返回值=1024
null
***8、用于测试thenAcceptAsync(),可以接收上一步的返回值,不能返回结果,只有成功才能执行thenAcceptAsync()***
pool-1-thread-2》completableFuture8中的代码执行
thenAcceptAsync接收到返回值=1024
1025
ForkJoinPool.commonPool-worker-1》future1执行
ForkJoinPool.commonPool-worker-1》future2执行
***9、测试两个组合任务,只有future1和future2完成之后才会执行该任务,thenCombineAsync方法可以接收future1和future2的返回值,另外该方法还可以返回一个值***
ForkJoinPool.commonPool-worker-1》future3执行
pool-1-thread-1》completableFuture9执行
1024和hello
***10、测试两个组合任务,只有future1和future2完成之后才会执行该任务,thenAcceptBothAsync方法可以接收future1和future2的返回值,但是不会返回值***
***11、测试两个组合任务,只有future1和future2完成之后才会执行该任务,thenCombineAsync方法可以接收future1和future2的返回值,但是不会返回值***
pool-1-thread-2》completableFuture10执行
***12、测试两个组合任务,只要future1和future3有一个完成之后就会执行该任务,thenCombineAsync方法可以接收future1或者future3的返回值,但是会返回值,另外要求future1和future3返回值类型一致***
pool-1-thread-3》completableFuture11执行
pool-1-thread-1》completableFuture12执行
1024
***13、测试两个组合任务,只要future1和future3有一个完成之后就会执行该任务,acceptEitherAsync方法可以接收future1或者future3的返回值,但是不会返回值,另外要求future1和future3返回值类型一致***
***14、测试两个组合任务,只要future1和future2有一个完成之后就会执行该任务,runAfterEitherAsync方法不会接收future1或者future3的返回值,也不会返回值,不要求future1和future2返回值类型一致***
pool-1-thread-2》completableFuture13执行
***15、测试多个组合任务,只有全部任务完成才算完成***
pool-1-thread-3》completableFuture14执行
多组合任务完成,最终future1.get()=1024、future2.get()=hello、future3.get()=2021
***16、测试多个组合任务,只要有一个完成就算完成***
多组合任务完成,最终completableFuture16.get()=1024

解释:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

第十九集:Volatile关键字

代码:

public class Test {
    public static void main(String[] args) throws Exception {
        Demo demo = new Demo();
        new Thread(demo).start();
        while (true) {
            if (demo.isFlag()) {
                System.out.println("______________");
                break;
            }
        }
    }
}

class Demo implements Runnable {
    private boolean flag = false;

    @Override
    public void run() {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;
        System.out.println("flag=" + isFlag());
    }

    public boolean isFlag() {
        return this.flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

结果:

出现flag=true之后线程就阻塞了

解释:

可以看到有主线程和分支线程一共两个线程,其实flag存在于主存中,然后分支线程运行的时候会把flag读入到当前线程的缓存中,之后主线程也会把flag读入当前线程的的缓存中,当然他们读到的都是false,然后分支线程把flag改成了true,由于while是一个很快的操作,所以此时主线程不知道flag已经发生改变了,那么通过demo.isFlag()获取的一直就是false,因此线程不会结束,例如:

在这里插入图片描述
如果想解决这个问题,我们需要把

private boolean flag = false;

变成

private volatile boolean flag = false;

这样对flag的所有操作都只在主存中进行了,其他的操作该在缓存中还在缓存中进行,因此上面的那幅图就可以这样画:
在这里插入图片描述
总结:

  1. 可以保证内存中的数据可见性,也就是在多个线程操作共享数据的时候,相当于这些操作都在主存中进行,和各个线程的缓存没有关系
  2. volatile不具备互斥性质,也就是说被对该数据的操作虽然在主存中进行,但是依然是多个线程同时可以对该数据进行读写,当然这也可以说成是原子性
  3. volatile可以禁止指令重排,指令重排就是对象创建的的时候有几个步骤,这几个步骤要按照顺序来,不能反了,由于要提高效率,所以默认是反的,而volatile的作用就是不让反

第二十集:原子类

1、概念讲解

代码:

public class Test {
    public static void main(String[] args) throws Exception {
        Demo demo = new Demo();
        for (int i = 0; i < 10; i++) {
            new Thread(demo).start();
        }
    }
}

class Demo implements Runnable {
    private int serialNumber = 0;

    @Override
    public void run() {
        // 避免出不来原子性
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 打印出数字
        System.out.println(getSerialNumber());
    }
    // 执行加1操作
    public int getSerialNumber() {
        return this.serialNumber++;
    }
}

结果:

多个相同数字出现,说明修改的时候发生了冲突

改编代码:

public class Test {
    public static void main(String[] args) throws Exception {
        Demo demo = new Demo();
        for (int i = 0; i < 10; i++) {
            new Thread(demo).start();
        }
    }
}

class Demo implements Runnable {
    // AtomicInteger是原子类,里面有volatile关键字,可以保证内存中的数据可见性
    private AtomicInteger serialNumber = new AtomicInteger();

    @Override
    public void run() {
        // 避免出不来原子性
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 打印出数字
        System.out.println(getSerialNumber());
    }

    // 执行加1操作
    private int getSerialNumber() {
        return this.serialNumber.getAndIncrement();
    }
}

结果:

所有数字都是不相同的

解释:

本次使用的AtomicInteger是java.util.concurrent.atomic中的类,这里面的类可以保证数据可见性和原子性操作,具体解释如下:

  1. 使用volatile关键字保证了数据的可见性,相当于对于数据的操作都在主存中进行,不涉及到单个线程中缓存的事情
  2. JVM在里面使用了CAS算法(Compare-And-Swap),其中CAS算法可以保证数据的原子性,它是硬件对于并发操作共享数据的支持,CAS中包含了三个操作数,分别是内存值V(比较之前从主存中取出来的value值)、预估值A(比较之时从主存中取出来的value值)、更新值B(即我们想设置的值,如果通过比较发现内存值V和预估值A相等,就会将主存中的value值改成更新值B),解释这三个值之后我说一下如何进行数据更新,当且仅当内存值V==预估值A时,才将内存中的value值即内存值V设置成更新值B,否则更新失败,然后就执行循环操作,也就是继续进行更新操作,直到更新成功,这种做法比使用synchronized锁的效率更高,因为如果更新失败的话,该线程会立即继续进行尝试更新操作,这样不会浪费CPU的时间片(执行权),所以效率更高,至于CAS图解如下:
    在这里插入图片描述

2、公司使用案例

// 使用背景:
// 通过三方合作公司提供的json文件,生成sql语句文件
// 由于是多线程代码,所以计数功能使用到了AtomicInteger
@Override
public String generateKItemExtSql(String fileUrl, String saveUrl) {
	try {
		// 开始时间
		final long startTime = System.currentTimeMillis();
		log.info("generateKItemExtSql方法现在开始执行,当前时间:"+DateUtil.now(DateUtil.DATETIME_FORMAT));

		File file = new File(fileUrl);
		// json文件不能为空
		if (!file.isFile()) {
			log.info("json文件不能为空");
			return "json文件不能为空";
		}
		// 快速回收
		file = null;

		// sql文件和图片保存地址必须是目录
		File saveUrlDir = new File(saveUrl);
		if (!saveUrlDir.isDirectory()) {
			return "saveUrl有问题";
		}

		log.info("生成sql文件之前的配置工作,当前时间:" + DateUtil.now(DateUtil.DATETIME_FORMAT));
		// 生成sql文件之前的配置工作
		Constant.url = saveUrl;
		com.leadal.kms.wiki.utils.FileUtil.createIO2();

		log.info("配置完成,准备生成sql文件,当前时间:" + DateUtil.now(DateUtil.DATETIME_FORMAT));
		// 开始生成sql和图片
		final AtomicInteger count = new AtomicInteger();
		ThreadPoolExecutor poolHolder = ThreadPoolHolder.getThreadPoolExecutor();
		BufferedReader reader = null;
		try {
			reader = new BufferedReader(new FileReader(fileUrl));
			String text = null;
			while ((text = reader.readLine()) != null) {
				if (!text.contains(":")) {
					continue;
				}
				final String finalText = text;
				poolHolder.execute(new Runnable() {
					@Override
					public void run() {
						String[] split = finalText.split("\"");
						String fileId = split[1];
						String kgCategoryName = split[3];

						if (StringUtils.isBlank(kgCategoryName)) {
							log.info("柯基中的词条分类名称不存在,fileId:" + fileId);
							return;
						}

						// 生成sql文件
						JsoupXpathFactory factory = JsoupXpathFactory.getInstance(application);
						KItemExt kItemExt = new KItemExt();
						kItemExt.setId(UUIDUtil.create());
						kItemExt.setFileId(fileId);
						kItemExt.setKgCategoryName(WikiUtil.getKgCategoryName(kItemExt));
						try {
							factory.parseForKItem(null, kItemExt);
						} catch (Exception e) {
							log.info("sql文件生成失败,fileId:" + fileId);
							e.printStackTrace();
						}

						// 回收资源
						factory = null;
						kItemExt = null;
						split = null;
						fileId = null;
						kgCategoryName = null;

						// 词条计数
						count.getAndIncrement();
						if (count.get() % 2000 == 0) {
							long currentTime = System.currentTimeMillis();
							log.info("生成词条总数目前是:" + count.get() + "条数据;耗时:" + (currentTime - startTime) / 1000 / 60 + "分钟" + ";当前时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
						}
					}
				});
			}

			// 回收对象
			try {
				reader.close();
			} catch (IOException e) {
				e.printStackTrace();
			}

			long endTime = System.currentTimeMillis();
			log.info("生成词条总数一共是:"+ count.get() + "条数据;耗时:" + (endTime - startTime) / 1000 / 60 + "分钟"+";当前时间:"+DateUtil.now(DateUtil.DATETIME_FORMAT));
			log.info("生成词条总数一共是:"+ count.get() + "条数据;耗时:" + (endTime - startTime) / 1000 / 60 + "分钟"+";当前时间:"+DateUtil.now(DateUtil.DATETIME_FORMAT));
			log.info("生成词条总数一共是:"+ count.get() + "条数据;耗时:" + (endTime - startTime) / 1000 / 60 + "分钟"+";当前时间:"+DateUtil.now(DateUtil.DATETIME_FORMAT));

			// 保证程序绝对完成
			int minute = 0;
			for(int i = 1; i < 100; i++) {
				try {
					// 睡眠10分钟,保证数据处理完成之前程序不结束
					Thread.sleep(1000 * 60 * 10L);
					minute += 10;
					log.info("程序已经结束"+minute+"分钟了,词条总数一共是:"+ count.get() + "条数据;耗时:" + (endTime - startTime) / 1000 / 60 + "分钟"+";当前时间:"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
				} catch (Throwable e) {
					e.printStackTrace();
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if (reader!=null){
				try {
					reader.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
		return "成功";
	} catch (Exception e) {
		e.printStackTrace();
		return "失败";
	}
}
  • 2
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值