线程的状态

  • 新建

    new 线程对象

  • 就绪状态

    调用线程对象的start()方法

  • 阻塞状态

    获取不到锁对象

  • 等待状态

    调用锁对象wait();方法

  • 计时等待

    调用Thread的sleep方法

  • 退出状态

    线程中的所有代码执行完毕

线程池

概念

一个用于存储线程对象的容器。传统的编程,需要通过线程执行任务的时候,都是临时创建线程对象,使用完毕线程自动会处于退出状态。比较浪费时间和资源。线程池的基本思想就是将创建的线程对象存入一个容器中,需要使用线程时候从容器中获取,使用完毕之后将线程归还到容器,可以让线程对象得到复用

步骤:

  1. 创建池子
  2. 将需要执行的任务提交给线程池
  3. 关闭线程池

创建默认线程池

  1. 创建线程池,返回一个操作线程池的对象

    ExecutorsService service =  Executors.newCachedThreadPool();
  2. 通过ExecutorsService提交需要执行的任务

    service.submit(Runnable runnable);
    service.submit(Callable callable);

    示例:

    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.submit(()->{
    System.out.println(Thread.currentThread().getName()+"在执行了");
    });

    Thread.sleep(3000);

    executorService.submit(()->{
    System.out.println(Thread.currentThread().getName()+"在执行了");
    });
  3. 如果所有线程都使用完毕,而且后续没有其他线程需要使用,则可以关闭整个线程池

    executorService.shutDown();

创建一个指定上限的线程池

ExecutorService executorService = Executors.newFixedThreadPool(10);// 参数10代表最大线程数量

基于ThreadPoolExecutor创建线程池

  1. 示例代码:

    ThreadPoolExecutor pool = new ThreadPoolExecutor(
    2,
    5,
    5,
    TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(10),
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.AbortPolicy()
    );
  2. 参数详解

    | 参数 | 解释 |
    | ——- | —————————————————————————————— |
    | 参数1 | 核心线程数量(就算达到指定空闲时间,核心线程不会被自动回收) |
    | 参数2 | 最大线程数量(当达到指定的空闲时间,(最大线程数-核心线程数)的空闲线程会被自动回收,减少资源浪费) |
    | 参数3 | 空闲时间(达到多长时间,开始进行空闲线程的回收) |
    | 参数4 | 时间单位,需要通过TimeUnit的枚举进行指定 |
    | 参数5 | 阻塞队列,当提交的线程数量超过最大最大线程数时,会将其它任务放到阻塞队列中进行排队 |
    | 参数6 | 创建线程的工厂,线程池内部创建线程是通过线程工厂进行创建 |
    | 参数7 | 任务的拒绝策略,当提交的任务数量超过 (最大线程数量+阻塞队列的长度) 就会触发拒绝策略 |

volatile问题

  1. 多线程共享数据的可见性的问题:

    当一个线程修改了共享数据的值后,其它线程不一定能够及时获取到修改后的最新值

  2. 问题产生的原因

    线程操作共享数据时,默认是先将共享数据中的值拷贝到线程本地的变量副本中,后续的操作操作的是变量副本的值。

  3. 解决方式

    • volatile关键字

      在共享数据前面通过volatile进行修饰。强制要求线程每次都重新获取共享数据中的最新值。

    • synchronized关键字

      在每次执行时,先清空变量副本,重新从共享数据中拷贝最新值到变量副本中。

原子性(Atomic)

概念:

一个包含多个操作的逻辑单元,要么同时执行成功,要么同时执行失败。强调的是一个逻辑单元是一个整体,不可分割。

  1. count++问题

    count++ 包含如下三个步骤:

    • 拷贝共享数据中的值到变量副本中
    • 修改变量副本中的值(+1)
    • 将变量副本中的值重新赋值给共享数据

    在执行到任意步骤的时候都可能会被其它线程打断,最终造成数据有误。所以count++的操作不是原子性的。

  2. 原子性问题产生的本质原因:

    一个线程中的某个操作可能包含多个步骤,在执行到某一步的时候被其它线程给打断。

  3. 为什么synchronized能够解决原子性的问题:

    加锁之后,不会出现多个线程同时操作共享数据的问题。操作不可能被打断,所以肯定不会出现问题。

原子类AtomicInteger

基本使用:

public static void main(String[] args) {
// 1. 创建AtomicInteger对象
// AtomicInteger integer = new AtomicInteger(); // 初始值默认为0
AtomicInteger integer = new AtomicInteger(10);
System.out.println(integer);

// 2. 获取值并自增的方法(返回的原始值)
int value = integer.getAndIncrement();
System.out.println(value);
System.out.println(integer.get());

// 3. 获取值并自增的方法(返回的自增后的值)
int value2 = integer.incrementAndGet();
System.out.println(value2);

// 4. 获取值并设置新的值(返回的是原来的值)
int value3 = integer.getAndSet(20);
System.out.println(value3);
System.out.println(integer.get());
}

CAS算法

compare and swarp

核心:在修改共享数据(内存值)的时候比较线程中记录的获取到旧值和共享数据中的内存值是否一致

  1. 如果一致,证明没有其他线程操作过内存值,那么直接进行修改
  2. 如果不一致,证明已经有其它线程操作过内存值,那么需要重新将最新值获取到,重新进行操作。这个操作叫自旋。

ConcurrentHashMap jdk1.7原理

  • 创建对象时

    1. 创建固定长度为16的大数组,加载因子是0.75
    2. 创建了一个长度为2的小数组,并将其地址值赋值给大数组的0索引
  • 存入元素时

    1. 根据键的hash值,计算出在大数组中的索引
    2. 判断大数组对应的索引值是否是null
      • 是 null
        • 以大数组0号索引为模板创建长度为2的小数组,将其地址值赋值大数组的当前索引
        • 会根据键进行二次hash,计算出小数组中的索引,直接存入
      • 不是null
        • 根据地址值找到小数组
        • 会根据键进行二次hash,计算出小数组中的索引,判断是否需要扩容,如果需要则扩容两倍
        • 判断小数组的索引位置是否为null
          • 如果为null,直接存入
          • 如果不为null,调用equals方法判断属性值是否一致
            • 如果一致,不会进行存入
            • 如果不一致,将元素存入小数组的对应索引,将老的元素挂载到下面形成hash桶结构
  • 如何解决安全问题

    当有一个线程在操作大数组中的某一个索引时,会将对应索引下的小数组锁住。其它索引的位置,还是可以被其它线程操作。

ConcurrentHashMap jdk1.8原理

  1. 如果使用空参构造创建ConcurrentHashMap对象,则什么事情都不做。
    在第一次添加元素的时候创建哈希表
  2. 计算当前元素应存入的索引。
  3. 如果该索引位置为null,则利用cas算法,将本结点添加到数组中。
  4. 如果该索引位置不为null,则利用volatile关键字获得当前位置最新的结点地址,挂在他下面,变成链表。
  5. 当链表的长度大于等于8时,大数组的长度等于64,自动转换成红黑树
  6. 以链表或者红黑树头结点为锁对象,配合悲观锁保证多线程操作集合时数据的安全性

CountDownLatch

应用场景:某一个线程需要等待其它线程之后完毕之后再执行

image-20211018165524678

Semaphore

应用场景:控制同时执行的线程的数量

编码步骤:

// 场景Semaphore对象,并且传入允许同时执行线程数量
Semaphore semaphore = new Semaphore(数量);

run(){
semaphore.acquire(); // 获取许可证(如果能够获取到,数量会减1,如果数量为0,则获取不到,那么代码会在这一行阻塞)
//需要执行的核心代码
//需要执行的核心代码
semaphore.release(); // 归还许可证(数量会加1)
}

复习

  1. 线程的状态,状态切换

    • 新建

      创建线程对象

    • 就绪

      调用start方法

    • 阻塞

      获取不到锁

    • 等待

      wait()方法

    • 计时等待

      sleep()方法

    • 退出

      run方法中的代码执行完毕

  2. 线程池

    • 概念

      用于存储线程对象的容器。当我们需要执行任务的时候,只需要将任务提交给线程池,线程池会自动创建线程执行任务。当线程使用完毕后,会将线程归还到线程池中,方便复用。

    • Executors创建线程池

      • 创建默认的线程池

        ExecutorService service = Executors.newCachedThreadPool();

      • 创建有上限的线程池

        ExecutorService service = Executors.newFixedThreadPool(最大数量);

      • 通过ExecutorService 执行任务

        service.submit(Runnable r);

        service.submit(Callable c);

      • 销毁线程池

        service.shutdown();

    • ThreadPoolExecutor创建

      new ThreadPoolExecutor(
      核心线程数量,
      最大线程数量,
      空闲线程的最大空闲时间,
      时间单位(TimeUnit),
      阻塞队列,
      创建线程的工厂,Executors.defaultThreadFactory();,
      拒绝策略ThreadPoolExecutor.AbortPolicy();
      )
  3. volatile问题

    • 可见性问题

      当一个线程修改了共享数据中的值,另一个线程可能无法获取到最新的值

    • 产生原因

      默认情况下,线程操作共享数据时,先拷贝共享数据中的值到变量副本中,后续的操作是操作变量副本。

    • 解决办法

      • volatile关键字

        在共享数据的数据类型前添加volatile,强制要求线程每次操作都去获取最新值

      • synchronized关键字

        每次操作时,先清空变量副本,需要重新获取最新值

  4. 原子性

    • 概念

      一个包含多个操作的逻辑单元,要么同时成功,要么同时失败。这一个逻辑单元是一个不可分割的整体。

    • count++的问题

      • count++底层的操作过程:

        1. 获取到共享数据中的内存值,存入变量副本
        2. 对变量副本中的值进行+1
        3. 将最新的值重新赋值给共享数据
      • 上述的3个步骤,执行到任意一步,都有可能被其它线程中断。

      • 解决方式

        1. synchronized关键字

          加锁后,同一时刻只有一个线程在操作共享数据,所以不可能被其它线程中断。

        2. 原子类AtomicInteger

    • AtomicInteger使用

      • 创建对象

        AtomicInteger i = new AtomicInteger(); // 默认值是0

        AtomicInteger i = new AtomicInteger(初始值);

      • 核心方法

        • int get();

          获取值

        • int getAndIncrement();

          获取旧值,并自增

        • int incrementAndGet();

          先自增,并获取新值

        • int getAndSet(int value);

          获取旧值,并重新设置新值

    • CAS算法

      • 概念

        Compare And Swarp 算法

      • 核心原理

        • 获取共享数据中的旧值,并且将其记录到线程中

        • 操作变量副本中的值

        • 在将变量副本中的最新的值,赋值给共享数据之前,会检查线程中记录的旧值和共享数据中的值是否一致

          • 相同

            没有其他线程操作过共享数据,直接修改

          • 不相同

            证明其他线程已经操作过共享数据,重新获取共享数据的值,到线程中,重新操作。这个操作叫做自旋

  5. 并发工具包下的常见类

    • ConcurrentHashMap

      • 概念

        jdk1.5版本中提供的一种兼顾性能和安全的集合类

      • 使用

        和传统的hashmap没有区别

      • 在jdk1.7和jdk1.8底层实现原理的区别

    • CountDownLatch

      • 作用

        实现让某一个线程在其它的线程执行完毕之后再执行

      • 核心方法

        • 创建对象

          CountDownLatch latch = new CountDownLatch (等待线程数量);

        • 被等待的线程

          当核心代码执行完毕之后,调用latch .countDown();

        • 等待的线程

          在执行核心代码前,调用latch.await();

    • Semaphore

      • 作用

        限定同时执行的线程的数量

      • 核心方法

        • 创建对象

          Semaphore s = new Semaphore(允许的线程数量)

        • 线程中的代码

          s.acquire(); // 获取许可证(计数器-1,如果计数器的值等于0,则无法获取徐克成)
          // 核心代码
          // 核心代码
          s.release(); // 归还许可证(计数器+1)