多线程
什么是上下文切换?
当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
如何避免线程死锁
我们只要破坏产生死锁的四个条件中的其中一个就可以了。
破坏互斥条件
这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
破坏请求与保持条件
一次性申请所有的资源。
破坏不剥夺条件
占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
破坏循环等待条件
靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
我们对线程 2 的代码修改成下面这样就不会产生死锁了。
创建线程有哪几种方式?
创建线程有四种方式:
1、继承 Thread 类;
2、实现 Runnable 接口;
3、实现 Callable 接口,创建 FutureTask 对象(FutureTask 也是 Runnable 接口的实现类),与 Runnable 的区别是有返回值;
4、使用创建线程池
说一下 runnable 和 callable 有什么区别?
相同点:
1、都是接口
2、都可以编写多线程程序
3、都采用 Thread.start()启动线程
主要区别:
1、Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,是个 mk ign njoim,kuuif 泛型,和 Future、FutureTask 配合可以用来获取异步执行的结果。
2、Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息。
注:Callalbe 接口支持返回执行结果,需要调用 FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
线程的 run()和 start()有什么区别?
每个线程都是通过某个特定 Thread 对象所对应的方法 run()来完成其操作的,run()方法称为线程体。通过调用 Thread 类的 start()方法来启动一个线程。
start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。
start()方法来启动一个线程,真正实现了多线程运行。调用 start()方法无需等待 run 方法体代码执行完毕,可以直接继续执行其他的代码; 此时线程是处于就绪状态,并没有运行。 然后通过此 Thread 类调用方法 run()来完成其运行状态, run()方法运行结束, 此线程终止。然后 CPU 再调度其它线程。
run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用 run(),其实就相当于是调用了一个普通函数而已,直接调用 run()方法必须等待 run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用 start()方法而不是 run()方法。
为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
这是另一个非常经典的 java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!
new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。
而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。
start()方法为什么能开启多线程?
真正实现开启多线程的是 start() 方法中的 start0() 方法。
调用 start0()方法后,该线程并不一定会立马执行,只是将线程变成了可运行状态(NEW ---> RUNNABLE);具体什么时候执行,取决于 CPU ,由 CPU 统一调度;我们又知道 Java 是跨平台的,可以在不同系统上运行,每个系统的 CPU 调度算法不一样,所以就需要做不同的处理,这件事情就只能交给 JVM 来实现了,start0() 方法自然就表标记成了 native。
线程的 6 种状态是什么?
1、新建状态(new):创建线程对象。
2、就绪状态(runnable):start 方法。
3、阻塞状态(blocked):无法获得锁对象(线程没抢到)。
4、等待状态(waiting):wait 方法。
5、计时状态(timed_waiting):sleep 方法。
6、死亡状态(terminated):全部代码运行完毕。
线程的调度模式是什么?
两分时调度和抢占式式调度。
分时调度:轮流获取 CPU 使用权。
抢占式调度:优先级高的线程占用 CPU。
请说出与线程同步以及线程调度相关的方法。
(1)wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
(2)sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 InterruptedException 异常;
(3)notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关;
(4)notifyAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
sleep() 和 wait() 有什么区别?
相同点:两者都可以暂停线程的执行。不同点:
sleep 方法,不会释放资源(本质是占用线程),如果占具锁资源,则其他线程不可进;wait 方法会释放锁资源,即其他线程可进来。
wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒。
Java 中你怎样唤醒一个阻塞的线程?
首先 ,wait()、notify() 方法是针对对象的,调用任意对象的 wait()方法都将导致线程阻塞,阻塞的同时也将释放该对象的锁,相应地,调用任意对象的 notify()方法则将随机解除该对象阻塞的线程,但它需要重新获取该对象的锁,直到获取成功才能往下执行;
其次,wait、notify 方法必须在 synchronized 块或方法中被调用,并且要保证同步块或方法的锁对象与调用 wait、notify 方法的对象是同一个,如此一来在调用 wait 之前当前线程就已经成功获取某对象的锁,执行 wait 阻塞后当前线程就将之前获取的对象锁释放。
notify() 和 notifyAll() 有什么区别?
如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
notifyAll() 会唤醒所有的线程,notify() 只会唤醒一个线程。
notifyAll() 调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而 notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。
为什么 wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调用?
这是 JDK 强制的,wait()方法和 notify()/notifyAll()方法在调用前都必须先获得对象的锁,也就是 synchronized 对象锁。
Java 线程数过多会造成什么异常?
1、线程的生命周期开销非常高
2、消耗过多的 CPU
资源如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争 CPU 资源时还将产生其他性能的开销。
3、降低稳定性 JVM
在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约,包括 JVM 的启动参数、Thread 构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出 OutOfMemoryError 异常。
你了解 ThreadLocal 的原理吗?
threadlocal 是一个线程内部的存储类,提供了线程内存储变量的能力,可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据。这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。
其内部维护了一个 ThreadLocalMap,该 Map 用于存储每一个线程的变量副本。并且 key 为线程对象,value 为对应线程的变量副本。
线程池
Executors 类有哪几种常见的线程池?
4 种:单例线程池、固定大小线程池、可缓存线程池、大小无限线程池。
(1)newSingleThreadExecutor:创建一个单例线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
(2)newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。如果希望在服务器上使用线程池,建议使用 newFixedThreadPool 方法来创建线程池,这样能获得更好的性能。
(3) newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。
(4)newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
在 Java 中 Executor 和 Executors 的区别?
Executors 工具类的可以直接创建不同的线程池。
Executor 是个接口。
ExecutorService 接口继承了 Executor 接口并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。
ThreadPoolExecutor 是 Executor 接口的实现类,可以创建自定义线程池。
线程池中 submit() 和 execute() 方法有什么区别?
接收参数:execute()只能执行 Runnable 类型的任务。submit()可以执行 Runnable 和 Callable 类型的任务。
返回值:submit()方法可以返回持有计算结果的 Future 对象,而 execute()没有。
异常处理:submit()方便 Exception 处理。
Executors 的弊端是什么?
newFixedThreadPool 和 newSingleThreadExecutor:
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM(内存溢出)。
newCachedThreadPool 和 newScheduledThreadPool:
主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。
线程池之 ThreadPoolExecutor
你对 ThreadPoolExecutor 熟悉吗?
ThreaPoolExecutor 可以自定义创建线程池,具体参数可以走它的构造函数。
ThreadPoolExecutor 的核心参数有哪些?
七个核心参数:
参数一:核心线程数(不能小于 0)
参数二:最大线程数(>=核心线程数)
参数三:临时线程最大存活时间(不能小于 0)
参数四:时间单位(参数三的单位)
参数五:等待列队(不能为 null)
参数六:创建线程工厂(不能为 null,一般用默认线程工厂)
参数七:任务的拒绝策略(不能为 null)
ThreadPoolExecutor 的拒绝策略有哪些?
4 种:
1、ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出异常(默认);
2、ThreadPoolExecutor.DiscardPolicy:丢弃任务,不抛异常(不推荐);
3、ThreadPoolExecutor.DiscardOldestPolicy:丢弃等待最久的线程;
4、ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程(main)运行 run 方法。
当任务过多时,ThreadPoolExeccutor 的执行顺序是怎么样的?
1、核心线程满后;
2、阻塞队列满后;
3、临时线程满后(最大线程数 - 核心线程数 = 临时线程数);
4、拒绝策略。
并发工具类
什么是原子操作?在 Java Concurrency API 中有哪些原子类(atomic classes)?
原子操作(atomic operation)意为”不可被中断的一个或一系列操作” 。
处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。在 Java 中可以通过锁和循环 CAS 的方式来实现原子操作。 CAS 操作——Compare & Set,或是 Compare & Swap,现在几乎所有的 CPU 指令都支持 CAS 的原子操作。
java.util.concurrent.atomic(JUC 包下) 包提供了 int 和 long 类型的原子包装类,它们可以自动的保证对于他们的操作是原子的并且不需要使用同步。
原子类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
原子数组:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
说一下 atomic 的原理?
Atomic 包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。
有使用过什么并发工具类吗?
CountdownLatch 和 Semaphore。
CountdownLatch 有什么作用?
CountDownLatch(倒计时器)是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。
Semaphore 有什么作用?
Semaphore(信号量/通行令牌)-允许多个线程同时访问: synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。
synchronized 关键字与 ReentrantLock 的区别
共同点
- 都是可重入锁:自己可以再次获取自己的内部锁【避免一个线程获取锁之后,再次尝试获取锁时造成的死锁】。同一线程每次获取锁,计数器加一,释放锁,计数器减一,计数为 0,代表完全释放该锁。
不同点
-
synchronized 依赖于 JVM 实现,ReentrantLock 依赖于 API。
-
相比 synchronized,ReentrantLock 增加了一些高级功能。主要来说主要有三点:
- 等待可中断 :
ReentrantLock
提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()
来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 - 可实现公平锁 :
ReentrantLock
可以指定是公平锁还是非公平 - 锁。而
synchronized
只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock
默认情况是非公平的,可以通过ReentrantLock
类的ReentrantLock(boolean fair)
构造方法来制定是否是公平的。 - 可实现选择性通知(锁可以绑定多个条件):
synchronized
关键字与wait()
和notify()
/notifyAll()
方法相结合可以实现等待/通知机制。ReentrantLock
类当然也可以实现,但是需要借助于Condition
接口与newCondition()
方法
- 等待可中断 :
总结
-
JVM 在 JDK 1.6 中引入了分级锁机制来优化 synchronized
-
当一个线程获取锁时,首先对象锁成为一个偏向锁
- 这是为了避免在同一线程重复获取同一把锁时,用户态和内核态频繁切换
-
如果有多个线程竞争锁资源,锁将会升级为轻量级锁
- 这适用于在短时间内持有锁,且分锁交替切换的场景
- 轻量级锁还结合了自旋锁来避免线程用户态与内核态的频繁切换
-
如果锁竞争太激烈(自旋锁失败),同步锁会升级为重量级锁
-
优化 synchronized 同步锁的关键:减少锁竞争
-
应该尽量使 synchronized 同步锁处于轻量级锁或偏向锁,这样才能提高 synchronized 同步锁的性能
-
常用手段
- 减少锁粒度:降低锁竞争
- 减少锁的持有时间,提高 synchronized 同步锁在自旋时获取锁资源的成功率,避免升级为重量级锁
-
-
在锁竞争激烈时,可以考虑禁用偏向锁和禁用自旋锁
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于