目录
26、什么是线程组,为什么在 Java 中不推荐使用?
27、为什么使用 Executor 框架比使用应用创建和管理线程好?
27.1 为什么要使用 Executor 线程池框架
27.2 使用 Executor 线程池框架的优点
28、java 中有几种方法可以实现一个线程?
29、如何停止一个正在运行的线程?
30、notify()和 notifyAll()有什么区别?
31、什么是 Daemon 线程?它有什么意义?
32、java 如何实现多线程之间的通讯和协作?
33、什么是可重入锁(ReentrantLock)?
34、当一个线程进入某个对象的一个 synchronized 的实例方法后,其它线程是否可进入此对象的其它方法?
35、乐观锁和悲观锁的理解及如何实现,有哪些实现方式?
36、SynchronizedMap 和 ConcurrentHashMap 有什么区别?
37、CopyOnWriteArrayList 可以用于什么应用场景?
38、什么叫线程安全?servlet 是线程安全吗?
26、什么是线程组,为什么在 Java 中不推荐使用?
线程组和线程池是两个不同的概念,他们的作用完全不同,前者是为了方便线程的管理,后者是为了管理线程的生命周期,复用线程,减少创建销毁线程的开销。
27、为什么使用 Executor 框架比使用应用创建和管理线程好?
27.1 为什么要使用 Executor 线程池框架
1
、每次执行任务创建线程
new Thread()
比较消耗性能,创建一个线程是比较耗时、耗资源的。
2
、调用
new Thread()
创建的线程缺乏管理,被称为野线程,而且可以无限制的创建,线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪,还有线程之间的频繁交替也会消耗很多系统资源。
3
、直接使用
new Thread()
启动的线程不利于扩展,比如定时执行、定期执行、定时定期执行、线程中断等都不便实现。
27.2 使用 Executor 线程池框架的优点
1
、能复用已存在并空闲的线程从而减少线程对象的创建从而减少了消亡线程的开销。
2
、可有效控制最大并发线程数,提高系统资源使用率,同时避免过多资源竞争。
3
、框架中已经有定时、定期、单线程、并发数控制等功能。
综上所述使用线程池框架
Executor
能更好的管理线程、提供系统资源使用率。
28、java 中有几种方法可以实现一个线程?
继承
Thread
类
实现
Runnable
接口
实现
Callable
接口,需要实现的是
call()
方法
29、如何停止一个正在运行的线程?
使用共享变量的方式
在这种方式中,之所以引入共享变量,是因为该变量可以被多个执行相同任务的线程用来作为是否中断的信号,通知中断线程的执行。
使用
interrupt
方法终止线程
如果一个线程由于等待某些事件的发生而被阻塞,又该怎样停止该线程呢?这种情况经常会发生,比如当一个线程由于需要等候键盘输入而被阻塞,或者调用Thread.join()方法,或者
Thread.sleep()
方法,在网络中调用ServerSocket.accept()方法,或者调用了
DatagramSocket.receive()
方法时,都有可能导致线程阻塞,使线程处于处于不可运行状态时,即使主程序中将该线程的共享变量设置为 true
,但该线程此时根本无法检查循环标志,当然也就无法立即中断。这里我们给出的建议是,不要使用 stop()
方法,而是使用
Thread
提供的interrupt()方法,因为该方法虽然不会中断一个正在运行的线程,但是它可以使一个被阻塞的线程抛出一个中断异常,从而使线程提前结束阻塞状态,退出堵塞代码。
30、notify()和 notifyAll()有什么区别?
当一个线程进入
wait
之后,就必须等其他线程
notify/notifyall,
使用
notifyall,
可以唤醒所有处于 wait
状态的线程,使其重新进入锁的争夺队列中,而
notify
只能唤醒一个。
如果没把握,建议
notifyAll
,防止
notigy
因为信号丢失而造成程序异常。
31、什么是 Daemon 线程?它有什么意义?
所谓后台
(daemon)
线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这个线程并不属于程序中不可或缺的部分。因此,当所有的非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程。反过来说, 只要有任何非后台线程还在运行,程序就不会终止。必须在线程启动之前调用 setDaemon()方法,才能把它设置为后台线程。注意:后台进程在不执行
finally子句的情况下就会终止其 run()
方法。
比如:
JVM
的垃圾回收线程就是
Daemon
线程,
Finalizer
也是守护线程。
32、java 如何实现多线程之间的通讯和协作?
中断 和 共享变量
33、什么是可重入锁(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
都是可重入的锁,可重入锁相对来说简化了并发编程的开发。
34、当一个线程进入某个对象的一个 synchronized 的实例方法后,其它线程是否可进入此对象的其它方法?
如果其他方法没有
synchronized
的话,其他线程是可以进入的。
所以要开放一个线程安全的对象时,得保证每个方法都是线程安全的。
35、乐观锁和悲观锁的理解及如何实现,有哪些实现方式?
悲观锁
:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如 Java
里面的同步原语
synchronized
关键字的实现也是悲观锁。
乐观锁
:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition
机制,其实都是提供的乐观锁。在
Java中 java.util.concurrent.atomic
包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS
实现的。
乐观锁的实现方式:
1
、使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。
2
、
java
中的
Compare and Swap
即
CAS
,当多个线程尝试使用
CAS
同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。 CAS
操作中包含三个操作数 ——
需要读写的内存位置(
V
)、进行比较的预期原值(
A
)和拟写入的新值(B)
。如果内存位置
V
的值与预期原值
A
相匹配,那么处理器会自动将该位置值更新为新值 B
。否则处理器不做任何操作。
CAS
缺点:
1
、
ABA
问题:
比如说一个线程
one
从内存位置
V
中取出
A
,这时候另一个线程
two
也从内存中取出 A
,并且
two
进行了一些操作变成了
B
,然后
two
又将
V
位置的数据变成
A
,这时候线程 one
进行
CAS
操作发现内存中仍然是
A
,然后
one
操作成功。尽管线程 one
的
CAS
操作成功,但可能存在潜藏的问题。从
Java1.5
开始
JDK
的
atomic包里提供了一个类 AtomicStampedReference
来解决
ABA
问题。
2
、循环时间长开销大:
对于资源竞争严重(线程冲突严重)的情况,
CAS
自旋的概率会比较大,从而浪费更多的 CPU
资源,效率低于
synchronized
。
3
、只能保证一个共享变量的原子操作:
当对一个共享变量执行操作时,我们可以使用循环
CAS
的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS
就无法保证操作的原子性,这个时候就可以用锁。
36、SynchronizedMap 和 ConcurrentHashMap 有什么区别?
SynchronizedMap
一次锁住整张表来保证线程安全,所以每次只能有一个线程来访为 map
。
ConcurrentHashMap
使用分段锁来保证在多线程下的性能。
ConcurrentHashMap
中则是一次锁住一个桶。
ConcurrentHashMap
默认将hash 表分为
16
个桶,诸如
get,put,remove
等常用操作只锁当前需要用到的桶。 这样,原来只能一个线程进入,现在却能同时有 16
个写线程执行,并发性能的提升是显而易见的。
另外
ConcurrentHashMap
使用了一种不同的迭代方式。在这种迭代方式中,当iterator 被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时
new
新的数据从而不影响原有的数据 ,iterator
完成后再将头指针替换为新的数据 ,这样
iterator 线程可以使用原来老的数据,而写线程也可以并发的完成改变。
37、CopyOnWriteArrayList 可以用于什么应用场景?
CopyOnWriteArrayList(
免锁容器
)
的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出 ConcurrentModificationException
。在CopyOnWriteArrayList 中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。
1
、由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致 young gc
或者
full gc
;
2
、不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个
set操作后,读取到数据可能还是旧的,
虽然
CopyOnWriteArrayList
能做到最终一致性,
但是还是没法满足实时性要求;
CopyOnWriteArrayList
透露的思想
1
、读写分离,读和写分开
2
、最终一致性
3
、使用另外开辟空间的思路,来解决并发冲突
38、什么叫线程安全?servlet 是线程安全吗?
线程安全是编程中的术语,指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。
Servlet 不是线程安全的,
servlet
是单实例多线程的,当多个线程同时访问同一个方法,是不能保证共享变量的线程安全性的。
Struts2
的
action
是多实例多线程的,是线程安全的,每个请求过来都会
new
一个新的 action
分配给这个请求,请求完成后销毁。
SpringMVC
的
Controller
是线程安全的吗?不是的,和
Servlet
类似的处理流程。
Struts2
好处是不用考虑线程安全问题;
Servlet
和
SpringMVC
需要考虑线程安全问题,但是性能可以提升不用处理太多的 gc
,可以使用
ThreadLocal
来处理多线程的问题。
要想了解更多:
千题千解·Java面试宝典_时光の尘的博客-CSDN博客