线程模型
JVM线程对不同操作系统上的原生线程进行了高级抽象。
操作系统内核线程:例如Linux线程又被称为轻量级进程。(因为Linux没有为线程定义数据结构和算法,本质还是用进程实现,区别只是在是否有自己独立的地址空间(轻量级进程组下地址空间共享),都是使用clone系统调用,只是参数不同)
1 v 1:即用户线程和内核线程建立一对一关系,缺点1.是用户线程阻塞和唤醒会直接映射到内核线程容易频繁引起用户态到内核态的切换降低性能,一些语言引入CAS来避免一部分状态切换,Java通过引用AQS这种函数级别的锁减少使用内核级别的锁提升性能。2.Linux能创建线程数量有限,在一定程度上会限制并发量。
N v 1:即多个用户线程映射到一个内核线程。用户线程调度由用户空间完成,能有效提升并发上限,大部分调度和同步操作在用户空间完成能有效提升性能。比较致命缺点是如果有一个线程进行内核调用并且阻塞,其他线程在这段时间内都无法进行内核调用。(JVM早期使用此模型,后被放弃)
M v N:即多个用户线程映射到多个内核线程,可解决以上两种模型的缺点,但实现难度较高。Go语言GMP采用此模型实现,Java Loom也在进行这方面探索。
主流JVM都是采用1v1模型
线程使用
守护线程:也称为后台线程,jvm不必等待守护线程完成即可退出,例如垃圾回收线程。
非守护线程:即用户线程,jvm会等待所有用户线程运行完成才退出。
# 设置一个线程是否守护线程,默认为false
Thread.setDaemon(ture|false)
# 查询线程是否为守护线程
Thread.isDaemon()
可通过 Thread类、Runnable、Callable接口 创建。
1.继承Thread类
public class TestThread extends Thread {
private int i;
public TestThread(int i){
this.i=i;
}
@override
public void run() {
System.out.println(i);
}
public static void main(String[] args) {
new TestThread(1).start();
new TestThread(2).start();
}
}
2.实现Runable接口
public class RunnableThread implements Runnable {
private int i;
public TestThread(int i){
this.i=i;
}
@override
public void run() {
System.out.println(i);
}
public static void main(String[] args) {
rtt = new RunnableThreadTest();
new Thread(new RunnableThread(1), "线程1").start();
new Thread(new RunnableThread(2), "线程2").start();
}
}
3.实现Callable接口
public class CreateThreadTest {
public static void main(String[] args) {
FutureTask<Integer> futureTask = new FutureTask<>(new CallableTest(5));
new Thread(futureTask).start();
try {
System.out.println("返回值: " + futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class CallableTest implements Callable{
private int i;
public CallableTest(int i){
this.i=i;
}
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() + " is running: " + i);
return i;
}
}
线程状态
NEW 尚未启动的线程状态,即线程创建,还未调用start方法
RUNNABLE 就绪状态(调用start,等待调度)+正在运行
BLOCKED 等待监视器锁时,陷入阻塞状态
WAITING 等待状态的线程正在等待另一线程执行特定的操作(如notify)
TIMED_WAITING 具有指定等待时间的等待状态
TERMINATED 线程完成执行,终止状态
状态变迁:
sleep( ) 和 wait( ) 区别
sleep 可以在任何地方使用;wait 方法则只能在同步方法或同步块中使用。
sleep 只让出了CPU,没有释放同步资源锁,等待时间到,等待状态转化为就绪状态继续运行。wait 让出CPU同时还会退出同步资源锁,进入等待队列,以便其他正在等待该资源的线程得到该资源进而运行。待调用notify()/notifyAll() 唤醒指定线程或所有线程,才会进入锁池,再次获得对象锁后才会进入运行状态,在没有获取对象锁之前不会继续执行。
# sleep()
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
# wait()
Object lock = new Object();
synchronized (lock) {
try {
lock.wait(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
# notify/notifyAll
synchronized (lock) {
// 随机唤醒
lock.notify();
// 唤醒全部
lock.notifyAll();
}
线程中断
Java的中断是一种协作式机制,所谓协作式,是通过一个中断标识位来实现的。其它线程如果想要中断线程A,就对线程A的中断标识位做一个标记,代表着是否有中断请求(请求可以来自任何线程,包括被中断的线程本身),线程A自己通过轮询去检查标识位,然后自己做处理。例如,当线程t1想中断线程t2,只需要在线程t1中将线程t2对象的中断标识置为true,然后线程2可以选择在合适的时候处理该中断请求,甚至可以不理会该请求,就像这个线程没有被中断一样。
Java中线程中断的标识位是由本地方法维护的,在Java层面仅留了几个API给用户获取和操作。
轮询是如何实现的?不同的场景实现方式不同。线程睡眠、等待等场景,是通过JVM自己的轮询实现的。而在一些Java并发工具里面,轮询是在Java代码层面去实现的。
线程不同场景中断区别
-
针对线程处于由 sleep, wait, join 方法调用产生的阻塞状态时,调用interrupt方法,会抛出异常InterruptedException,同时会清除中断标记位,自动改为false。
-
针对线程处于由 LockSupport.park 方法调用产生的阻塞状态时,它在遇到线程中断标识被设置为true后,会立即返回不再阻塞,也不会抛出异常和清除中断标记位,仍为true,交给用户自己去处理中断。
- nio能够响应中断。
中断方法
void interrupt() 中断线程,设置线程的中断位true。
isInterrupted() 检查线程的中断标记位,true-中断状态, false-非中断状态
interrupted() 静态方法,返回当前线程的中断标记位,同时清除中断标记,改为false。比如当前线程已中断,调用interrupted(),返回true, 同时将当前线程的中断标记位改为false, 再次调用interrupted(),会发现返回false。
线程池
类图
ThreadPoolExecutor
/**
* Creates a new {@code ThreadPoolExecutor} with the given initial
* parameters.
*
* @param corePoolSize the number of threads to keep in the pool, even
* if they are idle, unless {@code allowCoreThreadTimeOut} is set
* @param maximumPoolSize the maximum number of threads to allow in the
* pool
* @param keepAliveTime when the number of threads is greater than
* the core, this is the maximum time that excess idle threads
* will wait for new tasks before terminating.
* @param unit the time unit for the {@code keepAliveTime} argument
* @param workQueue the queue to use for holding tasks before they are
* executed. This queue will hold only the {@code Runnable}
* tasks submitted by the {@code execute} method.
* @param threadFactory the factory to use when the executor
* creates a new thread
* @param handler the handler to use when execution is blocked
* because the thread bounds and queue capacities are reached
* @throws IllegalArgumentException if one of the following holds:<br>
* {@code corePoolSize < 0}<br>
* {@code keepAliveTime < 0}<br>
* {@code maximumPoolSize <= 0}<br>
* {@code maximumPoolSize < corePoolSize}
* @throws NullPointerException if {@code workQueue}
* or {@code threadFactory} or {@code handler} is null
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
拒绝策略:
AbortPolicy:不执行新任务,直接抛出异常,提示线程池已满,线程池默认策略
DiscardPolicy:不执行新任务,也不抛出异常,基本上为静默模式
DisCardOldSetPolicy:将消息队列中的第一个任务替换为当前新进来的任务执行
CallerRunPolicy:用于被拒绝任务的处理程序,它直接在 execute 方法的调用线程中运行被拒绝的任务;如果执行程序已关闭,则会丢弃该任务。
线程创建策略
线程池回收策略
当线程池中的线程数超过核心线程数时,且有线程空闲时间超过keepAliveTime
则会被回收。
Executors 提供几种常用线程池:
线程池 配合 CompletableFuture 应用
ExecutorService executor = Executors.newFixedThreadPool(5);
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
System.out.println("执行step 1");
return "step1 result";
}, executor);
CompletableFuture<String> cf2 = CompletableFuture.supplyAsync(() -> {
System.out.println("执行step 2");
return "step2 result";
});
cf1.thenCombine(cf2, (result1, result2) -> {
System.out.println(result1 + " , " + result2);
System.out.println("执行step 3");
return "step3 result";
}).thenAccept(result3 -> System.out.println(result3));
关于 CompletableFuture 可参考美团文章