多线程相关实例

x33g5p2x  于2022-03-18 转载在 其他  
字(15.1k)|赞(0)|评价(0)|浏览(429)

一、多线程案例

1.单例模式

单例模式是一种常见的“设计模式”。在软件开发中,涉及到的场景有很多,变化也有很多。很多新手如果不加限制的乱去开发,此时会造成的麻烦会有很多。于是有很多强者就把一些常见场景的一些常见解决方案整理成一份“谱”,如果按照这个谱去开发,那么开发的代码不会差到哪里去。“谱”指的就是设计模式。

场景:代码中的有些概念,不应该存在多个实例,此时应该使用单例模式来解决。例如MySQL中的DataSource类,在一个程序中就应该只有一个实例,不应该实例化多个DataSource对象。

在工作中会接触到一些服务器,服务器里都需要依赖一些数据(可能在数据库中,可能在内存中),如果是在内存中的话,往往需要一个“数据管理器”实例来管理这些数据。像这种“数据管理器”对象也应该是单例的。因此可以在代码中用单例模式来解决(如果尝试创建多个实例,直接编译就报错)。

1.1 饿汉模式

只要类被加载了,就会立刻实例化Singleton实例,后续无论什么操作,只要严格使用getInstance方法去实例化Singleton,就不会出现其它的实例。

如果在main方法里面new一个Singleton对象也没报错的原因:Singleton是ThreadDemo1的内部类,则在ThreadDemo1内是可以访问内部类的private成员的。

因为实例化的instance被static所修饰,因此它在内存中只有一份,类加载也只有一次机会,不可能会并发执行。

  1. public class ThreadDemo1 {
  2. static class Singleton {
  3. //把 构造方法 变为私有,此时在该类外部就无法new在这个类的实例了
  4. private Singleton() {};
  5. //再来创建一个static成员,表示 Singleton 类的唯一的实例
  6. //static和类相关,和实例无关。类在内存中只有一份,static成员也就只有一份
  7. private static Singleton instance = new Singleton();
  8. public static Singleton getInstance() {
  9. return instance;
  10. }
  11. }
  12. public static void main(String[] args) {
  13. Singleton s1 = Singleton.getInstance();
  14. Singleton s2 = Singleton.getInstance();
  15. System.out.println(s1==s2);
  16. }
  17. }
  18. 打印结果:true

1.2 懒汉模式

懒汉模式是在getInstance方法的内部去实例化instance,如果不去调用getInstance方法,则永远都不会实例化instance。因此相对于饿汉模式来说,懒汉模式执行代码的效率更高,因为减少了实例化的开销。因为如果不使用该实例的话,实例化该对象就没有意义了。这种情况称为“延时加载”。

例如:各种编辑器,有两种主要的打开方式:
1.记事本,会尝试把整个文件的内容都读取到内存中,然后再展示给用户。(饿汉模式)
2.vscode/sublime,只是会把当前这个屏幕内的内容(以及周围的一小点内容)加载到内存中,随着翻页,会继续加载内存。(懒汉模式)

最原始的形态:

  1. public class ThreadDemo2 {
  2. static class Singleton {
  3. private Singleton() {};
  4. private static Singleton instance = null;
  5. //类加载的时候没有立刻实例化,第一次调用getInstance才真正地实例化。
  6. public static Singleton getInstance() {
  7. if(singleton==null) {
  8. singleton = new Singleton();
  9. }
  10. return singleton;
  11. }
  12. }
  13. }

1.3 两个模式的线程安全问题

单例模式和线程之间有什么关联呢?两个模式是否都是线程安全的呢?
线程不安全即代码的执行的逻辑与我们的逻辑不符合就是线程不安全的。假设多个线程并发地调用getInstance,是否会导致逻辑的错误?
首先我们回顾一下导致线程不安全的主要四个原因:
1.抢占式执行
2.操作是非原子的(用锁来解决)
3.多个线程去修改同一个变量
4.内存的不可见性(用volatile解决)

那么先来分析饿汉模式是否是线程安全的:
对于饿汉模式来说,多个线程同时调用getInstance,由于getInstance里只做了一件事:读取instance实例的地址->多个线程在同时读取同一个变量。则饿汉模式的线程是安全的!

再来分析下懒汉模式:
对于懒汉模式来说,多线程同时调用getInstance时,getInstance做了四件事情。读取instance的内容->判断instance是否为空->如果instance为null,就new实例(此处会修改instance的地址)->返回实例的地址。

因为懒汉模式会修改instance的值,那么由于抢占式执行和操作非原子的原因,如果一个线程已经读取了instance进入if内部,而另一个线程已经修改了instance的地址,那么就会造成逻辑上的错误

时间线:

因此懒汉模式是线程不安全的,只有在实例化之前去调用,存在线程安全的问题,如果要是把实例创建好了,后面再去并发调用getInstance就不再存在线程安全问题。

如何改进懒汉模式。让线程变得安全呢?

1.4 用锁来解决懒汉模式的非原子操作

因为多个线程并发执行懒汉模式时,为了保证一个线程先去修改instance的值,而另一个线程等到instance的值被修改后再去读取,那么就可以用锁来解决问题

这样能否保证线程安全呢?这样写,此时读取判断和new修改操作仍然不是原子的,这样改不行。

图2:那么改为这种:这样是可以的,因为此时读取判断和new修改操作是原子的。

改为下面这种也是可以的:执行过程和图2是一样的。ruturn操作是在锁内部来完成的,由于return只是在读,这个操作放到锁里面或者外面不影响结果。虽然两种写法都可以,但是图2的写法的锁粒度更小。下面的锁粒度更大。锁粒度指的是:锁中包含的代码越多,就认为锁的粒度越大。

一般写代码的时候,都希望锁粒度小更好,锁的粒度越大,说明这段代码的并发能力就越受限,即能够并发执行的代码就越少。

对原始的懒汉模式的代码进行改进后有:
只有解锁了才会执行return的代码。

  1. public class ThreadDemo2 {
  2. static class Singleton {
  3. private Singleton() {};
  4. private static Singleton instance = null;
  5. public static Singleton getInstance() {
  6. synchronized (Singleton.class) {
  7. if(instance==null) {
  8. instance = new Singleton();
  9. }
  10. }
  11. return instance;
  12. }
  13. }
  14. }

时间线:

1.5 解决第二次调用getInstance的锁的问题

但是对于上面的代码,还是有问题的。因为两个线程第一次调用getInstance时,如果不加锁就会导致线程不安全,但是两个线程都结束后,instance就已经实例化了。此时两个线程再去调用getInstance时,由于instance都已经不是null,则不会进入if内部,而是直接返回。此处只涉及到instance的读,而没有修改instance的值,因此锁的存在就没有意义了。如果不去掉锁,则加锁和解锁的都是会有开销的,并且基本与高性能无缘。

因此可以对已经加锁的懒汉模式代码这样修改:

  1. public class ThreadDemo2 {
  2. static class Singleton {
  3. private Singleton() {};
  4. private static Singleton instance = null;
  5. public static Singleton getInstance() {
  6. if (instance==null) {
  7. synchronized (Singleton.class) {
  8. if(instance==null) {
  9. instance = new Singleton();
  10. }
  11. }
  12. }
  13. return instance;
  14. }
  15. }
  16. }

时间线:
实例化前的时间线:

实例化后的时间线:

该代码第一次判断instance是否为空和第二次判断instance是否为空的意义不一样。第一次判断为空是并发执行的。即使第一次调用getInstance都会进入第一个if内部,但是有一个线程会加锁修改instance的值,另一个线程在等待。当修改instance的值完毕后,在等待的线程就获取到锁,此时instance就不为null,直接返回instance。那第二次调用getInstance两个线程都能并发判断instance已经是不为空的了,因此直接返回instance即可

1.6 用volatile解决懒汉模式的内存可见性

对于1.5节,我们能清楚认识到当两个线程第一次调用getInstance时一开始会并发读取instance判断是否为null。判断后都进入第一个if中。此时一个线程获取到锁则往下执行,另一个线程则阻塞等待。待一个线程修改完instance的值后释放锁,阻塞等待的线程则获取锁再次读取instance的值判断是否为空。实际上此时的instance已经被实例化,但是多次的读取instance的值可能编译器会优化为直接从CPU上读取,就存在了内存可见性问题。

为了防止内存可见性问题,我们可以将要被多次读取的值加上volatile去修饰,则解决了内存不可的问题。

代码:
这才是一个懒汉模式完整的代码。

  1. public class ThreadDemo2 {
  2. static class Singleton {
  3. private Singleton() {};
  4. private volatile static Singleton instance = null;
  5. public static Singleton getInstance() {
  6. if (instance==null) {
  7. synchronized (Singleton.class) {
  8. if(instance==null) {
  9. instance = new Singleton();
  10. }
  11. }
  12. }
  13. return instance;
  14. }
  15. }
  16. }

总结:为了保证安全,懒汉模式的代码涉及到三个要点:
1.加锁 保证线程安全
2.双重 if 保证效率
3.volatile解决内存可见性导致的问题

注:面试时不要一次性将完整版的代码写出来,要一步一步去”进化“代码。

2. 阻塞队列

2.1 概念

阻塞队列是并发编程的一个重要的基础组件,能够帮助我们实现”生产者-消费者模型“,这个模型是典型的处理多并发编程模式。阻塞队列可以作为生产者-消费者模型中的交易场所

阻塞队列的特点:
如果入阻塞队列(以下的队列都是阻塞队列)操作太快,队列满了,继续入队列,就会阻塞。一直阻塞到有其它线程去消费队列了,才能继续入队列。
如果出队列的操作太快,队列空了,继续出队列。一直阻塞到有其它线程生产了元素,才能继续出队列

阻塞队列的最大用途:
1.解耦合。它能让多个服务器之间的关系不用直接传输数据那么复杂。而是通过阻塞队列,如果有服务器要数据,就去阻塞队列中取;没有取数据那么数据就在阻塞队列中等待。

2.削峰填谷

2.2 用代码来模拟”生产者-消费者“模型的阻塞队列

我们都知道,队列有两种实现方式。1.用链表来实现,记录头结点和尾结点,对头结点处理和对尾结点处理即为入队和出队。2.用数组来实现,当然实现的队列是一个循环队列来处理假溢出。

我们可以创建一个静态内部类来实现循环队列。此处只是将循环队列的入队和出队操作简单地模拟出来。因为此队列是阻塞队列,因此如果队列中是满的或者队列是空的就让阻塞队列阻塞。反之如果有元素产生并且进入到阻塞队列,阻塞队列之前为空就的话就被告知有元素可以出队列了;如果阻塞队列已满了也会进行阻塞,若有其它线程去消耗队列中的元素,它才能被告知有空位使得有新元素进入阻塞队列。

在两个线程中都会对size这个变量去读与写。因此从线程安全性的角度出发,要保证在一个线程对size变量读和写的时候是原子的并且有一个线程读与写的时候,其它线程是不能读的。

  1. public class ThreadDemo3 {
  2. static class BlockedQueue {
  3. private int[] queue = new int[10];
  4. private int front ;
  5. private int rear ;
  6. // 为了区分空还是满, 就需要额外引入一个 size 来表示.
  7. private int size = 0;
  8. public void put(int val) throws InterruptedException {
  9. synchronized (this) {
  10. if (size == queue.length) {
  11. wait();
  12. }
  13. queue[rear] = val;
  14. rear = (rear + 1) % queue.length;
  15. size++;
  16. notify();
  17. }
  18. }
  19. public int take() throws InterruptedException {
  20. synchronized (this) {
  21. int ret;
  22. if (size == 0) {
  23. wait();
  24. }
  25. ret = queue[front];
  26. front = (front + 1) % queue.length;
  27. size--;
  28. notify();
  29. return ret;
  30. }
  31. }
  32. }
  33. public static void main(String[] args) {
  34. BlockedQueue blockedQueue = new BlockedQueue();
  35. // 搞两个线程, 分别模拟生产者和消费者.
  36. // 第一次, 让给消费者消费的快一些, 生产者生产的慢一些.
  37. // 此时就预期看到, 消费者线程会阻塞等待. 每次有新生产的元素的时候, 消费者才能消费
  38. // 第二次, 让消费者消费的慢一些, 生产者生产的快一些.
  39. // 此时就预期看到, 生产者线程刚开始的时候会快速的往队列中插入元素, 插入满了之后就会阻塞等待.
  40. // 随后消费者线程每次消费一个元素, 生产者才能生产新的元素.
  41. Thread t1 = new Thread(){
  42. @Override
  43. public void run() {
  44. for (int i = 0; i < 10; i++) {
  45. try {
  46. blockedQueue.put(i);
  47. System.out.println("生产元素");
  48. Thread.sleep(500);
  49. } catch (InterruptedException e) {
  50. e.printStackTrace();
  51. }
  52. }
  53. }
  54. };
  55. t1.start();
  56. Thread t2 = new Thread(){
  57. @Override
  58. public void run() {
  59. try {
  60. while(true) {
  61. int ret = blockedQueue.take();
  62. System.out.println("消费元素");
  63. }
  64. } catch (InterruptedException e) {
  65. e.printStackTrace();
  66. }
  67. }
  68. };
  69. t2.start();
  70. }
  71. }

运行结果:每次生产了元素,才能对应去消费元素。

理解下图:

注:
1.假设有三个线程并且两个线程是入队列,一个线程是出队列。
那么此时如果队列已经满了,两个入队列的线程就都阻塞了。此时如果出队列线程被调用,调用完后其中一个入队列线程会被唤醒,就继续插入元素。此时如果队列已经空了,出队列线程就会阻塞,直到两个入队列线程任何一个插入成功,出队列线程都会被唤醒。

2.如果没有wait但是执行了notify是否对线程有影响
没有影响。
如果有线程在wait,notify会唤醒一个线程。
如果没有线程wait,notify不会有任何负面影响。

3.假设把notify改为notifyAll,此时的代码是否可行?
若只有两个线程,是可行的。但是涉及到三个线程的话,是不可行的。
假设现在有三个线程,一个线程生产,两个线程消费。
假设消费的速度快,生产的速度慢。那么两个消费的线程都有机会触发wait方法。当那个生产的线程生产了一个元素就notifyAll的话,两个消费线程就被唤醒,于是两个线程去尝试重新获取锁。
消费者1,先获取到了锁,于是就执行下面的出队列的操作(执行完毕,释放锁)。
消费者2,后获取到了锁,于是也会执行下面的出队列操作。但是此时阻塞队列为空,再出队的时候就是代码的错误了。

改进
如果仍想使用notifyAll,则需要对上面的代码做出什么改变呢?
我们可以将判断size的条件判断改为while。

这种实现方式更加严谨稳健。能够保证条件成立的时候再真的去执行后面的操作。否则就能够强行去wait。一般使用wait的时候,都建议搭配使用while。

wait方法的三个步骤:
1.释放锁
2.等待通知
3.收到通知后,获取锁

将if改为while的解释:
假设此时消费线程1获取到了锁,那么此时阻塞队列中没有元素,则消费线程1阻塞等待。再假设此时消费线程2获取到了锁,因为阻塞队列中没有元素,此时也是阻塞等待。再假设生产线程中生产了一个元素(size为1)后调用notifyAll,此时通知的是所有使用相同锁对象的锁,假设消费者1获取到了锁,此时wait方法结束。再进行循环中的判断,因为此时的size已经不为1了,因此消费线程1就执行后面的消费的具体步骤(size变为0)。待消费线程1执行完毕后,消费线程2获取到了锁,再根据循环进行判断,此时的size仍然为0,因此消费线程2进入wait中继续等待

因为根据上面的分析,消费线程2对size的读取可能是多次的,那么就在size的定义处加上volatile解决内存的不可见性,防止编译器读取的优化

最终的代码:

  1. public class ThreadDemo3 {
  2. static class BlockedQueue {
  3. private int[] queue = new int[10];
  4. private int front ;
  5. private int rear ;
  6. private volatile int size = 0;
  7. public void put(int val) throws InterruptedException {
  8. synchronized (this) {
  9. while (size == queue.length) {
  10. wait();
  11. }
  12. queue[rear] = val;
  13. rear = (rear + 1) % queue.length;
  14. size++;
  15. notify();
  16. }
  17. }
  18. public int take() throws InterruptedException {
  19. int ret;
  20. synchronized (this) {
  21. while (size == 0) {
  22. wait();
  23. }
  24. ret = queue[front];
  25. front = (front + 1) % queue.length;
  26. size--;
  27. notify();
  28. }
  29. return ret;
  30. }
  31. }
  32. public static void main(String[] args) {
  33. BlockedQueue blockedQueue = new BlockedQueue();
  34. Thread t1 = new Thread(){
  35. @Override
  36. public void run() {
  37. for (int i = 0; i < 10; i++) {
  38. try {
  39. blockedQueue.put(i);
  40. System.out.println("生产元素");
  41. Thread.sleep(500);
  42. } catch (InterruptedException e) {
  43. e.printStackTrace();
  44. }
  45. }
  46. }
  47. };
  48. t1.start();
  49. Thread t2 = new Thread(){
  50. @Override
  51. public void run() {
  52. try {
  53. while(true) {
  54. int ret = blockedQueue.take();
  55. System.out.println("消费元素");
  56. }
  57. } catch (InterruptedException e) {
  58. e.printStackTrace();
  59. }
  60. }
  61. };
  62. t2.start();
  63. }
  64. }

运行结果:

3. 定时器

3.1 定时器的介绍

在网络编程中,定时器其实是非常关键的。比如访问某个网站,网卡了,浏览器就会一直转圈(阻塞等待),但是这个等待不是无限的等待,等到一定的时间后,就显示“访问超时”

使用阻塞队列,能够达到“削峰填谷”的效果。峰值中会有大量的数据涌入导队列中,如果后续的服务器消费速度比较慢的话,队列里有的元素就会滞留很久。使用定时器让滞留时间太久的请求直接被删除掉,这样能够及时更新更加新的请求。

在Java的标准库中也提供了定时器的组件为“Timer”,实例化Timer后调用schedule方法,传入的两个参数:
第一个:TimerTask的接口,在接口中利用内部类来重写run方法,描述的是要执行的代码。
第二个:定一个时间,单位为ms,过了这个时间之后才去执行run方法里面的代码。

  1. public class ThreadDemo4 {
  2. public static void main(String[] args) {
  3. System.out.println("代码开始执行");
  4. Timer timer = new Timer();
  5. timer.schedule(new TimerTask() {
  6. @Override
  7. public void run() {
  8. System.out.println("触发定时器");
  9. }
  10. },3000);//3s之后才去执行
  11. }
  12. }

3.2 定时器的实现

一个定时器里面是可以安排很多任务的,这些任务就会按照时间,谁先到了时间就执行谁。

主要分为两个步骤:
1.描述任务:可以直接使用Runnable来描述这个任务。
2.组织任务:需要有一个数据结构,把很多的任务放在一起,此处的需求是需要能够在一大堆任务中,找到那个最先要到点的任务。

因此就要使用优先级队列。但是这里涉及到了多线程并发的问题,可能导致线程不安全,因此可以使用有阻塞功能的优先级队列(PriorityBlockingQueue).

提供一个schedule方法,这个方法就是往阻塞队列中插入元素就行了。还需要让timer内部有一个线程,这个线程一直去扫描队首元素,看看队首元素是不是到点了。如果到点,就立刻执行任务;如果没到点,就把这个队首元素塞回队列中,继续扫描。

注:因为该优先级队列中是按照定的时间来排序的,因此如果一个类要在优先级队列中排序,需要指定排序的属性是谁,要用到比较器

  1. public class ThreadDemo5 {
  2. static class Task implements Comparable<Task> {
  3. private Runnable command;
  4. private long time;
  5. public Task(Runnable command, long time) {
  6. this.command = command;
  7. this.time = time+System.currentTimeMillis();
  8. }
  9. private void run() {
  10. command.run();
  11. }
  12. @Override
  13. public int compareTo(Task o) {
  14. return (int)(this.time-o.time);
  15. }
  16. }
  17. static class Timer {
  18. private PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>();
  19. private Object locker = new Object();
  20. private void schedule(Runnable command, long time) {
  21. Task task = new Task(command, time);
  22. queue.put(task);
  23. }
  24. public Timer() {
  25. Thread t = new Thread() {
  26. @Override
  27. public void run() {
  28. while (true) {
  29. try {
  30. Task task = queue.take();
  31. long curTime = System.currentTimeMillis();
  32. if (task.time > curTime) {
  33. queue.put(task);
  34. } else {
  35. task.run();
  36. }
  37. } catch (InterruptedException e) {
  38. e.printStackTrace();
  39. break;
  40. }
  41. }
  42. }
  43. };
  44. t.start();
  45. }
  46. }
  47. public static void main(String[] args) {
  48. System.out.println("程序启动");
  49. Timer timer = new Timer();
  50. timer.schedule(new Runnable() {
  51. @Override
  52. public void run() {
  53. System.out.println("hello");
  54. }
  55. }, 3000);
  56. }
  57. }

运行结果:此处打印hello后不动是因为该队列中已经没有元素了,此时该队列堵塞(执行了wait方法)。

但是有一个非常严重的问题:

代码改进:如果有一个任务的时间还没到,就让它阻塞等待,避免CPU空转。直到有新的元素插入后notify或者该任务到点执行了,那么该wait就结束。

  1. public class ThreadDemo5 {
  2. static class Task implements Comparable<Task> {
  3. private Runnable command;
  4. private long time;
  5. public Task(Runnable command, long time) {
  6. this.command = command;
  7. this.time = time+System.currentTimeMillis();
  8. }
  9. private void run() {
  10. command.run();
  11. }
  12. @Override
  13. public int compareTo(Task o) {
  14. return (int)(this.time-o.time);
  15. }
  16. }
  17. static class Timer {
  18. private PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>();
  19. private Object locker = new Object();
  20. private void schedule(Runnable command, long time) {
  21. Task task = new Task(command, time);
  22. queue.put(task);
  23. synchronized (locker) {
  24. locker.notify();
  25. }
  26. }
  27. public Timer() {
  28. Thread t = new Thread() {
  29. @Override
  30. public void run() {
  31. while (true) {
  32. try {
  33. Task task = queue.take();
  34. long curTime = System.currentTimeMillis();
  35. if (task.time > curTime) {
  36. queue.put(task);
  37. synchronized (locker) {
  38. locker.wait(task.time - curTime);
  39. }
  40. } else {
  41. task.run();
  42. }
  43. } catch (InterruptedException e) {
  44. e.printStackTrace();
  45. break;
  46. }
  47. }
  48. }
  49. };
  50. t.start();
  51. }
  52. }
  53. public static void main(String[] args) {
  54. System.out.println("程序启动");
  55. Timer timer = new Timer();
  56. timer.schedule(new Runnable() {
  57. @Override
  58. public void run() {
  59. System.out.println("hello");
  60. }
  61. }, 3000);
  62. }
  63. }

此处为什么要用wait而不用sleep呢?因为wait可以设定时间来阻塞,而sleep不行。假设现在为7:00,而此时最先结束的任务为8:00,但现在又有一个元素入到队列中,该任务的结束时间为7:30,如果使用sleep的话要等到8:00的结束才行,sleep没法提前唤醒就错过了新任务的时间。若使用wait,一旦插入新的任务,可以使用notify来唤醒,就可以及时的感知到新的任务啥时候执行了。

4. 线程池

4.1 概念与介绍

本来多进程就是解决并发编程的方案,但是进程有点太重量了(创建和销毁的开销比较大),因此引入了线程,线程比进程轻量很多。即便如此,如果某些场景中,需要频繁地创建销毁线程,此时,线程的创建和开销比较大,也就无法忽视了。

线程池目的是解决线程频繁创建和开销的,实际上使用线程的的时候,不是说用的时候才创建好的,而是提前创建好,放到一个“池子”中

当我们需要线程的时候,直接从线程池中取一个线程出来。当我们不需要这个线程的时候,就把这个线程还回池子中。此时我们的操作就会比创建销毁线程更加高效

如果是真的创建/销毁线程,涉及到用户态和内核态的切换(切换到内核态,然后创建出对应的PCB来)。
如果不是真的创建/销毁线程,而是将线程放到池子中,就相当于操作全部都放在用户态来执行。在用户态中操作更高效,切换到内核态中操作可能会低效。

补充:用户态与内核态的介绍:
用户态就是应用程序执行的代码。内核态就是操作系统内核执行的代码。一般认为,用户态和内核态之间的切换是一个开销较大的操作。
内核是很忙的,要给N个线程同时提供服务。因此一旦要是某个线程把某个任务交给内核,此时内核何时会处理完毕是很难控制的。

4.2 刨析Java标准库中线程池构造方法的参数

在Java的标准库中,提供了现成的线程池——ThreadPoolExcutor 。它的使用是比较复杂的,有很多的参数。

线程池可以理解为一个工厂,线程为里面的员工(分为正式员工和临时员工)。
1.corePoolSize:称为核心线程数(正式员工)
2.maximumPoolSize:称为最大线程数(正式员工+临时员工)
3.keepAliveTime与unit:keepAliveTime描述临时工可以摸鱼多长时间;unit是keepAliveTime的时间单位,可以是s,ms,minutes。
4.workQueue:阻塞队列,组织了线程池要执行的任务
5.threadFactory:线程的创建方式,通过这个参数可以设定不同的线程的创建方式。
6.handler:拒绝策略。当任务满了又来新任务,则可以有几种策略来给阻塞队列处理:丢弃最新的任务;丢弃最老的任务;阻塞等待;抛出异常等。根据具体的任务场景,来选取具体的拒绝策略,用来处理极端情况的。

4.3 工厂模式的介绍

工厂模式也是一种设计模式,和单例模式是并列的关系。它存在的意义是给构造方法填坑。
那么构造方法有什么坑呢?
1.构造方法的名字,必须是固定的(类名相同)
2.如果需要多个版本的构造方式,就只能依赖构造方法的重载(但是又要求参数的个数或类型不相同)。

因此会导致有个问题:
假设我们有一个Point类,表示平面上的一个点。我们希望通过笛卡尔坐标和极坐标这两种方式来构造这个点:

但是这时候两个方法的参数和个数都是相同的,会导致编译错误。而且方法名只能是固定的,光看方法名并不知道是用哪种方式去构造。

为了解决这个问题,就不适应构造方法去实例化了,而是用其它的方法来进行实例构造,即返回的是该实例的构造方法,这样用来构造实例的方法,就称为“工厂方法”

工厂方法就是一个普通的方法,这个工厂里面会调用对应的构造方法,并开始执行一些初始化操作,并返回这个对象的实例

4.4 ThreadPoolExcutor与Excutors的相互使用

由于ThreadPoolExcutor使用起来比较复杂,标准库中又提高了一组其它的类,相当于对ThreadPoolExcutor又进行了一次封装

标准库中提供了一个Excutors类,这个类相当于一个“工厂类”,通过这个类提供的一组构工厂方法,就可以创建出风格不同的线程池实例了。

1.newFixedThreadPool:创建出一个固定线程数量的线程池(完全没有临时工的版本)。
2.newCachedThreadPool:创建出一个数量可变的线程池(完全没有正式员工,全是临时员工)
3.newSingleThreadPool:创建出一个只包含一个线程的线程池(只在特定场景下使用)
4.newScheduleThreadPool:能够设定延时时间的线程池(插入的任务能够过一会再执行),相当于进阶版的定时器。

这四个工厂方法里面,调用了ThreadPoolExcutor的构造方法,同时把对应的参数进行了传递。并返回了ThreadPoolExcutor的实例。

例如使用了newFixedThreadPool的工厂方法:

newFixedThreadPool工厂方法的使用:
解释:当线程池里有十个工作线程,往任务中加入了20个任务,此时这十个工作线程就会从任务队列中,先取出10个任务,然后并发执行这十个任务,这些线程谁执行完了当前的任务,谁就去队列中取新的任务去执行。直到把线程池任务队列中的任务执行完了,此时线程池中的线程就可以阻塞等待了(等待新任务的到来)。

  1. public class ThreadDemo6 {
  2. public static void main(String[] args) {
  3. ExecutorService service = Executors.newFixedThreadPool(10);
  4. for (int i = 0; i < 20; i++) {
  5. service.submit(new Runnable() {
  6. @Override
  7. public void run() {
  8. System.out.println("hello");
  9. }
  10. });
  11. }
  12. }
  13. }

对ThreadPoolExcutor与Excutors的使用说明:

4.5 模拟线程池

步骤:
1.描述一个任务,直接使用Runnable即可。
2.组织若干个任务,可以使用阻塞队列。
3.有些工作线程

  1. public class ThreadDemo7 {
  2. static class Worker extends Thread{
  3. private BlockingQueue<Runnable> queue = null;
  4. public Worker(BlockingQueue<Runnable> queue) {
  5. this.queue = queue;
  6. }
  7. @Override
  8. public void run() {
  9. // 工作线程的具体的逻辑.
  10. // 需要从阻塞队列中取任务.
  11. while(true) {
  12. try {
  13. Runnable command = queue.take();
  14. // 通过 run 来执行这个具体的任务.
  15. command.run();
  16. } catch (InterruptedException e) {
  17. e.printStackTrace();
  18. }
  19. }
  20. }
  21. }
  22. static class ThreadPool {
  23. // 包含一个阻塞队列, 用来组织任务.
  24. private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
  25. // 这个 list 就用来存放当前的工作线程.
  26. private List<Thread> workers = new ArrayList<>();
  27. private static final int MAX_WORKER_COUNT = 10;
  28. // 通过这个方法, 把任务加入到线程池中.
  29. // submit 不光可以把任务放到阻塞队列中, 同时也可以负责创建线程.
  30. public void submit(Runnable command) throws InterruptedException {
  31. if(workers.size()<MAX_WORKER_COUNT) {
  32. // 如果当前工作线程的数量不足线程数目上限, 就创建出新的线程.
  33. // 工作线程就专门搞一个类来完成
  34. // Worker 内部要能够取到队列的内容. 就需要把这个队列实例通过 Worker 的构造方法, 传过去.
  35. Worker worker = new Worker(queue);
  36. worker.start();
  37. workers.add(worker);
  38. }
  39. queue.put(command);
  40. }
  41. }
  42. public static void main(String[] args) throws InterruptedException {
  43. ThreadPool threadPool = new ThreadPool();
  44. for(int i=0;i<100;i++) {
  45. threadPool.submit(new Runnable() {
  46. @Override
  47. public void run() {
  48. System.out.println("hello");
  49. }
  50. });
  51. }
  52. }
  53. }

相关文章