前言

  • JAVA线程状态经常有人搞混,说5种6种甚至7种都有。其实5种是操作系统的线程状态,JAVA有6种,Thread源码的枚举类型statue有提现。
  1. NEW:
    被创建,还没有调用start()方法;
  2. RUNNABLE
    运行中,JAVA中把操作系统的就绪(ready),运行(running)统称为”运行中“。
    线程对象被创建后,其他线程(如main)调用了该对象的start方法,该状态的线程位于可运行线程池中,等待被线程调度选择,获取cpu权限,此时是就绪(ready)。
    就绪状态的线程获取cpu时间片后变为运行中(running)状态。
  3. BlOCKED:表示线程进入等待状态,也就是线程因为某种原因放弃了 CPU 使用权。
    • 等待阻塞:运行的线程执行了Thread.sleep() 、wait()、 join() 等方法, JVM 会把当前线程设置为等待状态,当 sleep 结束、join 线程终止或者线程被唤醒后,该线程从等待状态进入到阻塞状态,重新抢占锁后进行线程恢复;
    • 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被其他线程锁占用了,那么jvm会把当前的线程放入到锁池中 ;
    • 其他阻塞:发出了 I/O请求时,JVM 会把当前线程设置为阻塞状态,当 I/O处理完毕则线程恢复;
  4. WAITING:
    等待状态,没有超时时间,要被其他线程或者有其它的中断操作。
    无条件等待,当线程 调用wait()、join()、LockSupport.park() 不加超时时间的方法之后所处的状态,如果没有被唤醒或等待的线程没有结束,那么将一直等待,当前状态的线程不会被分配CPU资源和持有锁;
  5. TIMED_WAITING:
    超时等待状态,超时以后自动返回;
    有条件的等待,当线程调用 sleep(long)、wait(long)、join(long)、LockSupport.park(long)、LockSupport.parkNanos(long)、LockSupport.parkUntil(long)方法之后所处的状态,在指定的时间没有被唤醒或者等待线程没有结束,会被系统自动唤醒,正常退出。
  6. TERMINATED:
    终止状态,表示当前线程执行完毕 。
    执行完了run()方法。其实这只是Java语言级别的一种状态,在操作系统内部可能已经注销了相应的线程,或者将它复用给其他需要使用线程的请求,而在Java语言级别只是通过Java代码看到的线程状态而已。

    创建

  • 继承Thread类,重新run()
  • 定义Runnable接口的实现类,重新run(),然后new Thread(new xxx())

    run()方法的返回值是void,

  • 创建Callable接口的实现类,并实现call()

    call()方法是有返回值的,返回值是泛型,和Future,FutureTask配合可以用来获取异步执行的结果。

  • 线程池创建

    • newFixedThreadPool(int nThreads)固定长度,当线程发生未预期的错误而结束时,线程池会补充一个新的线程。
    • newCachedThreadPool()可缓存的线程池,若线程池规模超过了处理需求,自动回收空闲线程,当需求增加,自动添加新线程,线程池规模不受任何限制。
    • newSingleThreadExecutor()单线程的Executor,他创建单个工作线程来执行任务,若线程异常结束,会创建一个新的线程代替,特定时确保任务在队列中顺序串行执行。
    • newScheduledThreadPool(int corePoolSize)固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。

      状态切换

      JAVA基础--多线程 - 图1
      JAVA基础--多线程 - 图2
  • 《并发编程的艺术》
    JAVA基础--多线程 - 图3

  • 代码示例
    1. package shen.example.demo.mutithread;
    2. import java.util.concurrent.locks.LockSupport;
    3. /**
    4. * 线程状态demo
    5. */
    6. public class ThreadStatusDemo {
    7. public static void main(String[] args) throws InterruptedException {
    8. //创建一个线程
    9. System.out.println("创建子线程new");
    10. Thread thread = new Thread(()->{
    11. //2.线程状态:RUNNABLE
    12. System.out.println(Thread.currentThread().getState());
    13. System.out.println("子线程运行中...开始调用park()");
    14. LockSupport.park();
    15. //睡眠
    16. try {
    17. System.out.println("子线程运行中...调用sleep(long)");
    18. Thread.sleep(1000);
    19. } catch (InterruptedException e) {
    20. e.printStackTrace();
    21. }
    22. //获取同步锁
    23. System.out.println("子线程运行中...进入同步代码块");
    24. synchronized (ThreadStatusDemo.class){
    25. try {
    26. Thread.sleep(500);
    27. } catch (InterruptedException e) {
    28. e.printStackTrace();
    29. }
    30. }
    31. });
    32. //1.线程状态:NEW
    33. System.out.println(thread.getState());
    34. System.out.println("子线程启动start");
    35. //启动线程
    36. thread.start();
    37. //主线程睡眠,等待子线程thread调用park()
    38. Thread.sleep(500);
    39. //3.线程状态:WAITING
    40. Thread.sleep(500);
    41. System.out.println(thread.getState());
    42. //唤醒子线程,使子线程调用sleep(long)
    43. LockSupport.unpark(thread);
    44. //4.TIMED_WAITING
    45. System.out.println(thread.getState());
    46. //主线程获取当前类锁
    47. synchronized (ThreadStatusDemo.class){
    48. Thread.sleep(1200);//等待子线程进入同步代码块阻塞
    49. //5.BLOCKED
    50. System.out.println(thread.getState());
    51. }
    52. Thread.sleep(1200);
    53. System.out.println("子线程运行结束");
    54. //5.TERMINATED
    55. System.out.println(thread.getState());
    56. }
    57. }

  • 程序计数器为什么私有?
    • 为了多线程中线程切换后能够恢复到正确的执行位置。
    • 字节码解释器通过改程序计数器来依次执行指令,从而实现代码的流程控制。
  • 虚拟机栈为什么私有
    • java方法执行时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用信息等,从方法调用到完成,对应栈帧入栈出栈。
  • 本地方法栈为什么私有
    • 与虚拟机栈类似,区别为native方法服务
  • 什么是上下文切换
    • CPU分配时间片轮转模拟多核心,当一个时间片用完重新进入就绪状态让其他线程执行,这就是一次上下文切换。
    • 任务从保存到加载的过程。
  • 死锁的四个条件

    • 互斥
    • 请求与保持
    • 不剥夺
    • 循环等待
      1. package com.example.demo.Lock;
      2. /**
      3. * TODO
      4. *
      5. * @author Skiray
      6. * @date 2021/6/17 10:51
      7. */
      8. public class DeadLock {
      9. private static Object re1 = new Object();
      10. private static Object re2 = new Object();
      11. public static void main(String[] args){
      12. new Thread(()->{
      13. synchronized (re1){
      14. System.out.println(Thread.currentThread().getName() + "get re1");
      15. try{
      16. Thread.sleep(1000);
      17. }catch (InterruptedException e){
      18. e.printStackTrace();
      19. }
      20. System.out.println(Thread.currentThread() + "waiting get re2");
      21. synchronized (re2){
      22. System.out.println(Thread.currentThread() + "get re2");
      23. }
      24. }
      25. },"线程 1 ").start();
      26. new Thread(()->{
      27. synchronized (re2){
      28. System.out.println(Thread.currentThread()+"get re2");
      29. try{
      30. Thread.sleep(1000);
      31. }catch (InterruptedException e){
      32. e.printStackTrace();
      33. }
      34. System.out.println(Thread.currentThread() + "waiting get re1");
      35. synchronized (re1){
      36. System.out.println(Thread.currentThread() + "get re1");
      37. }
      38. }
      39. },"线程2").start();
      40. }
      41. }
  • sleep与 wait

    • 区别sleep没有释放锁,wait释放锁
    • wait常用于线程间交互/通信,sleep常用于暂停。
    • wait需要手动唤醒,notify,notifyAll。
    • sleep执行完成自动苏醒,超时等待wait(long xx)也会自动苏醒。
  • 为什么调用start时会执行run
    • new一个Thread,线程进入新建状态,start后,启动线程并进入就绪,等分配到时间片就可运行。start会执行线程的相应准备工作,然后自动执行run内容。直接执行run会把run方法当成一个main线程下的普通方法去执行,并不会在某个线程中执行。
  • synchronized
    • 保证修饰的方法或代码块在任意时刻只能有一个线程执行。早期版本属于重量级锁。低效。
    • 因监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock实现的,java的线程是映射到OS的原生线程之上的,挂起或唤醒线程都需要OS帮忙,而OS实现线程间切换需要从用户态转换到内核态,相对比较耗时,耗成本。
    • 6之后引入了自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等减少锁的开销。现在开源很多用了synchronized。
  • synchronized使用

    • 修饰实例方法

      对当前对象实例加锁,进入同步代码前需要获得当前对象实例的锁。 synchronized void f(){}

    • 静态方法

      给当前类加锁,会作用于类的所有对象实例,进入同步代码前要获得当前calss的锁。因静态成员变量不属于任何实例对象,是类成员(不管new多少,static只有一份),所以,一线程A调用一个实例对象的非静态synchronized()方法,线程B需要调用这个实例对象所属类的静态synchronized方法是允许的,不会发送互斥现象,因访问静态synchronized()方法占用的锁是当前类的锁,而非静态synchronized()方法占用的锁是当前实例的锁

    • 代码块

      指定加锁对象,对给定对象/类加锁。synchronized(this)表示进入同步代码块前要获得给定对象的锁,synchronized(类.class)表示进入同步代码块前要获得当前class的锁。 synchronized(this){}

  • synchronized可保证方法或代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时还保证贡献变量的内存可见性,java每个对象都可作为锁,这是synchronized实现同步的基础;普通同步锁当前实例对象;静态同步方法锁当前类的class对象;同步方法块锁括号里的对象。

  • synchronized与volatile

    volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可访问该变量,其他线程被阻塞。volatile仅能使用在变量级别:synchronized可使用在变量、方法和类级别volatile仅能实现变量的修改可见性;不能保证原子性;synchronized可保证变量的修改可见性和原子性。volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。volatile编辑的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

  • synchronized与Lock

    首先synchronized是java关键字,Lock是java类;synchronized无法判断是否获取锁的状态;Lock可判断是否获取到锁;synchronized会自动释放锁(a、线程执行完同步代码会释放锁b、线程执行过程中发生异常会释放锁)Lock需要在finally中手动释放(unlock())容易造成线程死锁。synchronized锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平;

直接lock.unlock 会报IllegalMonitorStateException异常。 直接;

  • 总结

    • synchronized加static和synchronized(class)代码块都是给Class类上锁。
    • synchronized关键字加到实例方法是给对象实例上锁。
    • 尽量不用synchronized(String a )字符串缓冲池具有缓存功能。
      1. package com.example.demo.Lock;
      2. /**
      3. * TODO
      4. *
      5. * @author Skiray
      6. * @date 2021/6/17 17:18
      7. */
      8. //双重校验锁实现单例(线程安全)
      9. public class Singleton {
      10. private volatile static Singleton uniqueInstance;
      11. private Singleton() {
      12. }
      13. public static Singleton getUniqueInstance(){
      14. // 先判断是否已经实例化过,没有才加锁
      15. if (null == uniqueInstance ){
      16. synchronized (Singleton.class){
      17. if (null== uniqueInstance){
      18. uniqueInstance = new Singleton();
      19. }
      20. }
      21. }
      22. return uniqueInstance;
      23. }
      24. }
  • volatile关键字修饰也很重要,uniqueInstance = new Singleton(); 分三步

    • 1为uniqueInstance分配空间;
    • 2初始化 uniqueInstance;
    • 3将 uniqueInstance 指向分配的内存地址;

      但是JVM具有指令重排的特性,执行顺序不一定是123.指令重排多线程下会导致一个线程还没有初始化。如线程1执行1和3,此时线程2调用方法,发下uniqueInstance不为空 null == uniqueInstance不成立,因此返回uniqueInstance,但此时uniqueInstance还没初始化。

  • 构造方法可synchronized 修饰吗?

    • 不,构造方法本身就是线程安全的。
  • 为什么要CPU缓存?
    • 解决内存cpu速度不匹配的问题。
  • synchronized 和 volatile区别
    • volatile是线程同步的轻量级实现,性能好,只能用于变量。
    • volatile能保证数据的可见性,但不能保证数据的原子性,synchronized都能保证。
    • volatile主要用于解决变量在多个线程之间的可见性,synchronized解决的是多个线程之间访问资源的同步性。
  • ThreadLocal
    • 类的作用是创建线程私有的变量。
    • 若创建了一个ThreadLocal变量,访问这个变量的每个线程都会有这个变量的本地副本,可get/set获取默认值或将其更改为当前线程所存的副本的值,避免了线程安全的问题。
  • Runnable 和 Callbale
    • Runnbale没有返回值,或抛出检查异常
  • execute 和submit区别
    • execute体检不需要返回值的任务,无法判断是否成功。
    • submit提交返回值任务,线程会返回一个Future类,通过Fulture可判断任务是否执行成功,get获取返回值

  • wait/notify/notifyAll

    • wait()将当前运行的线程挂起(让其他进入阻塞状态),直到notify或者notifyAll方法来唤醒线程。
    • wait(Long timeout):没手动唤醒,时间到自动唤醒。

      wait是一个本地方法,底层通过监视器锁的对象完成,没有获取到monitor对象的权限会报错,获取monitor权限java中只能通过synchronized关键字完成。 必须再同步范围内使用,否则抛IllegaMonitorStateException异常 这三个方法作用是线程间的协作,位于Object类中。wait等待其实是对象mointor,由于每个对象都有一个内置的monitor对象,所以每个类都理应由wait/notify方法

  • sleep/yield/join

    • sleep暂停当前线程指定实现(ms)、区别:wait依赖于同步,sleep可以直接使用。深层区别:sleep方法只是暂时让出cpu执行权限,并不释放锁。而wait方法则需要释放锁。
    • yield暂停当前线程,以便其他线程有机会执行,不过不能指定暂停的时间,也不能保证当前线程马上停止。yield方法只是将Running状态转变为Runnable状态。
    • join方法左作用是父线程等待子线程执行完成后再执行,就是将异步执行的线程合并为同步的线程

参考1
参考2


操作系统

进程间五种通信方式

  1. 管道:速度慢,容量有限,只有父子进程能通讯。
  2. FIFO:任何进程间都能通讯,但速度慢。
  3. 消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题。
  4. 信号量:不能传递复杂消息,只能用来同步。
  5. 共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存。

    死锁条件

  6. 互斥条件:一个资源每次只能被一个线程使用;

  7. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放;
  8. 不剥夺条件:进程已经获得的资源,在未使用完之前,不能强行剥夺;
  9. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。

    避免死锁

  10. 破坏“请求和保持”条件:让进程在申请资源时,一次性申请所有需要用到的资源,不要一次一次来申请,当申请的资源有一些没空,那就让线程等待。不过这个方法比较浪费资源,进程可能经常处于饥饿状态。还有一种方法是,要求进程在申请资源前,要释放自己拥有的资源。

  11. 破坏“不可抢占”条件:允许进程进行抢占,方法一:如果去抢资源,被拒绝,就释放自己的资源。方法二:操作系统允许抢,只要你优先级大,可以抢到。
  12. 破坏“循环等待”条件:将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序提出(指定获取锁的顺序,顺序加锁)。

线程池

三大方法

  • 工具类创建

    • Executors.newSingleThreadExecutor()一池一线程
    • Executors.newFixedThreadPool(int ) 固定线程
    • Executors.newCachedThreadPool() 可扩展integer.MAX_VALUE 几十亿
      底层
  • ThreadPoolExecutor()

    七大参数

    public ThreadPoolExecutor(int corePoolSize,  // 核心线程数,最小线程数,常驻核心线程数
                                int maximumPoolSize  //最大线程数
                                long keepAliveTime, // 空闲线程存活时间
                                TimeUnit unit, // 时间单位
                                BlockingQueue<Runnable> workQueue, // 任务队列,尚未执行的任务
                                ThreadFactory threadFactory, // 线程工厂,创建线程
                                RejectedExecutionHandler handler){ // 拒绝策略 
                                }
    

    四种拒绝策略

  • JDK内置四种拒绝策略,均实现; RejectedExecutionHandle接口

  • AbortPolicy():默认,直接抛出RejectedExecutionException异常阻止系统正常运行。
  • CallerRunsPolicy:不抛异常,将任务会退给调用者,降低任务流量。
  • DiscardOldesPolicy:抛弃队列中等待最久的任务。
  • DiscardPolicy:丢弃,不处理也不抛出异常。

  • 在工作中单一的/固定数的/可变的三种创建线程池的方法哪个用的多?超级大坑
  • 我们只是用自定义的ThreadPoolExecutor()

线程池数量怎么确定

  1. 一般来说,如果是CPU密集型应用,则线程池大小设置为N+1。
  2. 一般来说,如果是IO密集型应用,则线程池大小设置为2N+1。
  3. 在IO优化中,线程等待时间所占比例越高,需要越多线程,线程CPU时间所占比例越高,需要越少线程。这样的估算公式可能更适合:最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目