跳至主要內容

Java多线程

观风大约 17 分钟sychronizedReentrantLock线程池

多线程

Java有哪几种方式可以创建线程

  • 继承Thread类,重写run方法
  • 实现Runnable接口,实现run方法
  • Thread+Runnable的匿名内部类
  • Callable+FurtureTask
  • 线程池

为什么不建议用Executors创建线程池?

因为Executors里的阻塞队列是无界的,是Integer.MAX_VALUE,可能会导致队列过长占用过多内存而引发OOM

线程池有哪几种状态?

  • RUNNING:运行

  • SHUTDOWN:不接受新任务,但会继续把队列处理完

  • STOP:不接受新任务,也不会处理队列任务

  • TIDYING:线程池没有线程运行的状态

  • TERMINATED:

Sychronized和ReentranLock的区别?

  • 自动加锁
  • 公平非公平
  • 锁的对象
  • 锁升级

ThreadLocal有哪些应用场景?底层如何实现?

将数据缓存在某个线程内部。

底层有一个ThreadLocalMap,Map的key是ThreadLocal对象,Map的value是需要缓存的值。

关于threadlocal的内存泄漏,在用完threadlocal之后要去调用remove方法。

ReentrantLock分为公平锁和非公平锁,那底层分别是如何实现的?

公平锁:检查AQS队列是否有现成在排队,如果有在排队,则也排队。

非公平锁:不会去检查是否有线程排队,直接竞争锁。

img

公平和非公平只体现在加锁阶段,不体现在唤醒阶段。

Sychronized的锁升级过程是怎么样的?

  • 偏向锁:在锁对象头记录线程ID,下一次来获取锁的时候可以直接获取到,也就是支持可重入
  • 轻量级锁:如果有第二个线程来竞争锁,需要升级轻量级锁来自旋,不会阻塞线程
  • 重量级锁:如果自旋次数过多,就会升级为重量级锁,会导致线程阻塞。

Tomcat为什么要使用自定义类加载器?

Tomcat可以部署多个应用,每个应用可能出现相同的类名,而tomcat使用了WebAppClassLoader类加载器,每个应用使用自己的类加载器去加载。

说说对线程安全的理解

现成安全来说,安全是对一段代码来说,在多线程的竞争的情况下,这段代码是否能够正常执行,不会出现并发问题。

那么一般来说并发需要考虑的三个方面分别是:有序性、可见性、原子性。

一般来说需要去争抢资源的内容需要去加上锁,而锁的力度尽量的细。

并发、并行、串行的区别

并发:两个任务看似同时,实际上是拆分了很多份,一个个执行。

并行:同时执行

串行:一个执行完再执行下一个。

Java死锁如何避免

必要条件:

1.一个资源只能被一个线程使用

2.一个线程在阻塞的时候,不释放资源

3.一个线程已经获得的资源,在解锁之前不能被强行抢夺

4.若干线程形成头尾相接的循环等待

解决方式:

1.保证每个线程按同样的顺序加锁

2.针对加锁时限,设置一个超时时间

3.JVM会检查死锁

死锁的示例

public class DeadlockDemo {
    private static Object lock1 = new Object();
    private static Object lock2 = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread 1 acquired lock 1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2) {
                    System.out.println("Thread 1 acquired lock 2");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("Thread 2 acquired lock 2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock1) {
                    System.out.println("Thread 2 acquired lock 1");
                }
            }
        });

        t1.start();
        t2.start();
    }
}

谈谈对AQS的理解,AQS如何实现可重入锁?

1.AQS是Java线程的同步框架。

2.在AQS中,维护了一个信号量state和一个线程组成的双向链表队列。现成队列是给线程排队的,而state就像一个红绿灯,用来控制排队和放形。

3.在可重入锁的场景下,state就表示加锁的次数。0标识无锁,每加一次锁state就加1,释放锁就减1。

线程池的底层工作原理

线程池内部是通过队列+线程实现的。

1.如果此时线程池中的线程数量小于corePoolsize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务.

2.如果此时线程池中的线程数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。

3,如果此时线程池中的线程数量大于等于crePoolSize,缓中队列workQueue满,并且线程池中的数量小于maximumpoolsize,建新的线程来处理被添加的任务

4.如果此时线程池中的线程数量大于corePooSize,缓中队列workQueue满,并且线程池中的数量等于maximumPodlsize,那么通 handler所指定的策略来处理业任 务。 5.当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程

为什么线程池是先添加队列而不是先创建最大线程?

当核心线程池在忙,会继续添加任务在队列,当队列满了会添加新的线程。

谈谈ConcurrentHashMap的扩容机制

1.7版本

  • 1.7版本是双重数组,把segment的数组进行扩容。

1.8版本

  • 1.8版本只有一个Node数组
  • 对Node数组进行扩容,多线程同时对某些位置上的元素进行转移

如果线程池满了,会发生什么?

1.如果使用无界队列,还可以继续提交任务

2.如果使用有界队列,如果核心线程数没有达到上限,则增加线程,如果达到上限了,就采取拒绝策略

什么是线程和进程?

  • 进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。

  • 线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。

程序计数器为什么是私有的?

字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。

在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

虚拟机栈和本地方法栈为什么是私有的?

  • 虚拟机栈: 每个 Java 方法在执行之前会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
  • 本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。

为什么要使用多线程?

现在CPU可以有多核,可以同时操作多个任务,提升效率。

使用多线程可能带来什么问题?

内存泄漏、死锁、线程不安全

如何理解线程安全和不安全?

线程安全指的是在多线程环境下,对于同一份数据,不管有多少个线程同时访问,都能保证这份数据的正确性和一致性。

线程不安全则表示在多线程环境下,对于同一份数据,多个线程同时访问时可能会导致数据混乱、错误或者丢失。

单核 CPU 上运行多个线程效率一定会高吗?

如果任务是CPU密集型的,那么开很多线程会影响效率;如果任务是IO密集型的,那么开很多线程会提高效率。

* 线程有哪几种状态?

NEW: 初始状态,线程被创建出来但没有被调用 start()

RUNNABLE: 运行状态,线程被调用了 start()等待运行的状态。

BLOCKED:阻塞状态,需要等待锁释放。

WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。

TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。

TERMINATED:终止状态,表示该线程已经运行完毕。

Java 线程状态变迁图

什么是线程上下文切换?

  • 主动让出 CPU,比如调用了 sleep(), wait() 等。
  • 时间片用完,因为操作系统要防止一个线程或者进程长时间占用 CPU 导致其他线程或者进程饿死。
  • 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。
  • 被终止或结束运行

这其中前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 上下文切换

sleep() 方法和 wait() 方法对比

共同点:两者都可以暂停线程的执行。

区别

  • sleep() 方法没有释放锁,而 wait() 方法释放了锁
  • wait() 通常被用于线程间交互/通信,sleep()通常被用于暂停执行。
  • wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者notifyAll() 方法。sleep()方法执行完成后,线程会自动苏醒,或者也可以使用 wait(long timeout) 超时后线程会自动苏醒。
  • sleep()Thread 类的静态本地方法,wait() 则是 Object 类的本地方法。

为什么 wait() 方法不定义在 Thread 中?

wait() 是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。每个对象(Object)都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入 WAITING 状态,自然是要操作对应的对象(Object)而非当前的线程(Thread)。

可以直接调用 Thread 类的 run 方法吗?

直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

volatile 关键字

如何保证变量的可见性?

因为JMM的内存模型的线程之间是不共享的,而且线程里有自己的内存,在数据给线程处理的时候是从本地主内存读取数据到CPU线程的高速缓存中操作的,而线程之间的内存是不共享的,就会出现不可见性。

因为Java有一套数据一致性协议,在一个线程里的数据进行了修改之后,会立刻进行store和write进入内存,其他线程会监听总线内存变化,进而重新load和read共享的内存变量。

JMM(Java 内存模型)

如何禁止指令重排序?

在 Java 中,volatile 关键字除了可以保证变量的可见性,还有一个重要的作用就是防止 JVM 的指令重排序。 如果我们将变量声明为 volatile ,在对这个变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序。

乐观锁和悲观锁

什么是悲观锁?

也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程

什么是乐观锁?

乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。

不过,大量失败重试的问题也是可以解决的,像我们前面提到的 LongAdder以空间换时间的方式就解决了这个问题。

如何实现乐观锁?

乐观锁一般会使用版本号机制或 CAS 算法实现,CAS 算法相对来说更多一些,这里需要格外注意。

乐观锁存在哪些问题?

ABA 问题是乐观锁最常见的问题。

1、ABA问题

线程1:A->B

线程2:A->C->A

ABA 问题的解决思路是在变量前面追加上版本号或者时间戳

2、循环时间长开销大

LongAdder

3、只能保证一个共享变量的原子操作

AtomicReference

synchronized 关键字

synchronized 是什么?有什么用?

synchronized 是 Java 中的一个关键字,翻译成中文是同步的意思,主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

在 Java 早期版本中,synchronized 属于 重量级锁,效率低下。

不过,在 Java 6 之后, synchronized 引入了大量的优化如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

如何使用 synchronized?

  1. 修饰实例方法
  2. 修饰静态方法
  3. 修饰代码块

synchronized 底层原理了解吗?

synchronized 同步语句块的实现使用的是 monitorentermonitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

上面的字节码中包含一个 monitorenter 指令以及两个 monitorexit 指令,这是为了保证锁在同步代码块代码正常执行以及出现异常的这两种情况下都能被正确释放。

synchronized 和 volatile 有什么区别?

synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在!

  • volatile 关键字是线程同步的轻量级实现,所以 volatile性能肯定比synchronized关键字要好 。但是 volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块 。
  • volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
  • volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。

ReentrantLock

ReentrantLock 是什么?

ReentrantLock 实现了 Lock 接口,是一个可重入且独占式的锁,和 synchronized 关键字类似。都是为了实现线程之间访问资源的同步性和安全性。

不过,ReentrantLock 更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。

ReentrantReadWriteLock

ReentrantReadWriteLock 是什么?

ReentrantReadWriteLock 实现了 ReadWriteLock ,是一个可重入的读写锁,既可以保证多个线程同时读的效率,同时又可以保证有写入操作时的线程安全。

ReentrantReadWriteLock 其实是两把锁,一把是 WriteLock (写锁),一把是 ReadLock(读锁) 。读锁是共享锁,写锁是独占锁。读锁可以被同时读,可以同时被多个线程持有,而写锁最多只能同时被一个线程持有。

线程池常见参数有哪些?如何解释?

ThreadPoolExecutor 3 个最重要的参数:

  • corePoolSize : 任务队列未达到队列容量时,最大可以同时运行的线程数量。
  • maximumPoolSize : 任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
  • workQueue: 新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。

ThreadPoolExecutor其他常见参数 :

  • keepAliveTime:线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,多余的空闲线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁,线程池回收线程时,会对核心线程和非核心线程一视同仁,直到线程池中线程的数量等于 corePoolSize ,回收过程才会停止。
  • unit : keepAliveTime 参数的时间单位。
  • threadFactory :executor 创建新线程的时候会用到。
  • handler :饱和策略。关于饱和策略下面单独介绍一下。

线程池的饱和策略有哪些?

ThreadPoolExecutor.AbortPolicy 抛出 RejectedExecutionException来拒绝新任务的处理。

ThreadPoolExecutor.CallerRunsPolicy 调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。

ThreadPoolExecutor.DiscardPolicy 不处理新任务,直接丢弃掉。

ThreadPoolExecutor.DiscardOldestPolicy 此策略将丢弃最早的未处理的任务请求。

线程池常用的阻塞队列有哪些?

  • 容量为 Integer.MAX_VALUELinkedBlockingQueue(无界队列):FixedThreadPoolSingleThreadExector 。由于队列永远不会被放满,因此FixedThreadPool最多只能创建核心线程数的线程。

  • SynchronousQueue(同步队列):CachedThreadPoolSynchronousQueue 没有容量,不存储元素,目的是保证对于提交的任务,如果有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务。也就是说,CachedThreadPool 的最大线程数是 Integer.MAX_VALUE ,可以理解为线程数是可以无限扩展的,可能会创建大量线程,从而导致 OOM。

  • DelayedWorkQueue(延迟阻塞队列):ScheduledThreadPoolSingleThreadScheduledExecutorDelayedWorkQueue 的内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构,可以保证每次出队的任务都是当前队列中执行时间最靠前的。DelayedWorkQueue 添加元素满了之后会自动扩容原来容量的 1/2,即永远不会阻塞,最大扩容可达 Integer.MAX_VALUE,所以最多只能创建核心线程数的线程。

线程池处理任务的流程了解吗?

图解线程池实现原理

如何设定线程池的大小?

CPU 密集型任务(N+1)

I/O 密集型任务(2N)

如何动态修改线程池的参数?

img

serCorePoolSize();

setMaximumPoolSize();

阻塞队列需要去自定义一个ResizeableCapacityLinkedBolckIngQueue队列,把LinkedBolckingQueue的capacity的final关键字去掉。

AQS

AQS 是什么?

AQS 的全称为 AbstractQueuedSynchronizer ,翻译过来的意思就是抽象队列同步器。这个类在 java.util.concurrent.locks 包下面。

AQS 的原理是什么?

如果资源没有被加锁,就可以从队列中取出第一个线程来使用。