java并发编程(总结学习笔记)

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.线程池提交executesubmit有什么区别?

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. 单机线程池执⾏断电了应该怎么处理?
我们可以对正在处理和阻塞队列的任务做事务管理或者对阻塞队列中的任务持久化处理,并
且当断电或者系统崩溃,操作⽆法继续下去的时候,可以通过回溯⽇志的⽅式来撤销 正在处
的已经执⾏成功的操作。然后重新执⾏整个阻塞队列。
也就是说,对阻塞队列持久化;正在处理任务事务控制;断电之后正在处理任务的回滚,通
过⽇志恢复该次操作;服务器重启后阻塞队列中的数据再加载。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值