线程池(多线程)

1.什么是线程池

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

2.为什么使用线程池??

new Thread的弊端:

  • 每次new Thread新建对象性能差。
  • 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机。
  • 缺乏更多功能,如定时执行、定期执行、线程中断。

线程池的优点:

  • 降低资源消耗。重用存在的线程,减少对象创建、消亡的开销,性能佳。
  • 提高响应速度,节省时间。当任务到达时候,任务可以不需要再等到线程创建就能立即执行。在系统启动的时候,我们预先初始化一定数量的线程放到线程池中,这样,如果有需要用的线程的地方,就可以直接调用了,不用再等待了(可用理解为游泳池馆在开门之前,就已经把水注入到池子中了,这样,开门后,有人过来,不用再等待)。.
  • 提高线程的可管理性。可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
  • 提供定时执行、定期执行、单线程、并发数控制等功能。

线程池的缺点

  • 容易造成死锁,是由于多个线程之间共享变量造成的,其次线程池在一次性创建了很多线程的情况下,虽然调用的时候效率更高,但是一旦线程池被创建出来,资源就会一直被占用,可能会造成资源不足。

3.线程池创建方法

  1. newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。 线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。 可能导致内存溢出,一般使用newFixedThreadPool代替
  2. newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  3. newScheduledThreadPool: 创建一个定长线程池,支持定时及周期性任务执行。
  4. newSingleThreadExecutor*:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
  5. newSingleThreadScheduledExecutor:创建只有一条线程的线程池,他可以在指定延迟后执行线程任务
  6. newWorkStealingPool(在jdk1.8出来的)会更加所需的并行层次来动态创建和关闭线程。它同样会试图减少任务队列的大小,所以比较适于高负载的环境。同样也比较适用于当执行的任务会创建更多任务,如递归任务。适合使用在很耗时的操作,但是newWorkStealingPool不是ThreadPoolExecutor的扩展,它是新的线程池类ForkJoinPool的扩展,但是都是在统一的一个Executors类中实现,由于能够合理的使用CPU进行对任务操作(并行操作),所以适合使用在很耗时的任务中

4.线程池常见问题

  1. 主线程和子线程谁先运行?
    答:主线程就是main()方法,是最先执行的,然后创建一个子线程,这两个线程就会同时执行,但互不影响
package Thread;

import javax.sound.midi.Soundbank;

public class ThreadTest1 {

    private static int  i=0;

    public static void main(String[] args) throws  Exception {

        new Thread(()->{
            for (int j = 1; j <=5; j++) {
                i++;
                System.out.println("thread2 thread :" + j);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        for (int j = 1; j <=2; j++) {
            i++;
            System.out.println("main thread :"+j);
            Thread.sleep(100);
        }

        System.out.println("main thread end ,i result:"+i);

     /*   main thread :1
        thread2 thread :1
        thread2 thread :2
        main thread :2
        thread2 thread :3
        main thread end ,i result:5
        thread2 thread :4
        thread2 thread :5*/

    }
}
private static int  i=0;

    public static void main(String[] args) throws  Exception {
        for (int j = 0; j <1000; j++) {
            Thread t = new Thread(()->i++);
            t.start();
        }
        System.out.println(i);
        /*
            result: 995
            why?:主线程先运行,然后创建1000个子线程,分别对i++,然后主线程和子线程分别进行,当主线程走完时,1000个子线程					并未走完,所以得到的值不是1000
         */
    }
private static int  i=0;

    public static void main(String[] args) throws  Exception {
        for (int j = 0; j <1000; j++) {
            Thread t = new Thread(()->i++);
            t.start();
            t.join();
        }
        System.out.println(i);
        /*
            result: 1000
            why?:join方法会让主线程在子线程执行完毕之后结束
         */
    }

2.多个线程共享一个变量可能出现的问题及原因

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VyHR0kLl-1592270610021)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200615150711074.png)]

如上图所示,当一个JVM线程进入“运行”状态后(这个状态的实际切换由操作系统进行控制),这个线程使用的变量(实际上存储的可能是某个变量实际的值,也可能是某个对象的内存地址)将基于缓存行的概念被换入CPU缓存(既L1、L2缓存)。通常情况下CPU在工作中将优先尝试命中L1、L2缓存中的数据,如果没有命中才会到主存中重新读取最新的数据,这是因为从L1、L2缓存中读取数据的时间远远小于从主存中读取数据的时间,且由于边际效应的原因,往往L1、L2中的数据命中率都很高(参见下表)

设备类型 操作时间范围
CPU L1 缓存 1ns—3ns
CPU L2 缓存 2ns—10ns
CPU L3 缓存 10ns左右
接入北桥的主存 80ns—100ns
(上表中时间的单位是纳秒。1秒=1000000000纳秒,也就是说1纳米极其短暂,短到光在1纳秒的时间内只能前进30厘米)。

请注意:每一个CPU物理内核都有其独立使用的L1、 L2缓存,一些高级的CPU中又存在可供多核共享的L3缓存,以便MESI的工作过程中能在L3中进行数据可见性读取。另外请注意,当CPU内核对数据进行修改时,通常来说被修改的数据不会立即回存到主存中(但最终会回写到主存中)。

那么当某一个数据(对象)A在多个处于“运行”状态的线程中进行读写共享时(例如ThreadA、ThreadB和ThreadC),就可能出现多种问题:首先是多个线程可能在多个独立的CPU内核中“同时”修改数据A,导致系统不知应该以哪个数据为准;又或者由于ThreadA进行数据A的修改后没有即时写会内存ThreadB和ThreadC也没有即时拿到新的数据A,导致ThreadB和ThreadC对于修改后的数据不可见。

3.MESI 协议及 RFO 请求

为了解决这个问题,CPU工程师设计了一套数据状态的记录和更新协议——MESI(中文名:CPU缓存一致性协议)。这个规则实际上由四种数据状态的描述构成:

M(修改,Modified):本地处理器已经修改缓存行,即是脏行,它的内容与内存中的内容不一样,并且此 cache 只有本地一个拷贝(专有);
E(专有,Exclusive):缓存行内容和内存中的一样,而且其它处理器都没有这行数据;
S(共享,Shared):缓存行内容和内存中的一样, 有可能其它处理器也存在此缓存行的拷贝;
I(无效,Invalid):缓存行失效, 不能使用。

MESI 协议存在的问题

由于寄存器中处于“M”状态的数据不会立即更新到主存(虽然最终会写入主存),那么就导致在其它寄存器中的相同数据会出现短暂的数值差异。这个短暂的时间真的是非常短——一个纳秒级别的时间,但是在高并发情况下,依然会出现偶发性问题。也就是说在某一个变量值被多个线程共享的情况下,当某一个线程改变了这个变量的值,由于MESI协议的固有问题,另一个线程在很短暂的时间内是无法知晓值的变化的(但最终会知晓)。

要解决这个问题,其中一种方式就是使用锁(java中的synchronized方式或者lock锁的方式都行),但是这种解决方式又比较“重量级”,因为实际上这种场景下我们并不需要保证操作的原子性,所以还有一种更“轻量级”的解决方法,就是使用volatile关键字(这是volatile关键字的主存一致性场景,将在后面一篇文章中专门介绍)。