# 一.并发基础

# 1.什么是并行和并发?

并行和并发都是多任务处理的概念,但它们的含义不同。

并行是指两个或多个任务在同一时刻执行,即在同一时刻有多个任务在同时进行。在计算机领域,多核 CPU 可以实现并行处理,即多个 CPU 内核同时执行不同的任务。在并行处理中,任务之间相互独立,不需要等待其他任务的完成。

并发是指两个或多个任务在同一时间段内执行,即在同一时间段内有多个任务在交替进行。在计算机领域,单核 CPU 可以通过轮流执行各个任务来实现并发处理。在并发处理中,任务之间可能会相互影响,需要考虑任务的顺序和优先级,也需要考虑任务之间的同步和通信问题。

简单来说,如果是多个任务同时执行,就是并行;如果是多个任务交替执行,就是并发。并行处理通常需要多核 CPU 来支持,可以提高处理速度;而并发处理可以在单核 CPU 上实现,但需要考虑任务之间的同步和通信问题。

# 2.什么是活锁?

假设有两个线程,线程 1 和线程 2,它们都需要资源 A/B,假设线程 1 占有了 A 资源,线程 2 占有了 B 资源;

由于两个线程都需要同时拥有这两个资源才可以工作,为了避免死锁,1 号线程释放了 A 资源占有锁,2 号线程释放了 B 资源占有锁;此时 AB 空闲,两个线程又同时抢锁,再次出现上述情况,此时发生了活锁。

简单类比,电梯遇到人,一个进的一个出的,对面占路,两个人同时往一个方向让路,来回重复,还是堵着路。如果线上应用遇到了活锁问题,恭喜你中奖了,这类问题比较难排查。

# 3.单线程创建方式

单线程创建方式比较简单,一般只有两种方式:

  • 继承 Thread 类
  • 实现 Runnable 接口;

需要注意的问题有:

  • 不管是继承 Thread 类还是实现 Runable 接口,业务逻辑写在 run 方法里面,线程启动的时候是执行 start()方法;
  • 开启新的线程,不影响主线程的代码执行顺序也不会阻塞主线程的执行;
  • 新的线程和主线程的代码执行顺序是不能够保证先后的;
  • 对于多线程程序,从微观上来讲某一时刻只有一个线程在工作,多线程目的是让 CPU 忙起来;
  • 通过查看 Thread 的源码可以看到,Thread 类是实现了 Runnable 接口的,所以这两种本质上来讲是一个;

单线程创建举例:

public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
        for (int i = 0; i < 100; i++) {
            System.out.println(i);
        }
    }
}
1
2
3
4
5
6
7
8
9
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(i);
        }
    }
}
1
2
3
4
5
6
7
8
public static void main(String[] args) {
  Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
      for (int i = 0; i < 10; i++) {
        System.out.println(i);
      }
    }
  });
  thread.start();
}
1
2
3
4
5
6
7
8
9
10
11

# 4.终止线程运行的情况?

线程调度器选择优先级最高的线程运行。但是,如果发生以下情况,就会终止线程的运行:

  • 线程体中调用了 yield()方法,让出了对 CPU 的占用权;
  • 线程体中调用了 sleep()方法,使线程进入睡眠状态;
  • 线程由于 I/O 操作而受阻塞;
  • 另一个更高优先级的线程出现;
  • 在支持时间片的系统中,该线程的时间片用完。

# 5.如何减少上下文切换

减少上下文切换的方法有无锁并发编程、CAS 算法、使用最少线程和使用协程。

  • 无锁并发编程。多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的 ID 按照 Hash 算法取模分段,不同的线程处理不同段的数据。
  • CAS 算法。Java 的 Atomic 包使用 CAS 算法来更新数据,而不需要加锁。
  • 使用最少线程。避免创建不需要的线程,比如任务很少,但是创建了很多多线程来处理,这样会造成大量线程都处于等待状态。
  • 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

# 6.什么是死锁?

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去

如上图例子:线程 1 己经持有了资源 2,它同时还想申请资源 1,线程 2 已经持有了资源 1,它同时还想申请资源 2,所以线程 1 和线程 2 就因为相互等待对方已经持有的资源,而进入了死锁状态。

# 7.写出死锁代码?

死锁的产生必须具备以下四个条件。

  • 互斥条件:一个资源同时只能有一个线程占有.其他线程只能等待.
  • 请求并持有条件:当前线程已经获取到一个资源,又获取其他资源,其他资源被别的线程占有,当前线程等待,但是不释放持有资源.
  • 不可剥夺条件:占有资源期间,不能被其他线程剥夺,只能自己释放.
  • 环路等待条件:等待资源形成环形链.a 被 A 占有,b 被 B 占有,A 想获取 b,B 想获取 a
//死锁代码
public class DeadLockDemo {
  private static String A = "A";
  private static String B = "B";

  public static void main(String[] args){
    new DeadLockDemo().deadLock();
  }

  private void deadLock(){
    Thread t1 = new Thread(new Runnable(){
      @Override
      public void run(){
        //线程1获取A的锁
        synchronized (A){
          try {
            Thread.currentThread().sleep(2000);
          } catch (InterruptedException e){
            e.printStackTrace();
          }
          synchronized (B){
            System.out.println("1");
          }
        }
      }
    });
    Thread t2 = new Thread(new Runnable(){
      @Override
      public void run(){
        //线程2获取B的锁
        synchronized (B){
          //A对象已经被线程1持有
          synchronized (A){
            System.out.println("2");
          }
        }
      }
    });
    t1.start();
    t2.start();
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# 8.如何避免死锁呢?

要想避免死锁,只需要破坏掉至少一个构造死锁的必要条件即可,目前只有请求并持有条件和环路等待条件是可以被破坏的。考虑死锁产生的条件:互斥访问、占有并保持、循环等待。针对以上几点,可以:资源一次性分配、占有时可被打断、考虑资源分配顺序。

  • 避免一个线程同时获取多个锁。
  • 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
  • 尝试使用定时锁,使用 locktryLock(timeou t)来替代使用内部锁机制。
  • 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。

如上题代码中,在线程 B 中获取资源的顺序和在线程 A 中获取资源的顺序保持一致,其实资源分配有序性就是指,假如线程 A 和线程 B 都需要资源 1,2,3,..., n 时,对资源进行排序,线程 A 和线程 B 只有在获取了资源 n-1 时才能去获取资源 n.

public class DeadLockRelessDemo {
  private static String A = "A";
  private static String B = "B";

  public static void main(String[] args){
    new DeadLockRelessDemo().deadLock();
  }

  private void deadLock(){
    Thread t1 = new Thread(new Runnable(){
      @Override
      public void run(){
        //线程1获取A的锁
        synchronized (A){
          try {
            Thread.currentThread().sleep(2000);
          } catch (InterruptedException e){
            e.printStackTrace();
          }
          synchronized (B){
            System.out.println("1");
          }
        }
      }
    });
    Thread t2 = new Thread(new Runnable(){
      @Override
      public void run(){
        //线程2也获取A的锁
        synchronized (A){
          synchronized (B){
            System.out.println("2");
          }
        }
      }
    });
    t1.start();
    t2.start();
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

# 9.线程,进程,协程的区别?

进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位.

线程则是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。操作系统在分配资源时是把资源分配给进程的,但是 CPU 资源比较特殊,它是被分配到线程的,因为真正要占用 CPU 运行的是线程,所以也说线程是 CPU 分配的基本单位。在 Java 中,当我们启动 main 函数时其实就启动了一个 JVM 进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程

一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈区域。

  • 进程:静态分配内存资源的最小单位

  • 线程:动态执行任务的最小单位

  • 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。协程也叫纤程.

线程是在内核态调用的,协程在用户态调用,避免了上下文切换

# 10.继承 Thread 类的优劣?

使用继承方式的好处是,在 run()方法内获取当前线程直接使用 this 就可以了,无须使用 Thread.currentThread()方法;不好的地方是 Java 不支持多继承,如果继承了 Thread 类,那么就不能再继承其他类。另外任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码, Runable 则没有这个限制。

public class DemoTest extends Thread {
  // private int tickets = 20;
  private volatile int tickets = 20;

  @Override
  public void run(){
    synchronized (this){
      while (tickets > 0){
        System.out.println(Thread.currentThread().getName()+"卖出一张票"+ tickets);
        tickets--;
      }
    }
  }

  public static void main(String[] args){
    //实际上一共卖出了80张票,每个线程都有自己的私有的非共享数据。都认为自己有20张票
    DemoTest test4 = new DemoTest();
    DemoTest test5 = new DemoTest();
    DemoTest test6 = new DemoTest();
    DemoTest test7 = new DemoTest();
    test4.setName("一号窗口:");
    test5.setName("二号窗口:");
    test6.setName("三号窗口:");
    test7.setName("四号窗口:");
    test4.start();
    test5.start();
    test6.start();
    test7.start();
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 11.缓存一致性协议?

CPU 多级缓存,切换后如何保证一致性?

image-20240126160137155

一种处理一致性问题的办法是使用 Bus Locking(总线锁)。当一个 CPU 对其缓存中的数据进行操作的时候,往总线中发送一个 Lock 信号。这个时候,所有 CPU 收到这个信号之后就不操作自己缓存中的对应数据了,当操作结束,释放锁以后,所有的 CPU 就去内存中获取最新数据更新。但是用锁的方式总是避不开性能问题。总线锁总是会导致 CPU 的性能下降。所以出现另外一种维护 CPU 缓存一致性的方式,MESI。

MESI 是保持一致性的协议。它的方法是在 CPU 缓存中保存一个标记位,这个标记位有四种状态:

  • M: Modify,修改缓存,当前 CPU 的缓存已经被修改了,即与内存中数据已经不一致了
  • E: Exclusive,独占缓存,当前 CPU 的缓存和内存中数据保持一致,而且其他处理器并没有可使用的缓存数据
  • S: Share,共享缓存,和内存保持一致的一份拷贝,多组缓存可以同时拥有针对同一内存地址的共享缓存段
  • I: Invalid,实效缓存,这个说明 CPU 中的缓存已经不能使用了

CPU 的读取遵循下面几点:

  • 如果缓存状态是 I,那么就从内存中读取,否则就从缓存中直接读取。
  • 如果缓存处于 M 或 E 的 CPU 读取到其他 CPU 有读操作,就把自己的缓存写入到内存中,并将自己的状态设置为 S。
  • 只有缓存状态是 M 或 E 的时候,CPU 才可以修改缓存中的数据,修改后,缓存状态变为 M。

# 12.如何实现线程安全的单例模式?

安全的单例模式

1.使用 volatile 禁止重排序

2.可以重排序,但重排序对其他线程不可见

image-20220418162112331

初始化一个类,包括执行这个类的静态初始化和初始化在这个类中声明的静态字段。根据 Java 语言规范,在首次发生下列任意一种情况时,一个类或接口类型 T 将被立即初始化。

  1. T 是一个类,而且一个 T 类型的实例被创建。

  2. T 是一个类,且 T 中声明的一个静态方法被调用。

  3. T 中声明的一个静态字段被赋值。

  4. T 中声明的一个静态字段被使用,而且这个字段不是一个常量字段。

  5. T 是一个顶级类(Top Level Class,见 Java 语言规范的 7.6),而且一个断言语句嵌套在 T 内部被执行。

public class InstanceFactory {
   private static class InstanceHolder {
   public static Instance instance=new Instance();

   public static Instance getinstance(){
   return InstanceHolderinstance; //这里将导致InstanceHolder类被初始化
}
1
2
3
4
5
6
7

# 13.Finally 不执行的情况

在 Java 的finally块中通常执行的是一些必须要在退出try块时执行的代码,不受异常是否抛出的影响。然而,有几种情况下finally块不会执行:

  1. System.exit() 或 JVM 崩溃:
    • 当程序调用System.exit()方法时,Java 虚拟机会立即退出,不再执行任何未完成的代码,包括finally块。
    • 如果在finally块中执行了一个会使 JVM 崩溃的操作(如死循环或异常死锁等),则finally块不会完成执行。
  2. 线程无法停止:
    • 如果在finally块中有一个无法停止的线程(如Thread.stop()方法),finally块可能无法完成执行。
  3. 处理器或虚拟机崩溃:
    • 在某些极端情况下,如处理器错误或 Java 虚拟机崩溃,finally块可能不会执行。
  4. 在 try 块中出现死循环:
    • 如果在try块中出现一个无限循环,finally块将不会执行,因为代码永远不会跳出try块。
  5. System.exit() 或 Runtime.halt() 在 finally 块中被调用:
    • 如果在finally块中调用了System.exit()Runtime.halt()来退出 JVM,那么 JVM 将立即终止,finally块的执行也将被中断。
  6. **守护线程中的 finally:**当只剩下守护线程在运行时,JVM 认为它已经完成了所有需要执行的任务,于是会立即退出,不会等待守护线程的finally块执行完毕。这意味着,如果守护线程中有finally块,当 JVM 退出时,这些finally块可能不会被执行。

总之,finally块在大多数情况下都会执行,但在某些极端情况下,如程序终止或 JVM 崩溃等情况下,finally块可能不会被执行。因此,在使用finally块时,应该确保其中的代码是可靠的,不依赖于异常处理或 JVM 的状态。

# 14.Thread 类中的方法

java.lang.Thread 类是 Java 中用于多线程编程的关键类,它提供了管理线程的各种方法。以下是 Thread 类中常用的一些方法:

  1. start(): 启动线程,使其进入可运行状态。一旦调用了 start() 方法,线程会在适当的时机被调度执行,而不是立即执行。

  2. run(): 线程的主要执行代码应该放在这个方法中。当线程被启动并进入运行状态时,run() 方法会被调用。

  3. sleep(long millis): 使当前线程休眠指定的毫秒数。在这段时间内,线程不会执行任何操作。

  4. join(): 等待调用此方法的线程结束。如果在线程 A 中调用了线程 B 的 join() 方法,线程 A 将会等待直到线程 B 执行完毕。

  5. interrupt(): 中断线程,给线程设置中断状态。可以在其他线程中调用该方法来中断目标线程。

  6. isInterrupted(): 检查线程是否被中断,但不会清除中断状态。

  7. currentThread(): 静态方法,返回当前正在执行的线程对象。

  8. setName(String name): 设置线程的名字,便于识别和调试。

  9. setPriority(int priority): 设置线程的优先级,用于调度决策。优先级范围是 1(最低)到 10(最高)。

  10. yield(): 提示调度器当前线程愿意放弃 CPU 使用权,但不保证会被采纳。

  11. isAlive(): 检查线程是否处于活动状态(已启动但尚未终止)。

  12. getState(): 返回线程的状态,如 NEW、RUNNABLE、BLOCKED、WAITING 等。

  13. setDaemon(boolean on): 设置线程是否为守护线程。守护线程在非守护线程全部结束后会自动终止。

  14. getId(): 返回线程的唯一标识符。

这些方法只是 Thread 类中的一部分,它们允许你控制线程的创建、运行、暂停、中断等操作。在多线程编程中,了解如何使用这些方法非常重要,以确保线程能够按照预期进行协调和执行。

# 15.Object 类中的方法

java.lang.Object 是 Java 中所有类的根类,因此它的方法在所有对象中都可用。以下是一些 Object 类中常用的方法:

  1. toString(): 返回对象的字符串表示。默认情况下,返回的是对象的类名和哈希码的组合。可以在子类中覆盖这个方法以返回更有意义的字符串。

  2. equals(Object obj): 用于比较对象是否相等。默认情况下,比较的是对象的引用地址。应该在子类中覆盖这个方法,根据业务逻辑来判断对象是否相等。

  3. hashCode(): 返回对象的哈希码,是一个整数。用于在哈希表等数据结构中进行快速查找。通常需要与 equals 方法一起覆盖。

  4. getClass(): 返回对象的运行时类(Class 对象),该对象包含有关类的信息。

  5. wait(), notify(), notifyAll(): 用于线程之间的同步与通信。wait() 使线程等待,直到另一个线程调用该对象的 notify()notifyAll() 方法,唤醒等待线程。

  6. finalize(): 在对象被垃圾回收之前调用。通常不建议使用,因为现代 Java 已经提供更好的资源管理方式(如 try-with-resources 语句块)。

  7. clone(): 用于创建对象的浅拷贝(复制引用,不复制对象本身)。需要实现 Cloneable 接口,并在子类中覆盖该方法来实现深拷贝。

  8. finalize(): 已过时,曾经是在对象被垃圾回收之前调用的方法。现在不推荐使用,因为更好的资源管理方式已经可用。

这些只是 Object 类中的一些方法。在实际编程中,你会经常使用其中的一些方法,特别是 toString()equals()hashCode() 等。

# 二.线程状态

# 1.线程的状态?

状态名称 说明
NEW 初始状态,线程被构建,但是还没有调用 start 方法
RUNNABLE 运行状态,Java 线程将操作系统中的就绪和运行两种状态笼统地称作“运行中”
BLOCKED 阻塞状态,表示线程阻塞于锁
WAITING 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断)
TIME WAITING 超时等待状态,该状态不同于 WAITING,它是可以在指定的时间自行返回的
TERMINATED 终止状态,表示当前线程已经执行完毕
  • NEW :创建了线程对象但尚未调用 start()方法时的状态。
  • RUNNABLE:线程对象调用 start()方法后,线程处于可运行状态,此时线程等待获取 CPU 执行权。
  • BLOCKED:线程等待获取锁时的状态。
  • WAITING:线程处于等待状态,处于该状态标识当前线程需要等待其他线程做出一些特定的操作唤醒自己。
  • TIME_WAITING:超时等待状态,与 WAITING 不同,在等待指定的时间后会自行返回。
  • TERMINATED:终止状态,表示当前线程已执行完毕。

# 2.线程池的状态

线程池的状态:

image-20230727112857606

image-20230727112914578

# 3.线程状态变迁?

  • RUNNABLE 有 2 种状态,一种是 running,调用 yield 变为 ready,一种是 ready,调用 run 为 running
  • New 状态为初始状态,New Thread 的时候是 new 这种状态,调用 start 方法后,变为 runnable
  • Terminated 任务执行完成后线程的状态为终止
  • runnable 状态到 waiting 状态
    • wait()
    • join() 不需要主动唤醒
    • park()
  • waiting 状态到 runnable 状态
    • 唤醒
    • notify()
    • notifyAll()
    • unpark(thread)
  • runnable 状态到 timed_waiting 状态
    • sleep(long)
    • wait(long)
    • join(long) 不需要主动唤醒
    • parkNanos()
    • parkUntil()
  • timed_waiting 状态到 runnable 状态
    • notify()
    • notifyAll()
    • unpark(thread)
  • blocked 到 runnable
    • 获取到锁
  • runnable 到 blocked
    • synchronized 方法
    • synchronized 块

image-20220609152528859

# 4.CPU 术语定义?

术语 英文单词 描述
内存屏障 memory barriers 是一组处理器指令,用于实现对内存操作的顺序限制
缓冲行 cache line 缓存中可以分配的最小存储单位。处理器填写缓存线时会加载整个缓存线,需要使用多个主内存读周期
原子操作 atomic operations 不可中断的一个或一系列操作
缓存行填充 cache line fill 当处理器识别到从内存中读取操作数是可缓存的,处理器读取整个缓存行到适当的缓存(L1L2.L3 的或所有)
缓存命中 cache hit 如果进行高速缓存行填充操作的内存位置仍然是下次处理器访问的地址时,处理器从缓存中读取操作数,而不是从内存读取
写命中 write hit 当处理器将操作数写回到一个内存缓存的区域时,它首先会检查这个缓存的内存地址是否在缓存行中,如果存在一个有效的缓存行,则处理器将这个操作数写回到缓存,而不是写回到内存,这个操作被称为写命中
写缺失 write misses the cache 一个有效的缓存行被写人到不存在的内存区域

# 5.java 内存模型?

JMM内存模型中定义了 8 种指令操作,这 8 种操作具有原子性。

  • lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程的独占状态。
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 操作使用。(工作在主存,传输数据到从存)
  • load(载入):作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将执行该操作。
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行该操作。
  • store(存储):作用于工作内存的变量,它把工作内存中的一个变量的值传递到主内存中,以便随后的 write 操作使用。(工作在从存,传输数据到主存)
  • write(写入):作用于主内存的变量,它把 store 操作从工作内存中得到的变量值放入主内存的变量中(更新操作)。

:从主内存到工作内存,需顺序执行 read 和 load,从工作内存到主内存,需顺序执行 store 和 wirte 操作,JMM 只要求这些操作是顺序执行的,并不保证连续执行。

Java 线程之间的通信由 Java 内存模型(本文简称为 JMM)控制,JMM 决定一个线程对共享变量的写入何时对另一个线程可见。

JMM 关于同步的规定:

  1. 线程解锁前,必须把共亨变量的值刷新回主内存
  2. 线程加锁前,必须读取主内存的最新值到自己的工作内存
  3. 加锁解锁是同一把锁

image-20240126151413556

# 6.什么是 Daemon 线程?

Daemon 线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个 Java 虚拟机中不存在非 Daemon 线程的时候,Java 虚拟机将会退出。可以通过调用 Thread.setDaemon(true)将线程设置为 Daemon 线程。注意 Daemon 属性需要在启动线程之前设置,不能在启动线程之后设置。Daemon 线程被用作完成支持性工作,但是在 Java 虚拟机退出时 Daemon 线程中的 fnally 块并不一定会执行.

public class Daemon{
  public static void main(String[] args){
    Thread thread=new Thread(new DaemonRunner()"DaemonRunner");
    thread.setDaemon(true);
    thread.start();
  }
  static class DaemonRunner implements Runnable{
    @Override
    public void run() {
      try{
        SleepUtilssecond(10);
      } finally{
        System.out.println("DaemonThread finally run.");
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  • 支持型线程
  • 守护线程
  • finally 里面的代码不一定会执行
public final void setDaemon(boolean on) {
  checkAccess();
  if (isAlive()) {
    //告诉我们,必须要先设置线程是否为守护线程,
    //然后再调用start方法。如果你先调用start,后设置守护线程,则会抛出异常
    throw new IllegalThreadStateException();
  }
  daemon = on;
}
1
2
3
4
5
6
7
8
9

# 7.说说三个中断方法?

说说 interrupt(),interrupted(),isInterrupted()的区别

interrupt()、interrupted()和 isInterrupted()都是 Java 中与线程中断相关的方法,但它们的含义和用法有所不同。

interrupt()方法:

interrupt()方法是一个实例方法,用于中断当前线程或指定线程。当该线程处于阻塞状态(如 sleep、wait、join 等方法)时,会抛出 InterruptedException 异常,从而使线程退出阻塞状态。当该线程处于非阻塞状态时,会将线程的中断状态设置为 true,但并不会中断线程的执行。该方法并不会直接中止线程的执行,而是设置线程的中断标志位,需要用户自行检查标志位并处理中断操作。

interrupted()方法:

interrupted()方法是一个静态方法,可以用于检查当前线程是否被中断,并清除中断状态。如果线程被中断,该方法会返回 true,同时将线程的中断状态设置为 false。该方法会清除当前线程的中断状态,因此如果希望保留中断状态,可以使用 isInterrupted()方法。

isInterrupted()方法:

isInterrupted()方法是一个实例方法,用于检查当前线程或指定线程的中断状态。如果线程被中断,该方法会返回 true,否则返回 false。该方法不会清除线程的中断状态,因此可以用于在判断中断状态的同时保留中断状态。

需要注意的是,interrupted()方法和 isInterrupted()方法都是用于检查中断状态的方法,但它们的使用场景略有不同。interrupted()方法是静态方法,用于检查当前线程的中断状态,并清除中断状态,因此只能用于检查当前线程的中断状态。而 isInterrupted()方法是实例方法,可以用于检查当前线程或指定线程的中断状态,并且不会清除中断状态,因此可以用于检查线程的中断状态而不影响后续的中断处理。

方法传参:true 代表清除中断标志,false 代表不清除中断标志

public void interrupt(){
  if (this != Thread.currentThread())
    checkAccess();

  synchronized (blockerLock){
    Interruptible b = blocker;
    if (b != null){
      interrupt0(); //Just to set the interrupt flag
      b.interrupt(this);
      return;
    }
  }
  interrupted();
}

public static boolean interrupted(){
  return currentThread().isInterrupted(true);
}

public boolean isInterrupted(){
  return isInterrupted(false);
}

private native boolean isInterrupted(boolean ClearInterrupted);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 8.说说 join 方法和 yeild 方法?

join 方法和 yield 方法都是 Java 中与线程控制相关的方法,但它们的含义和用法有所不同。

join方法:

join()方法是一个实例方法,用于等待指定线程的执行完成。当线程 A 调用线程 B 的 join()方法时,线程 A 会被阻塞,直到线程 B 执行完成或超时。join()方法可以用于等待某个线程执行完成,然后再继续执行后续操作,常用于线程之间的协作。

yield方法:

yield()方法是一个静态方法,用于提示线程调度器当前线程愿意放弃当前的 CPU 资源,让其他线程执行。调用 yield()方法并不会让线程进入阻塞状态,当一个线程调用 yield 方法时,当前线程会让出 CPU 使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出 CPU 的那个线程来获取 CPU 执行权。

# 9.说说 sleep 和 yeild 和 wait 区别?

  • sleep 方法与 yeild 方法的区别在于,当线程调用 sleep 方法时调用线程会被阻塞挂起指定的时间,在这期间线程调度器不会去调度该线程。不会释放资源锁,只是让出 CPU 的时间片.线程会阻塞.

  • sleep 方法不释放锁,释放 cpu,可响应中断.先清除中断标志,再打印异常

  • sleep 必须捕获异常,而 wait,notify 和 notifyAll 不需要捕获异常

  • wait,notify 和 notifyAll 只能在同步控制方法或者同步控制块里面使用,而 sleep 可以在任何地方使用

  • 调用 yield 方法时,线程只是让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,线程调度器下一次调度时就有可能调度到当前线程执行。

  • wait 方法会阻塞,会释放锁,会让出 CPU 的使用权,且不会参与锁竞争,除非被唤醒后才参与竞争.

# 10.说说 notify 和 wait 方法?

一个线程调用共享对象的 notify()方法后,会唤醒一个在该共享变量上调用 wait()方法后被挂起的线程。此外,被唤醒的线程不能马上从 wait 方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回(调用 wait 方法后,会释放当前共享对象的锁,如果不释放会造成死锁),也就是唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程也不一定会立即获取到共享对象的监视器锁,这是因为该线程还需要和其他线程一起竞争该锁,只有该线程竞争到了共享变量的监视器锁后才可以继续执行。

类似 wait 系列方法,只有当前线程获取到了共享变量的监视器锁后,才可以调用共享变量的 notify()方法,否则会抛出 IllegaMonitorStateException 异常。

  • Thread 类的方法:sleep(),yield()
  • Object 的方法:wait()和 notify()、notifyAll()
  • Condition 接口中的方法: await()、signal()、signalAll()

线程阻塞和唤醒方法对比:

wait()、notify()、notifyAll() 这三个方法是 Object 超类中的方法.

await()、signal()、signalAll() 这三个方法是 Lock 的 Condition 中的方法.

# 11.说说对象的监视器锁?

一个线程如何才能获取到对象的监视器锁呢

  • 执行 synchronized 同步代码块时,使用该共享变量作为参数。
synchronized (MonitorTest.class){
//do something
}
1
2
3
  • 调用该共享变量的方法,并且该方法使用了 synchronized 修饰。
synchronized void add(int a){
//do something
}
1
2
3

# 12.直接调用 wait 方法?

会出现 IllegalMonitorStateException 异常,出现的原因如下:

如果调用 wait()方法的线程没有先获取该对象的监视器锁,则调用 wait 方法时调用线程会抛出 IllegalMonitorState Exception 异常。Object.notify(), Object.notifyAll(), Object.wait(), Object.wait(long), Object.wait(long, int)都会存在这个问题

public class ExceptionTest {
  public static void main(String[] args){
    Object object = new Object();
    try {
      object.wait();
    } catch (InterruptedException e){
      e.printStackTrace();
    }
  }
}
1
2
3
4
5
6
7
8
9
10

# 13.什么是虚假唤醒?

什么是虚假唤醒?如何避免虚假唤醒?

在一个线程没有被其他线程调用 notify()、 notifyAll()方法进行通知,或者被中断,或者等待超时,这个线程仍然可以从挂起状态变为可以运行状态(也就是被唤醒),这就是所谓的虚假唤醒。

虽然虚假唤醒在应用实践中很少发生,但要防患于未然,做法就是不停地去测试该线程被唤醒的条件是否满足,不满足则继续等待,也就是说在一个 while 循环中调用 wait()方法进行防范.退出循环的条件是满足了唤醒该线程的条件。

synchronized (obj){
  //do something
  while (条件不满足){
    obj.wait();
  }
}
1
2
3
4
5
6

While 在这里是防止虚假唤醒的关键,试想下,一旦发生虚假唤醒,线程会根据 while 添加再次进行判断,一旦条件不满足,会立即再次将线程挂起.

# 14.线程优先级?

优先级是针对 CPU 的时间片的长短,不是先后.

prio=5 和 os_prio=0 默认优先级为 5,os_prio 是 jvm 相对于 os 的优先级,优先级的数值在 1~10,越大优先级越高 setPriority 这个方法,他是 jvm 提供的一个方法,并且能够调用本地方法 setPriority().我们发现优先级貌似没有起作用,为什么?

1.我们现在的计算机都是多核的,t1,t2 会让哪个 cpu 处理不好说。由不同的 cpu 同时提供资源执行。

2.优先级不代表先后顺序。哪怕你的优先级低,也是有可能先拿到我们的 cpu 时间片的,只不过这个时间片比高优先级的线程的时间片短。优先级针对的是 cpu 时间片的长短问题。

3.目前工作中,实际项目里,不必要使用 setPriority 方法。我们现在都是用 hystrix,sential 也好,一些开源的信号量控制工具,都能够实现线程资源的合理调度。这个 setPriority 方法,很难控制。实际的运行环境太复杂。

# 三.ThreadLocal 介绍

# 1.说说 ThreadLocal?

ThreadLocal 是 Java 中的一个类,用于在多线程环境下维护线程本地变量。在 Java 中,每个线程都有自己的线程栈,用于存储局部变量,而 ThreadLocal 提供了一种线程级别的变量,每个线程都可以独立地访问和修改它,而不会影响其他线程的访问。

# 2.ThreadLocal 要点

使用 ThreadLocal 有以下几个要点:

  1. 独立副本:每个 ThreadLocal 对象在每个线程中都会保存一个独立的副本。当线程访问 ThreadLocal 对象时,实际上是在操作当前线程自己的副本,因此线程之间互不干扰。
  2. 初始值:ThreadLocal 提供了初始值的设置,可以通过覆盖initialValue()方法来设置初始值。当线程第一次访问 ThreadLocal 对象时,如果没有设置初始值,initialValue()方法将被调用来提供默认值。
  3. 内存泄漏:使用 ThreadLocal 时要注意内存泄漏的问题。由于 ThreadLocal 中的对象只在当前线程中有效,如果线程一直存在而 ThreadLocal 没有被清理,那么其中的对象也不会被释放,可能导致内存泄漏。因此,在不再需要使用 ThreadLocal 时,应当调用remove()方法来清理其中的对象,或者使用 Java 8 引入的ThreadLocal.remove()方法。
  4. 用途:ThreadLocal 在某些场景下非常有用,特别是在多线程环境下需要保存线程特有的状态或数据时。常见的应用场景包括线程池、Web 应用的用户会话信息、数据库连接管理等。

# 3.ThreadLocal 的原理?

Thread 类中有一个 threadLocals 和一个 inheritableThreadLocals,它们都是 ThreadLocalMap 类型的变量,而 ThreadLocalMap 是一个定制化的 HashMap.在默认情况下,每个线程中的这两个变量都为 null,只有当前线程第一次调用 ThreadLocal 的 set 或者 get 方法时才会创建它们.

其实每个线程的本地变量不是存放在 ThreadLocal 实例里面,而是存放在调用线程的 threadLocals 变量里面.也就是说,ThreadLocal 类型的本地变量存放在具体的线程内存空间中。ThreadLocal 就是一个工具壳,它通过 set 方法把 value 值放入调用线程的 threadLocals 里面并存放起来,当调用线程调用它的 get 方法时,再从当前线程的 threadLocals 变量里面将其拿出来使用.如果调用线程一直不终止,那么这个本地变量会一直存放在调用线程的 threadLocals 变量里面,所以当不需要使用本地变量时可以通过调用 ThreadLocal 变量的 remove 方法,从当前线程的 threadLocals 里面删除该本地变量。

Thread 里面的 threadLocals 为何被设计为 map 结构,很明显是因为每个线程可以关联多个 ThreadLocal 变量

在 Thread 类中有以下变量

ThreadLocalMap 是 ThreadLocal 的一个静态内部类.

/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class.*/
 ThreadLocal.ThreadLocalMap threadLocals = null;

/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
* Inheritable 可继承的
*/
 ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
1
2
3
4
5
6
7
8
9
10
  • ThreadLocal 中在 set 操作时,key 为当前 ThreadLocal 对象。
  • ThreadLocal 会为每个线程都创建一个 ThreadLocalMap,对应程序中的 t.threadLocals = new ThreadLocalMap(this, firstValue),ThreadLocalMap 为当前线程的属性。
  • 通过对每个线程创建一个 ThreadLocalMap 实现本地副本。当取值时,实际上就是通过 key 在 map 中取值,当然此时的 key 为 ThreadLocal 对象,而 map 为每个线程独有的 map,从而实现变量的互不干扰。

# 4.ThreadLocal 中 set 方法?

public class ThreadLocalTest {
  public static void main(String[] args){
    ThreadLocal<String> t1 = new ThreadLocal<>();
    t1.set("1");
    t1.set("2");
    System.out.println(t1.get());
  }
}
//输出结果 2
1
2
3
4
5
6
7
8
9

看下 set 方法的源代码:先获取当前线程 t,然后以 t 为 key 获取当前 ThreadLocalMap.如果 Map 存在则设置,注意设置的 key 为 this,this 代表当前对象,key 不变,所以 value 会被覆盖.如果 map 不存在则进行 createMap。

public void set(T value){
  Thread t = Thread.currentThread();
  ThreadLocalMap map = getMap(t);
  if (map != null){
    map.set(this, value);
  } else {
    createMap(t, value);
  }
}

void createMap(Thread t, T firstValue){
  t.threadLocals = new ThreadLocalMap(this, firstValue);
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 5.ThreadLocal 继承?

public class ThreadLocalTest2 {
  public static void main(String[] args){
    ThreadLocal<String> t1 = new ThreadLocal<>();
    t1.set("1");
    new Thread(new Runnable(){
      @Override
      public void run(){
        System.out.println("我是子线程,t1:"+ t1.get());
      }
    }).start();
    System.out.println("我是主线程,t1:"+ t1.get());
  }
}
//我是主线程,t1:1
//我是子线程,t1:null
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

也就是说,同一个 ThreadLocal 变量在父线程中被设置值后,在子线程中是获取不到的。根据之前题目的线程私有的介绍,这应该是正常现象,因为在子线程 thread 里面调用 get 方法时当前线程为 thread 线程,而这里调用 set 方法设置线程变量的是 main 线程,两者是不同的线程,自然子线程访问时返回 null.

# 6.子线程访问主线程变量?

有没有办法让子线程访问到主线程的 ThreadLocal 变量

InheritableThreadLocal 继承自 ThreadLocal,其提供了一个特性,就是让子线程可以访问在父线程中设置的本地变量.下面看一下 InheritableThreadLocal 的代码。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
  protected T childValue(T parentValue){
    return parentValue;
  }
  ThreadLocalMap getMap(Thread t){
    return t.inheritableThreadLocals;
  }
  void createMap(Thread t, T firstValue){
    t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
  }
}
1
2
3
4
5
6
7
8
9
10
11

这是 InheritableThreadLocal 的全部代码,他继承了 ThreadLocal,并复写了三个方法.

  • 一个是 getMap,获取一个新的 map,
  • 一个是 ceateMap,创建一个新的 ThreadLocalMap,并赋值给 inheritableThreadLocals
  • 一个是 childValue,返回父线程的值

# 7.InheritableThreadLocal?

InheritableThreadLocal 是如何让子线程可以访问在父线程中设置的本地变量的

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
  protected T childValue(T parentValue){
    return parentValue;
  }
  ThreadLocalMap getMap(Thread t){
    return t.inheritableThreadLocals;
  }
  void createMap(Thread t, T firstValue){
    t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
  }
}
1
2
3
4
5
6
7
8
9
10
11

从代码上看,getMapcreateMap 没什么稀奇的,无非是创建和获取.这不是原理所在。除了 getMapcreateMap,只能来看看 childValue 这个方法了.我们看到代码逻辑是 return parentValue;为了说清楚 childValue 这个方法,我们得先看 ThreadLocalMap 构造方法:从代码可以看出,初始化的时候进行了判断,如果父类的 inheritableThreadLocals 不为空,则进行 createInheriteMap 方法创建,继续点进去看.发现在该函数内部把父线程 inhritblThreadLocal 的值复制到新的 ThreadLocalMap 对象

private ThreadLocalMap(ThreadLocalMap parentMap){
  Entry[] parentTable = parentMap.table;
  int len = parentTable.length;
  setThreshold(len);
  table = new Entry[len];

  for (int j = 0; j < len; j++){
    Entry e = parentTable[j];
    if (e != null){
      @SuppressWarnings("unchecked")
      ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
      if (key != null){
        //重点此处调用了 childValue,返回 parent的value,
        //在该函数内部把父线程 inhritblThreadLocal 的值复制到新的 ThreadLocalMap 对象
        Object value = key.childValue(e.value);
        Entry c = new Entry(key, value);
        int h = key.threadLocalHashCode &(len -1);
        while (table[h]!= null)
          h = nextIndex(h, len);
        table[h]= c;
        size++;
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

总结:InheritableThreadLocal 类通过重写代码.getMap 和 createMap 让本地变量保存到了具体线程的 inheritableThreadLocals 变量里面,那么线程在通过 InheritableThreadLocal 类实例的 set 或者 get 方法设置变量时,就会创建当前线程的 inheritableThreadLocals 变量.当父线程创建子线程时,构造函数会把父线程中 inheritableThreadLocals 变量里面的本地变量复制一份保存到子线程的 inheritableThreadLocals 变量里面。

public class InheritableThreadLocalTest {

  public static void main(String[] args){
    InheritableThreadLocal<String> t1 = new InheritableThreadLocal<>();
    t1.set("1");
    new Thread(new Runnable(){
      @Override
      public void run(){
        System.out.println("我是子线程,t1:"+ t1.get());
      }
    }).start();
    System.out.println("我是主线程,t1:"+ t1.get());
  }
}
//我是主线程,t1:1
//我是子线程,t1:1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

说说 InheritableThreadLocal 的使用场景?

情况还是蛮多的,比如子线程需要使用存放在 ThreadLocal 变量中的用户登录信息,再比如一些中间件需要把统一的 id 追踪的整个调用链路记录下来.其实子线程使用父线程中的 ThreadLocal 方法有多种方式,比如创建线程时传入父线程中的变量,并将其复制到子线程中,或者在父线程中构造一个 map 作为参数传递给子线程,但是这些都改变了我们的使用习惯,所以在这些情况下 InheritableThreadLocal 就显得比较有用。

# 8.为什么不用 HashMap?

1.在设计 ThreadLocal 时,参考 jdk 都是鉴于效率性能优先。ThreadLocalMap 对 ThreadLocal 场景做了优化,这些场景是特定的,而不一定适用于原先的 HashMap 适用的场景。

ThreadLocalMap 是由一个个 Entry 键值对组成,key 是 ThreadLocal 对象,value 为线程变量的副本。每一个 Thread 都有一个 ThreadLocalMap 类型的成员变量 threadLocals ,它存储本线程中所 有 ThreadLocal 对象及其对应的值。并且他是一个 WeakReference 弱引用,当没有指向 key 的强引用后,该 key 就会被垃圾回收器回收。如果不是弱引用,那么因为这个 Map 的强引用导致这个线程的 ThreadLocalMap 对应的 ThreadLocal key 一直不能被回收。

2.减少哈希碰撞,如果你的程序需要多个 ThreadLocal,并且每个线程都使用了 这些 ThreadLocal。 Entry[] table 的大小一直是 2 的 n 次方,这样根据 Hash 值放入的时候,取余变成对于 2 的 n 次方 -1 取与运算。Hash 值计算是开放地址法,每新建一个 ThreadLocal 则将全局的 nextHashCode + 0x61c88647,这个魔法数保证了大部分情况下无论 Entry[] table 扩容到什么程度,都可以保证生成的 Hash 值 对于目前 table 大小的值 - 1 取与运算落入尽量不同的位置,减少哈希碰撞,增加效率。

那如果 ThreadLocal 发生内存泄漏,原因一定是弱引用吗?

ThreadLocal 发生内存泄漏的关键在于,ThreadLocalMap 和 Thread 有相同的生命周期,当外部没有强引用指向 ThreadLocal 时,在 ThreadLocalMap 里面的 key 就会被移除,而 value 还存在着强引用,只有当 Thread 退出线程后,value 的强引用才会断,如果线程一直不结束的话,j 这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链。

# 9.this 和 t 区别?

public void set(T value){
  Thread t = Thread.currentThread();
  ThreadLocalMap map = getMap(t);
  if (map != null){
    map.set(this, value);
  } else {
    createMap(t, value);
  }
}

void createMap(Thread t, T firstValue){
  t.threadLocals = new ThreadLocalMap(this, firstValue);
}
1
2
3
4
5
6
7
8
9
10
11
12
13

thist都是对象引用,但是指向的对象不同。

this是一个关键字,表示当前对象的引用。在上述代码中,this指的是ThreadLocal对象,即当前正在调用set方法的ThreadLocal对象。

t是一个局部变量,代表当前线程的引用。在上述代码中,t通过Thread.currentThread()方法获取当前线程的引用。

因此,thist指向的是不同的对象,this指向ThreadLocal对象,t指向当前线程对象。

# 10.key 弱引用

ThreadLocalMap 的 Entry 中的 key 使用的是对 ThreadLocal 对象的弱引用,这在避免内存泄漏方面是一个进步,因为如果是强引用,即使其他地方没有对 ThreadLocal 对象的引用,ThreadLocalMap 中的 ThreadLocal 对象还是不会被回收,而如果是弱引用则 ThreadLocal 引用是会被回收掉的。

但是对应的 value 还是不能被回收,这时候 ThreadLocalMap 里面就会存在 key 为 null 但是 value 不为 null 的 entry 项,虽然 ThreadLocalMap 提供了 set、get 和 remove 方法,可以在一些时机下对这些 Entry 项进行清理,但是这是不及时的,也不是每次都会执行,所以在一些情况下还是会发生内存漏,因此在使用完毕后及时调用 remove 方法才是解决内存泄漏问题的王道。

创建一个 ThreadLocal 变量后,每个线程都会复制一个变量到自己的本地内存,如下图所示。

image-20231021174620487

# 11.TransmittableThreadLocal?

简介:

  • 解决线程池之间 ThreadLocal 本地变量传递的问题
  • 继承了 InheritableThreadLocal,是解决父子线程的关键
  • InheritableThreadLocal 解决父子线程的问题,它是在线程创建的时候进行复制上下文的。那么对于线程池的已经创建完了就无从下手了,所以在线程提交的时候要进行上下文的复制。这就是 TransmittableThreadLocal 想要解决的问题

InheritableThreadLocal 是在 new Thread 对象的时候复制父线程的对象到子线程的.是只有在创建的时候才拷贝,只拷贝一次,然后就放到线程中的 inheritableThreadLocals 属性缓存起来。由于使用了线程池,该线程可能会存活很久甚至一直存活,那么 inheritableThreadLocals 属性将不会看到父线程的本地变量的变化.

holder 这是一个 TTL 类型的对象,持有一个全局的 WeakMap(weakMap 的 key 是弱引用,同 TL 一样,也是为了解决内存泄漏的问题),里面存放了 TTL 对象 并且重写了 initialValue 和 childValue 方法,尤其是 childValue,可以看到在即将异步时父线程的属性是直接作为初始化值赋值给子线程的本地变量对象(TTL)的.

TransmittableThreadLocal 使用场景:

  • 分布式跟踪系统
  • 日志收集记录系统上下文
  • Session 级 Cache
  • 应用容器或上层框架跨应用代码给下层 SDK 传递信息

# 12.如何使用 ThreadLocal

public class ThreadLocalExample {
    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>() {
        @Override
        protected Integer initialValue() {
            return 0; // 设置初始值为0
        }
    };

    public static void main(String[] args) {
        Runnable task = () -> {
            int value = threadLocal.get(); // 获取当前线程的ThreadLocal副本
            System.out.println(Thread.currentThread().getName() + ": " + value);

            threadLocal.set(value + 1); // 修改当前线程的ThreadLocal副本
        };

        // 创建并启动多个线程
        Thread thread1 = new Thread(task, "Thread-1");
        Thread thread2 = new Thread(task, "Thread-2");
        thread1.start();
        thread2.start();
    }
}

//输出结果
//Thread-1: 0
//Thread-2: 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

这表明每个线程都拥有独立的 ThreadLocal 副本,并且可以独立地进行读取和修改操作。

# 13.ThreadLocal 信息丢失原因

同一线程内部 threadlocal 信息丢失了 可能的原因有哪些?

在同一线程内部,ThreadLocal 信息丢失通常是由以下一些原因导致的:

  1. 未正确设置 ThreadLocal 值:最常见的原因是没有正确设置ThreadLocal的值或没有在需要的地方设置值。确保在每个线程中正确地调用set方法来设置ThreadLocal的值。

  2. 未正确清除 ThreadLocal 值:如果不需要的话,确保在使用完ThreadLocal值后,调用remove方法将其从当前线程中清除。如果不清除,可能会导致内存泄漏或数据污染。

  3. 线程池和线程重用:如果您在使用线程池时不小心共享了ThreadLocal变量,或者线程重用了ThreadLocal变量,可能导致线程间的ThreadLocal数据混乱。确保在线程池中适当地初始化和清除ThreadLocal变量。

  4. 异常或非正常流程:在某些情况下,如果线程在异常或非正常流程中退出,可能会导致ThreadLocal的数据丢失。确保您的代码在异常情况下能够正确清理ThreadLocal数据。

  5. 使用 ThreadLocal 的注意事项:在多线程编程中,ThreadLocal应该谨慎使用。不合理的使用可能导致资源泄漏、内存占用过高等问题。请确保只在需要在线程间传递数据的情况下使用它,而不是用它作为全局变量的替代品。

  6. 序列化问题ThreadLocal的默认实现在序列化时可能会导致数据丢失。如果需要在多个线程之间传递ThreadLocal数据,并且您的应用程序需要序列化,考虑使用InheritableThreadLocal或自定义ThreadLocal的子类来处理序列化问题。

ThreadLocal的信息丢失通常是由于不正确的使用或未考虑到特殊情况导致的。在使用ThreadLocal时,确保正确地设置和清理值,避免在不同线程之间共享ThreadLocal,并小心处理异常情况。此外,了解ThreadLocal的工作原理和使用场景也是确保它正常工作的关键。

# 14.创建 ThreadLocal 对象

//创建线程安全的DateFormat
public final static ThreadLocal<DateFormat> formatter = withInitial(() -> new SimpleDateFormat("dd-MMM-yyyy"));
1
2

源码:

//可以看到参数是Supplier,表示不要参数,返回一个值
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
    return new SuppliedThreadLocal<>(supplier);
}
1
2
3
4

# 四.用户态与内核态

# 1.CPU 用户态和内核态?

在操作系统中,CPU 的运行模式分为用户态和内核态。

用户态是指 CPU 在执行进程的用户程序时所处的运行模式。在用户态下,CPU 只能访问该进程的用户空间,不能直接访问操作系统的内核空间。用户态下的程序运行速度较快,但是受到一些限制,例如无法直接访问硬件设备、无法执行特权指令等。

内核态是指 CPU 在执行操作系统内核代码时所处的运行模式。在内核态下,CPU 可以访问所有的内存空间和硬件设备,并且可以执行特权指令,例如修改内存映射、切换进程、中断处理等。因此,内核态下的程序具有更高的权限和更广的操作能力,但是运行速度较慢。

当进程需要执行一些需要特权级别的操作时,例如进行 I/O 操作、创建新进程、访问共享内存等,就需要切换到内核态。这时,进程会通过系统调用的方式进入内核态,请求操作系统执行特定的操作。操作系统会根据请求的操作类型进行相应的处理,并返回结果给进程。当操作完成后,进程再次切换回用户态,继续执行自己的程序。

在操作系统中,用户态和内核态的切换需要一定的时间和开销,因此尽可能减少进程在用户态和内核态之间的切换次数,可以提高系统的性能和响应速度。

对比:

操作系统中的内核态(Kernel Mode)和用户态(User Mode)是两个重要的特权级别,用于管理计算机硬件和运行应用程序。它们之间的主要区别包括权限和功能:

  1. 权限

    • 内核态拥有最高的系统权限,可以直接访问和操作计算机的硬件资源,包括内存、CPU、设备驱动程序等。内核态的代码可以执行特权指令,如读写控制寄存器和执行 I/O 操作。
    • 用户态受到限制,不能直接访问硬件资源或执行特权指令。它只能通过操作系统提供的接口来请求内核态执行某些任务。
  2. 功能

    • 内核态执行操作系统的核心功能,包括进程管理、内存管理、文件系统管理、设备驱动程序管理、安全性管理等。内核态的代码负责响应中断、调度进程、分配和释放内存等任务。
    • 用户态执行应用程序和用户级别的任务。这些程序不具备直接访问或操作硬件资源的能力,它们必须通过系统调用(System Calls)等方式请求内核态执行相关操作。
  3. 安全性

    • 内核态的代码通常需要更高级别的安全保护,因为它可以影响整个系统的稳定性和安全性。错误或恶意的内核态代码可能导致系统崩溃或数据损坏。
    • 用户态的代码受到较严格的限制,因此即使应用程序发生错误,也不太可能对整个系统造成严重影响。
  4. 切换开销

    • 从用户态切换到内核态和从内核态切换回用户态都需要切换开销。这包括保存和恢复寄存器状态、修改特权级别等操作。因此,频繁的上下文切换可能会导致性能下降。

内核态和用户态的区别在于权限、功能和安全性。操作系统的设计目标之一是保护内核态免受错误或恶意代码的影响,同时允许用户态的应用程序以受控的方式访问系统资源。这种分层的特权级别有助于确保系统的稳定性和安全性。

image-20230531090531117

# 2.用户态/内核态切换条件?

当在系统中执行一个程序时,大部分时间是运行在用户态下的,在其需要操作系统帮助完成一些用户态自己没有特权和能力完成的操作时就会切换到内核态。

用户态切换到内核态的 3 种方式

(1)系统调用:这是用户态进程主动要求切换到内核态的一种方式.用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作.例如 fork()就是执行了一个创建新进程的系统调用.系统调用的机制是使用了操作系统为用户特别开放的一个中断来实现.

(2)异常:当 cpu 在执行运行在用户态下的程序时,发生了一些没有预知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关进程中,也就是切换到了内核态,如缺页异常。

(3)外围设备的中断:当外围设备完成用户请求的操作后,会向 CPU 发出相应的中断信号,这时 CPU 会暂停执行下一条即将要执行的指令而转到与中断信号对应的处理程序去执行,如果前面执行的指令是用户态下的程序,那么转换的过程自然就会是由用户态到内核态的切换.如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后边的操作等。

这三种方式是系统在运行时由用户态切换到内核态的最主要方式,其中系统调用可以认为是用户进程主动发起的,异常和外围设备中断则是被动的.从触发方式上看,切换方式都不一样,但从最终实际完成由用户态到内核态的切换操作来看,步骤又是一样的,都相当于执行了一个中断响应的过程.系统调用实际上最终是中断机制实现的,而异常和中断的处理机制基本一致。

# 3.用户态切换内核态步骤?

当进程需要执行一些需要特权级别的操作时,例如进行 I/O 操作、创建新进程、访问共享内存等,就需要切换到内核态。下面是用户态切换到内核态的大致步骤:

  1. 用户程序执行系统调用指令,例如 int 0x80(Linux)、syscall(Mac OS X)、Trap(Windows)等,触发 CPU 从用户态切换到内核态。
  2. CPU 从用户态切换到内核态,将当前进程的状态(包括 PC、寄存器、栈指针等)保存在内核栈中,以便后续恢复。
  3. CPU 执行操作系统内核代码,开始进行系统调用的处理。操作系统会根据请求的操作类型进行相应的处理,例如 I/O 操作、进程调度、内存管理等。
  4. 当操作完成后,CPU 从内核态切换回用户态,将之前保存的进程状态恢复。此时,进程重新开始执行用户程序。

需要注意的是,由于用户态和内核态之间的切换需要一定的时间和开销,因此尽可能减少进程在用户态和内核态之间的切换次数,可以提高系统的性能和响应速度。

# 五.实际应用

# 1.CPU 密集型

CPU 密集型:CPU 密集型也叫计算密集型,指的是系统的硬盘、内存性能相对 CPU 要好很多,此时,系统运作大部分的状况是 CPU Loading 100%,CPU 要读/写 I/O(硬盘/内存),I/O 在很短的时间就可以完成,而 CPU 还有许多运算要处理,CPU Loading 很高。

# 2.I/O 密集型

I/O 密集型:IO 密集型指的是系统的 CPU 性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是 CPU 在等 I/O (硬盘/内存) 的读/写操作,此时 CPU Loading 并不高。

# 3.线程池与密集型关系

线程池与 CPU 密集型的关系:

一般情况下,CPU 核心数 == 最大同时执行线程数.在这种情况下(设 CPU 核心数为 n),大量客户端会发送请求到服务器,但是服务器最多只能同时执行 n 个线程.设线程池工作队列长度为 m,且 m>>n,则此时会导致 CPU 频繁切换线程来执行(如果 CPU 使用的是 FCFS,则不会频繁切换,如使用的是其他 CPU 调度算法,如时间片轮转法,最短时间优先,则可能会导致频繁的线程切换).所以这种情况下,无需设置过大的线程池工作队列,(工作队列长度 = CPU 核心数 || CPU 核心数+1) 即可.

与 I/O 密集型的关系:

1 个线程对应 1 个方法栈,线程的生命周期与方法栈相同.比如某个线程的方法栈对应的入站顺序为:controller()->service()->DAO(),由于 DAO 长时间的 I/O 操作,导致该线程一直处于工作队列,但它又不占用 CPU,则此时有 1 个 CPU 是处于空闲状态的.所以,这种情况下,应该加大线程池工作队列的长度(如果 CPU 调度算法使用的是 FCFS,则无法切换),尽量不让 CPU 空闲下来,提高 CPU 利用率

# 4.如何设置核心线程数和最大线程数?

  • 需要进行压测

  • 并发访问量是多大

  • 不要用无界队列,且有界队列的最大值要合理

  • 充分利用 cpu

    • 一个线程处理计算型,100%
    • 50%计算型,需要 2 个线程
    • 25%计算型,需要 4 个线程
    • 多任务操作系统,对 CPU 都是分时使用的:比如 A 任务占用 10ms,然后 B 任务占用 30ms,然后空闲 60ms,再又是 A 任务占 10ms, B 任务占 30ms,空闲 60ms;如果在一段时间内都是如此,那么这段时间内的利用率为 40%,因为整个系统中只有 40%的时间是 CPU 处理数据的时间。
  • 任务的性质:CPU 密集型任务、lO 密集型任务和混合型任务。

    • CPU 密集型要设置尽量少的线程数
    • IO 密集型要设置尽量多的线程数
  • 任务的优先级:高、中和低。

    • 使用优先队列
    • 建议使用有界队列,且数量合理
  • 任务的执行时间:

  • 任务的依赖性:

    • 是否依赖其他系统资源,如数据库连接。
    • CPU 空闲时间越长,线程数应该设置的越大,更好的利用 CPU

# 5.CPU 飙升 100%?

发现程序 CPU 飙升 100%,内存和 I/O 利用正常,是什么原因?如何排查?

原因:死锁

排查: dump 线程数据

image-20220414183245074

# 6.什么情况下单线程比多线程快?

redis 是单线程的,redis 为什么快?

首先分配 cpu 资源的单位是进程。一个进程所获得到的 cpu 资源是一定的。程序在执行的过程中消耗的是 cpu,比如一个单核 cpu,多个线程同时执行工作时,需要不断切换执行(上下文切换),单个线程时间耗费更多了,而单线程只是一个线程跑。

先来解释一下什么是上下文切换。在多任务处理系统中,作业数通常大于 CPU 数。为了让用户觉得这些任务在同时进行,CPU 给每个任务分配一定时间,把当前任务状态保存下来,当前运行任务转为就绪(或者挂起、删除)状态,另一个被选定的就绪任务成为当前任务。之后 CPU 可以回过头再处理之前被挂起任务。上下文切换就是这样一个过程,它允许 CPU 记录并恢复各种正在运行程序的状态,使它能够完成切换操作。在这个过程中,CPU 会停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后继续运行。

总结:线程切换是有开销的,这会导致程序运行变慢。所以单线程比多线程的运行速度更快。

# 7.伪共享内存顺序冲突?

什么是伪共享内存顺序冲突?如何避免?

由于存放到 CPU 缓存行的是内存块而不是单个变量,所以可能会把多个变量存放到同一个缓存行中,当多个线程同时修改这个缓存行里面的多个变量时,由于同时只能有一个线程操作缓存行,此时有两个线程同时修改同一个缓存行下的两个不同的变量,这就是伪共享,也称内存顺序冲突。当出现伪共享时,CPU 必须清空流水线,会造成 CPU 比较大的开销。

如何避免:JDK1.8 之前一般都是通过字节填充的方式来避免该问题,也就是创建一个变量时使用填充字段填充该变量所在的缓存行,这样就避免了将多个变量存放在同一个缓存行中,例如如下代码:

public final static class FilledLong{
  public volatile long value=0L;
  public long pl,p2,p3,p4,p5,p6;
}
1
2
3
4

假如缓存行为 64 字节,那么我们在 FilledLong 类里填充了 6 个 long 类型的变量,一个 long 类型变量占用 8 字节,加上自己的 value 变量占用的 8 个字节,总共 56 字节.另外,这里 FilledLong 是一个类对象,而类对象的字节码的对象头占用 8 字节,所以一个 FilledLong 对象实际会占用 64 字节的内存,这正好可以放入同一个缓存行。

JDK 提供了 sun.misc Contended 注解,用来解决伪共享问题.将上面代码修改为如下。

@sun.misc.Contended
  public final static class FilledLong{
    public volatile longvalue=0L;
  }
1
2
3
4

特别注意 在默认情况下,@Contended 注解只用于 Java 核心类,比如 rt 包下的类。如果用户类路径下的类需要使用这个注解,需要添加 JVM 参数:- XX:-RestrictContended.填充的宽度默认为 128,要自定义填充宽度则可以通过参数-XX:ContendedPaddingWidth 参数进行设置。

# 8.双重检查锁的单例模式

public class Juc_book_fang_11_Dcl {
  private static volatile Person instance;
  public static Person getInstance(){
    if (instance == null){//步骤一
      synchronized (Juc_book_fang_11_Dcl.class){//步骤二
        if (instance == null){//步骤三
          instance = new Person();//步骤四
        }
        return instance;
      }
    }
    return instance;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

看着图中的注释,假设线程 A 执行 getInstance 方法

步骤一: instance 为 null,则进入 if 判断;

步骤二:获取 synchronized 锁,成功,进入同步代码块;

步骤三:继续判断 instance,为 null 则进入 if 判断;

步骤四: instance = new Instance().看似是一句代码,其实是三句代码。

memory=allocate(); //1:分配对象的内存空间
ctorInstance(memory);//2:初始化对象
instance=memory; //3:设置instance指向刚分配的内存地址
1
2
3

上面 2 和 3 两者没有依赖关系,设置 instance 指向刚分配的内存地址和初始化对象会存在重排序.

使用 volatile 并不会解决 2 和 3 的重排序问题,因为 2 和 3 都在一个 new指令里面,内存屏障是针对指令级别的重排序,双重检查锁 volatile 禁止重排序的原理,new 指令是单一指令,也就是前面加 StoreStore 屏障,后面加 StoreLoad 屏障,后面的线程必不会读到 instance 为 null

有 2 种解决方案:

  • 使用 volatile 禁止重排序,原理还是其他线程不可见

  • 允许 2 和 3 重排序,但是不允许其他线程可见

    • 基于类初始化
    • CLASS 对象的初始化锁只能有一个线程访问,对其他线程不可见

基于类的初始化:

public class InstanceFactory {
  private static class InstanceHolder {
    public static Instance instance=new Instance();
  }
  public static Instance getinstance() {
    return InstanceHolder.instance; // 这里将导致InstanceHolder类被初始化
  }
}
1
2
3
4
5
6
7
8

java 中,一个类或接口类型 T 将被立即初始化的情况如下

  1. T 是一个类,而且一个 T 类型的实例被创建。

  2. T 是一个类,且 T 中声明的一个静态方法被调用。

  3. T 中声明的一个静态字段被赋值。

  4. T 中声明的一个静态字段被使用,而且这个字段不是一个常量字段。

  5. T 是一个顶级类(TopLevelClass,见 Java 语言规范的§7.6),而且一个断言语句嵌套在 T 内部被执行。

    在示例代码中,首次执行 getlnstance)方法的线程将导致 InstanceHolder 类被初始化(符合情况 4)。

# 9.long 和 double 的非原子性协定

Java 内存模型要求 lock、unlock、read、load、assign、use、store、write 这 8 个操作都具有原子性,但是对于 64 位的数据类型(double、long)定义了相对宽松的规定:允许虚拟机将没有被 volatile 修饰的 64 位数据的读写操作划分为两次的 32 位操作来进行,即允许虚拟机可以不保证 64 位数据类型的 load、store、read 和 write 操作的原子性。

非原子性协定可能导致的问题: 如果有多个线程共享一个未申明为 volatile 的 long 或 double 类型的变量,并且同时对其进行读取和修改操作,就有可能会有线程读取到"半个变量"的数值或者是一半正确一半错误的失效数据。

在实际应用中的解决: 因为上述可能造成的问题,势必在对 long 和 double 类型变量操作时要加上 volatile 关键字,实际上如下:

1、64 位的 java 虚拟机不存在这个问题,可以操作 64 位的数据

2、目前商用 JVM 基本上都会将 64 位数据的操作作为原子操作实现

所以我们编写代码时一般不需要将 long 和 double 变量专门申明为 volatile

# 10.CompletableFuture使用

List<CompletableFuture<UserMessage>> futures = new ArrayList<>();
for (int i = 0; i < 1; i++) {
    CompletableFuture<UserMessage> future = CompletableFuture.supplyAsync(() -> this.submitAnswerByTitle(id, title));
    futures.add(future);
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
    .thenAccept(v -> {
        for (CompletableFuture<UserMessage> future : futures) {
            try {
                UserMessage userMessage = future.get();
                // 处理任务的返回结果
                // ...
            } catch (InterruptedException | ExecutionException e) {
                // 处理异常
                // ...
            }
        }
    }).join();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
上次更新: 10/29/2024, 10:27:50 AM