# 技术栈知识点巩固——Java并发多线程
Posted MarlonBrando1998
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了# 技术栈知识点巩固——Java并发多线程相关的知识,希望对你有一定的参考价值。
开发中锁的使用
synchronized 的实现原理以及锁优化
实现原理
Synchronized
通过一个monitor
的对象来完成,在进入该方法之前先获取相应的锁,锁的计数器加1,方法结束后计数器-1,如果获取失败就阻塞,直到该锁被释放。直到monitor
的进入数为0,再重新尝试获取monitor
的所有权。
锁优化
- 锁消除:当资源不存在竞争问题的时候,可以取消锁。例如 没有必要使用一些线程安全的对象,没有必要加一些多余的锁。
- 锁粗化:锁粗化是虚拟机对另一种极端情况的优化处理,通过扩大锁的范围,避免反复加锁和释放锁。例如:
for
循环中的加锁操作可以放在for
循环外面进行加锁操作。 - 自旋锁:让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。怎么等待呢?执行一段无意义的循环即可(自旋)。
- 重量级锁:监视器锁(
Monitor
)依赖于底层的操作系统的MutexLock
来实现的,依赖于操作系统MutexLock
所实现的锁我们称之为 重量级锁。
AQS(AbstractQueuedSynchronizer)
- 同步队列:双向链表,里面储存的是处于等待状态的线程,正在排队等待唤醒去获取锁。
- 条件队列:单向链表,里面储存的也是处于等待状态的线程,这些线程唤醒的结果是加入到了同步队列的队尾。
AQS
所做的就是管理这两个队列里面线程之间的等待状态——唤醒的工作。
AQS组件,实现原理
- 图片来自:https://cloud.tencent.com/developer/article/1710265
CAS
CAS
有3个操作数,内存值V
,旧的预期值A
,要修改的新值B
。当且仅当预期值A
和内存值V
相同时,将内存值V
修改为B
,否则什么都不做。
CAS 有什么缺陷,如何解决?
- 线程
t1
将它的值从A
变为B
,再从B
变为A
。同时有线程t2
要将值从A
变为C
。但CAS
检查的时候会发现没有改变,但是实质上它已经发生了改变 。可能会造成数据的缺失。
synchronized和ReentrantLock的区别
共同点
- 可重入,同一线程可以多次获得同一个锁。
- 保证共享对象的可见性、互斥性。
- 都可以协调多线程对共享对象、变量的访问。
不同
synchronized
代码执行完后系统会自动让线程释放对锁的占用。ReentrantLock
则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象synchronized
是不可中断类型的锁。ReentrantLock
则可以中断。synchronized
为非公平锁ReentrantLock
则即可以选公平锁也可以选非公平锁,通过构造方法new ReentrantLock
时传入boolean
值进行选择,为空默认false
非公平锁,true
为公平锁。synchronzied
锁的是对象,ReentrantLock
锁的是线程。
公平锁、非公平锁
- 公平锁与非公平锁的差异主要在获取锁:公平锁就相当于买票,后来的人需要排到队尾依次买票,不能插队。而非公平锁则没有这些规则,是抢占模式,每来一个人不会去管队列如何,直接尝试获取锁。
ReentrantLock
公平锁和非公平锁:
static final class NonfairSync extends Sync {}
static final class FairSync extends Sync {}
可重入锁
- 可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。
- 加锁时判断锁是否已经被获取,如果已经被获取,则判断获取锁的线程是否是当前线程。如果是当前线程,则给获取次数加1。如果不是当前线程,则需要等待。
- 释放锁时,需要给锁的获取次数减1,然后判断,次数是否为0了。如果次数为0了,则需要调用锁的唤醒方法,让锁上阻塞的其他线程得到执行的机会。
ReentrantLock
和synchronized
都是可重入锁。
独享锁、共享锁
- 独享锁:该锁每一次只能被一个线程所持有。
ReentrantLock
是互斥锁,ReadWriteLock
中的写锁是互斥锁 - 共享锁 :该锁可被多个线程共有。
Semaphore
、CountDownLatch
是共享锁,ReadWriteLock
中的读锁是共享锁。
偏向锁、轻量级锁、重量级锁
-
偏向锁:偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
-
轻量级:轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
-
重量级锁:重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
Lock使用
private ReentrantLock lock = new ReentrantLock();
public void run() {
lock.lock();
try {
//TODO
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
ReentrantLock实现原理
ReentrantLock()
private final Sync sync;
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
ReentrantLock
就是一个普通的类,它是基于AQS来实现的。是一个重入锁:一个线程获得了锁之后仍然可以反复的加锁,不会出现自己阻塞自己的情况。
ReentrantLock获取锁的过程
ReentrantLock
先通过CAS
尝试获取锁。- 如果此时锁已经被占用,该线程加入
AQS
队列并wait()
。 - 当前驱线程的锁被释放,挂在
CLH
队列为首的线程就会被notify
(),然后继续CAS
尝试获取锁,此时:非公平锁,如果有其他线程尝试lock()
,有可能被其他刚好申请锁的线程抢占。公平锁,只有在CLH
队列头的线程才可以获取锁,新来的线程只能插入到队尾。
同步方法和同步代码块的区别是什么?
- 同步方法:有
synchronized
关键字修饰的方法,关键字 修饰方法。 - 同步代码块: 有
synchronized
关键字修饰的语句块,修饰代码块。
Lock接口比synchronized块的优势是什么
lock
接口在多线程和并发编程中最大的优势是它们为读和写分别提供了锁- 读锁
private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void read() {
ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
try {
// 加读锁
readLock.lock();
logger.info("读锁加锁成功!......");
Thread.sleep(1000);
logger.info("===> num:{}", num);
} catch (Exception e) {
logger.error("Error Occur:{}", e.getMessage(), e);
} finally {
// 释放锁
readLock.unlock();
}
}
volatile
volatile
让变量每次在使用的时候,都从主存中取。- 禁止指令重排序:解决单例双重锁乱序的问题。
synchronized
关键字可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和互斥性。
Java 中能创建 volatile数组吗
Java
中可以创建volatile
类型数组,不过只是一个指向数组的引用,而不是整个数组。
// volatile 数组
volatile int[] num = {1, 2, 3};
并发与并行
- 并发:是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。
- 并行:当系统有一个以上
CPU
时,当一个CPU
执行一个进程时,另一个CPU
可以执行另一个进程,两个进程互不抢占CPU
资源,可以同时进行,这种方式我们称之为并行。
继承Thread和实现Runnable方式创建线程有什么区别
Thread
实现了Runnable
接口。java
只能单继承,因此如果是采用继承Thread
的方法,那么在以后进行代码重构的时候可能会遇到问题- 如果一个类继承
Thread
,则不适合资源共享。但是如果实现了Runable
接口的话,则很容易的实现资源共享。
保证线程的执行顺序
Runnable和 Callable
Callable
的核心是call
方法,允许返回值,runnable
的核心是run
方法,没有返回值。call
方法可以抛出异常,但是run
方法不行。
public class DoOneCallable implements Callable<Object> {
private static final Logger logger = LoggerFactory.getLogger(DoOneCallable.class);
@Override
public Object call() throws Exception {
try {
logger.info("test Thread!");
// 该异常可以在主线程中拿到
return 4 / 0;
} catch (Exception e) {
throw e;
}
}
}
怎么预防死锁?死锁四个必要条件
- 当资源的使用是互斥的、资源占用后不能释放、循环等待请求资源、当进程请求的资源没有满足又去请求但是没有释放,也不能请求到资源这四种情况会发生死锁。
- 通过破坏发生死锁的条件预防死锁的产生。
FutureTask
FutureTask
可以执行异步计算,可以查看异步程序是否执行完毕,并且可以开始和取消程序,并取得程序最终的执行结果。- 详见博客:https://blog.csdn.net/qq_39654841/article/details/90631795
CountDownLatch与CyclicBarrier 区别
CountDownLatch
: 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行。CyclicBarrier
: N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。- 使用方法见:https://blog.csdn.net/qq_37248504/article/details/110502016
ThreadLocal原理,使用注意点,应用场景有哪些?
原理
- 当使用
ThreadLocal
维护变量时,ThreadLocal
为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
注意场景
- 用完之后就调用
remove()
进行释放内存,防止内存泄漏。
场景
- 最常见的
ThreadLocal
使用场景为用来解决数据库连接、Session
管理、变量计算等。
ThreadLocal如何解决Hash冲突?
- 和
HashMap
的最大的不同在于,ThreadLocalMap
结构非常简单,没有next
引用,也就是说ThreadLocalMap
中解决Hash
冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始key
的hashcode
值确定元素在table
数组中的位置,如果发现这个位置上已经有其他key
值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
ThreadLocal 的内存泄露
- 每一个
Thread
维护一个ThreadLocalMap
,key
为使用弱引用的ThreadLocal
实例,value
为线程变量的副本。 - 如果一个
ThreadLocal
不存在外部强引用时,ThreadLocal
势必会被GC
回收,这样就会导致ThreadLocalMap
中key
为null
, 而value
还存在着强引用,只有thead
线程退出以后,value
的强引用链条才会断掉。 - 如果当前线程再迟迟不结束的话,这些
key
为null
的Entry
的value
就会一直存在一条强引用链。
为什么ThreadLocalMap 的 key是弱引用,设计理念是?
- 当
hreadLocalMap
的key
为强引用回收ThreadLocal
时,因为ThreadLocalMap
还持有ThreadLocal
的强引用,如果没有手动删除,ThreadLocal
不会被回收,导致Entry
内存泄漏。 - 当
ThreadLocalMap
的key
为弱引用回收ThreadLocal
时,由于ThreadLocalMap
持有ThreadLocal
的弱引用,即使没有手动删除,ThreadLocal
也会被回收。当key
为null
,在下一次ThreadLocalMap
调用set
(),get
(),remove()
方法的时候会被清除value
值。
Fork、Join框架
- 用于并行执行任务的框架, 是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。
调用start()方法、调用run()方法
- 通过调用
Thread
类的start()
方法来启动一个线程,这时此线程处于就绪(可运行)状态。 - 当线程调用了
start( )
方法后,一旦线程被CPU
调度,处于运行状态,那么线程才会去调用这个run()
方法。
如果线程过多,会怎样?
- CPU飙升
- 内存溢出
- 性能不稳定
信号量(Semaphore)
- 通常用于那些资源有明确访问数量限制的场景,常用于限流 。
public class SemaphoreTest {
public static void main(String[] args) {
// 每次 2 个 线程 acquire
Semaphore semaphore = new Semaphore(2);
for (int i = 0; i < 5; i++) {
SemaphoreThread semaphoreThread = new SemaphoreThread(semaphore);
new Thread(semaphoreThread).start();
}
}
}
class SemaphoreThread implements Runnable {
private static final Logger logger = LoggerFactory.getLogger(SemaphoreThread.class);
private Semaphore semaphore;
public SemaphoreThread(Semaphore semaphore) {
this.semaphore = semaphore;
}
@Override
public void run() {
try {
semaphore.acquire();
logger.info(Thread.currentThread().getId() + " acquire");
Thread.sleep(1000);
semaphore.release();
logger.info(Thread.currentThread().getId() + " release ");
} catch (Exception e) {
logger.error("Error Occur:{}", e.getMessage(), e);
}
}
}
Condition接口及其实现原理
Condition
接口提供了与Object
阻塞(wait()
)与唤醒(notify()
或notifyAll()
)相似的功能,只不过Condition
接口提供了更为丰富的功能,如:限定等待时长等。Condition
需要与Lock
结合使用,需要通过锁对象获取Condition
。
为什么要用线程池?
- 可以重用线程,减少创建和销毁线程带来的消耗。
- 管理手动创建的线程,减少开销。
- 提高响应速度,对线程进行统一的分配和监控。
Java的线程池内部机制,参数作用
@Test
public void test1() {
// Cpu 核数
int cpuNum = Runtime.getRuntime().availableProcessors();
// 核心线程数:线程池中默认可用的线程数
int coreSize = cpuNum;
// 最大线程数:当阻塞队列中放满之后,使用最大线程数中的线程
int maxSize = 2 * coreSize + 1;
// 有界的阻塞队列
ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue(20);
ThreadFactory threadFactory = Executors.defaultThreadFactory();
// 当没有可用的线程的时候,在来任务则执行策略
ThreadPoolExecutor.AbortPolicy abortPolicy = new ThreadPoolExecutor.AbortPolicy();
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(coreSize,
maxSize,
THREAD_TIME_OUT,
TimeUnit.SECONDS,
arrayBlockingQueue,
threadFactory,
abortPolicy
);
}
线程池都有哪几种工作队列?
ArrayBlockingQueue
:是一个基于数组结构的有界阻塞队列,此队列按FIFO
(先进先出)原则对元素进行排序LinkedBlockingQueue
一个基于链表结构的阻塞队列,此队列按FIFO
(先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue
。静态工厂方法Executors.newFixedThreadPool()
使用了这个队列SynchronousQueue
:不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool
使用了这个队列。PriorityBlockingQueue
:具有优先级的无限阻塞队列。
线程池的拒绝策略
当提交的任务数大于workQueue.size() + maximumPoolSize
就会触发线程池的拒绝策略。
AbortPolicy
:ThreadPoolExecutor
中默认的拒绝策略就是AbortPolicy
,直接抛出异常。CallerRunsPolicy
:使用该策略时线程池饱和后将由调用线程池的主线程自己来执行任务,因此在执行任务的这段时间里主线程无法再提交新任务,从而使线程池中工作线程有时间将正在处理的任务处理完成。DiscardPolicy
:不做任何处理直接抛弃任务。DiscardOldestPolicy
:先将阻塞队列中进入最早的任务丢弃,再尝试提交任务。
线程池如何调优,最大数目如何确认?
N
为CPU
的个数。- 如果是
CPU
密集型应用,则线程池大小设置为N+1
。 - 如果是
IO
密集型应用,则线程池大小设置为2N+1
。
java并发包concurrent及常用的类
线程池中 submit()和 execute()方法有什么区别?
-
execute()
方法只能接收实现Runnable
接口类型的任务。 -
submit()
方法则既可以接收Runnable
类型的任务,也可以接收Callable
类型的任务。 -
execute()
的返回值是void
,线程提交后不能得到线程的返回值。 -
submit()
的返回值是Future
。
说说几种常见的线程池及使用场景?
newFixedThreadPool
(固定数目线程的线程池)newCachedThreadPool
(可缓存线程的线程池)newSingleThreadExecutor
(单线程的线程池)newScheduledThreadPool
(定时及周期执行的线程池)- 详见博客:https://blog.csdn.net/qq_37248504/article/details/107850480
使用无界队列的线程池会导致内存飙升吗?
- 使用无界队列的线程池会导致内存飙升 ,
newFixedThreadPool
使用了无界的阻塞队列LinkedBlockingQueue
,如果线程获取一个任务后,任务的执行时间比较长,会导致队列的任务越积越多,导致机器内存使用不停飙升, 最终导致OOM
。
为什么阿里发布的 Java开发手册中强制线程池不允许使用 Executors 去创建?
-
FixedThreadPool
SingleThreadPool
允许请求队列长度为Integer.MAX_VALUE
可能会堆积大量请求从而导致OOM
-
CachedThreadPool
ScheduledThreadPool
允许创建线程数量为Integer.MAX_VALUE
可能会创建大量线程从而导致OOM
如何保证多线程下 i++ 结果正确?
- 使用
Lock
- 使用
Synchronized
Thread.sleep(1000)
Thread.sleep()
是Thread
类的一个静态方法,使当前线程休眠,进入阻塞状态(暂停执行),如果线程在睡眠状态被中断,将会抛出IterruptedException
中断异常。
说说线程的生命周期和状态?
- 图片来自:https://blog.csdn.net/houbin0912/article/details/77969563
- 新建状态
- 就绪状态
- 阻塞状态
- 运行状态
- 死亡状态
Java 内存模型
- 每个线程都有自己的工作内存,线程都有自己的共享变量副本。
- 在主内存中有共享变量。
怎么实现所有线程在等待某个事件的发生才会去执行?
- 使用
CountDownLatch
计数器机制。 - 使用
Semaphore
信号量机制。
wait()、notify()、suspend()、resume()
wait()
使当前线程阻塞,前提是 必须先获得锁,一般配合synchronized
关键字使用,即,一般在synchronized
同步代码块里使用wait()、notify、notifyAll()
方法。notify
方法只唤醒一个等待(对象的)线程并使该线程开始执行。notifyAll
会唤醒所有等待(对象的)线程。suspend
方法使得线程进入阻塞状态,并且不会自动恢复,必须其对应的resume
方法被调用,才能使得线程重新进入可执行状态。
一个线程如果出现了运行时异常会怎么样
- 如果该异常被捕获或抛出,则程序继续运行。
- 如果异常没有被捕获该线程将会停止执行。
生产者消费者模型的作用是什么
- 为了达到生产者和消费者生产数据和消费数据之间的平衡,需要一个缓冲区用来存储生产者生产的数据,所以就引入了生产者-消费者模式。
用Java写代码来解决生产者——消费者
- 使用
wait()、notify()
,详细代码见:https://gitee.com/Marlon_Brando/JavaTest/blob/master/src/main/java/designpatterns/produceconsume/WaitNotiFyTest.java
Java中用到的线程调度算法是什么?
- 分时调度模型是指让所有的线程轮流获得
cpu
的使用权,并且平均分配每个线程占用的CPU
的时间片这个也比较好理解。 Java
虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU
,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU
。处于运行状态的线程会一直运行,直至它不得不放弃CPU
。
Java中你怎样唤醒一个阻塞的线程?
suspend
与resume
wait
与notify
await
与singal
park
与unpark
什么是不可变对象,它对写并发应用有什么帮助?
-
对象创建之后其状态就不可改变。对象所有域都是final类型。
-
java
中可以使用Collections
工具类中的方法得到不可变对象。
@Test
public void testOne() {
Map<String, String> map = new HashMap<>();
map.put("one", "one");
map.put("two", "one");
map.put("three", "one");
Map<String, String> mp = Collections.unmodifiableMap(map);
// 会报错
mp.put("one", "two");
}
你是如何调用 wait()方法的?使用 if 块还是循环?
wait()
方法应该在循环调用,因为当线程获取到CPU
开始执行的时候,其他条件可能还没有满足,所以在处理前,循环检测条件是否满足会更好。
class ProducerRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (LOCK) {
while (count == FULL) {
try {
LOCK.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
count++;
System.out.println(Thread.currentThread().getName() + "生产者生产,目前总共有" + count);
LOCK.notifyAll();
}
}
}
}
在多线程环境下,SimpleDateFormat是线程安全的吗
SimpleDateFormat
类并不是线程安全的,但在单线程环境下是没有问题的。- 下面代码有问题:
public class ThreadSafetyTest {
private static final Logger logger = LoggerFactory.getLogger(ThreadSafetyTest.class);
private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Test
public void test() {
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
try {
String dateString = simpleDateFormat.format(new Date());
Date parse = simpleDateFormat.parse(dateString);
logger.info(simpleDateFormat.format(parse));
} catch (Exception e) {
logger.error("Error Occur:{}", e.getMessage(), e);
}
}).start();
}
}
}
- 使用
java8
中的Api
代替。 - 使用
ThreadLocal
- 测试代码见:https://gitee.com/Marlon_Brando/onlineshop_back/commit/ee8b9a0d0f6b261201334a1a19ece356c68cb171
为什么Java中 wait 方法需要在 synchronized 的方法中调用?
Java
中的synchronized
方法或synchronized
块调用Java
中的wait(),notify()
或notifyAll()
方法来避免:Java
会抛出IllegalMonitorStateException
。
怎么检测一个线程是否持有对象监视器
Thread
类提供了一个holdsLock(Object obj)
方法,当且仅当对象obj
的监视器被某条线程持有的时候才会返回true
。
什么情况会导致线程阻塞
- 在某一时刻某一个线程在运行一段代码的时候,这时候另一个线程也需要运行,但是在运行过程中的那个线程执行完成之前,另一个线程是无法获取到CPU执行权的(调用
sleep
方法是进入到睡眠暂停状态,但是CPU
执行权并没有交出去,而调用wait
方法则是将CPU
执行权交给另一个线程),这个时候就会造成线程阻塞。
如何在两个线程间共享数据
-
如果每个线程执行的代码相同,可以使用同一个
Runnable
对以上是关于# 技术栈知识点巩固——Java并发多线程的主要内容,如果未能解决你的问题,请参考以下文章