JUC学习笔记二

193 阅读20分钟

8、Callable

这里前面在讲线程三种创建方式时说过,这里就放一下脑图给大家看一下FutureTaskRunnable的关系

public Thread(Runnable target, String name) {
    init(null, target, name, 0);
}

juc19.png

FutureTask<Integer> futureTask = new FutureTask<>(new MyThread2());

new Thread(futureTask,"A").start();

9、JUC强大的辅助类讲解

面试题:线程间通信CountDownLatch/CyclicBarrier/Semaphore使用过吗?

9.1、CountDownLatch

  • 让一些线程阻塞,直到另一些线程完成一系列操作后才被唤醒
  • CountDownLatch 维护了一个计数器,有两个核心方法:countDown() 和 await()
  • 调用 countDown() 方法会将计数器减一
  • 当计数器的值不为零时,线程调用 await() 方法时,会被阻塞
  • 当计数器的值变为0时,因调用 await() 方法被阻塞的线程会被唤醒,继续执行 假设实现以下情景:假设一个教室里有7个人,其中有一个是班长,班长的主要职责就是在其它6个同学走了后,关灯,锁教室门,然后走人,因此班长是需要最后一个走的,那么有什么方法能够控制班长这个线程是最后一个执行,而其它线程是随机执行的
package com.hong.juc;

import java.util.concurrent.CountDownLatch;

/**
 * CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞。
 * 其他线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞),
 * 当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行
 */
public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {

        CountDownLatch countDownLatch = new CountDownLatch(6);

        for (int i = 0; i < 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName()+"\t离开教室");
                countDownLatch.countDown();
            },String.valueOf(i)).start();
        }
        countDownLatch.await();
        System.out.println(Thread.currentThread().getName()+"\t班长锁门走人");
    }

    private static void closeDoor(){
        for (int i = 1; i <= 6; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"\t离开教室");
            },String.valueOf(i)).start();
        }
    }
}

执行结果如下

juc21.png

9.2、CountDownLatch + 枚举类的使用

import java.util.Objects;

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

	private Integer retcode;
	private String retMessage;

	CountryEnum(Integer retcode, String retMessage) {
		this.retcode = retcode;
		this.retMessage = retMessage;
	}

	public static CountryEnum forEach_countryEnum(int index) {
		
		CountryEnum[] myArray = CountryEnum.values();
		
		for(CountryEnum ce : myArray) {
			if(Objects.equals(index, ce.getRetcode())) {
				return ce;
			}
		}
		
		return null;
	}

	public Integer getRetcode() {
		return retcode;
	}

	public void setRetcode(Integer retcode) {
		this.retcode = retcode;
	}

	public String getRetMessage() {
		return retMessage;
	}

	public void setRetMessage(String retMessage) {
		this.retMessage = retMessage;
	}

}
import java.util.concurrent.CountDownLatch;

public class UnifySixCountriesDemo {

	public static void main(String[] args) throws InterruptedException {
        // 计数器
        CountDownLatch countDownLatch = new CountDownLatch(6);

        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "国被灭了!");
                countDownLatch.countDown();
            }, CountryEnum.forEach_countryEnum(i).getRetMessage()).start();
        }

        countDownLatch.await();

        System.out.println(Thread.currentThread().getName() + " 秦国统一中原。");
	}
}

9.3、CyclicBarrier

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

CyclicBarrierCountDownLatch的区别:CyclicBarrier可重复多次,而CountDownLatch只能是一次

程序演示集齐7个龙珠,召唤神龙

package com.hong.juc;

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

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

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

juc22.png

9.4、Semaphore

  • Semaphore 即信号量,信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
  • 构造器 Semaphore(int) 用于指定共享资源的数目,如果设定为 1 ,则 Semaphore 信号量退化为 Lock 锁或者 synchronized
  • 调用 semaphore.acquire() 方法获取对共享资源的使用,调用 semaphore.release() 释放对共享资源的占用 下面来看一个抢车位的案例,使用 Semaphore 完成对共享资源的并发控制
package com.hong.juc;

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

public class SemaphoreDemo {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(3);

        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire(); // 尝试抢车位(获取信号量)
                    System.out.println(Thread.currentThread().getName()+"\t抢占到车位");
                    // 暂停一会线程
                    TimeUnit.SECONDS.sleep(3);
                    System.out.println(Thread.currentThread().getName()+"\t离开车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    semaphore.release(); // 释放车位(释放信号量)
                }
            },String.valueOf(i)).start();
        }
    }
}

juc23.png

10、ReadWriteLock读写锁

多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行。 但是,如果有一个线程想去写共享资源来,就不应该再有其他线程可以对改资源进行读或写 代码演示

package com.hong.juc;

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

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

    public void put(String key, Object value) {
        readWriteLock.writeLock().lock();
        System.out.println(Thread.currentThread().getName() + "\t 写入数据" + key);
        try {
            // 暂停一会线程毫秒
            TimeUnit.MICROSECONDS.sleep(300);
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "\t 写入完成");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            readWriteLock.writeLock().unlock();
        }
    }

    public void get(String key) {
        readWriteLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t 读取数据");
            TimeUnit.MICROSECONDS.sleep(300);
            Object result = map.get(key);
            System.out.println(Thread.currentThread().getName() + "\t 读取完成"+ result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            readWriteLock.readLock().unlock();
        }
    }
}
/**
 * 多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行。
 * 但是,如果有一个线程想去写共享资源来,就不应该再有其他线程可以对改资源进行读或写
 * 小总结:
 *      读-读能共存
 *      读-写不能共存
 *      写-写不能共存
 *
 *      写操作: 原子 + 独占
 */
public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache myCache = new MyCache();

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

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

11、阻塞队列BlockingQueue

相关面试题:阻塞队列知道吗?

11.1、队列+阻塞队列

  1. 阻塞队列,顾名思义,首先它是一个队列,而一个阻塞队列在数据结构中所起的作用大致如下图所示
  2. 当阻塞队列是满时,往队列里添加元素的操作将会被阻塞。当队列是空时,从队列中取出元素的操作将被阻塞
  3. 试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。
  4. 试图往己满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程从列中移除一个或者多个元素,或者完全清空队列后使队列重新变得空闲起来并后续新增一些元素

11.2、为什么用阻塞队列?有什么好处?

  1. 在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒
  2. 为什么需要BlockingQueue?好处是我们不需要关心什么时候需要阻塞线程,不用我们自己去控制线程的唤醒(notify)和阻塞(wait)
  3. 什么时候需要唤醒线程?这一切BlockingQueue都给你一手包办了
  4. 在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度

11.3、阻塞队列分类

juc3.png

11.4、阻塞队列用法(核心方法)

juc4.png 抛出异常

  • 当阻塞队列满时,再往队列里add插入元素会抛IllegalStateException:Queue full
  • 当阻塞队列空时,再往队列里remove移除元素会抛NoSuchElementException 返回布尔
  • 插入方法,成功 ture 失败 false
  • 移除方法,成功返回出队列的元素,队列里面没有就返回null 一直阻塞
  • 当阻塞队列满时,生产者线程继续往队列里put元素,队列会一直阻塞生产线程直到put数据成功或者响应中断退出。
  • 当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程直到队列可用。 超时退出\
  • 当阻塞队列满时,队列会阻塞生产者线程一定时间,超过后限时后生产者线程会退出 ArrayBlockingQueue
/**
 * ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO原则对元素进行排序
 * LinkedBlockingQueue:是一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量高于ArrayBlockingQueue
 * SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移出操作,否则插入操作一直处于阻塞状态,吞吐量通常要高
 *
 * 阻塞队列
 *   1.阻塞队列有没有好的一面
 *   2.不得不阻塞,你如何管理
 */
public class BlockingQueueDemo {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<String>(3);
        //addAndRemove(blockingQueue);
        //offerAndPoll(blockingQueue);
        //putAndTake(blockingQueue);
        outOfTime(blockingQueue);
    }

    private static void addAndRemove(BlockingQueue<String> blockingQueue) {
        System.out.println(blockingQueue.add("a"));
        System.out.println(blockingQueue.add("b"));
        System.out.println(blockingQueue.add("c"));
        System.out.println(blockingQueue.add("e"));
        System.out.println(blockingQueue.element());
        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());
    }

    private static void offerAndPoll(BlockingQueue<String> blockingQueue) {
        System.out.println(blockingQueue.offer("a"));
        System.out.println(blockingQueue.offer("b"));
        System.out.println(blockingQueue.offer("c"));
        System.out.println(blockingQueue.offer("e"));
        System.out.println(blockingQueue.peek());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
    }

    private static void putAndTake(BlockingQueue<String> blockingQueue) throws InterruptedException {
        blockingQueue.put("a");
        blockingQueue.put("b");
        blockingQueue.put("c");
        blockingQueue.put("d");
        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
    }

    private static void outOfTime(BlockingQueue<String> blockingQueue) throws InterruptedException {
        System.out.println(blockingQueue.offer("a",2L, TimeUnit.SECONDS));
        System.out.println(blockingQueue.offer("a",2L, TimeUnit.SECONDS));
        System.out.println(blockingQueue.offer("a",2L, TimeUnit.SECONDS));
        System.out.println(blockingQueue.offer("a",2L, TimeUnit.SECONDS));
    }

}

阻塞队列之同步SynchronousQueue队列

  • SynchronousQueue没有容量。与其他BlockingQueue不同,SynchronousQueue是一个不存储元素的BlockingQueue
  • 每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然。
  • 一句话总结:SynchronousQueue 时零库存阻塞 代码演示
package com.hong.juc;

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

/**
 * 阻塞队列SynchronousQueue演示
 */
public class SynchronousQueueDemo {
    public static void main(String[] args) {
        BlockingQueue<String> blockingQueue = new SynchronousQueue<>();

        new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + "\t put 1");
                blockingQueue.put("1");
                System.out.println(Thread.currentThread().getName() + "\t put 2");
                blockingQueue.put("2");
                System.out.println(Thread.currentThread().getName() + "\t put 3");
                blockingQueue.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "AAA").start();

        new Thread(() -> {
            try {
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "\t" + blockingQueue.take());

                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "\t" + blockingQueue.take());

                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "\t" + blockingQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "BBB").start();
    }
}

执行结果:程序运行结果:线程 AAA 生产完新的数据后,必须等待线程 BBB 取走后,才能继续生产

juc25.png

11.5、阻塞队列使用场景

生产者消费者模式- 传统版(synchronized, wait, notify) 阻塞队列版(lock, await, signal)、线程池、消息中间件

生产者-消费者模式上面详细讲过,这里就不多说了

11.6、Synchronized和Lock有什么区别

  1. synchronized属于JVM层面,属于java的关键字
    • monitorenter(底层是通过monitor对象来完成,其实wait/notify等方法也依赖于monitor对象 只能在同步块或者方法中才能调用 wait/ notify等方法)
    • Lock是具体类(java.util.concurrent.locks.Lock)是api层面的锁
  2. 使用方法:
    • synchronized:不需要用户去手动释放锁,当synchronized代码执行后,系统会自动让线程释放对锁的占用。
    • ReentrantLock:则需要用户去手动释放锁,若没有主动释放锁,就有可能出现死锁的现象,需要lock()unlock() 配置try catch语句来完成
  3. 等待是否中断
    • synchronized:不可中断,除非抛出异常或者正常运行完成。
    • ReentrantLock:可中断,可以设置超时方法
      • 设置超时方法,trylock(long timeout, TimeUnit unit)
      • lockInterrupible() 放代码块中,调用interrupt() 方法可以中断
  4. 加锁是否公平
    • synchronized:非公平锁
    • ReentrantLock:默认非公平锁,构造函数可以传递boolean值,true为公平锁,false为非公平锁
  5. 锁绑定多个条件Condition
    • synchronized:没有,要么随机,要么全部唤醒
    • ReentrantLock:用来实现分组唤醒需要唤醒的线程,可以精确唤醒,而不是像synchronized那样,要么随机,要么全部唤醒 从字节码角度看
  • 有一条 monitorenter 指令,表示获取锁
  • 有两条 monitorexit 指令,其中第二条 monitorexit 指令保证程序出了一场,照样能释放锁
  • 使用 ReentrantLock 类,在字节码层面就是 new 了一个对象
 0 new #2 <java/lang/Object>
 3 dup
 4 invokespecial #1 <java/lang/Object.<init>>
 7 dup
 8 astore_1
 9 monitorenter
10 aload_1
11 monitorexit
12 goto 20 (+8)
15 astore_2
16 aload_1
17 monitorexit
18 aload_2
19 athrow
20 new #3 <java/util/concurrent/locks/ReentrantLock>
23 dup
24 invokespecial #4 <java/util/concurrent/locks/ReentrantLock.<init>>
27 astore_1
28 return

11.7、线程通信之生产者消费者阻塞队列版

package com.hong.juc;

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

class MyResource {
    // 默认开启,进行生产消费
    // 这里用到了volatile是为了保持数据的可见性,也就是当TLAG修改时,要马上通知其它线程进行修改
    private volatile boolean FLAG = true;

    // 使用原子包装类,而不用number++
    private AtomicInteger atomicInteger = new AtomicInteger();

    // 这里不能为了满足条件,而实例化一个具体的SynchronousBlockingQueue
    BlockingQueue<String> blockingQueue = null;

    // 而应该采用依赖注入里面的,构造注入方法传入
    public MyResource(BlockingQueue<String> blockingQueue) {
        this.blockingQueue = blockingQueue;
        // 查询出传入的class是什么
        System.out.println(blockingQueue.getClass().getName());
    }


    public void myProducer() throws Exception{
        String data = null;
        boolean retValue;
        // 多线程环境的判断,一定要使用while进行,防止出现虚假唤醒
        // 当FLAG为true的时候,开始生产
        while(FLAG) {
            data = atomicInteger.incrementAndGet() + "";

            // 2秒存入1个data
            retValue = blockingQueue.offer(data, 2L, TimeUnit.SECONDS);
            if(retValue) {
                System.out.println(Thread.currentThread().getName() + "\t 插入队列:" + data  + "成功" );
            } else {
                System.out.println(Thread.currentThread().getName() + "\t 插入队列:" + data  + "失败" );
            }

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

        System.out.println(Thread.currentThread().getName() + "\t 停止生产,表示FLAG=false,生产介绍");
    }


    public void myConsumer() throws Exception{
        String retValue;
        // 多线程环境的判断,一定要使用while进行,防止出现虚假唤醒
        // 当FLAG为true的时候,开始生产
        while(FLAG) {
            // 2秒存入1个data
            retValue = blockingQueue.poll(2L, TimeUnit.SECONDS);
            if(retValue != null && retValue != "") {
                System.out.println(Thread.currentThread().getName() + "\t 消费队列:" + retValue  + "成功" );
            } else {
                FLAG = false;
                System.out.println(Thread.currentThread().getName() + "\t 消费失败,队列中已为空,退出" );

                // 退出消费队列
                return;
            }
        }
    }

    /**
     * 停止生产的判断
     */
    public void stop() {
        this.FLAG = false;
    }

}
public class ProducerConsumerWithBlockingQueueDemo {
    public static void main(String[] args) {
        // 传入具体的实现类, ArrayBlockingQueue
        MyResource myResource = new MyResource(new ArrayBlockingQueue<String>(10));

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t 生产线程启动\n\n");

            try {
                myResource.myProducer();
                System.out.println("\n");

            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "producer").start();


        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t 消费线程启动");

            try {
                myResource.myConsumer();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "consumer").start();

        // 5秒后,停止生产和消费
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }


        System.out.println("\n\n5秒中后,生产和消费线程停止,线程结束");
        myResource.stop();
    }
}

juc26.png

12、ThreadPool线程池

相关面试:线程池用过吗?ThreadPoolExecutor谈谈你的理解?

12.1、为什么用线程及其优势

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

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

第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控

12.2、线程池如何使用

  1. Java 中的线程池是通过Executor框架实现的
  2. 该框架中用到了ExecutorExecutorsExecutorServiceThreadPoolExecutor这几个类 了解:
  3. Executor:newScheduledThreadPool(),带时间调度的线程池
  4. Executors.newWorkStealingPool(int):java8新增,使用目前机器上可用的处理器作为它的并行级别 重点

12.2.1、Executors.newSingleThreadExecutor()

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
  • 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。
  • newSingleThreadExecutorcorePoolSize和maximumPoolSize都设置为1,它使用的LinkedBlockingQueue

12.2.1、Executors.newFixedThreadPool(int)

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
  • 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  • newFixedThreadPool创建的线程池corePoolSizemaximumPoolSize值是相等的,它使用的LinkedBlockingQueue

12.2.2、Executors.newCachedThreadPool()

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
  • 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  • newCachedThreadPoolcorePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,使用的SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。 juc7.png 代码演示
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolDemo {
    public static void main(String[] args) {

    	// 一池5个处理线程(用池化技术,一定要记得关闭)
//    	ExecutorService threadPool = Executors.newFixedThreadPool(5);

    	// 创建一个只有一个线程的线程池
//    	ExecutorService threadPool = Executors.newSingleThreadExecutor();

    	// 创建一个拥有N个线程的线程池,根据调度创建合适的线程
    	ExecutorService threadPool = Executors.newCachedThreadPool();

        // 模拟10个用户来办理业务,每个用户就是一个来自外部请求线程
        try {

            // 循环十次,模拟业务办理,让5个线程处理这10个请求
            for (int i = 0; i < 10; i++) {
                final int tempInt = i;
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "\t 给用户:" + tempInt + " 办理业务");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }
}

juc8.png

12.3、线程池里7大参数

juc28.png

  1. corePoolSize:线程池中的常驻核心线程数
  • 在创建了线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务,近似理解为今日当值线程。
  • 当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。
  1. maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1
  2. keepAliveTime:多余的空闲线程的存活时间。
  • 当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止
  1. unit:keepAliveTime的单位。
  2. workQueue:任务队列,被提交但尚未被执行的任务。
  3. threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可。
  4. handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数( maximumPoolSize)。

12.4、线程池底层工作原理

juc29.png

juc30.png

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

12.5、线程池拒绝策略

面试题:线程池的拒绝策略请你谈谈

12.5.1、拒绝策略是什么?

  • 等待队列也已经排满了,再也塞不下新任务了,同时线程池中的max线程也达到了,无法继续为新任务服务JDK内置拒绝策略(以下内置策略均实现了RejectedExecutionHandler接口)
  • AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行

juc27.png

  • CallerRunsPolicy:调用者运行,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退给调用者,从而降低新任务的流量

juc31.png

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

juc32.png

  • DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案

juc33.png

  • 这时候我们就需要拒绝策略机制合理的处理这个问题。

12.6、创建线程池方法

面试题:你在工作中单一的/固定数的/可变你的三种创建线程池的方法,你用哪个多?超级大坑

【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。 juc11.png

12.7、自定义线程池(手写)

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

  • JDK 自带线程池的缺陷:底层使用了 LinkedBlockingQueue 阻塞队列,该阻塞队列默认是无界的,允许的请求队列长度为Integer.MAX_VALUE

12.8、线程池配置合理线程数

面试题:合理配置线程池你是如何考虑的?

考虑如下方面
CPU 密集型

  1. CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。
  2. CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那些。
  3. CPU密集型任务配置尽可能少的线程数量,一般公式:CPU核数+1个线程的线程池 IO 密集型
  4. 由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如CPU核数*2。
  5. IO密集型,即该任务需要大量的IO,即大量的阻塞。
  6. 在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。
  7. 所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。
  8. IO密集型时,大部分线程都阻塞,故需要多配置线程数:参考公式:CPU核数/(1-阻塞系数),阻塞系数在0.8~0.9之间,比如8核CPU:8/(1-0.9)=80个线程数

13、Java8之流式计算复习

13.1、四大函数式接口

juc15.png

13.2、流式计算

题目: 请按照给出数据,找出同时满足以下条件的用户,也即以下条件全部满足

  • 偶数ID且年龄大于24且用户名转为大写且用户名字母倒排序
  • 只输出一个用户名字
package com.hong.stream;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;

@Data
@AllArgsConstructor
@NoArgsConstructor
class User{
    private Integer id;
    private String userName;
    private int age;
}

/**
 * 题目: 请按照给出数据,找出同时满足以下条件的用户,也即以下条件全部满足
 *      偶数ID且年龄大于24且用户名转为大写且用户名字母倒排序
 *      只输出一个用户名字
 */
public class StreamDemo {
    public static void main(String[] args) {
        User u1 = new User(11, "a", 23);
        User u2 = new User(12, "a", 24);
        User u3 = new User(13, "a", 22);
        User u4 = new User(14, "a", 28);
        User u5 = new User(16, "a", 26);

        List<User> list = Arrays.asList(u1, u2, u3, u4, u5);

        /**
         * filter(Predicate<? super T> predicate)
         * forEach(Consumer<? super T> action)
         * map(Function<? super T,? extends R> mapper)
         * sorted(Comparator<? super T> comparator)
         * limit(long maxSize) 
         */
        list.stream().filter(user -> {return user.getId() % 2 == 0;})
                .filter(user -> {return user.getAge() > 24;})
                .map(user -> {return user.getUserName().toUpperCase();})
                .sorted(((o1, o2) -> {return o1.compareTo(o2);}))
                .limit(1).forEach(System.out::println);


//        Function<String, Integer> function = new Function<String, Integer>() {
//            @Override
//            public Integer apply(String s) {
//                return 1024;
//            }
//        };

//        Function<String, Integer> function = s -> {return s.length();};
//        System.out.println(function.apply("消费"));

//        Predicate<String> predicate = new Predicate<String>() {
//            @Override
//            public boolean test(String o) {
//                return false;
//            }
//        };
        Predicate<String> predicate = s -> {return s.isEmpty();};
        System.out.println(predicate.test("断定"));

//        Consumer<String> consumer = new Consumer<String>() {
//            @Override
//            public void accept(String o) { }
//        };
        Consumer<String> consumer = s -> { System.out.println("消费"); };
        consumer.accept("消费");

//        Supplier<String> supplier = new Supplier<String>() {
//            @Override
//            public String get() {
//                return null;
//            }
//        };
        Supplier<String> supplier = () -> {return "供给";};
        System.out.println(supplier.get());
    }
}

interface MyInterface{

    public int myInt(int x);
}

14、分支合并框架

Fork:把一个复杂任务进行分拆,大事化小 Join:把分拆任务的结果进行合并

14.1、ForkJoinPool

原理

  • Fork:把一个复杂任务进行分拆,大事化小
  • Join:把分拆任务的结果进行合并
递归任务:继承后可以实现递归(自己调自己)调用的任务

 class Fibonacci extends RecursiveTask<Integer> {
   final int n;
   Fibonacci(int n) { this.n = n; }
   Integer compute() {
     if (n <= 1)
       return n;
     Fibonacci f1 = new Fibonacci(n - 1);
     f1.fork();
     Fibonacci f2 = new Fibonacci(n - 2);
     return f2.compute() + f1.join();
   }
 }

ForkJoinPool 不是为了替代 ExecutorService,而是它的补充,在某些应用场景下性能比 ExecutorService 更好。
ForkJoinPool 主要用于实现“分而治之”的算法,特别是分治之后递归调用的函数,例如 quick sort 等。
ForkJoinPool 最适合的是计算密集型的任务,如果存在 I/O,线程间同步,sleep() 等会造成线程长时间阻塞的情况时,最好配合使用 ManagedBlockerjuc16.png

package com.hong.forkjoin;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;

/**
 * public abstract class RecursiveTask<V> extends ForkJoinTask<V>
 */
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;
    }

    //此方法为ForkJoin的核心方法:对任务进行拆分  拆分的好坏决定了效率的高低
    @Override
    protected Integer compute() {
        if ((end - begin) <= ADJUST_VALUE){
            for (int i = begin; i < end; i++) {
               result = result + i;
            }
        }else {
            int middle = (end + begin)/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;
    }
}

/**
 *
 */
public class ForkJoinDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyTask myTask = new MyTask(0, 100);
        ForkJoinPool threadPool = new ForkJoinPool();

        ForkJoinTask<Integer> forkJoinTask = threadPool.submit(myTask);

        System.out.println(forkJoinTask.get());

        threadPool.shutdown();
    }
}

15、异步回调

  • CompletableFuture 实现了 Future 以及 ComplatableStage 接口, 实现 Future 接口代表其本身可以作为生产者和消费者的 “桥梁”, 而 ComplatableStage 接口定义了以上所有的组合条件。
  • 完成条件,后置处理的多种类型众多的 API, 可以说 Future 接口只是描述了单个任务处理的方式, 而 CompletableStage 接口更进一步的从实际的编程需求出发,满足了多个任务协同处理的场景需求,包括多个任务任一完成或全部完成时。
  • 任务串行顺序执行, 并行执行,并且创造性的提出了 applyacceptrun 三种后置处理器的类型,本质上后置处理还是链式顺序执行的。这样在众多子任务的场景需求中, CompletableFuture 可以很好的胜任
package com.hong.completable;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class completableFutureDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
            System.out.println(Thread.currentThread().getName() + "没用返回,update sql ok");
        });

        completableFuture.get();

        CompletableFuture<Integer> completableFuture1 = completableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + "有返回,insert sql ok");
            int age = 1/0;
            return 1024;
        });

        // BiConsumer<? super T, ? super Throwable> action
        System.out.println(completableFuture1.whenComplete((t, u) -> {
            System.out.println("****t" + t);
            System.out.println("****u" + u);
            // Function<Throwable, ? extends T> fn
        }).exceptionally(f -> {
            System.out.println("****exception" + f.getMessage());
            return 444;
        }).get());
    }
}

执行正确 juc35.png 异常 juc34.png