Java的基本知识之线程池篇

  1、基本概念

  1、共享资源

  多个线程对同一份资源进行访问(读写操作),该资源被称为共享资源。如何保证多个线程访问到的数据是一致的,则被称为数据同步或资源同步。

  2、线程通信

  线程通信,又叫进程内通信,和网络通信等进程间通信不同;多个线程实现互斥资源访问的时候,会互相发送信号。

  2、同步、异步、阻塞、非阻塞

  同步和异步是 获取结果的方式,阻塞和非阻塞是 等待结果中是否能够完成其他事情。

  同步阻塞(BIO),需要等待读取完客户端的数据,同时需要阻塞的判断客户端是否有数据。

  同步非阻塞(NIO),需要等待读取完客户端的数据,但是轮询的方式判断客户端是否有数据,可以去做其他事情。

  异步阻塞,等待结果回调通知,cpu处于等待的休眠中。

  异步非阻塞(AIO),操作系统来完成读取的操作,读取完毕通知回调,使用线程池的方式去轮询客户端是否有数据,可以去做其他事情。

  并发,单个 cpu 可以处理多个任务,但是同一时刻只有一个任务在运行。

  并行,多个任务在多个 cpu 上运行,实现真正的同一时刻运行。

  2、生命周期

  3、守护线程

  一般使用 new Thread 创建的线程都是非守护线程,也称用户线程。设置为守护进行的方式就是,在 run 之前,调用setDaemon(true)。守护线程,仅仅是为用户线程提供服务,那么一旦所有的用户线程都运行完毕,守护进行也会结束。相反,只要有非守护线程存在,那么守护线程就不会终止。

  守护线程的应用场景:垃圾回收、心跳检测、拼音检查线程

  package com.vim;

  import java.util.concurrent.TimeUnit;

  public class App {

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

  Thread t = new Thread(()->{

  while (true){

  try {

  TimeUnit.SECONDS.sleep(1);

  System.out.println("......");

  }catch (Exception e){

  e.printStackTrace();

  }

  }

  });

  //只有t设置了此处,才会随着父进程的退出而退出

  t.setDaemon(true);

  t.start();

  TimeUnit.SECONDS.sleep(5);

  System.out.println("main is over!");

  }

  }

  4、线程 yield、sleep

  yield,称为线程让步,从 RUNNING 状态转为 RUNNABLE 状态,有可能切换之后,再次抢到执行权,进入 RUNNING 状态。

  sleep,会阻塞该线程,进入 BLOCKED 状态,此时是挂起,让出cpu;只有阻塞时间到了,才会进入 RUNNABLE 状态,并不一定马上获取到 cpu 的执行权。

  sleep(0) 的作用是,触发操作系统立刻重新进行一次CPU竞争,竞争的结果也许是当前线程仍然获得CPU控制权,也许会换成别的线程获得CPU控制权。

  package com.vim;

  public class App {

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

  new Thread(()->{

  try {

  while (true){

  Thread.sleep(0);

  }

  }catch (Exception e){

  e.printStackTrace();

  }

  }).start();

  }

  }

  5、线程 interrupt

  wait、sleep、join 使当前线程进入阻塞状态,而 interrupt 可以打断阻塞,不过仅仅是打算当前的阻塞状态。当前线程会抛出一个 InterruptException 异常,就像一个信号通知。此通知会将 interrupt flag 置为 true,不过针对阻塞状态下的中断,该状态位会被重置为 false。通过 thread.isInterrupted() 来判断,当然,阻塞状态下的,会被清除,从而影响该方法的结果。

  package com.sample.modules.test;

  import java.util.concurrent.TimeUnit;

  public class Test {

  //中断一个线程,中断位 true

  public static void test1() throws Exception{

  Thread t = new Thread(()->{

  //2.获取当前的 interrupt flag 状态为 true

  System.out.println(Thread.currentThread().isInterrupted());

  });

  t.start();

  //1.中断线程

  t.interrupt();

  TimeUnit.MINUTES.sleep(4);

  }

  //中断一个线程,中断位 false

  public static void test2() throws Exception{

  Thread t = new Thread(()->{

  //2.清除中断位

  Thread.interrupted();

  //3.获取当前的 interrupt flag 状态为 false

  System.out.println(Thread.currentThread().isInterrupted());

  });

  t.start();

  //1.中断线程

  t.interrupt();

  TimeUnit.MINUTES.sleep(4);

  }

  //中断一个线程,中断位 false

  public static void test3() throws Exception{

  Thread t = new Thread(()->{

  //2.中断wait、sleep、join导致的阻塞

  try {

  TimeUnit.SECONDS.sleep(5);

  }catch (InterruptedException e){

  System.out.println("i am interrupted");

  }

  //3.获取当前的 interrupt flag 状态为 false

  System.out.println(Thread.currentThread().isInterrupted());

  });

  t.start();

  //1.中断线程

  t.interrupt();

  TimeUnit.MINUTES.sleep(4);

  }

  //阻塞之前中断结果:false、true、i am interrupted、false

  public static void test4(){

  Thread.interrupted();

  System.out.println("interrupt flag: "+Thread.currentThread().isInterrupted());

  Thread.currentThread().interrupt();

  System.out.println("interrupt flag: "+Thread.currentThread().isInterrupted());

  try {

  TimeUnit.SECONDS.sleep(1);

  }catch (InterruptedException e){

  System.out.println("i am interrupted");

  }

  System.out.println("interrupt flag: "+Thread.currentThread().isInterrupted());

  }

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

  test3();

  }

  }

  6、线程 join

  parent 线程调用 child 线程的 join 方法,实际上调用的是 join(0) ,该方法加了锁,循环判断 child 线程的存活状态,当然,每次循环中调用 wait(0) 方法,这样的话可以让其他线程也进入 join(0) 方法。

  //当前方法没有上锁

  public final void join() throws InterruptedException {

  join(0);

  } 无锡看男科医院哪家好 https://yyk.familydoctor.com.cn/20612/

  //此方法上锁,此时其他线程可以进入 join(),但不可以进入 join(0)

  //不断的检查线程是否 alive,调用 wait(0),这样就释放了锁,其他的线程就可以进入 join(0),也就是可以有多个线程等待某个线程执行完毕

  //一旦线程不在 alive,那么就会返回到 join() 方法,调用的线程就可以继续执行下去

  public final synchronized void join(long millis){

  if (millis == 0) {

  while (isAlive()) {

  wait(0);

  }

  }

  }

  7、线程通知 notify、wait

  这两个方法,来源于 Object 类,两者配合使用。wait 方法属于对象方法,在调用之前必须先获取该对象的 monitor 锁,调用之后,就会释放该对象的 monitor 锁,从而进入该对象关联的 waitset 中,等待其他线程使用 notify 唤醒。

  典型的生产消费场景:

  生产者,在生产产品时,对仓库(同步资源)进行上锁,如果当前仓库没有满,则放入产品,使用 notifyAll 通知消费者;如果当前仓库满了,则使用 wait 释放锁,进入 waitset 阻塞等待 notifyAll 通知。

  消费者,来到仓库消费,对仓库(同步资源)进行上锁,如果当前仓库有产品,则拿走产品,使用 notifyAll 通知生产者;如果当前仓库是空的,则使用 wait 释放锁,进入 waitset 阻塞等待 notifyAll 通知。

  当 notifyAll 来临的时候,针对所有的生产者和消费者来说,都有拿到仓库钥匙的机会,就会再去竞争,再次进入以上判断逻辑。

  8、wait 和 sleep 的区别

  相同之处:

  使线程进入到阻塞状态;可以被 interrupt 中断;

  不同之处:

  wait 是 Object 共有,sleep 是 Thread 特有。

  wait 必须运行在同步方法中,sleep不需要。

  wait 会释放锁,如果sleep放在同步方法中,并不会释放锁。

  9、线程异常处理

  package com.sample.modules.test;

  public class Test {

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

  Thread t = new Thread(()->{

  int i = 1/0;

  });

  t.setUncaughtExceptionHandler((thread, e)->{

  System.out.println("exception...");

  });

  t.start();

  }

  }

  10、计算机内存模型 和 Java 内存模型

  原理追溯:cpu 在执行指令的时候,数据来源于主内存(RAM),由于两者速度的严重不对等,之间出现了缓存 cache;一般分为 L1、L2、L3 缓存,每个 CPU 核心包含一套 L1,共享 L2 和 L3 缓存。

  缓存一致性问题:当多个处理器的运算任务都涉及同一块主内存区域时,每个 cpu 从主内存中取出变量,放到本地 cache 中,进行计算之后,写入到 cache 中,再由 cache 刷新到主内存中。

  读取主内存 i 到 cache 中

  对 i 进行 ++

  将结果写回 cache

  将 cache 刷新回主内存。

  缓存一致性协议:cpu 在操作 cache 中的数据时,如果发现是一个共享变量,那么在写入的时候,会发出信号通知其他的 cpu 将该变量的 cache line 置为无效状态,其他 cpu 在进行该变量读取的时候,就需要去主内存中读取。

  相比计算机内存模型,Java 内存模型: 线程 == CPU, 工作内存 == CPU cache,主内存 == 主内存。

  12、并发编程三大特性

  原子性,多次操作中,要么全部得到执行,要么全部不执行。

  可见性,一个线程对共享变量,作了修改,那么其他线程立即可以看到修改后的值。

  有序性,程序代码在执行过程中的先后顺序。

  13、synchronized 关键字

  synchronized 关键字提供了一种锁的机制,能够保证共享变量的互斥访问,即同一时刻,只能有一个线程访问同步资源。

  内存方面,monitor enter 和 monitor exit 两个 JVM 指令,保证了任何线程在 monitor enter 之前必须从主内存中获取数据,在 monitor exit 之后,必须把更新的值刷新到主内存中。这两个 JVM 指令,严格的遵守 happends-before 原则,即一个 monitor exit 指令之前必须有一个 monitor enter 指令存在。

  该关键字对于同步资源的排他性访问,很有效,但是其他没有获取到 monitor 的线程,到底阻塞多久,能不能提前解除阻塞,这些都是未知的。为此,java 为我们提供了其他的解决方案,显式锁。如 ReentrantLock。

  14、AQS

  独占模式

  #获取

  1、尝试获取资源成功立即返回;失败的话,创建独占节点,利用CAS加入到队列尾部,进入自旋状态

  2、如果前一个节点是头节点,再次尝试获取资源,成功设置为头节点;否则挂起,等待被前驱节点唤醒

  #针对可中断来说

  1、普通的获取,在发生了中断后,会清除中断位,并在获取资源成功后,触发一次中断

  2、可中断的获取,在发生了中断后,也会清除中断位,但是直接抛出 interrupted 异常

  #释放

  1、释放同步状态

  2、获取当前节点的下一个节点,唤醒

  共享模式

  #获取

  1、获取同步状态,如果返回值>=0,则说明同步状态(state)有剩余,获取锁成功直接返回

  2、失败,向队列尾部添加一个共享类型的Node节点,随即该节点进入自旋状态

  3、前驱节点如果为头节点,再次判断同步状态是否(state)有剩余

  4、如果是,则说明当前节点可执行,同时把当前节点设置为头节点,并且唤醒所有后继节点

  两者区别

  1、独占锁的同步状态值为1,即同一时刻只能有一个线程成功获取同步状态;共享锁的同步状态>1,取值由上层同步组件确定

  2、独占锁队列中头节点运行完成后释放它的直接后继节点;共享锁队列中头节点运行完成后释放它后面的所有节点

  3、共享锁中会出现多个线程(即同步队列中的节点)同时成功获取同步状态的情况

  15、重入锁 ReentrantLock

  #获取

  1、如果当前有锁,且拥有者是当前线程,再次增加重入

  2、如果当前无锁,直接尝试获取锁,公平模式会判断是否有等待的线程,非公平模式下直接尝试独占资源

  16、计数器 CountDownLatch

  1、使用的是共享模式,初始化时设置 state 一个固定的数量

  2、await 方法,调用 sync 的中断 acquireShared 方法,重写 tryAcquireShared 获取方式,当 state 到达0的时候,才表示资源可获取

  3、countDown 方法,调用 sync 的 releaseShared 方法,不断的对 state 进行减一

  17、读写锁 ReentrantReadWriteLock

  写锁

  #获取

  1、当前处于无锁状态,独占模式获取写锁

  2、如果设置了 writerShouldBlock,直接返回 false

  1、当前处于有锁状态

  2、有读锁,直接返回 false

  3、有写锁,如果是当前线程,则重入,否则返回 false

  读锁

  1、如果有写锁,并且写锁不是当前线程,返回 -1

  2、如果没有写锁,尝试获取读锁,如果设置了 readerShouldBlock,进入再次判断

猜你喜欢

转载自www.cnblogs.com/djw12333/p/11206025.html