1.并⾏跟并发有什么区别?
从操作系统的⾓度来看,线程是
CPU
分配的最⼩单位。
并⾏就是同⼀时刻,两个线程都在执⾏。这就要求有两个
CPU
去分别执⾏两个线程。
并发就是同⼀时刻,只有⼀个执⾏,但是⼀个时间段内,两个线程都执⾏了。并发的实现
依赖于
CPU
切换线程,因为切换的时间特别短,所以基本对于⽤户是⽆感知的。
2.
说说什么是进程和线程?
要说线程,必须得先说说进程。
进程:进程是代码在数据集合上的⼀次运⾏活动,是系统进⾏资源分配和调度的基本单
位。
线程:线程是进程的⼀个执⾏路径,⼀个进程中⾄少有⼀个线程,进程中的多个线程共享
进程的资源。
操作系统在分配资源时是把资源分配给进程的, 但是
CPU
资源⽐较特殊,它是被分配到线程
的,因为真正要占⽤
CPU
运⾏的是线程,所以也说线程是
CPU
分配的基本单位。
⽐如在
Java
中,当我们启动
main
函数其实就启动了⼀个
JVM
进程,⽽
main
函数在的线程就
是这个进程中的⼀个线程,也称主线程。
⼀个进程中有多个线程,多个线程共⽤进程的堆和⽅法区资源,但是每个线程有⾃⼰的程序
计数器和栈。
3.
说说线程有⼏种创建⽅式?
Java
中创建线程主要有三种⽅式,分别为继承
Thread
类、实现
Runnable
接⼜、实现
Callable
接
⼜。
7.
什么是线程上下⽂切换?
使⽤多线程的⽬的是为了充分利⽤
CPU
,但是我们知道,并发其实是⼀个
CPU
来应付多个线
程。
8.
守护线程了解吗?
Java
中的线程分为两类,分别为
daemon
线程(守护线程)和
user
线程(⽤户线程)。
在
JVM
启动时会调⽤
main
函数,
main
函数所在的钱程就是⼀个⽤户线程。其实在
JVM
内部
同时还启动了很多守护线程, ⽐如垃圾回收线程。
那么守护线程和⽤户线程有什么区别呢?区别之⼀是当最后⼀个⾮守护线程束时,
JVM
会正
常退出,⽽不管当前是否存在守护线程,也就是说守护线程是否结束并不影响
JVM
退出。换
⽽⾔之,只要有⼀个⽤户线程还没结束,正常情况下
JVM
就不会退出。
volatile
和
synchronized
关键字
关键字
volatile
可以⽤来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从
共享内存中获取,⽽对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的
可见性。
关键字
synchronized
可以修饰⽅法或者以同步块的形式来进⾏使⽤,它主要确保多个线程在同
⼀个时刻,只能有⼀个线程处于⽅法或者同步块中,它保证了线程对变量访问的可见性和排
他性。
等待
/
通知机制
可以通过
Java
内置的等待
/
通知机制(
wait()/notify()
)实现⼀个线程修改⼀个对象的值,⽽另⼀
个线程感知到了变化,然后进⾏相应的操作。
管道输⼊
/
输出流
管道输⼊
/
输出流和普通的⽂件输⼊
/
输出流或者⽹络输⼊
/
输出流不同之处在于,它主要⽤于线
程之间的数据传输,⽽传输的媒介为内存。
管道输⼊
/
输出流主要包括了如下
4
种具体实现:
PipedOutputStream
、
PipedInputStream
、
PipedReader
和
PipedWriter
,前两种⾯向字节,⽽后两种⾯向字符。
使⽤
Thread.join()
如果⼀个线程
A
执⾏了
thread.join()
语句,其含义是:当前线程
A
等待
thread
线程终⽌之后才从
thread.join()
返回。。线程
Thread
除了提供
join()
⽅法之外,还提供了
join(long millis)
和
join(long
millis,int nanos)
两个具备超时特性的⽅法。
使⽤
ThreadLocal
ThreadLocal
,即线程变量,是⼀个以
ThreadLocal
对象为键、任意对象为值的存储结构。这个
结构被附带在线程上,也就是说⼀个线程可以根据⼀个
ThreadLocal
对象查询到绑定在这个线
程上的⼀个值。
可以通过
set(T)
⽅法来设置⼀个值,在当前线程下再通过
get()
⽅法获取到原先设置的值。
关于多线程,其实很⼤概率还会出⼀些笔试题,⽐如交替打印、银⾏转账、⽣产消费模
型等等,后⾯⽼三会单独出⼀期来盘点⼀下常见的多线程笔试题。
ThreadLocal
ThreadLocal
其实应⽤场景不是很多,但却是被炸了千百遍的⾯试⽼油条,涉及到多线程、数
据结构、
JVM
,可问的点⽐较多,⼀定要拿下。
11.
你在⼯作中⽤到过
ThreadLocal
吗?
有⽤到过的,⽤来做⽤户信息上下⽂的存储。
我们的系统应⽤是⼀个典型的
MVC
架构,登录后的⽤户每次访问接⼜,都会在请求头中携带
⼀个
token
,在控制层可以根据这个
token
,解析出⽤户的基本信息。那么问题来了,假如在服
务层和持久层都要⽤到⽤户信息,⽐如
rpc
调⽤、更新⽤户获取等等,那应该怎么办呢?
⼀种办法是显式定义⽤户相关的参数,⽐如账号、⽤户名
……
这样⼀来,我们可能需要⼤⾯
积地修改代码,多少有点⽠⽪,那该怎么办呢?
这时候我们就可以⽤到
ThreadLocal
,在控制层拦截请求把⽤户信息存⼊
ThreadLocal
,这样我
们在任何⼀个地⽅,都可以取出
ThreadLocal
中存的⽤户数据。
很多其它场景的
cookie
、
session
等等数据隔离也都可以通过
ThreadLocal
去实现。
我们常⽤的数据库连接池也⽤到了
ThreadLocal
:
数据库连接池的连接交给
ThreadLoca
进⾏管理,保证当前线程的操作都是同⼀个
Connnection
。
15.ThreadLocalMap
怎么解决
Hash
冲突的?
我们可能都知道
HashMap
使⽤了链表来解决冲突,也就是所谓的链地址法。
ThreadLocalMap
没有使⽤链表,⾃然也不是⽤链地址法来解决冲突了,它⽤的是另外⼀种⽅
式
——
开放定址法
。开放定址法是什么意思呢?简单来说,就是这个坑被⼈占了,那就接着
去找空着的坑。
如上图所⽰,如果我们插⼊⼀个
value=27
的数据,通过
hash
计算后应该落⼊第
4
个槽位中,
⽽槽位
4
已经有了
Entry
数据,⽽且
Entry
数据的
key
和当前不相等。此时就会线性向后查找,
⼀直找到
Entry
为
null
的槽位才会停⽌查找,把元素放到空的槽中。
在
get
的时候,也会根据
ThreadLocal
对象的
hash
值,定位到
table
中的位置,然后判断该槽位
Entry
对象中的
key
是否和
get
的
key
⼀致,如果不⼀致,就判断下⼀个位置。
18.说⼀下你对Java内存模型(JMM)的理解?
Java
内存模型(
Java Memory Model
,
JMM
),是⼀种抽象的模型,被定义出来屏蔽各种硬件
和操作系统的内存访问差异。
JMM
定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在
主内存
(
Main
Memory
)中,每个线程都有⼀个私有的
本地内存
(
Local Memory
),本地内存中存储了该线
程以读
/
写共享变量的副本。
Java
内存模型的抽象图:
图⾥⾯的是⼀个双核
CPU
系统架构 ,每个核有⾃⼰的控制器和运算器,其中控制器包含⼀组
寄存器和操作控制器,运算器执⾏算术逻辅运算。每个核都有⾃⼰的⼀级缓存,在有些架构
⾥⾯还有⼀个所有
CPU
共享的⼆级缓存。 那么
Java
内存模型⾥⾯的⼯作内存,就对应这⾥
的
Ll
缓存或者
L2
缓存或者
CPU
寄存器。
19.
说说你对原⼦性、可见性、有序性的理解?
原⼦性、有序性、可见性是并发编程中⾮常重要的基础概念,
JMM
的很多技术都是围绕着这
三⼤特性展开。
原⼦性 :原⼦性指的是⼀个操作是不可分割、不可中断的,要么全部执⾏并且执⾏的过
程不会被任何因素打断,要么就全不执⾏。
可见性 :可见性指的是⼀个线程修改了某⼀个共享变量的值时,其它线程能够⽴即知道
这个修改。
有序性 :有序性指的是对于⼀个线程的执⾏代码,从前往后依次执⾏,单线程下可以认
为程序是有序的,但是并发时有可能会发⽣指令重排。
原⼦性、可见性、有序性都应该怎么保证呢?
原⼦性:
JMM
只能保证基本的原⼦性,如果要保证⼀个代码块的原⼦性,需要使
⽤
synchronized
。
可见性:
Java
是利⽤
volatile
关键字来保证可见性的,除此之外,
final
和
synchronized
也能保证可见性。
有序性:
synchronized
或者
volatile
都可以保证多线程之间操作的有序性。
20.
那说说什么是指令重排?
在执⾏程序时,为了提⾼性能,编译器和处理器常常会对指令做重排序。重排序分
3
种类型。
1.
编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执
⾏顺序。
2.
指令级并⾏的重排序。现代处理器采⽤了指令级并⾏技术(
Instruction-Level Parallelism
,
ILP
)来将多条指令重叠执⾏。如果不存在数据依赖性,处理器可以改变语句对应 机器指
令的执⾏顺序。
3.
内存系统的重排序。由于处理器使⽤缓存和读
/
写缓冲区,这使得加载和存储操作看上去
可能是在乱序执⾏。
从
Java
源代码到最终实际执⾏的指令序列,会分别经历下⾯
3
种重排序,如图:
我们⽐较熟悉的双重校验单例模式就是⼀个经典的指令重排的例⼦,
Singleton
instance=new Singleton()
;
对应的
JVM
指令分为三步:分配内存空间
-->
初始化对象
--->
对象指向分配的内存空间,但是经过了编译器的指令重排序,第⼆步和第三步就可能会重排
序。
JMM
属于语⾔级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁⽌特
定类型的编译器重排序和处理器重排序,为程序员提供⼀致的内存可见性保证。
21.
指令重排有限制吗?
happens-before
了解吗?
指令重排也是有⼀些限制的,有两个规则
happens-before
和
as-if-serial
来约束。
happens-before
的定义:
如果⼀个操作
happens-before
另⼀个操作,那么第⼀个操作的执⾏结果将对第⼆个操作可
见,⽽且第⼀个操作的执⾏顺序排在第⼆个操作之前。
两个操作之间存在
happens-before
关系,并不意味着
Java
平台的具体实现必须要按照
happens-before
关系指定的顺序来执⾏。如果重排序之后的执⾏结果,与按
happens-before
关系来执⾏的结果⼀致,那么这种重排序并不⾮法
happens-before
和我们息息相关的有六⼤规则:
23.volatile
实现原理了解吗?
volatile
有两个作⽤,保证
可见性
和
有序性
。
volatile
怎么保证可见性的呢?
相⽐
synchronized
的加锁⽅式来解决共享变量的内存可见性问题,
volatile
就是更轻量的选择,
它没有上下⽂切换的额外开销成本。
volatile
可以确保对某个变量的更新对其他线程马上可见,⼀个变量被声明为
volatile
时,线程
在写⼊变量时不会把值缓存在寄存器或者其他地⽅,⽽是会把值刷新回主内存 当其它线程读
取该共享变量 ,会从主内存重新获取最新值,⽽不是使⽤当前线程的本地内存中的值。
为了实现
volatile
的内存语义,编译器在⽣成字节码时,会在指令序列中插⼊内存屏障来禁⽌
特定类型的处理器重排序。
1.
在每个
volatile
写操作的前⾯插⼊⼀个
StoreStore
屏障
2.
在每个
volatile
写操作的后⾯插⼊⼀个
StoreLoad
屏障
3.
在每个
volatile
读操作的后⾯插⼊⼀个
LoadLoad
屏障
4.
在每个
volatile
读操作的后⾯插⼊⼀个
LoadStore
屏障
Version:0.9 StartHTML:0000000105 EndHTML:0000009655 StartFragment:0000000141 EndFragment:0000009615
24.synchronized
⽤过吗?怎么使⽤?
synchronized
经常⽤的,⽤来保证代码的原⼦性。
synchronized
主要有三种⽤法:
修饰实例⽅法
:
作⽤于当前对象实例加锁,进⼊同步代码前要获得 当前对象实例的锁
synchronized
void
method
() {
//
业务代码
}
修饰静态⽅法
:也就是给当前类加锁,会作⽤于类的所有对象实例 ,进⼊同步代码前要
获得当前
class
的锁。因为静态成员不属于任何⼀个实例对象,是类成员(
static
表明这
是该类的⼀个静态资源,不管
new
了多少个对象,只有⼀份)。
如果⼀个线程
A
调⽤⼀个实例对象的⾮静态
synchronized
⽅法,⽽线程
B
需要调⽤这个
实例对象所属类的静态
synchronized
⽅法,是允许的,不会发⽣互斥现象,因为访问静态
synchronized
⽅法占⽤的锁是当前类的锁,⽽访问⾮静态
synchronized
⽅法占⽤的锁是当
前实例对象锁。
synchronized
void
staic
method
() {
//
业务代码
}
修饰代码块 :指定加锁对象,对给定对象
/
类加锁。
synchronized(this|object)
表⽰进⼊同
步代码库前要获得给定对象的锁。
synchronized(
类
.class)
表⽰进⼊同步代码前要获得 当前
class
的锁
synchronized
(
this
) {
//
业务代码
}
25.synchronized
的实现原理?
synchronized
是怎么加锁的呢?
我们使⽤
synchronized
的时候,发现不⽤⾃⼰去
lock
和
unlock
,是因为
JVM
帮我们把这个事情做
了。
1.
synchronized
修饰代码块时,
JVM
采⽤
monitorenter
、
monitorexit
两个指令来实现
同步,
monitorenter
指令指向同步代码块的开始位置,
monitorexit
指令则指向同
步代码块的结束位置。
反编译⼀段
synchronized
修饰代码块代码,
javap -c -s -v -l
SynchronizedDemo.class
,可以看到相应的字节码指令。
synchronized
锁住的是什么呢?
monitorenter
、
monitorexit
或者
ACC_SYNCHRONIZED
都是
基于
Monitor
实现
的。
实例对象结构⾥有对象头,对象头⾥⾯有⼀块结构叫
Mark Word
,
Mark Word
指针指向了
monitor
。
所谓的
Monitor
其实是⼀种
同步⼯具
,也可以说是⼀种
同步机制
。在
Java
虚拟机(
HotSpot
)
中,
Monitor
是由
ObjectMonitor
实现
的,可以叫做内部锁,或者
Monitor
锁。
ObjectMonitor
的⼯作原理:
ObjectMonitor
有两个队列:
WaitSet
、
EntryList
,⽤来保存
ObjectWaiter
对象列表。
_owner
,获取
Monitor
对象的线程进⼊
_owner
区时,
_count + 1
。如果线程调⽤了
wait()
⽅法,此时会释放
Monitor
对象,
_owner
恢复为空,
_count - 1
。同时该等待线程进⼊
_WaitSet
中,等待被唤醒。
所以我们就知道了,同步是锁住的什么东西:
monitorenter
,在判断拥有同步标识
ACC_SYNCHRONIZED
抢先进⼊此⽅法的线程会优先
拥有
Monitor
的
owner
,此时计数器
+1
。
monitorexit
,当执⾏完退出后,计数器
-1
,归
0
后被其他进⼊的线程获得。
28.
说说
synchronized
和
ReentrantLock
的区别?
可以从锁的实现、功能特点、性能等⼏个维度去回答这个问题:
锁的实现:
synchronized
是
Java
语⾔的关键字,基于
JVM
实现。⽽
ReentrantLock
是基于
JDK
的
API
层⾯实现的(⼀般是
lock()
和
unlock()
⽅法配合
try/finally
语句块来完成。)
性能:
在
JDK1.6
锁优化以前,
synchronized
的性能⽐
ReenTrantLock
差很多。但是
JDK6
开
始,增加了适应性⾃旋、锁消除等,两者性能就差不多了。
功能特点:
ReentrantLock
⽐
synchronized
增加了⼀些⾼级功能,如等待可中断、可实现
公平锁、可实现选择性通知。
ReentrantLock
提供了⼀种能够中断等待锁的线程的机制,通过
lock.lockInterruptibly()
来
实现这个机制
ReentrantLock
可以指定是公平锁还是⾮公平锁。⽽
synchronized
只能是⾮公平锁。所谓
的公平锁就是先等待的线程先获得锁。
synchronized
与
wait()
和
notify()/notifyAll()
⽅法结合实现等待
/
通知机制,
ReentrantLock
类借助
Condition
接⼜与
newCondition()
⽅法实现。
ReentrantLock
需要⼿⼯声明来加锁和释放锁,⼀般跟
finally
配合释放锁。⽽
synchronized
不⽤⼿动释放锁。
29.AQS
了解多少?
AbstractQueuedSynchronizer
抽象同步队列,简称
AQS
,它是
Java
并发包的根基,并发包中的
锁就是基于
AQS
实现的。
AQS
是基于⼀个
FIFO
的双向队列,其内部定义了⼀个节点类
Node
,
Node
节点内部的
SHARED
⽤来标记该线程是获取共享资源时被阻挂起后放⼊
AQS
队列的,
EXCLUSIVE
⽤来标记线程是 取独占资源时被挂起后放⼊
AQS
队列
AQS
使⽤⼀个
volatile
修饰的
int
类型的成员变量
state
来表⽰同步状态,修改同步状态成
功即为获得锁,
volatile
保证了变量在多线程之间的可见性,修改
State
值时通过
CAS
机制
来保证修改的原⼦性
获取
state
的⽅式分为两种,独占⽅式和共享⽅式,⼀个线程使⽤独占⽅式获取了资源,其
它线程就会在获取失败后被阻塞。⼀个线程使⽤共享⽅式获取了资源,另外⼀个线程还可
以通过
CAS
的⽅式进⾏获取。
如果共享资源被占⽤,需要⼀定的阻塞等待唤醒机制来保证锁的分配,
AQS
中会将竞争
共享资源失败的线程添加到⼀个变体的
CLH
队列中。
AQS
中的
CLH
变体等待队列拥有以下特性:
AQS
中队列是个双向链表,也是
FIFO
先进先出的特性
通过
Head
、
Tail
头尾两个节点来组成队列结构,通过
volatile
修饰保证可见性
Head
指向节点为已获得锁的节点,是⼀个虚拟节点,节点本⾝不持有具体线程
获取不到同步状态,会将节点进⾏⾃旋获取锁,⾃旋⼀定次数失败后会将线程阻塞,相对
于
CLH
队列性能较好
ps:AQS
源码⾥⾯有很多细节可问,建议有时间好好看看
AQS
源码。
30.
ReentrantLock
实现原理?
ReentrantLock
是可重⼊的独占锁,只能有⼀个线程可以获取该锁,其它获取该锁的线程会被
阻塞⽽被放⼊该锁的阻塞队列⾥⾯。
看看
ReentrantLock
的加锁操作:
//
创建⾮公平锁
ReentrantLock lock
=
new
ReentrantLock();
//
获取锁操作
lock.lock();
try
{
//
执⾏代码逻辑
}
catch
(Exception ex) {
// ...
}
finally
{
//
解锁操作
lock.unlock();
}
new ReentrantLock()
构造函数默认创建的是⾮公平锁
NonfairSync
。
公平锁
FairSync
1.
公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进⼊队列中排队,队列中的第
⼀个线程才能获得锁
2.
公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对⾮公平锁要低,等待队
列中除第⼀个线程以外的所有线程都会阻塞,
CPU
唤醒阻塞线程的开销⽐⾮公平锁⼤
⾮公平锁
NonfairSync
⾮公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如
果此时锁刚好可⽤,那么这个线程可以⽆需阻塞直接获取到锁
⾮公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率⾼,因为线程有⼏率不阻塞
直接获得锁,
CPU
不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等
很久才会获得锁
默认创建的对象
lock()
的时候:
如果锁当前没有被其它线程占⽤,并且当前线程之前没有获取过该锁,则当前线程会获取
到该锁,然后设置当前锁的拥有者为当前线程,并设置
AQS
的状态值为
1
,然后直接返
回。如果当前线程之前⼰经获取过该锁,则这次只是简单地把
AQS
的状态值加
1
后返回。
如果该锁⼰经被其他线程持有,⾮公平锁会尝试去获取锁,获取失败的话,则调⽤该⽅法
线程会被放⼊
AQS
队列阻塞挂起。
31.ReentrantLock
怎么实现公平锁的?
new ReentrantLock()
构造函数默认创建的是⾮公平锁
NonfairSync
public
ReentrantLock() {
sync
=
new
NonfairSync();
}
同时也可以在创建锁构造函数中传⼊具体参数创建公平锁
FairSync
ReentrantLock lock
=
new
ReentrantLock(
true
);
---
ReentrantLock
// true
代表公平锁,
false
代表⾮公平锁
public
ReentrantLock(
boolean
fair) {
sync
=
fair
?
new
FairSync() :
new
NonfairSync();
}
FairSync
、
NonfairSync
代表公平锁和⾮公平锁,两者都是
ReentrantLock
静态内部类,只不过
实现不同锁语义。
⾮公平锁和公平锁的两处不同:
1.
⾮公平锁在调⽤
lock
后,⾸先就会调⽤
CAS
进⾏⼀次抢锁,如果这个时候恰巧锁没有被
占⽤,那么直接就获取到锁返回了。
2.
⾮公平锁在
CAS
失败后,和公平锁⼀样都会进⼊到
tryAcquire
⽅法,在
tryAcquire
⽅法
中,如果发现锁这个时候被释放了(
state == 0
),⾮公平锁会直接
CAS
抢锁,但是公平
锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后⾯。
相对来说,⾮公平锁会有更好的性能,因为它的吞吐量⽐较⼤。当然,⾮公平锁让获取锁的
时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。
32.CAS
呢?
CAS
了解多少?
CAS
叫做
CompareAndSwap
,⽐较并交换,主要是通过处理器的指令来保证操作的原⼦性的。
CAS
指令包含
3
个参数:共享变量的内存地址
A
、预期的值
B
和共享变量的新值
C
。
只有当内存中地址
A
处的值等于
B
时,才能将内存中地址
A
处的值更新为新值
C
。作为⼀条
CPU
指令,
CAS
指令本⾝是能够保证原⼦性的 。
ABA
问题
并发环境下,假设初始条件是
A
,去修改数据时,发现是
A
就会执⾏修改。但是看到的虽然是
A
,中间可能发⽣了
A
变
B
,
B
又变回
A
的情况。此时
A
已经⾮彼
A
,数据即使成功修改,也可
能有问题。
怎么解决
ABA
问题?
加版本号
每次修改变量,都在这个变量的版本号上加
1
,这样,刚刚
A->B->A
,虽然
A
的值没变,但是
它的版本号已经变了,再判断版本号就会发现此时的
A
已经被改过了。参考乐观锁的版本号,
这种做法可以给数据带上了⼀种实效性的检验。
Java
提供了
AtomicStampReference
类,它的
compareAndSet
⽅法⾸先检查当前的对象引⽤值是否
等于预期引⽤,并且当前印戳(
Stamp
)标志是否等于预期标志,如果全部相等,则以原⼦⽅
式将引⽤值和印戳标志的值更新为给定的更新值。
循环性能开销
⾃旋
CAS
,如果⼀直循环执⾏,⼀直不成功,会给
CPU
带来⾮常⼤的执⾏开销。
怎么解决循环性能开销问题?
在
Java
中,很多使⽤⾃旋
CAS
的地⽅,会有⼀个⾃旋次数的限制,超过⼀定次数,就停⽌⾃
旋。
只能保证⼀个变量的原⼦操作
CAS
保证的是对⼀个变量执⾏操作的原⼦性,如果对多个变量操作时,
CAS
⽬前⽆法直接保
证操作的原⼦性的。
怎么解决只能保证⼀个变量的原⼦操作问题?
可以考虑改⽤锁来保证操作的原⼦性
可以考虑合并多个变量,将多个变量封装成⼀个对象,通过
AtomicReference
来保证原⼦
性。
34.Java
有哪些保证原⼦性的⽅法?如何保证多线程下
i++
结果正确?
35.
原⼦操作类了解多少?
当程序更新⼀个变量时,如果多线程同时更新这个变量,可能得到期望之外的值,⽐如变量
i=1
,
A
线程更新
i+1
,
B
线程也更新
i+1
,经过两个线程操作之后可能
i
不等于
3
,⽽是等于
2
。因
为
A
和
B
线程在更新变量
i
的时候拿到的
i
都是
1
,这就是线程不安全的更新操作,⼀般我们会使
⽤
synchronized
来解决这个问题,
synchronized
会保证多线程不会同时更新变量
i
。
其实除此之外,还有更轻量级的选择,
Java
从
JDK 1.5
开始提供了
java.util.concurrent.atomic
包,这个包中的原⼦操作类提供了⼀种⽤法简单、性能⾼效、线程安全地更新⼀个变量的⽅
式。
因为变量的类型有很多种,所以在
Atomic
包⾥⼀共提供了
13
个类,属于
4
种类型的原⼦更新⽅
式,分别是原⼦更新基本类型、原⼦更新数组、原⼦更新引⽤和原⼦更新属性(字段)。
我们再来看⼀个
Semaphore
的⽤途:它可以⽤于做流量控制,特别是公⽤资源有限的应⽤场
景,⽐如数据库连接。
假如有⼀个需求,要读取⼏万个⽂件的数据,因为都是
IO
密集型任务,我们可以启动⼏⼗个
线程并发地读取,但是如果读到内存后,还需要存储到数据库中,⽽数据库的连接数只有
10
个,这时我们必须控制只有
10
个线程同时获取数据库连接保存数据,否则会报错⽆法获取数
据库连接。这个时候,就可以使⽤
Semaphore
来做流量控制,如下
44.
什么是线程池?
线程池:
简单理解,它就是⼀个管理线程的池⼦。
它帮我们管理线程,避免增加创建线程和销毁线程的资源损耗 。因为线程其实也是⼀个
对象,创建⼀个对象,需要经过类加载过程,销毁⼀个对象,需要⾛
GC
垃圾回收流程,
都是需要资源开销的。
提⾼响应速度。 如果任务到达了,相对于从线程池拿线程,重新去创建⼀条线程执⾏,
速度肯定慢很多。
重复利⽤。 线程⽤完,再放回池⼦,可以达到重复利⽤的效果,节省资源。
45.
能说说⼯作中线程池的应⽤吗?
之前我们有⼀个和第三⽅对接的需求,需要向第三⽅推送数据,引⼊了多线程来提升数据推
送的效率,其中⽤到了线程池来管理线程。
线程池的参数如下:
corePoolSize
:线程核⼼参数选择了
CPU
数
×2
maximumPoolSize
:最⼤线程数选择了和核⼼线程数相同
keepAliveTime
:⾮核⼼闲置线程存活时间直接置为
0
unit
:⾮核⼼线程保持存活的时间选择了
TimeUnit.SECONDS
秒
workQueue
:线程池等待队列,使⽤
LinkedBlockingQueue
阻塞队列
自己可以实现拒绝策略保存到数据库里 ,数据不能丢失
ArrayBlockingQueue
:
ArrayBlockingQueue
(有界队列)是⼀个⽤数组实现的有界阻塞队
列,按
FIFO
排序量。
LinkedBlockingQueue
:
LinkedBlockingQueue
(可设置容量队列)是基于链表结构的阻塞
队列,按
FIFO
排序任务,容量可以选择进⾏设置,不设置的话,将是⼀个⽆边界的阻塞队
列,最⼤长度为
Integer.MAX_VALUE
,吞吐量通常要⾼于
ArrayBlockingQuene
;
newFixedThreadPool
线程池使⽤了这个队列
DelayQueue
:
DelayQueue
(延迟队列)是⼀个任务定时周期的延迟执⾏的队列。根据指定
的执⾏时间从⼩到⼤排序,否则根据插⼊到队列的先后排序。
newScheduledThreadPool
线
程池使⽤了这个队列。
PriorityBlockingQueue
:
PriorityBlockingQueue
(优先级队列)是具有优先级的⽆界阻塞队
列
SynchronousQueue
:
SynchronousQueue
(同步队列)是⼀个不存储元素的阻塞队列,每个
插⼊操作必须等到另⼀个线程调⽤移除操作,否则插⼊操作⼀直处于阻塞状态,吞吐量通
常要⾼于
LinkedBlockingQuene
,
newCachedThreadPool
线程池使⽤了这个队列。
50.线程池提交execute和submit有什么区别?
1.
execute
⽤于提交不需要返回值的任务
threadsPool.execute(
new
Runnable() {
@Override
public
void
run() {
// TODO Auto-generated method stub }
});
2.
submit()
⽅法⽤于提交需要返回值的任务。线程池会返回⼀个
future
类型的对象,通过这个
future
对象可以判断任务是否执⾏成功,并且可以通过
future
的
get()
⽅法来获取返回值
Future
<
Object
>
future
=
executor.submit(harReturnValuetask);
try
{
Object
s
=
future.get(); }
catch
(InterruptedException e) {
//
处理中断异常
}
catch
(ExecutionException e) {
//
处理⽆法执⾏任务异常
}
finally
{
//
关闭线程池
executor.shutdown();
}
可以通过调⽤线程池的
shutdown
或
shutdownNow
⽅法来关闭线程池。它们的原理是遍历线
程池中的⼯作线程,然后逐个调⽤线程的
interrupt
⽅法来中断线程,所以⽆法响应中断的任务
可能永远⽆法终⽌。
shutdown()
将线程池状态置为
shutdown,
并不会⽴即停⽌
:
1.
停⽌接收外部
submit
的任务
2.
内部正在跑的任务和队列⾥等待的任务,会执⾏完
3.
等到第⼆步完成后,才真正停⽌
shutdownNow()
将线程池状态置为
stop
。⼀般会⽴即停⽌,事实上不⼀定
:
1.
和
shutdown()
⼀样,先停⽌接收外部提交的任务
2.
忽略队列⾥等待的任务
3.
尝试将正在跑的任务
interrupt
中断
4.
返回未执⾏的任务列表
shutdown
和
shutdownnow
简单来说区别如下:
shutdownNow()
能⽴即停⽌线程池,正在跑的和正在等待的任务都停下了。这样做⽴即⽣
效,但是风险也⽐较⼤。
shutdown()
只是关闭了提交通道,⽤
submit()
是⽆效的;⽽内部的任务该怎么跑还是怎么
跑,跑完再彻底停⽌线程池。
52.
线程池的线程数应该怎么配置?
线程在
Java
中属于稀缺资源,线程池不是越⼤越好也不是越⼩越好。任务分为计算密集型、
IO
密集型、混合型。
1.
计算密集型:⼤部分都在⽤
CPU
跟内存,加密,逻辑操作业务处理等。
2.
IO
密集型:数据库链接,⽹络通讯传输等。
⼀般的经验,不同类型线程池的参数配置:
1.
计算密集型⼀般推荐线程池不要过⼤,⼀般是
CPU
数
+ 1
,
+1
是因为可能存在 页缺失
(
就
是可能存在有些数据在硬盘中需要多来⼀个线程将数据读⼊内存
)
。如果线程池数太⼤,
可能会频繁的 进⾏线程上下⽂切换跟任务调度。获得当前
CPU
核⼼数代码如下:
Runtime.getRuntime().availableProcessors();
2.
IO
密集型:线程数适当⼤⼀点,机器的
Cpu
核⼼数
*2
。
3.
混合型:可以考虑根绝情况将它拆分成
CPU
密集型和
IO
密集型任务,如果执⾏时间相差不
⼤,拆分可以提升吞吐量,反之没有必要。
当然,实际应⽤中没有固定的公式,需要结合测试和监控来进⾏调整。
⼯作流程:
提交任务
线程池是否有⼀条线程在,如果没有,新建线程执⾏任务
如果有,将任务加到阻塞队列
当前的唯⼀线程,从队列取任务,执⾏完⼀个,再继续取,⼀个线程执⾏任务。
56.
能说⼀下线程池有⼏种状态吗?
线程池有这⼏个状态:
RUNNING,SHUTDOWN,STOP,TIDYING,TERMINATED
。
线程池各个状态切换图:
//
线程池状态
private static final
int
RUNNING
= -
1
<<
COUNT_BITS;
private static final
int
SHUTDOWN
=
0
<<
COUNT_BITS;
private static final
int
STOP
=
1
<<
COUNT_BITS;
private static final
int
TIDYING
=
2
<<
COUNT_BITS;
private static final
int
TERMINATED
=
3
<<
COUNT_BITS;
RUNNING
该状态的线程池会接收新任务,并处理阻塞队列中的任务
;
调⽤线程池的
shutdown()
⽅法,可以切换到
SHUTDOWN
状态
;
调⽤线程池的
shutdownNow()
⽅法,可以切换到
STOP
状态
;
SHUTDOWN
该状态的线程池不会接收新任务,但会处理阻塞队列中的任务;
队列为空,并且线程池中执⾏的任务也为空
,
进⼊
TIDYING
状态
;
STOP
该状态的线程不会接收新任务,也不会处理阻塞队列中的任务,⽽且会中断正在运⾏的任
务;
线程池中执⾏的任务为空
,
进⼊
TIDYING
状态
;
TIDYING
该状态表明所有的任务已经运⾏终⽌,记录的任务数量为
0
。
terminated()
执⾏完毕,进⼊
TERMINATED
状态
TERMINATED
该状态表⽰线程池彻底终⽌
上线之后要建⽴完善的线程池监控机制。
事中结合监控告警机制,分析线程池的问题,或者可优化点,结合线程池动态参数配置机制
来调整配置。
事后要注意仔细观察,随时调整。
60.
单机线程池执⾏断电了应该怎么处理?
我们可以对正在处理和阻塞队列的任务做事务管理或者对阻塞队列中的任务持久化处理,并
且当断电或者系统崩溃,操作⽆法继续下去的时候,可以通过回溯⽇志的⽅式来撤销
正在处
理
的已经执⾏成功的操作。然后重新执⾏整个阻塞队列。
也就是说,对阻塞队列持久化;正在处理任务事务控制;断电之后正在处理任务的回滚,通
过⽇志恢复该次操作;服务器重启后阻塞队列中的数据再加载。