1. Callable、Future、FutureTash详解
Callable与Future是在JAVA的后续版本中引入进来的,Callable类似于Runnable接口,实现Callable接口的类与实现Runnable的类都是可以被线程执行的任务。
三者之间的关系:
Callable是Runnable封装的异步运算任务
Future用来保存Callable异步运算的结果
FutureTask封装Future的实体类
1)Callable与Runnbale的区别
a、Callable定义的方法是call,而Runnable定义的方法是run。
b、call方法有返回值,而run方法是没有返回值的。
c、call方法可以抛出异常,而run方法不能抛出异常。
2)Future
Future表示异步计算的结果,提供了以下方法,主要是判断任务是否完成、中断任务、获取任务执行结果
3)FutureTask
可取消的异步计算,此类提供了对Future的基本实现,仅在计算完成时才能获取结果,如果计算尚未完成,则阻塞get方法。FutureTask不仅实现了Future接口,还实现了Runnable接口,所以不仅可以将FutureTask当成一个任务交给Executor来执行,还可以通过Thread来创建一个线程。
2. 创建线程池的几种方式
ThreadPoolExecutor、ThreadScheduledExecutor、ForkJoinPool
3. 线程的生命周期,什么时候会出现僵死进程
僵死进程是指子进程退出时,父进程并未对其发出的SIGCHLD信号进行适当处理,导致子 进程停留在僵死状态等待其父进程为其收尸,这个状态下的子进程就是僵死进程。
4. 说说线程安全问题,什么是线程安全,如何实现线程安全
线程安全 && 线程不安全
线程安全 - 如果线程执行过程中不会产生共享资源的冲突,则线程安全
线程不安全 - 如果有多个线程同时在操作主内存中的变量,则线程不安全
实现线程安全的三种方式
5. Java 多线程安全机制
在开始讨论java多线程安全机制之前,首先从内存模型来了解一下什么是多线程的安全性。我们都知道java的内存模型中有主内存和线程的工作内存之分,主内存上存放的是线程共享的变量(实例字段,静态字段和构成数组的元素),线程的工作内存是线程私有的空间,存放的是线程私有的变量(方法参数与局部变量)。线程在工作的时候如果要操作主内存上的共享变量,为了获得更好的执行性能并不是直接去修改主内存而是会在线程私有的工作内存中创建一份变量的拷贝(缓存),在工作内存上对变量的拷贝修改之后再把修改的值刷回到主内存的变量中去,JVM提供了8中原子操作来完成这一过程:lock, unlock, read, load, use, assign, store, write。深入理解java虚拟机-jvm最高特性与实践这本书中有一个图很好的表示了线程,主内存和工作内存之间的关系:
如果只有一个线程当然不会有什么问题,但是如果有多个线程同时在操作主内存中的变量,因为8种操作的非连续性和线程抢占cpu执行的机制就会带来冲突的问题,也就是多线程的安全问题。线程安全的定义就是:如果线程执行过程中不会产生共享资源的冲突就是线程安全的。
Java里面一般用以下几种机制保证线程安全:
1.互斥同步锁(悲观锁)
1)Synchorized
2)ReentrantLock
互斥同步锁也叫做阻塞同步锁,特征是会对没有获取锁的线程进行阻塞。要理解互斥同步锁,首选要明白什么是互斥什么是同步。简单的说互斥就是非你即我,同步就是顺序访问。互斥同步锁就是以互斥的手段达到顺序访问的目的。操作系统提供了很多互斥机制比如信号量,互斥量,临界区资源等来控制在某一个时刻只能有一个或者一组线程访问同一个资源。
Java里面的互斥同步锁就是Synchorized和ReentrantLock,前者是由语言级别实现的互斥同步锁,理解和写法简单但是机制笨拙,在JDK6之后性能优化大幅提升,即使在竞争激烈的情况下也能保持一个和ReentrantLock相差不多的性能,所以JDK6之后的程序选择不应该再因为性能问题而放弃synchorized。ReentrantLock是API层面的互斥同步锁,需要程序自己打开并在finally中关闭锁,和synchorized相比更加的灵活,体现在三个方面:等待可中断,公平锁以及绑定多个条件。但是如果程序猿对ReentrantLock理解不够深刻,或者忘记释放lock,那么不仅不会提升性能反而会带来额外的问题。另外synchorized是JVM实现的,可以通过监控工具来监控锁的状态,遇到异常JVM会自动释放掉锁。而ReentrantLock必须由程序主动的释放锁。互斥同步锁都是可重入锁,好处是可以保证不会死锁。但是因为涉及到核心态和用户态的切换,因此比较消耗性能。JVM开发团队在JDK5-JDK6升级过程中采用了很多锁优化机制来优化同步无竞争情况下锁的性能。比如:自旋锁和适应性自旋锁,轻量级锁,偏向锁,锁粗化和锁消除。
2.非阻塞同步锁
1) 原子类(CAS)
非阻塞同步锁也叫乐观锁,相比悲观锁来说,它会先进行资源在工作内存中的更新,然后根据与主存中旧值的对比来确定在此期间是否有其他线程对共享资源进行了更新,如果旧值与期望值相同,就认为没有更新,可以把新值写回内存,否则就一直重试直到成功。它的实现方式依赖于处理器的机器指令:
CAS(Compare And Swap)
JUC中提供了几个Automic类以及每个类上的原子操作就是乐观锁机制不激烈情况下,性能比synchronized略逊,而激烈的时候,也能维持常态。激烈的时候,Atomic的性能会优于ReentrantLock一倍左右。但是其有一个缺点,就是只能同步一个值,一段代码中只能出现一个
Atomic的变量,多于一个同步无效。因为他不能在多个Atomic之间同步。非阻塞锁是不可重入的,否则会造成死锁。
3.无同步方案
1)可重入代码
在执行的任何时刻都可以中断-重入执行而不会产生冲突。特点就是不会依赖堆上的共享资源
2)ThreadLocal/Volaitile
线程本地的变量,每个线程获取一份共享变量的拷贝,单独进行处理
3) 线程本地存储
如果一个共享资源一定要被多线程共享,可以尽量让一个线程完成所有的处理操作,比如生产者消费者模式中,一般会让一个消费者完成对队列上资源的消费。典型的应用是基于请求-应答模式的web服务器的设计
6. 线程池
0) 线程池的组成
0.1) 线程池管理器:用于创建并管理线程池
0.2) 工作线程:线程池中的线程
0.3) 任务接口:每个任务必须实现的接口,用于工作线程调度其运行
0.4) 任务队列:用于存放待处理的任务,提供一种缓冲机制
1)核心参数
2)工作原理
3)拒绝策略
线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也
塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。
JDK 内置的拒绝策略如下:
4)线程池大小分配
线程池究竟设置多大要看你的线程池执行的什么任务了,CPU密集型、IO密集型、混合型,任 务类型不同,设置的方式也不一样。
任务一般分为:CPU密集型、IO密集型、混合型,对于不同类型的任务需要分配不同大小的线 程池。
4.1)CPU密集型
尽量使用较小的线程池,一般Cpu核心数+1
4.2)IO密集型
方法一: 可以使用较大的线程池,一般CPU核心数 * 2
方法二:(线程等待时间与线程CPU时间之比 + 1)* CPU数目
4.3)混合型
可以将任务分为CPU密集型和IO密集型,然后分别使用不同的线程池去处理,按情况而定
5)线程池的好处
6)自带线程池的坏处
7. volatile、ThreadLocal的原理
7.1 volatile原理
volatile变量进行写操作时,JVM 会向处理器发送一条 Lock 前缀的指令,将这个变量所在缓 存行的数据写会到系统内存。
Lock 前缀指令实际上相当于一个内存屏障(也成内存栅栏),它确保指令重排序时不会把其 后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内 存屏障这句指令时,在它前面的操作已经全部完成。
7.2 ThreadLocal原理
ThreadLocal是用来维护本线程的变量的,并不能解决共享变量的并发问题。ThreadLocal是 各线程将值存入该线程的map中,以ThreadLocal自身作为key,需要用时获得的是该线程之前 存入的值。如果存入的是共享变量,那取出的也是共享变量,并发问题还是存在的。
8. ThreadLocal什么时候会出现OOM的情况
ThreadLocal变量是维护在Thread内部的,这样的话只要我们的线程不退出,对象的引用就会 一直存在。
ThreadLocal并不是一个Thread,而是Thread的一个局部变量,也许把它命名为ThreadLocalVariable 更容易让人理解一些。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
9. ThreadLocal全部方法和内部类
内部类:
;
方法:
10. JMM的理解
个人理解JMM:Java Memory Model(Java内存模型),根据并发过程中如何处理可见性、原子性和有序性这三个特性而建立的模型。
可见性:JMM提供了volatile变量定义、final、synchronized块来保证可见性。
原子性:个人理解是如果执行,就执行完,synchronized块来保证。
有序性:觉得有序是相对性的,根据从哪个线程观察,volatile和synchronized保证线程之间操作的有序性。
指令重排:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证各个语句的执行顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。
JMM处理过程:JMM是通过禁止特定类型的编译器重排序和处理器重排序来为程序员提供一致的内存可见性保证。例如A线程具体什么时候刷新共享数据到主内存是不确定的,假设我们使用了同步原语(synchronized,volatile和final),那么刷新的时间是确定的。
11. synchronized和volatile区别
1. volatile主要应用在多个线程对实例变量更改的场合,刷新主内存共享变量的值从而使得各个线程可以获得最新的值,线程读取变量的值需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。另外,synchronized还会创建一个内存屏障,内存屏障指令保证了所有CPU操作结果都会直接刷到主存中(即释放锁前),从而保证了操作的内存可见性,同时也使得先获得这个锁的线程的所有操作
2. volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、类级别的
3. volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞,比如多个线程争抢
synchronized锁对象时,会出现阻塞
4. volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性和有序性,因为线程获得锁才能进入临界区,从而保证临界区中的所有语句全部得到执行
5. volatile标记的变量不会被编译器优化,可以禁止进行指令重排;synchronized标记的变量可以被编译器优化
12. 三大性质总结
12.0 多线程的出现是要解决什么问题的? 本质什么? 三大性质来源?
CPU、内存、I/O 设备的速度是有极大差异的,为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献,主要体现为:
- CPU 增加了缓存,以均衡与内存的速度差异;// 导致可见性问题
- 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;// 导致原子性问题
- 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。// 导致有序性问题
在并发编程中分析线程安全的问题时往往需要切入点,那就是两大核心:JMM抽象内存模型以及
happens-before规则,三条性质:原子性,有序性和可见性。
并发编程三要素:
12.1 原子性
synchronized:
原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉。即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。
java内存模型中定义了8种操作都是原子的,不可再分的。
由原子性变量操作read,load,use,assign,store,write,可以大致认为基本数据类型的访问读写具备原子性。
上面一共有八条原子操作,其中六条可以满足基本数据类型的访问读写具备原子性,还剩下lock和
unlock两条原子操作。如果我们需要更大范围的原子性操作就可以使用lock和unlock原子操作。尽管jvm没有把lock和unlock开放给我们使用,但jvm以更高层次的指令monitorenter和monitorexit指令开放给我们使用,反应到java代码中就是---synchronized关键字,也就是说synchronized满足原子性。
volatile:
volatile并不能保证原子性。如果让volatile保证原子性,必须符合以下两条规则:
1)运算结果并不依赖于变量的当前值,或者能够确保只有一个线程修改变量的值;
2)变量不需要与其他的状态变量共同参与不变约束
12.2 有序性
有序性,即程序的执行顺序按照代码的先后顺序来执行。JMM是通过Happens-Before 规则来保证有序性的。
synchronized:
synchronized语义表示锁在同一时刻只能由一个线程进行获取,当锁被占用后,其他线程只能等待。因此,synchronized语义就要求线程在访问读写共享变量时只能“串行”执行,因此synchronized具有有序性。
volatile:
在java内存模型中说过,为了性能优化,编译器和处理器会进行指令重排序;也就是说java程序天然的有序性可以总结为:如果在本线程内观察,所有的操作都是有序的;如果在一个线程观察另一个线程,所有的操作都是无序的。volatile包含禁止指令重排序的语义,其具有有序性。
12.3 可见性
可见性是指当一个线程修改了共享变量后,其他线程能够立即得知这个修改。通过之前对synchronzed内存语义进行了分析,当线程获取锁时会从主内存中获取共享变量的最新值,释放锁的时候会将共享变量同步到主内存中。从而,synchronized具有可见性。同样的在volatile分析中,会通过在指令中添加lock指令,以实现内存可见性。因此, volatile具有可见性。
13. 怎么提高并发量,请列举你所知道的方案?高并发解决方案——提升高并发量服务器性能解决思路
1)HTML静态化
2)图片服务器分离
3)数据库集群、库表散列
4)缓存
5)镜像
6)负载均衡:硬件/软件
7)CDN:CDN的实现分为三类:镜像、高速缓存、专线
14. Java阻塞队列ArrayBlockingQueue和LinkedBlockingQueue实现原理分析
Java阻塞队列ArrayBlockingQueue和LinkedBlockingQueue实现原理分析 | Format's Notes
ArrayBlockingQueue 有界
ArrayBlockingQueue的原理就是使用一个可重入锁和这个锁生成的两个条件对象进行并发控制(classic two-condition algorithm)
ArrayBlockingQueue只有1个锁,添加数据和删除数据的时候只能有1个被执行,不允许并行执行。
LinkedBlockingQueue 无界
LinkedBlockingQueue是一个使用链表完成队列操作的阻塞队列。链表是单向链表,而不是双向链表。内部使用放锁和拿锁,这两个锁实现阻塞(“two lock queue” algorithm)
LinkedBlockingQueue有2个锁,放锁和拿锁,添加数据和删除数据是可以并行进行的,当然添加数据和删除数据的时候只能有1个线程各自执行。
15. 用 Java 实现阻塞队列(生产者消费者模型)
16. 如何在两个线程之间共享数据
Java 里面进行多线程通信的主要方式就是共享内存的方式,共享内存主要的关注点有两个:可见性和有序性原子性。Java 内存模型(JMM)解决了可见性和有序性的问题,而锁解决了原子性的问题,理想情况下我们希望做到“同步”和“互斥”。有以下常规实现方法:
1)将数据抽象成一个类,并将对这个数据的操作作为这个类的方法,这么设计可以和容易做到同步,只要在方法上加”synchronized“。
2)将 Runnable 对象作为一个类的内部类,共享数据作为这个类的成员变量,每个线程对共享数据的操作方法也封装在外部类,以便实现对数据的各个操作的同步和互斥,作为内部类的各个
Runnable 对象调用外部类的这些方法。
3)ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
17. ThreadLocalMap(线程的一个属性)
18. synchronized 和 ReentrantLock 的区别
19. ConcurrentHashMap 并发
减小锁粒度
减小锁粒度是指缩小锁定对象的范围,从而减小锁冲突的可能性,从而提高系统的并发能力。减小锁粒度是一种削弱多线程锁竞争的有效手段,这种技术典型的应用是 ConcurrentHashMap(高性能的HashMap)类的实现。对于 HashMap 而言,最重要的两个方法是 get 与 set 方法,如果我们对整个HashMap 加锁,可以得到线程安全的对象,但是加锁粒度太大。Segment 的大小也被称为
ConcurrentHashMap 的并发度。
ConcurrentHashMap 分段锁
ConcurrentHashMap,它内部细分了若干个小的 HashMap,称之为段(Segment)。默认情况下一个ConcurrentHashMap 被进一步细分为 16 个段,既就是锁的并发度。
如果需要在 ConcurrentHashMap 中添加一个新的表项,并不是将整个 HashMap 加锁,而是首先根据hashcode得到该表项应该存放在哪个段中,然后对该段加锁,并完成put操作。在多线程环境中,如果多个线程同时进行put操作,只要被加入的表项不存放在同一个段中,则线程间可以做到真正的并行。
ConcurrentHashMap 是由Segment 数组结构和HashEntry 数组结构组成
ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。Segment 是一种可重入锁ReentrantLock,在 ConcurrentHashMap 里扮演锁的角色,HashEntry 则用于存储键值对数据。一个ConcurrentHashMap 里包含一个 Segment 数组,Segment 的结构和 HashMap 类似,是一种数组和链表结构, 一个 Segment 里包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素, 每个 Segment 守护一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得它对应的 Segment 锁。
20. Java 中用到的线程调度
21. 进程调度算法
优先调度算法
1)先来先服务调度算法(FCFS)
当在作业调度中采用该算法时,每次调度都是从后备作业队列中选择一个或多个最先进入该队列的
作业,将它们调入内存,为它们分配资源、创建进程,然后放入就绪队列。在进程调度中采用
FCFS 算法时,则每次调度是从就绪队列中选择一个最先进入该队列的进程,为之分配处理机,使
之投入运行。该进程一直运行到完成或发生某事件而阻塞后才放弃处理机,特点是:算法比较简
单,可以实现基本上的公平。 2. 短作业(进程)优先调度算法
高优先权优先调度算法
1)非抢占式优先权算法
在这种方式下,系统一旦把处理机分配给就绪队列中优先权最高的进程后,该进程便一直执行下去,直至完成;或因发生某事件使该进程放弃处理机时。这种调度算法主要用于批处理系统中;也可用于某些对实时性要求不严的实时系统中。
2)抢占式优先权调度算法
在这种方式下,系统同样是把处理机分配给优先权最高的进程,使之执行。但在其执行期间,只要又出现了另一个其优先权更高的进程,进程调度程序就立即停止当前进程(原优先权最高的进程) 的执行,重新将处理机分配给新到的优先权最高的进程。显然,这种抢占式的优先权调度算法能更好地满足紧迫作业的要求,故而常用于要求比较严格的实时系统中,以及对性能要求较高的批处理和分时系统中。
3)高响应比优先调度算法
在批处理系统中,短作业优先算法是一种比较好的算法,其主要的不足之处是长作业的运行得不到
保证。如果我们能为每个作业引入前面所述的动态优先权,并使作业的优先级随着等待时间的增加
而以速率a 提高,则长作业在等待一定的时间后,必然有机会分配到处理机。该优先权的变化规律
可描述为:
22. 什么是 AQS(抽象的队列同步器)
AbstractQueuedSynchronizer 类如其名,抽象的队列式的同步器,AQS 定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如:
ReentrantLock/Semaphore/CountDownLatch
它维护了一个 volatile int state(代表共享资源)和一个 FIFO 线程等待队列(多线程争用资源被阻塞时会进入此队列)。这里 volatile 是核心关键词,具体 volatile 的语义,在此不述。state 的访问方式有三种: getState() setState() compareAndSetState()
AQS 定义两种资源共享方式:
1)Exclusive独占资源-ReentrantLock
Exclusive(独占,只有一个线程能执行,如 ReentrantLock)
2)Share共享资源-Semaphore/CountDownLatch
Share(共享,多个线程可同时执行,如 Semaphore/CountDownLatch)。
AQS只是一个框架,具体资源的获取/释放方式交由自定义同步器去实现,AQS这里只定义了一个接口,具体资源的获取交由自定义同步器去实现了(通过state的get/set/CAS)之所以没有定义成 abstract,是因为独占模式下只用实现 tryAcquire-tryRelease,而共享模式下只用实现 tryAcquireSharedtryReleaseShared。如果都定义成abstract,那么每个模式也要去实现另一模式下的接口。不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/ 唤醒出队等),AQS 已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
同步器的实现是ABS核心(state资源状态计数)
同步器的实现是 ABS 核心,以 ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线程
lock()时,会调用 tryAcquire()独占该锁并将 state+1。此后,其他线程再 tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证 state 是能回到零态的。
以 CountDownLatch 以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后 countDown()一次,state 会 CAS 减1。等到所有子线程都执行完后(即state=0),会 unpark()主调用线程,然后主调用线程就会从 await()函数返回,继续后余动作。
ReentrantReadWriteLock 实现独占和共享两种方式
一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquiretryRelease、tryAcquireShared-tryReleaseShared 中的一种即可。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如 ReentrantReadWriteLock。
23. 终止线程 4 种方式
1)正常运行结束
2)使用退出标志退出线程
3)Interrupt 方法结束线程
4)stop 方法终止线程(线程不安全)
24. notify()和notifyAll()有什么区别?
notify可能会导致死锁,而notifyAll则不会
任何时候只有一个线程可以获得锁,也就是说只有一个线程可以运行synchronized 中的代码
使用notifyall,可以唤醒所有处于wait状态的线程,使其重新进入锁的争夺队列中,而notify只能唤醒一个。wait() 应配合while循环使用,不应使用if,务必在wait()调用前后都检查条件,如果不满足,必须调用notify() 唤醒另外的线程来处理,自己继续wait()直至条件满足再往下执行。notify() 是对notifyAll()的一个优化,但它有很精确的应用场景,并且要求正确使用。不然可能导致死锁。正确的场景应该是 WaitSet中等待的是相同的条件,唤醒任一个都能正确处理接下来的事项,如果唤醒的线程无法正确处理,务必确保继续notify()下一个线程,并且自身需要重新回到WaitSet中.
25. sleep()和wait() 有什么区别?
wait() 属于Object类,sleep() 属于Thread类。
wait() 会释放锁,sleep() 不会释放锁。
wait() 通常被用于线程间交互, sleep() 通常被用于暂停执行。
wait() 会放弃这个对象的监视器,sleep() 不会放弃这个对象的监视器
26. Thread 类中的start() 和 run() 方法有什么区别?
start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程 。
1)start()方法来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕,可以直接继续执行下面的代码。
2)通过调用 Thread 类的 start() 方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。
3)方法 run() 称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行 run 函数当中的代码。 Run 方法运行结束, 此线程终止。然后 CPU 再调度其它线程。
27. 为什么wait, notify 和 notifyAll这些方法不在thread类里面?
原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需
要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。
28. 为什么wait和notify方法要在同步块中调用?
1)只有在调用线程拥有某个对象的独占锁时,才能够调用该对象的wait(),notify()和notifyAll()方法。
2)如果你不这么做,你的代码会抛出IllegalMonitorStateException异常。
3)还有一个原因是为了避免wait和notify之间产生竞态条件。
wait()方法强制当前线程释放对象锁。这意味着在调用某对象的wait()方法之前,当前线程必须已经获得该对象的锁。因此,线程必须在某个对象的同步方法或同步代码块中才能调用该对象的wait()方法。
在调用对象的notify()和notifyAll()方法之前,调用线程必须已经得到该对象的锁。因此,必须在某个对象的同步方法或同步代码块中才能调用该对象的notify()或notifyAll()方法。
调用wait()方法的原因通常是,调用线程希望某个特殊的状态(或变量)被设置之后再继续执行。调用
notify()或notifyAll()方法的原因通常是,调用线程希望告诉其他等待中的线程:"特殊状态已经被设置"。这个状态作为线程间通信的通道,它必须是一个可变的共享状态(或变量)。
29. Java中interrupted 和 isInterruptedd方法的区别?
interrupted() 和 isInterrupted()的主要区别是前者会将中断状态清除而后者不会。Java多线程的中断机制是用内部标识来实现的,调用Thread.interrupt()来中断一个线程就会设置中断标识为true。
中断线程调用静态方法Thread.interrupted()来检查中断状态时,中断状态会被清零。
非静态方法isInterrupted()用来查询其它线程的中断状态且不会改变中断状态标识。简单的说就是任何抛出InterruptedException异常的方法都会将中断状态清零。无论如何,一个线程的中断状态有有可能被其它线程调用中断来改变 。
30. 多个线程如何保证顺序执行?
https://www.jb51.net/article/246666.htm
31. SynchronizedMap和ConcurrentHashMap有什么区别?
SynchronizedMap()和Hashtable一样,实现上在调用map所有方法时,都对整个map进行同步。而
ConcurrentHashMap的实现却更加精细,它对map中的所有桶加了锁。所以,只要有一个线程访问map,其他线程就无法进入map,而如果一个线程在访问ConcurrentHashMap某个桶时,其他线程,仍然可以对map执行某些操作。
所以,ConcurrentHashMap在性能以及安全性方面,明显比Collections.synchronizedMap()更加有优势。同时,同步操作精确控制到桶,这样,即使在遍历map时,如果其他线程试图对map进行数据修改,也不会抛出ConcurrentModificationException 。
32. Thread类中的yield方法有什么作用?
使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。
Yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行。
33. Java线程池中submit() 和 execute()方法有什么区别?
两个方法都可以向线程池提交任务,execute()方法的返回类型是void,它定义在Executor接口中, 而submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口,其它线程池类像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有这些方法 。
34. synchronized关键字最主要的三种使用方式
35. 如何在两个线程间共享数据?
在两个线程间共享变量即可实现共享。
一般来说,共享变量要求变量本身是线程安全的,然后在线程内使用的时候,如果有对共享变量的复合操作,那么也得保证复合操作的线程安全性
36. JAVA 后台线程
1)定义:守护线程--也称“服务线程”, 他是后台线程, 它有一个特性,即为用户线程 提供 公共服务, 在没有用户线程可服务时会自动离开。
2)优先级:守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。
3)设置:通过 setDaemon(true)来设置线程为“守护线程”;将一个用户线程设置为守护线程的方式是在 线程对象创建 之前 用线程对象的setDaemon 方法。
4)在 Daemon 线程中产生的新线程也是 Daemon 的。
5)线程则是 JVM 级别的,以 Tomcat 为例,如果你在 Web 应用中启动一个线程,这个线程的生命周期并不会和 Web 应用程序保持同步。也就是说,即使你停止了 Web 应用,这个线程依旧是活跃的。
6)example: 垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,
程序就不会再产生垃圾,垃圾回收器也就无事可做, 所以当垃圾回收线程是 JVM 上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
7)生命周期:守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依赖于系统,与系统“同生共死”。当 JVM 中所有的线程都是守护线程的时候, JVM 就可以退出了;如果还有一个或以上的非守护线程则 JVM 不会退出。
37. Synchronized 同步锁
synchronized 它可以把任意一个非 NULL 的对象当作锁。 他属于独占式的悲观锁,同时属于可重入锁。
38. ReentrantLock
39. Condition 类和 Object 类锁方法区别区别
1)Condition 类的 awiat 方法和 Object 类的 wait 方法等效
2)Condition 类的 signal 方法和 Object 类的 notify 方法等效
3)Condition 类的 signalAll 方法和 Object 类的 notifyAll 方法等效
4)ReentrantLock 类可以唤醒指定条件的线程,而 object 的唤醒是随机的
40. tryLock 和 lock 和 lockInterruptibly 的区别
1)tryLock 能获得锁就返回 true,不能就立即返回 false, tryLock(long timeout,TimeUnit unit),可以增加时间限制,如果超过该时间段还没获得锁,返回 false
2)lock 能获得锁就返回 true,不能的话一直等待获得锁
3)lock 和 lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程,lock 不会抛出异常,而 lockInterruptibly 会抛出异常。
41. Semaphore 信号量
Semaphore 是一种基于计数的信号量。它可以设定一个阈值,基于此,多个线程竞争获取许可信
号,做完自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。 Semaphore 可以用来
构建一些对象池,资源池之类的, 比如数据库连接池
实现互斥锁(计数器为 1)
我们也可以创建计数为 1 的 Semaphore,将其作为一种类似互斥锁的机制,这也叫二元信号量,
表示两种互斥状态。
42. Semaphore 与 ReentrantLock 区别
Semaphore 基本能完成 ReentrantLock 的所有工作,使用方法也与之类似,通过 acquire()与
release()方法来获得和释放临界资源。经实测, Semaphone.acquire()方法默认为可响应中断锁,
与 ReentrantLock.lockInterruptibly()作用效果一致,也就是说在等待临界资源的过程中可以被
Thread.interrupt()方法中断。
此外, Semaphore 也实现了可轮询的锁请求与定时锁的功能,除了方法名 tryAcquire 与 tryLock
不同,其使用方法与 ReentrantLock 几乎一致。 Semaphore 也提供了公平与非公平锁的机制,也
可在构造函数中进行设定。
Semaphore 的锁释放操作也由手动进行,因此与 ReentrantLock 一样,为避免线程因抛出异常而
无法正常释放锁的情况发生,释放锁的操作也必须在 finally 代码块中完成。
43. 可重入锁(递归锁)
本文里面讲的是广义上的可重入锁,而不是单指 JAVA 下的 ReentrantLock。 可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。在 JAVA 环境下 ReentrantLock 和 synchronized 都是 可重入锁。
44. 锁优化
1) 减少锁持有时间
只用在有线程安全要求的程序上加锁
2) 减小锁粒度
将大对象(这个对象可能会被很多线程访问),拆成小对象,大大增加并行度,降低锁竞争。
降低了锁的竞争,偏向锁,轻量级锁成功率才会提高。最最典型的减小锁粒度的案例就是
ConcurrentHashMap。
3) 锁分离
最常见的锁分离就是读写锁 ReadWriteLock,根据功能进行分离成读锁和写锁,这样读读不互
斥,读写互斥,写写互斥,即保证了线程安全,又提高了性能,具体也请查看[高并发 Java 五]
JDK 并发包 1。读写分离思想可以延伸,只要操作互不影响,锁就可以分离。比如LinkedBlockingQueue 从头部取出,从尾部放数据
4) 锁粗化
通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完
公共资源后,应该立即释放锁。但是,凡事都有一个度, 如果对同一个锁不停的进行请求、同步
和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化 。
5) 锁消除
锁消除是在编译器级别的事情。 在即时编译器时,如果发现不可能被共享的对象,则可以消除这
些对象的锁操作,多数是因为程序员编码不规范引起。
45. 线程方法
线程基本方法: wait, notify, notifyAll, sleep, join, yield 等。
线程其他方法:
1) sleep():强迫一个线程睡眠N毫秒。
2) isAlive(): 判断一个线程是否存活。
3) join(): 等待线程终止。
4) activeCount(): 程序中活跃的线程数。
5) enumerate(): 枚举程序中的线程。
6) currentThread(): 得到当前线程。
7) isDaemon(): 一个线程是否为守护线程。
8) setDaemon(): 设置一个线程为守护线程。 (用户线程和守护线程的区别在于,是否等待主线
程依赖于主线程结束而结束)
9) setName(): 为线程设置一个名称。
10) wait(): 强迫一个线程等待。
11) notify(): 通知一个线程继续运行。
11) setPriority(): 设置一个线程的优先级。
12) getPriority()::获得一个线程的优先级。
方法 名 | 描述 |
sleep() | 强迫一个线程睡眠N毫秒 |
isAlive() | 判断一个线程是否存活。 |
join() | 等待线程终止。 |
activeCount() | 程序中活跃的线程数。 |
enumerate() | 枚举程序中的线程。 |
currentThread() | 得到当前线程。 |
isDaemon() | 一个线程是否为守护线程。 |
setDaemon() | 设置一个线程为守护线程。 |
setName() | 为线程设置一个名称。 |
wait() | 强迫一个线程等待。 |
notify() | 通知一个线程继续运行。 |
setPriority() | 设置一个线程的优先级。 |
46. Join
join() 方法,等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞
状态,回到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸。
很多情况下,主线程生成并启动了子线程,需要用到子线程返回的结果,也就是需要主线程需要
在子线程结束后再结束,这时候就要用到 join() 方法 。
47. 多线程同步有哪几种方法?
Synchronized 关键字,Lock 锁实现,分布式锁等。
48. Java 中 Executor 和 Executors 的区别?
Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。
Executor 接口对象能执行我们的线程任务。
ExecutorService 接口继承了 Executor 接口并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。
使用 ThreadPoolExecutor 可以创建自定义线程池。
Future 表示异步计算的结果,他提供了检查计算是否完成的方法,以等待计算的完成,并可以使用 get()方法获取计算的结果。
49. 什么是 Executors 框架?
Executor 框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架。无限制的创建线程会引起应用程序内存溢出。所以创建一个线程池是个更好的的解决方案,因为可以限制线程的数量并且可以回收再利用这些线程。利用Executors 框架可以非常方便的创建一个线程池。
50. 什么是 Callable 和 Future?
Callable 接口类似于 Runnable,从名字就可以看出来了,但是 Runnable 不会返回结果,并且无法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值。可以认为是带有回调的 Runnable。
Future 接口表示异步任务,是还没有完成的任务给出的未来结果。所以说 Callable
用于产生结果,Future 用于获取结果。
51. 什么是 FutureTask?使用 ExecutorService 启动任务
在 Java 并发程序中 FutureTask 表示一个可以取消的异步运算。它有启动和取消运算、查询运算是否完成和取回运算结果等方法。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是调用了 Runnable接口所以它可以提交给 Executor 来执行。
52. 线程同步和线程互斥
线程同步是指线程之间所具有的一种制约/依赖关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。
线程间的同步方法大体可分为两类:用户模式和内核模式。顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。内核模式下的方法有:事件,信号量,互斥量。
53. 为什么我们调用 start()方法时会执行 run()方法,为什么我们不能直接调用 run()方
法?
当你调用 start()方法时你将创建新的线程,并且执行在 run()方法里的代码。但是如果你直接调用 run()方法,它不会创建新的线程也不会执行调用线程的代码,只会把 run 方法当作普通方法去执行。
54. Java 中你怎样唤醒一个阻塞的线程?
在 Java 发展史上曾经使用 suspend()、resume()方法对于线程进行阻塞唤醒,但随之出现很多问题,比较典型的还是死锁问题。
解决方案可以使用以对象为目标的阻塞,即利用 Object 类的 wait()和 notify()方法实现线程阻塞。
首先,wait、notify 方法是针对对象的,调用任意对象的 wait()方法都将导致线程阻塞,阻塞的同时也将释放该对象的锁,相应地,调用任意对象的 notify()方法则将随机解除该对象阻塞的线程,但它需要重新获取改对象的锁,直到获取成功才能往下执行;其次,wait、notify方法必须在 synchronized 块或方法中被调用,并且要保证同步块或方法的锁对象与调用 wait、notify 方法的对象是同一个,如此一来在调用 wait 之前当前线程就已经成功获取某对象的锁,执行 wait 阻塞后当前线程就将之前获取的对象锁释放。
55. Java 中 CycliBarriar 和 CountdownLatch 有什么区别?
CyclicBarrier 可以重复使用,而 CountdownLatch 不能重复使用。
56. 什么是不可变对象,它对写并发应用有什么帮助
不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,反之即为可变对象(Mutable Objects)。不可变对象的类即为不可变类(Immutable Class)。Java 平台类库中包含许多不可变类,如 String、基本类型的包装类、BigInteger 和 BigDecimal 等。不可变对象天生是线程安全的。它们的常量(域)是在构造函数中创建的。既然它们的状态无法修改,这些常量永远不会变。
不可变对象永远是线程安全的
只有满足如下状态,一个对象才是不可变的
它的状态不能在创建后再被修改
所有域都是 final 类型;并且,它被正确创建(创建期间没有发生 this 引用的逸出)
不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。
57. Java 中用到的线程调度算法是什么?
计算机通常只有一个 CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU 的使用权才能执行指令.所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得 CPU 的使用权,分别执行各自的任务.在运行池中,会有多个处于就绪状态的线程在等待 CPU,JAVA 虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配 CPU 的使用权.
有两种调度模型:分时调度模型和抢占式调度模型。分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的CPU 的时间片]。 java 虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU。
58. 用 Java 编程一个会导致死锁的程序
59. SynchronizedMap 和 ConcurrentHashMap 有什么区别?
SynchronizedMap 一次锁住整张表来保证线程安全,所以每次只能有一个线程来
访为 map。
ConcurrentHashMap 使用分段锁来保证在多线程下的性能。
ConcurrentHashMap 中则是一次锁住一个桶。ConcurrentHashMap 默认将
hash 表分为 16 个桶,诸如 get,put,remove 等常用操作只锁当前需要用到的桶。
这样,原来只能一个线程进入,现在却能同时有 16 个写线程执行,并发性能的提
升是显而易见的。
另外 ConcurrentHashMap 使用了一种不同的迭代方式。在这种迭代方式中,当iterator 被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时 new 新的数据从而不影响原有的数据 ,iterator 完成后再将头指针替换为新的数据 ,这样 iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。
60. CopyOnWriteArrayList 可以用于什么应用场景
CopyOnWriteArrayList 是什么:
CopyOnWriteArrayList 是一个并发容器。有很多人称它是线程安全的,我认为这句话不严谨,缺
少一个前提条件,那就是非复合场景下操作它是线程安全的。CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛
出 ConcurrentModificationException。在CopyOnWriteArrayList 中,写入将导致创建整个底层
数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。
CopyOnWriteArrayList使用场景:
合适读多写少的场景。
CopyOnWriteArrayList 的缺点:
由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致
young gc 或者 full gc。
不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set 操作后,读取到数
据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求。
由于实际使用中可能没法保证 CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点
多,每次 add/set 都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种
操作分分钟引起故障。
CopyOnWriteArrayList 设计思想:
1)读写分离,读和写分开
2)最终一致性
3)使用另外开辟空间的思路,来解决并发冲突
61. 什么叫线程安全?servlet,structs2,spring 是线程安全吗?
线程安全是编程中的术语,指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。
Servlet 不是线程安全的,servlet 是单实例多线程的,当多个线程同时访问同一个方法,是不能保证共享变量的线程安全性的。
Struts2 的 action 是多实例多线程的,是线程安全的,每个请求过来都会 new 一个新的 action 分配给这个请求,请求完成后销毁。
SpringMVC 的 Controller 是线程安全的吗?不是的,和 Servlet 类似的处理流程。
62. Java 中的同步集合与并发集合有什么区别?
同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。在 Java1.5 之前程序员们只有同步集合来用且在多线程并发的时候会导致争用,阻碍了系统的扩展性。Java5 介绍了并发集合像ConcurrentHashMap,不仅提供线程安全还用锁分离和内部分区等现代技术提高了可扩展性。
63. 怎么检测一个线程是否拥有锁?
在 java.lang.Thread 中有一个方法叫 holdsLock(),它返回 true 如果当且仅当当前线程拥有某个具体对象的锁。
64. 你如何在 Java 中获取线程堆栈?Java 中怎么获取一份线程 dump 文件?
kill -3 [java pid]
不会在当前终端输出,它会输出到代码执行的或指定的地方去。比如,kill -3
tomcat pid, 输出堆栈到 log 目录下。
Jstack [java pid]
这个比较简单,在当前终端显示,也可以重定向到指定文件中。
65. 什么是阻塞式方法?
阻塞式方法是指程序会一直等待该方法完成期间不做其他事情,ServerSocket 的 accept() 方法就是一直等待客户端连接。这里的阻塞是指调用结果返回之前,当前线程会被挂起,直到得到结果之后才会返回。此外,还有异步和非阻塞式方法在任务完成前就返回。
66. 你对线程优先级的理解是什么?
每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优先权,但这依赖于线程调度的实现,这个实现是和操作系统相关的(OS dependent)。我们可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。线程优先级是一个 int 变量(从 1-10),1 代表最低优先级,10 代表最高优先级。
java 的线程优先级调度会委托给操作系统去处理,所以与具体的操作系统优先级有关,如非特别需要,一般无需设置线程优先级。
67. 什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing )?
线程调度器是一个操作系统服务,它负责为 Runnable 状态的线程分配 CPU 时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。同上一个问题,线程调度并不受到 Java 虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让你的程序依赖于线程的优先级)。时间分片是指将可用的 CPU 时间分配给可用的 Runnable 线程的过程。分配 CPU
时间可以基于线程优先级或者线程等待的时间。
68. 线程的中断方式有哪些?
一个线程执行完毕之后会自动结束,如果在运行过程中发生异常也会提前结束。
- InterruptedException
通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。
- interrupted()
如果一个线程的 run() 方法执行一个无限循环,并且没有执行 sleep() 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt() 方法就无法使线程提前结束。
但是调用 interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提前结束线程。
- Executor 的中断操作
调用 Executor 的 shutdown() 方法会等待线程都执行完毕之后再关闭,但是如果调用的是 shutdownNow() 方法,则相当于调用每个线程的 interrupt() 方法。
69. 线程 B 怎么知道线程 A 修改了变量
1)volatile 修饰变量
2)synchronized 修饰修改变量的方法
3)wait/notify
4)while 轮询
70. 什么是上下文切换?
71. 引起线程上下文切换的原因
1) 当前执行任务的时间片用完之后,系统 CPU 正常调度下一个任务;
2) 当前执行任务碰到 IO 阻塞,调度器将此任务挂起,继续下一任务;
3) 多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一任务;
4) 用户代码挂起当前任务,让出 CPU 时间;
5) 硬件中断;
72. 线程的调度策略
线程调度器选择优先级最高的线程运行,但是,如果发生以下情况,就会终止线程的运行:
1)线程体中调用了 yield 方法让出了对 cpu 的占用权利
2)线程体中调用了 sleep 方法使线程进入睡眠状态
3)线程由于 IO 操作受到阻塞
4)另外一个更高优先级线程出现
5)在支持时间片的系统中,该线程的时间片用完
74. Linux 环境下如何查找哪个线程使用 CPU 最长
74. 并发编程有什么缺点
并发编程的目的就是为了能提高程序的执行效率,提高程序运行速度,但是并发编程并不总是能提
高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、线程安全、死锁等问题。
75. Java 程序中怎么保证多线程的运行安全?
75. 形成死锁的四个必要条件是什么
76. 如何避免线程死锁
1) 避免一个线程同时获得多个锁
2) 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
3) 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制
77. Java 如何实现多线程之间的通讯和协作?
78. 一个线程运行时发生异常会怎样?
如果异常没有被捕获该线程将会停止执行。Thread.UncaughtExceptionHandler是用于处理未捕
获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候,JVM
会使用 Thread.getUncaughtExceptionHandler()来查询线程的 UncaughtExceptionHandler 并将
线程和异常作为参数传递给 handler 的 uncaughtException()方法进行处理。
79. Java 线程数过多会造成什么异常?
80. 线程之间如何通信及线程之间如何同步
81. Collections.synchronized * 是什么?
注意:* 号代表后面是还有内容的
此方法是干什么的呢,他完完全全的可以把List、Map、Set接口底下的集合变成线程安全的集合
Collections.synchronized * :原理是什么,我猜的话是代理模式
82. 同步容器和并发容器
1)同步容器:可以简单地理解为通过 synchronized 来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。比如 Vector,Hashtable,以Collections.synchronizedSet,synchronizedList 等方法返回的容器。可以通过查看 Vector,Hashtable 等这些同步容器的实现代码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并在需要同步的方法上加上关键字 synchronized。
2)并发容器:使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性,例如在
ConcurrentHashMap 中采用了一种粒度更细的加锁机制,可以称为分段锁,在这种锁机制下,允
许任意数量的读线程并发地访问 map,并且执行读操作的线程和写操作的线程也可以并发的访问
map,同时允许一定数量的写操作线程并发地修改 map,所以它可以在并发环境下实现更高的吞
吐量
3)同步集合与并发集合有什么区别:
同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更
高。在 Java1.5 之前程序员们只有同步集合来用且在多线程并发的时候会导致争用,阻碍了系统的
扩展性。Java5 介绍了并发集合像ConcurrentHashMap,不仅提供线程安全还用锁分离和内部分区等现代技术提高了可扩展性
83. 并发队列
什么是并发队列:
消息队列很多人知道:消息队列是分布式系统中重要的组件,是系统与系统直接的通信
并发队列是什么:并发队列多个线程以有次序共享数据的重要组件
并发队列和并发集合的区别:
那就有可能要说了,我们并发集合不是也可以实现多线程之间的数据共享吗,其实也是有区别的:
队列遵循“先进先出”的规则,可以想象成排队检票,队列一般用来解决大数据量采集处理和显示
的。并发集合就是在多个线程中共享数据的。
怎么判断并发队列是阻塞队列还是非阻塞队列:
在并发队列上JDK提供了Queue接口,一个是以Queue接口下的BlockingQueue接口为代表的阻塞队列,另一个是高性能(无堵塞)队列。
阻塞队列和非阻塞队列区别:
1)当队列阻塞队列为空的时,从队列中获取元素的操作将会被阻塞。
2)或者当阻塞队列是满时,往队列里添加元素的操作会被阻塞。
3)或者试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。
4)试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程使队列重新变得空闲起来
常用并发列队的介绍:
并发队列的常用方法:
方法名 | 描述 |
add() | 在不超出队列长度的情况下插入元素,可以立即执行,成功返回true, 如果队列满了就抛出异常。 |
offer() | 在不超出队列长度的情况下插入元素的时候则可以立即在队列的尾部插 入指定元素,成功时返回true,如果此队列已满,则返回false。 |
put() | 插入元素的时候,如果队列满了就进行等待,直到队列可用。 |
take() | 从队列中获取值,如果队列中没有值,线程会一直阻塞,直到队列中有 值,并且该方法取得了该值。 |
poll(long timeout, TimeUnit unit) | 在给定的时间里,从队列中获取值,如果没有取到会抛出异常。 |
remainingCapacity() | 获取队列中剩余的空间。 |
remove(Object o) | 从队列中移除指定的值。 |
contains(Object o) | 判断队列中是否拥有该值。 |
drainTo(Collection c) | 将队列中值,全部移除,并发设置到给定的集合中。 |
84. 并发工具类
85. volitile
volatile是如何实现可见性的? 内存屏障。
volatile是如何实现有序性的? happens-before等
86. BlockingQueue
1) 什么是BlockingDeque? 适合用在什么样的场景?
BlockingQueue 通常用于一个线程生产对象,而另外一个线程消费这些对象的场景。
一个线程往里边放,另外一个线程从里边取的一个 BlockingQueue。
一个线程将会持续生产新对象并将其插入到队列之中,直到队列达到它所能容纳的临界点。也就是说,它是有限的。如果该阻塞队列到达了其临界点,负责生产的线程将会在往里边插入新对象时发生阻塞。它会一直处于阻塞之中,直到负责消费的线程从队列中拿走一个对象。 负责消费的线程将会一直从该阻塞队列中拿出对象。如果消费线程尝试去从一个空的队列中提取对象的话,这个消费线程将会处于阻塞之中,直到一个生产线程把一个对象丢进队列。
2) BlockingDeque大家族
LinkedBlockingDeque 是一个双端队列,在它为空的时候,一个试图从中抽取数据的线程将会阻塞,无论该线程是试图从哪一端抽取数据。
3) BlockingDeque 与BlockingQueue有何关系,请对比下它们的方法
BlockingDeque 接口继承自 BlockingQueue 接口。这就意味着你可以像使用一个 BlockingQueue 那样使用 BlockingDeque。如果你这么干的话,各种插入方法将会把新元素添加到双端队列的尾端,而移除方法将会把双端队列的首端的元素移除。正如 BlockingQueue 接口的插入和移除方法一样。
以下是 BlockingDeque 对 BlockingQueue 接口的方法的具体内部实现:
87. 简要说下线程池的任务执行机制?
execute –> addWorker –>runworker (getTask)
1) 线程池的工作线程通过Woker类实现,在ReentrantLock锁的保证下,把Woker实例插入到HashSet后,并启动Woker中的线程。
2) 从Woker类的构造方法实现可以发现: 线程工厂在创建线程thread时,将Woker实例本身this作为参数传入,当执行start方法启动线程thread时,本质是执行了Worker的runWorker方法。
3) firstTask执行完成之后,通过getTask方法从阻塞队列中获取等待的任务,如果队列中没有任务,getTask方法会被阻塞并挂起,不会占用cpu资源;
88. 线程池中任务是如何提交的?
1) bsubmit任务,等待线程池execute
2) 执行FutureTask类的get方法时,会把主线程封装成WaitNode节点并保存在waiters链表中, 并阻塞等待运行结果;
3) FutureTask任务执行完成后,通过UNSAFE设置waiters相应的waitNode为null,并通过LockSupport类unpark方法唤醒主线程;
在实际业务场景中,Future和Callable基本是成对出现的,Callable负责产生结果,Future负责获取结果。
1) Callable接口类似于Runnable,只是Runnable没有返回值。
2) Callable任务除了返回正常结果之外,如果发生异常,该异常也会被返回,即Future可以拿到异步执行任务各种结果;
3) Future.get方法会导致主线程阻塞,直到Callable任务执行完成;
89. 线程池中任务是如何关闭的?
- shutdown
将线程池里的线程状态设置成SHUTDOWN状态, 然后中断所有没有正在执行任务的线程.
- shutdownNow
将线程池里的线程状态设置成STOP状态, 然后停止所有正在执行或暂停任务的线程. 只要调用这两个关闭方法中的任意一个, isShutDown() 返回true. 当所有任务都成功关闭了, isTerminated()返回true
90. 在配置线程池的时候需要考虑哪些配置因素?
从任务的优先级,任务的执行时间长短,任务的性质(CPU密集/ IO密集),任务的依赖关系这四个角度来分析。并且近可能地使用有界的工作队列。
性质不同的任务可用使用不同规模的线程池分开处理:
- CPU密集型: 尽可能少的线程,Ncpu+1
- IO密集型: 尽可能多的线程, Ncpu*2,比如数据库连接池
- 混合型: CPU密集型的任务与IO密集型任务的执行时间差别较小,拆分为两个线程池;否则没有必要拆分
91. 如何监控线程池的状态?
可以使用ThreadPoolExecutor以下方法:
-
getTaskCount()
Returns the approximate total number of tasks that have ever been scheduled for execution. -
getCompletedTaskCount()
Returns the approximate total number of tasks that have completed execution. 返回结果少于getTaskCount()。 -
getLargestPoolSize()
Returns the largest number of threads that have ever simultaneously been in the pool. 返回结果小于等于maximumPoolSize -
getPoolSize()
Returns the current number of threads in the pool. -
getActiveCount()
Returns the approximate number of threads that are actively executing tasks
92. 为什么很多公司不允许使用Executors去创建线程池? 那么推荐怎么使用呢?
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors各个方法的弊端:
- newFixedThreadPool和newSingleThreadExecutor: 主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
- newCachedThreadPool和newScheduledThreadPool: 主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。
- 推荐方式 1 首先引入:commons-lang3包
- 推荐方式 2 首先引入:com.google.guava包
- 推荐方式 3 spring配置线程池方式:自定义线程工厂bean需要实现ThreadFactory,可参考该接口的其它默认实现类,使用方式直接注入bean调用execute(Runnable task)方法即可