Java并发编程题库

文章目录


并发编程三要素?

  • 原子性
    原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行。

  • 可见性

    可见性指多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他线程可以立即看到修改的结果。

  • 有序性

    有序性,即程序的执行顺序按照代码的先后顺序来执行。

实现可见性的方法有哪些?

synchronized或者Lock:保证同一个时刻只有一个线程获取锁执行代码,锁释放之前把最新的值刷新到主内存,实现可见性。

创建线程的有哪些方式?

  • 继承Thread类创建线程类
  • 通过Runnable接口创建线程类
  • 通过Callable和Future创建线程
  • 通过线程池创建

创建线程的三种方式的对比?

  1. 采用实现Runnable、Callable接口的方式创建多线程。
    • 优势是:
      • 线程类只是实现了 Runnable接口或Callable接口,还可以继承其他类。
      • 在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线 程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模 型,较好地体现了面向对象的思想。
    • 劣势是:
      • 编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
  2. 使用继承Thread类的方式创建多线程
    • 优势是:
      • 编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法, 直接使用this即可获得当前线程。
    • 劣势是:
      • 线程类已经继承了 Thread类,所以不能再继承其他父类。

Runnable 和 Callable 的区别

  • Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()
  • Callable的任务执行后可返回值,而Runnable的任务是不能返回值的
  • Call方法可以抛出异常,run方法不可以
  • 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future 对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

Java线程具有五中基本状态

  • 新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t =new MyThread();
  • 就绪状态(Runnable):当调用线程对象的start()方法,线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了 t.start()此线程立即就会执行;
  • 运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程 才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入 口,也就是说,线程要想进入运行状态扶,行,首先必须处于就绪状态中;
  • 阻塞状态(Blocked):处于运行状态中的线程由于某种原因,晳时放弃对CPU 的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再 次被CPU调用以进入到运行状态。
  • 根据阻塞产生的原因不同,阻塞状态又可以分为三种:
    1. 等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
    2. 同步阻塞:线程在获取synchronized同步锁失败(因为锁被其它线程所占用), 它会进入同步阻塞状态;
    3. 其他阻塞:通过调用线程的sleep()或join()或发出了 I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理 完毕时,线程重新转入就绪状态。
  • 死亡状态(Dead):线程执行完了或者因异常退出了 run()方法,该线程结束 生命周期。

什么是线程池?有哪几种创建方式?

  • 线程池就是提前创建若干个线程,如果有任务需要处理,线程池里的线程就会处 理任务,处理完之后线程并不会被销毁,而是等待下一个任务。由于创建和销毁线程都是消耗系统资源的,所以当你想要频繁的创建和销毁线程的时候就可以考 虑使用线程池来提升系统的性能。
  • java提供了一个java.util.concurrent.Executor接口的实现用于创建线程池。
  • 四种线程池的创建:
    1. newCachedThreadPool创建一个可缓存线程池
    2. newFixedThreadPool创建一个定长线程池,可控制线程最大并发数
    3. newScheduledThreadPool创建一个定长线程池,支持定时及周期性任务执行
    4. newSingleThreadExecutor创建一个单线程化的线程池,它只会用唯一的工 作线程来执行任务。

线程池的优点?

  • 重用存在的线程,减少对象创建销毁的开销。

  • 可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞 争,避免堵塞。

  • 提供定时执行、定期执行、单线程、并发数控制等功能。

常用的并发工具类有哪些?

  • CountDownLatch
  • CyclicBarrier
  • Semaphore
  • Exchanger

CyclicBarrier 和 CountDownLatch 的区别

  • CountDownLatch简单的说就是一个线程等待,直到他所等待的其他线程都执行完成并且调用countDownO方法发出通知后,当前线程才可以继续执行。
  • CyclicBarrier是所有线程都进行等待,直到所有线程都准备好进入await()方 法之后,所有线程同时开始执行!
  • CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使 用reset()方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果 计算发生错误,可以重置计数器,并让线程们重新执行一次。
  • CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获 得CyclicBarrier阻塞的线程数量0isBroken方法用来知道阻塞的线程是否被中断。 如果被中断返回true,否则返回false.

synchronized 的作用?

  • 在Java中,synchronized关键字是用来控制线程同步的,就是在多线程的环境 下,控制synchronized代码段不被多个线程同时执行。
  • synchronized既可以加在一段代码上,也可以加在方法上。

volatile关键字的作用

  • 对于可见性,Java提供了 volatile关键字来保证可见性。
  • 当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
  • 从实践角度而言,volatile的一个重要作用就是和CAS结合,保证了原子性,详细的可以参见 java.util.concurrent.atomic 包下的类,比如 Atomiclnteger

什么是CAS

  • CAS是compare and swap的缩写,即我们所说的比较交换。
  • cas是一种基于锁的操作,而且是乐观锁。在java中锁分为乐观锁和悲观锁。悲 观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访 问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通 过给记录加version来获取数据,性能较悲观锁有很大的提高。
  • CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如 果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS 是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被 b线程修改了,那么a线程需要自旋,至!下次循环才有可能机会执行。
  • java.util.concurrent.atomic包下的类大多是使用CAS操作来实现的
    (Atomiclnteger,AtomicBoolean,AtomicLong)

CAS的问题

  1. CAS容易造成ABA问题
    • 一个线程a将数值改成了 b,接着又改成了 a,此时CAS认为是没有变化,其实 是已经变化过了,而这个问题的解决方案可以使用版本号标识,每操作一次 version 加 1。在 java5 中,已经提供了 AtomicStampedReference 来解决问题。
  2. 不能保证代码块的原子性
    • CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。 比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized 了。
  3. CAS造成CPU利用率増加
    • 之前说过了 CAS里面是一个循环判断的过程,如果线程一直没有获取到状态,cpu 资源会一直被占用

ThreadLocal是什么?有什么用?

  • ThreadLocal是一个本地线程副本变量工具类。主要用于将私有线程和该线程存 放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可 以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。
  • 简单说ThreadLocal就是一种以空间换时间的做法,在每个Thread里面维护了 一个以开地址法实现的ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了。

Java死锁以及如何避免?

  • Java中的死锁是一种编程情况,其中两个或多个线程被永久阻塞,Java死锁情况 出现至少两个线程和两个或更多资源。
  • Java发生死锁的根本原因是:在申请锁时发生了交叉闭环申请。

死锁的原因

  • 是多个线程涉及到多个锁,这些锁存在着交叉,所以可能会导致了一个锁依赖的闭环。
  • 例如:线程在获得了锁A并且没有释放的情况下去申请锁B,这时,另一个线程已经获得了锁B,在释放锁B之前又要先获得锁A,因此闭环发生,陷入死锁循环。
  • 默认的锁申请操作是阻塞的。
  • 所以要避免死锁,就要在一遇到多个对象锁交叉的情况,就要仔细亩查这几个对 象的类中的所有方法,是否存在着导致被依赖的环路的可能性。总之是尽量避免 在一个同步方法中调用其它对象的延时方法和同步方法。

怎么唤醒一个阻塞的线程

  • 如果线程是因为调用了 wait()、sleep()或者join()方法而导致的阻塞,可以中断线 程,并且通过抛出InterruptedException来唤醒它;
  • 如果线程遇到了 I0阻塞, 无能为力,因为I0是操作系统实现的Java代码并没有办法直接接触到操作系统。

不可变对象对多线程有什么帮助

  • 前面有提到过的一个问题,不可变对象保证了对象的内存可见性,对不可变对象 的读取不需要进行额外的同步手段,提升了代码执行效率。

什么是多线程的上下文切换

  • 多线程的上下文切换是指CPU控制权由一个已经正在运行的线程切换到另外一个 就绪并等待获取CPU执行权的线程的过程。

如果你提交任务时,线程池队列已满,这时会发生什么

  • 这里区分一下:
  • 如果使用的是无界队列LinkedBlockingQueue,也就是无界队列的话,没关 系,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎 认为是一个无夯大的队列,可以无限存放任务
  • 如果使用的是有界队列比如ArrayBIcckingQueue,任务首先会被添加到 ArrayBlockingQueue 中,ArrayBlockingQueue 满了,会根据 maximumPoolSize的值瑁加线程数量,如果增加了线程数量还是处理不过来, ArrayBlockingQueue继续满,那么则会使用拒绝策略
    RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy

Java中用到的线程调度算法是什么

  • 抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等 数据算出一个总的优先级并分配下一个时间片给某个线程执行。

什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing)?

  • 线程调度器是一个操作系统服务,它负责为Runnable状态的线程分配CPU时间。 一旦我们创建一个线程并启动它,它的扶,行便依赖于线程调度器的实现。时间分 片是指将可用的CPU时间分配给可用的Runnable线程的过程。
  • 分配CPU时间可以基于线程优先级或者线程等待的时间。线程调度并不受到Java虚拟机控制,所 以由应用程序来控制它是更好的选择(也就是说不要让你的程序依赖于线程的优 先级)

什么是自旋

  • 很多synchronized里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。
  • 既然synchronized里面的代码执行得非常快,不妨让等待锁的线 程不要被阻塞,而是在synchronized的边界做忙循环,这就是自旋。如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。

Java Concurrency API 中的 Lock 接口(Lock interface)是什么?对比同步它有什么优势?

  • Lock接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结 构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。
  • 它的优势有:
    • 可以使锁更公平
    • 可以使线程在等待锁的时候响应中断
    • 可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
    • 可以在不同的范围,以不同的顺序获取和释放锁

单例模式的线程安全性

首先要说的是单例模式的线程安全意味着:某个类的实例在多线程环境下只会被创建一次出来。单例模式有很多种的写法,我总结一下:

  • 饿汉式单例模式的写法:线程安全
  • 懒汉式单例模式的写法:非线程安全
  • 双重检锁单例模式的写法:线程安全

Semaphore有什么作用

  • Semaphore就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore 有一个构造函数,可以传入一个int型整数n,表示某段代码最多只有n个线程可 以访问,如果超出了 n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果Semaphore构造函数中传入的int型整数n=1, 相当于变成了一个synchronized 了。

Executors 类是什么?

  • Executors 为 Executor, ExecutorServise, ScheduledExecutorService, ThreadFactory和Callable类提供了一些工具方法。
  • Executors可以用于方便的创建线程池

线程类的构造方法、静态块是被哪个线程调用的

  • 这是一个非常刁钻和狡猾的问题。请记住:线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。
  • 如果说上面的说法让你感到困惑,那么我举个例子,假设Thread2中new 了Threadl, main 函数中 new Thread2,那么:
    Thread2的构造方法、静态块是main线程调用的,Thread2的run()方法是Thread2自己调用的
    Threadl的构造方法、静态块是Th read 2调用的,Threadl的run()方法是Threadl自己调用的

同步方法和同步块,哪个是更好的选择?

  • 同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代 码的效率。一条原则:同步的范围越小越好。

Java线程数过多会造成什么异常?

  • 线程的生命周期开销非常高
  • 消耗过多的CPU资源
  • 如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空 闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争CPU 资源时还将产生其他性能的开销。
  • 降低稳定性
  • JVM在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同, 并且承受着多个因森制约,包括JVM的启动参数、Thread构造函数中请求栈的 大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出 OutOfMemoryError 异常。

在java中守护线程和本地线程区别?

  • java中的线程分为两种:守护线程(Daemon)和用户线程(User)。
  • 任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon(bool on); true则把该线程设置为守护线程,反之则为用户线程。Thread.setDaemon() 必须在Thread.start()之前调用,否则运行时会抛出异常。
  • 两者的区别:
    • 唯一的区别是判断虚拟机(JVM)何时离开,Daemon是为其他线程提供服务,如果全部的User Thread已经撤离,Daemon没有可服务的线程,JVM撤离。也可以理解为守护线程是JVM自动创建的线程(但不一定),用户线程是程序创建的 线程;比如JVM的垃圾回收线程是一个守护线程,当所有线程已经撤离,不再产生垃圾,守护线程自然就没事可干了,当垃圾回收线程是Java虚拟机上仅剩的线 程时,Java虚拟机会自动离开。

线程与进程的区别?

  • 进程是操作系统分配资源的最小单元,线程是操作系统调度的最小单元。
  • 一个程序至少有一个进程,一个进程至少有一个线程。

什么是多线程中的上下文切换?

  • 多线程会共同使用一组计算机上的CPU,而线程数大于给程序分配的CPU数量时, 为了让各个线程都有执行的机会,就需要轮转使用CPU。不同的线程切换使用CPU 发生的切换数据等就是上下文切换。

死锁与活锁的区别,死锁与饥饿的区别?

  • 死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成 的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
  • 产生死锁的必要条件:
    1. 互斥条件:所谓互斥就是进程在某一时间内独占资源。
    2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
    3. 不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
    4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
  • 活锁:任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。
  • 活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活",而 处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。
  • 饥饿:一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执 行的状态。
  • Java中导致饥饿的原因:
    1. 高优先级线程吞噬所有的低优先级线程的CPU时间。
    2. 线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前 持续地对该同步块进行访问。
    3. 线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的wait方 法),因为其他线程总是被持续地获得唤耍。

Java中用到的线程调度算法是什么?

采用时间片轮转的方式。可以设置线程的优先级,会映射到下层的系统上面的优 先级上,如非特别需要,尽量不要用,防止线程饥饿。

为什么使用Executor框架?

  • 每次执行任务创建线程new Thread()比较消耗性能,创建一个线程是比较耗时、耗资源的。
  • 调用new Thread()创建的线程缺乏管理,被称为野线程,而且可以无限制的创建, 线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪,还有线程之间的频繁交昔也会消耗很多系统资源。
  • 接使用new Thread()启动的线程不利于扩展,比如定时执行、定期执行、定时定期执行、线程中断等都不便实现。

在 Java 中 Executor 和 Executors 的区别?

  • Executors工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。
  • Executor接口对象能执行我们的线程任务。
  • ExecutorService接口继承了 Executor接口并进行了扩展,提供了更多的方法我 们能获得任务执行的状态并且可以获取任务的返回值。
  • 使用ThreadPoolExecutor可以创建自定义线程池。
  • Future表示异步计算的结果,他提供了检查计算是否完成的方法,以等待计算的 完成,并可以使用get()方法获取计算的结果。

什么是原子操作?在Java Concurrency API中有哪些原 子类(atomic classes)?

  • 原子操作(atomic operation)意为"不可被中断的一个或一系列操作"。 处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。
  • 在Java中可以通过锁和循环CAS的方式来实现原子操作。CAS操作- Compare & Set,或是Compare & Swap,现在几乎所有的CPU指令都支持CAS 的原子操作。
  • 原子操作是指一个不受其他操作影响的操作任务单元。原子操作是在多线程环境 下避免数据不一致必须的手段。
  • int ++并不是一个原子操作,所以当一个线程读取它的值并加1时,另外一个线程有可能会读到之前的值,这就会引发错误。
  • 为了解决这个问题,必须保证增加操作是原子的,在JDK1.5之前我们可以使用同步技术来做到这一点。到 JDK1.5, java.util.concurrent.atomic 包提供了 int 和 long类型的原子包装类,它们可以自动的保证对于他们的操作是原子的并且不需要使用同步。
  • java.util.concurrent这个包里面提供了一组原子类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当 某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个 线程进入,这只是一种逻辑上的理解。
  • 原子类:AtomicBoolean, Atomiclnteger, AtomicLong, AtomicReference
  • 原子数组:AtomicIntegerArray, AtomicLongArray, Atomic Reference Array
  • 原子属性更新器:AtomicLongFieldUpdater, AtomicIntegerFieldUpdater, AtomicReferenceFieldUpdater
    解决ABA问题的原子类:AtomicMarkableReference (通过引入一个boolean来反映中间有没有变过),AtomicStampedReference (通过引入一个int来累加来反映中间有没有变过)

Java Concurrency API 中的 Lock 接口(Lock interface)是什么?对比同步它有什么优势?

  • Lock接口比同步方法和同步块提供了更具扩展性的锁操作。
  • 他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。
  • 它的优势有:
    1. 可以使锁更公平
    2. 可以使线程在等待锁的时候响应中断
    3. 可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间 可以在不同的范围,以不同的顺序获取和释放锁
  • 整体上来说Lock是synchronized的扩展版,Lock提供了无条件的、可轮询的 (tryLock方法)、定时的(tryLock带参方法)、可中断的(locklnterruptibly)、可多条件队列的(newCondition方法)锁操作。另外Lock的实现类基本都支持非公平锁(默认)和公平锁,synchronized 只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择。

什么是Executors框架?

  • Executor框架是一个根据一组执行策略调用、调度,执行和控制的异步任务的框架。
  • 无限制的创建线程会引起应用程序内存溢出。所以创建一个线程池是个更好的的 解决方案,因为可以限制线程的数量并且可以回收再利用这些线程。利用Executors框架可以非常方便的创建一个线程池。

什么是阻塞队列?阻塞队列的实现原理是什么?如何使用阻塞队列来实现生产者-消费者模型?

  • 阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。
  • 这两个附加的操作是:在队列为空时,荻取元素的线程会等待队列变为非空。当 队列满时,存储元素的线程会等待队列可用。
  • 阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者 也只从容器里拿元素。
  • JDK7提供了 7个阻塞队列。分别是:
    1. ArrayBlockingQueue : 一个由数组结构组成的有界阻塞队列。
    2. LinkedBlockingQueue : 一个由链表结构组成的有界阻塞队列。
    3. PriorityBlockingQueue : 一个支持优先级排序的无界阻塞队列。
    4. DelayQueue: 一个使用优先级队列实现的无界阻塞队列。
    5. SynchronousQueue:—个不存储元素的阻塞队列。
    6. LinkedTransferQueue: 一个由链表结构组成的无界阻塞队列。
    7. LinkedBlockingDeque: 一个由链表结构组成的双向阻塞队列。
  • Java 5之前实现同步存取时,可以使用晋通的一个集合,然后在使用线程的协作 和线程同步可以实现生产者,消费者模式,主要的技术就是用好,wait ,notify,notifyAll,sychronized这些关键字。而在java 5之后,可以使用阻塞队列来实现,此方式大大简少了代码量,使得多线程编程更加容易,安全方面也有保障。
  • BlockingQueue接口是Queue的子接口,它的主要用途并不是作为容器,而是作为线程同步的的工具,因此他具有一个很明显的特性,当生产者线程试图向BlockingQueue放入元素时,如果队列已满,则线程被阻塞,当消费者线程试图 从中取出一个元素时,如果队列为空,则该线程会被阻塞,正是因为它所具有这个特性,所以在程序中多个线程交昔向BlockingQueue中放入元素,取出元素, 它可以很好的控制线程之间的通信。
    阻塞队列使用最经典的场景就是socket客户端数据的读取和解析,读取数据的线程不断将数据放入队列,然后解析线程不断从队列取数据解析。

什么是 Callable 和 Future?

  • Callable接口类似于Runnable,从名字就可以看出来了,但是Runnable不会返回结果,并且无法抛出返回结果的异常,而Callable功能更强大一些,被线程执行后,可以返回值,这个返回值可以被Future拿到,也就是说,Future可以拿到异步执行任务的返回值。
  • 可以认为是带有回调的Runnable。
  • Future接口表示异步任务,是还没有完成的任务给出的未来结果。所以说Callable 用于产生结果,Future用于获取结果。

什么是 FutureTask?使用 ExecutorService 启动任务。

  • 在Java并发程序中FutureTask表示一个可以取消的异步运算。它有启动和取消运算、查询运算是否完成和取回运算结果等方法。只有当运算完成的时候结果才能取回,如果运算尚未完成get方法将会阻塞。一个FutureTask对象可以对调用了 Callable和Runnable的对象进行包装,由于FutureTask也是调用了 Runnable接口所以它可以提交给Executor来执行

什么是并发容器的实现?

  • 何为同步容器:可以简单地理解为通过synchronized来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。比如Vector, Hashtable, 以及 Collections.synchronizedSet, synchronizedList 等方法返回的容器。 可以通过直看Vector, Hashtable等这些同步容器的实现代码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并在需要同步的方法上加上 关键字 synchronized。
  • 并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性, 例如在ConcurrentHashMap中采用了一种粒度更细的加锁机制,可以称为分段 锁,在这种锁机制下,允许任意数量的读线程并发地访问map,并且执行读操作 的线程和写操作的线程也可以并发的访问map,同时允许一定数量的写操作线程 并发地修改map,所以它可以在并发环境下实现更高的吞吐量。

多线程同步和互斥有几种实现方法,都是什么?

  • 线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。 线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它 要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成 是一种特殊的线程同步。
  • 线程间的同步方法大体可分为两类:用户模式和内核模式。顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态, 而用户模式就是不需要切换到内核态,只在用户态完成操作。
  • 用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。内核模式下的方法有:事件,信号量,互斥量。

什么是竞争条件?你怎样发现和解决竞争?

当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的 顺序时,则我们认为这发生了竞争条件(race condition)

在 Java 中 CydiBarriar 和 Countdown Latch 有什么区别?

  • CyclicBarrier可以重复使用,而CountcownLatch不能重复使用。
  • Java的concurrent包里面的CountDownLatch其实可以把它看作一个计数器, 只不过这个计数器的操作是原子操作,同时只能有一个线程去操作这个计数器, 也就是同时只能有一个线程去减这个计数器里面的值。
  • 你可以向CountDownLatch对象设置一个初始的数字作为计数值,任何调用这个 对象上的await()方法都会阻塞,直到这个计数器的计数值被其他的线程減为0为 止。
  • 所以在当前计数到达零之前,await方法会一直受阻塞。之后,会释放所有等待 的线程,await的所有后续调用都将立即返回。这种现象只出现一次——计数无法被重置。如果需要重置计数,请考虑使用CyclicBarrier。
    CountDownLatch的一个非常典型的应用场景是:有一个任务想要往下执行,但 必须要等到其他的任务执行完毕后才可以继续往下执行。假如我们这个想要继续 往下执行的任务调用一个CountDownLatch对象的await()方法,其他的任务执 行完自己的任务后调用同一个CountDownLatch对象上的countDown()方法,
    这个调用await()方法的任务将一直阻塞等待,直到这个CountDownLatch对象 的计数值减到0为止。
    CyclicBarrier-个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏 障点(common barrier point)。在涉及一组固定大小的线程的程序中,这些线程 必须不时地互相等待,此时CyclicBarrier很有用。因为该barrier在释放等待 线程后可以重用,所以称它为循环的barrier。

什么是不可变对象,它对写并发应用有什么帮助?

  • 不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,反之即为可变对象(Mutable 0bjects)
  • 不可变对象的类即为不可变类(Immutable Class). Java平台类库中包含许多不可变类如String.基本类型的包装类、Biginteger和BigDecimal等。
  • 不可变对象天生是线程安全的。它们的常量(域)是在构造函数中创建的。既然 它们的状态无法修改,这些常量永远不会变。
  • 不可变对象永远是线程安全的。
  • 只有满足如下状态,一个对象才是不可变的;
  • 它的状态不能在创建后再被修改;
  • 所有域都是final类型;并且,它被正确创建(创建期间没有发生this引用的逸出)。

什么是多线程中的上下文切换?

  • 在上下文切换过程中,CPU会停止处理当前运行的程序,并保存当前程序运行的 具体位置以便之后继续运行。从这个角度来看,上下文切换有点像我们同时阅读 几本书,在来回切换书本的同时我们需要记住每本书当前读到的页码。在程序中, 上下文切换过程中的"页码"信息是保存在进程控制块(PCB)中的。PCB还经 常被称作"切换桢"(switchframe)。"页码"信息会一直保存到CPU的内存 中,直到他们被再次使用。
  • 上下文切换是存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。上下文切换是多任务操作系统和多线程环境的基本特征。

Java中用到的线程调度算法是什么?

  • 计算机通常只有一个CPU,在任意时刻只能执行一条机器指令,每个线程只有获得 CPU的使用权才能执行指令.所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得CPU的使用权,分别执行各自的任务.在运行池中,会有多个处于就绪状 态的线程在等待CPUJAVA虚拟机的一项任务就是负责线程的调度,线程调度是指 按照特定机制为多个线程分配CPU的使用权
  • 有两种调度模型:分时调度模型和抢占式调度模型。
    • 分时调度模型是指让所有的线程轮流获徨cpu的使用权,并且平均分配每个线程占用的CPU的时间片这个也比较好理解。
    • java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用 CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用 CPU。处于运行状态的线程会一直运行,直至它不得不放弃CPU。

什么是可重入锁(ReentrantLock) ?

举例来说明锁的可重入性

public class UnReentrant {
    Lock lock = new Lock();

​```
public void outer() {
    lock.lock();
    inner();
    lock.unlock();
}

public void inner(){
    lock.lock();
    //do something
    lock.unlock();
}
​```
}

outer中调用了 inner, outer先锁住了 lock,这样inner就不能再获取lock。其实调用outer的线程已经获取了 lock锁。但是不能在inner中重复利用已经获取 的锁资源,这种锁即称之为不可重入。可重入就意味着:线程可以进入任何一个它已经拥有的锁所同步着的代码块。

synchronized、ReentrantLock都是可重入的锁,可重入锁相对来说简化了并发 编程的开发。

什么叫线程安全? servlet是线程安全吗?

  • 线程安全是编程中的术语,指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。
  • Servlet不是线程安全的,servlet是单实例多线程的,当多个线程同时访问同一个方法,是不能保证共享变量的线程安全性的。
  • truts2的action是多实例多线程的,是线程安全的,每个请求过来都会new 一个新的action分配给这个请求,请求完成后销毁。

SpringMVC的Controller是线程安全的吗?

  • 不是的,和Servlet类似的处理流程。
  • Struts2好处是不用考虑线程安全问题;Servlet和SpringMVC需要考虑线程安全问题,但是性能可以提升不用处理太多的gc,可以使用ThreadLocal来处理多线程的问题。

volatile有什么用?能否用一句话说明下volatile的应用场景?

  • volatile保证内存可见性和禁止指令重排。
  • volatile用于多线程环境下的单次操作(单次读或者单次写)。

为什么代码会重排序?

  • 在执行程序时,为了提供性能,处理器和编译器常常会对指令进行重排序,但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件:
    1. 在单线程环境下不能改变程序运行的结果
    2. 存在数据依赖关系的不允许重排序
  • 需要注意的是:重排序不会影响单线程环境的执行结果,但是会破坏多线程的执行语义。

如何在两个线程间共享数据?

  • 在两个线程间共享变量即可实现共享。
  • 一般来说,共享变量要求变量本身是线程安全的,然后在线程内使用的时候,如 果有对共享变量的复合操作,那么也得保证复合操作的线程安全性。

Java 中 notify 和 notifyAII 有什么区别?

  • notify()方法不能唤醒某个具体的线程,所以只有一个线程在等待的时候它才有用武之地。而notifyAII()唤醒所有线程并允许他们争夺锁确保了至少有一个线程能继续运行。

为什么wait, notify和notifyAII这些方法不在thread 类里面?

  • 一个很明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁, 通过线程获得。由于wait, notify和notifyAII都是锁级别的操作,所以把他们定 义在Object类中因为锁属于对象。

什么是ThreadLocal变量?

  • ThreadLocal是Java里一种特殊的变量。每个线程都有一个ThreadLocal就是每 个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了。它是为创建代价高昂的对象获取线程安全的好方法,比如你可以用ThreadLocal让 SimpleDateFormat変成线程安全的,因为那个类创建代价高昂且每次调用都需要创建不同的实例所以不值得在局部范围使用它,如果为每个线程提供一个自己 独有的变量拷贝,将大大提高效率。首先,通过冥用减少了代价高昂的对象的创 建个数。其次,你在没有使用高代价的同步或者不变性的情况下获得了线程安全。

为什么wait和notify方法要在同步块中调用?

Java API强制要求这样做,如果你不这么做,你的代码会抛出IHegalMonitorStateException 异常。还有一个原因是为了避免 wait 和 notify 之间产生竞态条件。

怎么检测一个线程是否拥有锁?

在java.Iang.Thread中有一个方法叫holdsLock(),它返回true如果当且仅当当 前线程拥有某个具体对象的锁。

Thread类中的yield方法有什么作用?

  • 使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。
  • 当前线程到了就绪状态,那么接下来哪个线程会从就绪状态变成执行状态呢?可 能是当前线程,也可能是其他线程,看系统的分配了。

Java线程池中submit。和execute。方法有什么区别?

  • 两个方法都可以向线程池提交任务,execute()方法的返回类型是void,它定义在Executor 接口中。
    而submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了 Executor接口,其它线程池类像ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 都有这些方法。

什么是阻塞式方法?

  • 阻塞式方法是指程序会一直等待该方法完成期间不做其他事情,Serversocket的 accept()方法就是一直等待客户端连接。这里的阻塞是指调用结果返回之前,当前线程会被挂起,直到得到结果之后才会追回。此外,还有异步和非阻塞式方法在任务完成前就返回。

Java 中的 ReadWriteLock 是什么?

  • 读写锁是用来提升并发程序性能的锁分离技术的成果。

volatile变量和atomic变量有什么不同?

  • Volatile变量可以确保先行关系,即写操作会发生在后续的读操作之前,但它并不 能保证原子性。例如用volatile修饰count变量那么count ++操作就不是原子性的。
  • 而Atomiclnteger类提供的atomic方法可以让这种操作具有原子性如 getAndlncrement()方法会原子性的进行瑁量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。

为什么wait(), notifyOffl notifyAII ()必须在同步方法或者同步块中被调用?

当一个线程需要调用对象的wait()方法啟时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的notify()方法。同样的,当一个线程需要调用对象的notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用。

Synchronized 用过吗, 其原理是什么?

  • Synchronized 是由 JVM 实现的一种实现互斥同步的一种方式, 如果你查看被 Synchronized 修饰过的程序块编译后的字节码, 会发现, 被 Synchronized 修饰过的程序块, 在编译前后被编译器生成了monitorenter 和 monitorexit 两个字节码指令。
  • 这两个指令是什么意思呢?
  • 在虚拟机执行到 monitorenter 指令时, 首先要尝试获取对象的锁: 如果这个对象没有锁定, 或者当前线程已经拥有了这个对象的锁, 把锁的计数器+ 1 ; 当执行 monitorexit 指令时将锁计数器- 1 ; 当计数器为 0 时, 锁就被释放了。
  • 如果获取对象失败了, 那当前线程就要阻塞等待, 直到对象锁被另外一个线程释放为止。Java中Synchronized通过在对象头设置标记, 达到了获取锁和释放锁的目的。

你刚才提到获取对象的锁, 这个“ 锁” 到底是什么? 如何确定对象的锁?

  • “ 锁” 的本质其实是 monitorenter 和 monitorexit 字节码指令的一个Reference 类型的参数, 即要锁定和解锁的对象。 我们知道, 使用Synchronized 可以修饰不同的对象, 因此,对应的对象锁可以这么确定。
  • 如果 Synchronized 明确指定了锁对象, 比如 Synchronized( 变量名) 、 Synchronized( this) 等, 说明加解锁对象为该对象。
  • 如果没有明确指定:
    • 若Synchronized修饰的方法为非静态方法, 表示此方法对应的对象为锁对象;
    • 若Synchronized修饰的方法为静态方法, 则表示此方法对应的类对象为锁对象。
    • 注意, 当一个对象被锁住时, 对象里面所有用Synchronized修饰的方法都将产生堵塞,而对象里非Synchronized修饰的方法可正常被调用, 不受锁影响。

什么是可重入性, 为什么说 Synchronized 是可重入锁?

  • 可重入性是锁的一个基本要求, 是为了解决自己锁死自己的情况。比如下面的伪代码, 一个类中的同步方法调用另一个同步方法, 假如Synchronized 不支持重入, 进入 method 2 方法时当前线程获得锁,method 2 方法里面执行method 1时当前线程又要去尝试获取锁, 这时如果不支持重入, 它就要等释放, 把自己阻塞, 导致自己锁死自己。
  • 对 Synchronized 来说, 可重入性是显而易见的, 刚才提到, 在执行monitorenter 指令时, 如果这个对象没有锁定, 或者当前线程已经拥有了这个对象的锁( 而不是已拥有了锁则不能继续获取) , 就把锁的计数器+1 , 其实本质上就通过这种方式实现了可重入性。

JVM 对 Java 的原生锁做了哪些优化?

  • 在Java 6之前,Monitor的实现完全依赖底层操作系统的互斥锁来实现, 也就是刚才上面所阐述的获取/ 释放锁的逻辑。
  • 由于Java层面的线程与操作系统的原生线程有映射关系, 如果要将一个线程进行阻塞或唤起都需要操作系统的协助, 这就需要从用户态切换到内核态来执行, 这种切换代价十分昂贵, 很耗处理器时间, 现代JDK 中做了大量的优化。
  • 一种优化是使用自旋锁, 即在把线程进行阻塞操作之前先让线程自旋等待一段时间, 可能在等待期间其他线程已经解锁, 这时就无需再让线程执行阻塞操作, 避免了用户态到内核态的切换。
  • 现代 JDK 中还提供了三种不同的 Monitor 实现, 也就是三种不同的锁:
    • 偏向锁( Biased Locking)
    • 轻量级锁
    • 重量级锁
  • 这三种锁使得JDK得以优化Synchronized的运行, 当JVM检测到不同的竞争状况时, 会自动切换到适合的锁实现, 这就是锁的升级、降级。
  • 当没有竞争出现时, 默认会使用偏向锁。
  • JVM会利用CAS操作, 在对象头上的Mark Word部分设置线程ID,以表示这个对象偏向于当前线程, 所以并不涉及真正的互斥锁, 因为在很多应用场景中, 大部分对象生命周期中最多会被一个线程锁定, 使用偏斜锁可以降低无竞争开销。
  • 如果有另一线程试图锁定某个被偏斜过的对象, JVM就撤销偏斜锁, 切换到轻量级锁实现。
    轻量级锁依赖CAS 操作 Mark Word 来试图获取锁, 如果重试成功, 就使用普通的轻量级锁; 否则, 进一步升级为重量级锁。

为什么说 Synchronized 是非公平锁?

  • 非公平主要表现在获取锁的行为上, 并非是按照申请锁的时间前后给等待线程分配锁的, 每当锁被释放后, 任何一个线程都有机会竞争到锁, 这样做的目的是为了提高执行性能, 缺点是可能会产生线程饥饿现象。

什么是锁消除和锁粗化?

  • 锁消除: 指虚拟机即时编译器在运行时, 对一些代码上要求同步, 但被检测到不可能存在共享数据竞争的锁进行消除。 主要根据逃逸分析。程序员怎么会在明知道不存在数据竞争的情况下使用同步呢? 很多不是程序员自己加入的。
  • 锁粗化: 原则上, 同步块的作用范围要尽量小。 但是如果一系列的连续操作都对同一个对象反复加锁和解锁, 甚至加锁操作在循环体内, 频繁地进行互斥同步操作也会导致不必要的性能损耗。
    锁粗化就是增大锁的作用域。

为什么说Synchronized是一个悲观锁? 乐观锁的实现原理又是什么? 什么是 CAS, 它有什么特性?

  • Synchronized 显然是一个悲观锁, 因为它的并发策略是悲观的:不管是否会产生竞争, 任何的数据操作都必须要加锁、 用户态核心态转换、 维护锁计数器和检查是否有被阻塞的线程需要被唤醒等操作。
  • 随着硬件指令集的发展, 我们可以使用基于冲突检测的乐观并发策略。先进行操作, 如果没有其他线程征用数据, 那操作就成功了;
  • 如果共享数据有征用, 产生了冲突, 那就再进行其他的补偿措施。 这种乐观的并发策略的许多实现不需要线程挂起, 所以被称为非阻塞同步。
  • 乐观锁的核心算法是CAS( Compareand Swap, 比较并交换) , 它涉及到三个操作数: 内存值、 预期值、 新值。 当且仅当预期值和内存值相等时才将内存值修改为新值。
  • 这样处理的逻辑是, 首先检查某块内存的值是否跟之前我读取时的一样, 如不一样则表示期间此内存值已经被别的线程更改过, 舍弃本次操作, 否则说明期间没有其他线程对此内存值操作, 可以把新值设置给此块内存。
  • CAS 具有原子性, 它的原子性由CPU硬件指令实现保证, 即使用JNI调用Native方法调用由C++编写的硬件级别指令, JDK 中提供了 Unsafe 类执行这些操作。

乐观锁一定就是好的吗?

  • 乐观锁避免了悲观锁独占对象的现象, 同时也提高了并发性能, 但它也有缺点:
    1. 乐观锁只能保证一个共享变量的原子操作。 如果多一个或几个变量, 乐观锁将变得力不从心, 但互斥锁能轻易解决, 不管对象数量多少及对象颗粒度大小。
    2. 长时间自旋可能导致开销大。 假如CAS长时间不成功而一直自旋, 会给 CPU 带来很大的开销。
    3. ABA问题。 CAS的核心思想是通过比对内存值与预期值是否一样而判断内存值是否被改过, 但这个判断逻辑不严谨, 假如内存值原来是A,后来被一条线程改为 B, 最后又被改成了A, 则CAS认为此内存值并没有发生改变, 但实际上是有被其他线程改过的, 这种情况对依赖过程值的情景的运算结果影响很大。 解决的思路是引入版本号, 每次变量更新都把版本号加一。

跟Synchronized相比, 可重入锁ReentrantLock其实现原理有什么不同?

  • 其实, 锁的实现原理基本是为了达到一个目的: 让所有的线程都能看到某种标记。
  • Synchronized 通过在对象头中设置标记实现了这一目的, 是一种 JVM 原生的锁实现方式, 而 ReentrantLock 以及所有的基于Lock接口的实现类, 都是通过用一个 volatile修饰的int型变量, 并保证每个线程都能拥有对该 int 的可见性和原子修改, 其本质是基于所谓的AQS框架。

那么请谈谈 AQS 框架是怎么回事儿?

  • AQS( Abstract Queued Synchronizer 类) 是一个用来构建锁和同步器的框架, 各种Lock包中的锁( 常用的有 ReentrantLock、ReadWriteLock) , 以及其他如 Semaphore、 CountDownLatch, 甚至是早期的 FutureTask 等, 都是基于AQS来构建。
  • AQS在内部定义了一个volatile int state变量, 表示同步状态: 当线程调用lock 方法时,如果 state= 0 ,说明没有任何线程占有共享资源的锁, 可以获得锁并将 state= 1 ; 如果 state= 1 , 则说明有线程目前正在使用共享变量, 其他线程必须加入同步队列进行等待。
  • AQS通过Node内部类构成的一个双向链表结构的同步队列, 来完成线程获取锁的排队工作, 当有线程获取锁失败后, 就被添加到队列末尾。
  • Node 类是对要访问同步代码的线程的封装, 包含了线程本身及其状态叫wait Status( 有五种不同取值, 分别表示是否被阻塞, 是否等待唤醒, 是否已经被取消等) , 每个Node结点关联其prev结点和next 结点, 方便线程释放锁后快速唤醒下一个在等待的线程, 是一个FIFO的过程。
  • Node类有两个常量, SHARED和EXCLUSIVE, 分别代表共享模式和独占模式。 所谓共享模式是一个锁允许多条线程同时操作( 信号量Semaphore就是基于AQS的共享模式实现的),独占模式是同一个时间段只能有一个线程对共享资源进行操作, 多余的请求线程需要排队等待( 如 Reentran Lock)
  • AQS通过内部类Condition Object构建等待队列( 可有多个) , 当Condition调用wait()方法后, 线程将会加入等待队列中, 而当Condition调用signal()方法后, 线程将从等待队列转移动同步队列中进行锁竞争
  • AQS和Condition各自维护了不同的队列,在使用Lock和Condition 的时候, 其实就是两个队列的互相移动。

请尽可能详尽地对比下Synchronized和ReentrantLock的异同。

  • ReentrantLock是 Lock 的实现类, 是一个互斥的同步锁。
  • 从功能角度, ReentrantLock比Synchronized的同步操作更精细( 因为可以像普通对象一样使用) , 甚至实现Synchronized 没有的高级功能, 如:
    • 等待可中断: 当持有锁的线程长期不释放锁的时候, 正在等待的线程可以选择放弃等待, 对处理执行时间非常长的同步块很有用。
    • 带超时的获取锁尝试: 在指定的时间范围内获取锁, 如果时间到了仍然无法获取则返回。
    • 可以判断是否有线程在排队等待获取锁。
    • 可以响应中断请求: 与Synchronized不同, 当获取到锁的线程被中断时, 能够响应中断, 中断异常将会被抛出, 同时锁会被释放。
    • 可以实现公平锁。
  • 从锁释放角度, Synchronized 在 JVM 层面上实现的, 不但可以通过一些监控工具监控 Synchronized 的锁定, 而且在代码执行出现异常时, JVM 会自动释放锁定; 但是使用 Lock 则不行, Lock 是通过代码实现的, 要保证锁定一定会被释放, 就必须将 un Lock() 放到f inally{} 中。
  • 从性能角度,Synchronized早期实现比较低效, 对比ReentrantLock, 大多数场景性能都相差较大。
    但是在 Java 6 中对其进行了非常多的改进, 在竞争不激烈时, Synchronized 的性能要优于 ReetrantLock;
  • 在高竞争情况下, Synchronized 的性能会下降几十倍, 但是 Reetrant Lock 的性能能维持常态。

ReentrantLock 是如何实现可重入性的?

  • ReentrantLock 内部自定义了同步器Sync( Sync 既实现了AQS,又实现了AOS, 而AOS提供了一种互斥锁持有的方式) , 其实就是加锁的时候通过 CAS 算法, 将线程对象放到一个双向链表中, 每次获取锁的时候, 看下当前维护的那个线程 ID 和当前请求的线程 ID 是否一样, 一样就可重入了。

除了 ReetrantLock, 你还接触过 JUC 中的哪些并发工具?

  • 通常所说的并发包( JUC) 也就是java. util. concurrent及其子包, 集中了 Java 并发的各种基础工具类, 具体主要包括几个方面:
  • 提供了CountDownLatch、 CyclicBarrier、 Semaphore等, 比Synchronized 更加高级, 可以实现更加丰富多线程操作的同步结构。
  • 提供了 ConcurrentHashMap、 有序的 ConcunrrentSkipListMap, 或者通过类似快照机制实现线程安全的动态数组 Copy OnWriteArrayList 等, 各种线程安全的容器。
  • 提供了ArrayBlockingQueue、 SynchorousQueue 或针对特定场景的PriorityBlockingQueue 等, 各种并发队列实现。
  • 强大的Executor框架, 可以创建各种不同类型的线程池, 调度任务运行等。

请谈谈 ReadWriteLock 和 StampedLock。

  • 虽然ReentrantLock和Synchronized简单实用, 但是行为上有一定局限性, 要么不占, 要么独占。 实际应用场景中, 有时候不需要大量竞争的写操作, 而是以并发读取为主, 为了进一步优化并发操作的粒度, Java 提供了读写锁。

  • 读写锁基于的原理是多个读操作不需要互斥, 如果读锁试图锁定时, 写锁是被某个线程持有, 读锁将无法获得, 而只好等待对方操作结束, 这样就可以自动保证不会读取到有争议的数据。

  • ReadWriteLock代表了一对锁, 下面是一个基于读写锁实现的数据结构, 当数据量较大, 并发读多、 并发写少的时候, 能够比纯同步版本凸显出优势:

在这里插入图片描述

  • 读写锁看起来比Synchronized的粒度似乎细一些, 但在实际应用中, 其表现也并不尽如人意, 主要还是因为相对比较大的开销。

  • 所以,JDK 在后期引入了StampedLock, 在提供类似读写锁的同时, 还支持优化读模式。 优化读基于假设, 大多数情况下读操作并不会和写操作冲突, 其逻辑是先试着修改, 然后通过validate方法确认是否进入了写模式, 如果没有进入, 就成功避免了开销; 如果进入, 则尝试获取读锁。

在这里插入图片描述

Java 中的线程池是如何实现的?

  • 在Java中,所谓的线程池中的"线程", 其实是被抽象为了一个静态内部类Worker, 它基于AQS实现, 存放在线程池的Hash Set workers 成员变量中;
  • 而需要执行的任务则存放在成员变量workQueue( Blocking Queue< Runnable> work Queue) 中。
    这样, 整个线程池实现的基本思想就是: 从workQueue中不断取出需要执行的任务, 放在Workers 中进行处理。

创建线程池的几个核心构造参数?

Java中的线程池的创建其实非常灵活, 我们可以通过配置不同的参数, 创建出行为不同的线程池, 这几个参数包括:

  • corePoolSize: 线程池的核心线程数。
  • maximumPoolSize: 线程池允许的最大线程数。
  • keepAliveTime: 超过核心线程数时闲置线程的存活时间。
  • workQueue: 任务执行前保存任务的队列, 保存由execute方法提交的 Runnable 任务。

线程池中的线程是怎么创建的? 是一开始就随着线程池的启动创建好的吗?

  • 显然不是的。 线程池默认初始化后不启动Worker, 等待有请求时才启动。
  • 每当我们调用execute()方法添加一个任务时, 线程池会做如下判断:
    1. 如果正在运行的线程数量小于corePoolSize, 那么马上创建线程运行这个任务;
    2. 如果正在运行的线程数量大于或等于corePoolSize, 那么将这个任务放入队列;
    3. 果这时候队列满了, 而且正在运行的线程数量小于maximumPoolSize, 那么还是要创建非核心线程立刻运行这个任务;
    4. 如果队列满了, 而且正在运行的线程数量大于或等于maximumPoolSize, 那么线程池会抛出异常RejectExecutionException。
    5. 当一个线程完成任务时, 它会从队列中取下一个任务来执行。 当一个线程无事可做, 超过一定的时间( keep Alive Time) 时, 线程池会判断。
    6. 如果当前运行的线程数大于 corePoolSize, 那么这个线程就被停掉。所以线程池的所有任务完成后, 它最终会收缩到corePoolSize的大小。

既然提到可以通过配置不同参数创建出不同的线程池, 那么Java 中默认实现好的线程池又有哪些呢? 请比较它们的异同。

  1. SingleThreadExecutor 线程池

    这个线程池只有一个核心线程在工作, 也就是相当于单线程串行执行所有任务。 如果这个唯一的线程因为异常结束, 那么会有一个新的线程来替代它。 此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

    • corePoolSize: 1 , 只有一个核心线程在工作
    • maximumPoolSize: 1
    • keepAliveTime: 0 L
    • workQueue: new Linked Blocking Queue< Runnable>(), 其缓冲队列是无界的。
  2. FixedThreadPool 线程池

    • FixedThreadPool 是固定大小的线程池, 只有核心线程。 每次提交一个任务就创建一个线程, 直到线程达到线程池的最大大小。 线程池的大小一旦达到最大值就会保持不变, 如果某个线程因为执行异常而结束, 那么线程池会补充一个新线程。
    • FixedThreadPool 多数针对一些很稳定很固定的正规并发线程, 多用于服务器。
      • corePoolSize: n Threads
      • maximumPoolSize: n Threads
      • keepAliveTime: 0 L
      • workQueue: new Linked Blocking Queue< Runnable>(), 其缓冲队列是无界的。
  3. CachedThreadPool 线程池

    CachedThreadPool 是无界线程池, 如果线程池的大小超过了处理任务所需要的线程, 那么就会回收部分空闲( 60 秒不执行任务) 线程, 当任务数增加时, 此线程池又可以智能的添加新线程来处理任务。

    线程池大小完全依赖于操作系统( 或者说 JVM) 能够创建的最大线程大小。 Synchronous Queue 是一个是缓冲区为 1 的阻塞队列。

    缓存型池子通常用于执行一些生存期很短的异步型任务, 因此在一些面向连接的 daemon 型 SERVER 中用得不多。 但对于生存期短的异步任务, 它是 Executor 的首选。

    • corePoolSize: 0
    • maximumPoolSize: Integer. MAX_ VALUE
    • keepAliveTime: 60 L
    • workQueue: new Synchronous Queue< Runnable>(), 一个是缓冲区为 1 的阻塞队列。
  4. ScheduledThreadPool 线程池

    ScheduledThreadPool: 核心线程池固定, 大小无限的线程池。 此线程池支持定时以及周期性执行任务的需求。 创建一个周期性执行任务的线程池。 如果闲置, 非核心线程池会在 DEFAULT_ KEEPALIVEMILLIS 时间内回收。

    • corePoolSize: core Pool Size
    • maximumPoolSize: Integer. MAX_ VALUE
    • keepAliveTime: DEFAULT_ KEEPALIVE_ MILLIS
    • workQueue: new Delayed Work Queue()

什么是Java的内存模型, Java中各个线程是怎么彼此看到对方的变量的?

  • Java 的内存模型定义了程序中各个变量的访问规则, 即在虚拟机中将变量存储到内存和从内存中取出这样的底层细节。
  • 此处的变量包括实例字段、 静态字段和构成数组对象的元素, 但是不包括局部变量和方法参数, 因为这些是线程私有的, 不会被共享, 所以不存在竞争问题。
  • Java 中各个线程是怎么彼此看到对方的变量的呢? Java 中定义了主内存与工作内存的概念:
  • 所有的变量都存储在主内存, 每条线程还有自己的工作内存, 保存了被该线程使用到的变量的主内存副本拷贝。
    线程对变量的所
  • 有操作( 读取、 赋值) 都必须在工作内存中进行, 不能直接读写主内存的变量。 不同的线程之间也无法直接访问对方工作内存的变量, 线程间变量值的传递需要通过主内存。

请谈谈 volatile 有什么特点, 为什么它能保证变量对所有线程的可见性?

关键字volatile是Java虚拟机提供的最轻量级的同步机制。 当一个变量被定义成volatile之后, 具备两种特性:

  1. 保证此变量对所有线程的可见性。 当一条线程修改了这个变量的值, 新值对于其他线程是可以立即得知的。 而普通变量做不到这一点。
  2. 禁止指令重排序优化。 普通变量仅仅能保证在该方法执行过程中, 得到正确结果, 但是不保证程序代码的执行顺序。
  3. Java 的内存模型定义了8种内存间操作:
  4. lock 和 unlock
    • 把一个变量标识为一条线程独占的状态。
    • 把一个处于锁定状态的变量释放出来, 释放之后的变量才能被其他线程锁定。
  5. read 和 write
    • 把一个变量值从主内存传输到线程的工作内存, 以便 load。
    • 把 store 操作从工作内存得到的变量的值, 放入主内存的变量中。
  6. load 和 store
    • 把 read 操作从主内存得到的变量值放入工作内存的变量副本中。
    • 把工作内存的变量值传送到主内存, 以便 write。
  7. use 和 assgin
    • 把工作内存变量值传递给执行引擎。
    • 将执行引擎值传递给工作内存变量值。
  8. volatile 的实现基于这 8 种内存间操作, 保证了一个线程对某个volatile 变量的修改, 一定会被另一个线程看见, 即保证了可见性。

既然 volatile 能够保证线程间的变量可见性, 是不是就意味着基于 volatile 变量的运算就是并发安全的?

  • 显然不是的。 基于 volatile 变量的运算在并发下不一定是安全的。
  • volatile变量在各个线程的工作内存, 不存在一致性问题( 各个线程的工作内存中 volatile 变量, 每次使用前都要刷新到主内存) 。
  • 但是 Java 里面的运算并非原子操作, 导致volatile 变量的运算在并发下一样是不安全的。

请对比下 volatile 对比 Synchronized 的异同。

  • Synchronized 既能保证可见性, 又能保证原子性, 而 volatile 只能保证可见性, 无法保证原子性。
  • ThreadLocal 和 Synchonized 都用于解决多线程并发访问, 防止任务在共享资源上产生冲突。 但是 ThreadLocal 与 Synchronized 有本质的区别。
  • Synchronized 用于实现同步机制, 是利用锁的机制使变量或代码块在某一时该只能被一个线程访问, 是一种“ 以时间换空间” 的方式。而 ThreadLocal 为每一个线程都提供了变量的副本, 使得每个线程在某一时间访问到的并不是同一个对象, 根除了对变量的共享, 是一种“ 以空间换时间” 的方式。

请谈谈 ThreadLocal 是怎么解决并发安全的?

  • ThreadLocal 这是Java 提供的一种保存线程私有信息的机制, 因为其在整个线程生命周期内有效, 所以可以方便地在一个线程关联的不同业务模块之间传递信息, 比如事务 ID、 Cookie 等上下文相关信息。
  • ThreadLocal 为每一个线程维护变量的副本, 把共享数据的可见范围限制在同一个线程之内, 其实现原理是, 在 ThreadLocal 类中有一个Map, 用于存储每一个线程的变量的副本。

很多人都说要慎用 ThreadLocal, 谈谈你的理解, 使用ThreadLocal 需要注意些什么?

  • 使用ThreadLocal 要注意 remove!
  • ThreadLocal 的实现是基于一个所谓的 ThreadLocalMap, 在Thread LocalMap 中, 它的 key 是一个弱引用。
  • 通常弱引用都会和引用队列配合清理机制使用, 但是 ThreadLocal 是个例外, 它并没有这么做。
    这意味着, 废弃项目的回收依赖于显式地触发, 否则就要等待线程结束, 进而回收相应 hreadLocalMap! 这就是很多 OOM 的来源, 所以通常都会建议, 应用一定要自己负责 remove, 并且不要和线程池配合, 因为 worker 线程往往是不会退出的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值