线程和进程
概念
区别
java 在设计之初就支持多线程,而且可以一对一映射到操作系统的内核线程。
多线程
原因:单线程效率能够满足自然不用考虑多线程。多线程主要为了提高CPU利用率(发挥多核作用、避免无效等待)、便于编程建模(任务拆分)、性能定律(阿姆达尔定律:处理器多,其对程序效率的提高取决于程序的并行任务比例)。
场景:同时做多件事、提高效率、大并发量
局限:上下文切换消耗、异构化任务(任务都不一样)难以并行、线程安全问题(数据准确、活跃性问题)。
并发、高并发
并发性:与串行性相对应,不同的部分可以无序或同时执行而不影响结果。
高并发是一种状态,多线程是解决的方法之一。
QPS、带宽、PV、UV、并发连接数(大于同时在线的用户数)、服务器平均请求等待时间
同步异步、阻塞非阻塞
同步与异步:被调用方是否提醒,有提醒为异步
阻塞和非阻塞:调用方是否可以在结果出来前做别的事
本质构造 Thread 一种
一般不用 Thread:1)从解耦角度,run应该和类本身的创建分开,Thread是将run()进行重写,Runnable则调用传入对象的方法;2)它用的是继承而非实现;3)不能借助线程池
Oracle说有两种,但从本质上来说只有一种方法创建线程,就是构造Thread类,而执行线程有两种方法,分别是Runnable,将Runnable对象传递给Thread类的run方法调用Runnable的run方法,Thread重写run方法并执行。如果从代码表面实现来看,则有很多种,比如线程池、匿名内部类、lambda、计时器等。
线程再多也就百级别,因为线程本身就消耗资源,再提高应该考虑异步。
对于IO密集型操作,多线程提升效果不大,重点是提高并发度(异步)
使用start
start涉及两个线程:主线程和子线程。主线程执行start实际上是告诉jvm有空时创建子线程去执行。当上下文、栈、pc等资源准备后(就绪状态),等待cpu资源,之后才执行。
start不能重复调用,比如当第一个start执行后,线程进入end,此时就不能调用start重新启动了。源码来看,start一开始会判断线程状态是否为0,即未启动状态,然后把线程加入线程组,调用start0方法,并修改started状态。
start是让JVM创建线程去执行run,而直接调用run,则是由main来执行run
使用interrupt通知而非强制。因为执行的线程本省更清楚如何来停止。
线程停止情况:正常执行完或者出现异常停止,其占据的资源会被JVM回收。
当线程在 sleep 等可响应中断的方法中被 interrupt,会抛异常。如果能保证 sleep 时收到 interrupt,就可以不使用 isInterrupted 判断。例如迭代中有 sleep,则不需要在迭代的条件中添加 isInterrupted。线程一旦响应中断就会把 isInterrupted 标记清除,所以如果在 while 里 try-catch sleep,是无法停止线程的。
响应中断的方法:
wait()
sleep()
join()
BlockingQueue.take()/put()
Lock.lockInterruptibly()
CountDownLatch.await()
CyclicBarrier.await()
Exchanger.exchange()
InterruptibleChannel相关方法
Selector相关方法
interrupt 之所以能够停止阻塞,是因为底层调用的是 cpp 代码,那涉及到操作系统层面的操作。
在开发中,如果 run 函数中调用一个方法,这个方法的异常不应该在这个方法内 catch,而是应该抛出,让调用方来觉得怎么处理。如果方法的编写者自己想做些处理,可以 catch,但在处理完后要 interrupt,这样调用方才能检查到 interrupted。在 run 方法中已经不能往上抛异常了。
直接调用stop方法,解锁已锁定的所有监视器,强制终止线程而非完成再终止。
suspend和resume已经被弃用,因为挂起线程时带上锁,容易出现死锁。
使用 volatile 加 标记变量 并不能很好的停止线程,因为在线程阻塞时不能抛异常,进而判断终止。
static boolean interrupted() 判断后会清除 interrupted 标记。判断的是当前主线程而不是调用者
boolean isInterrupted() 不会清除
面试回答:正确停止线程的方法是 interrupt请求,这样能够保证任务正常中断。但这个方法需要请求方(发送interrupt)、被请求方(适当时候检查isInterrupted或处理InterruptedException)、子方法被调用方(抛出异常或者自己处理后再恢复interrupted)互相配合。对于interrupt无法中断的阻塞,那就只能根据不同的类调用不同的方法来中断,比如 ReentrantLock.lockInterruptibly()
java runnable 对应系统的 ready 和 running。右边三种都是阻塞状态,它跟长时间运行的方法不同在于返回时间不可控。实际上,waiting 可以到 blocked(刚被唤醒),waiting 和 timed_waiting 可以到 terminated(出现异常)
Block:线程请求锁失败时进入阻塞队列,该状态线程会不断请求,成功后返回runnable
Waiting:调用特定方法主动进入等待队列,释放cpu和已获得的锁,等待其他线程唤醒
Timed_waiting:timeout 时自动进入阻塞队列。
sleep仅放弃cpu而不释放锁
wait和notify必须配套使用,即必须使用同一把锁调用。调用wait和notify的对象必须是他们所处同步块的锁对象。
wait 让当前线程(不管是谁调用)进入阻塞并释放 monitor,只有调用该对象的 notify 且刚好是本对象本唤醒、notifyAll、interrupt、wait的timeout、interrupt,才会被唤醒。如果遇到中断,则会释放掉 monitor。wait、notify、notifyAll只能在 synchronized 代码块或方法中使用,这是为了防止准备执行wait时被切换到notify,结果切换回来执行wait后就没有人notify了。而 sleep 只针对自己,并不需要配合,所以不需要在 synchronized 中。另外,代码处于 synchronized,意味着即便已经调用 notify/notifyAll,monitor仍要等到执行完 synchronized 部分才会被释放。而此时被唤醒的线程就会进入 Blocked 状态。
wait、notify、notifyAll的调用都必须先有 monitor,即进入 synchronized,这是一个锁级别的操作,粒度更细,所以定义在 Object 下(对象头就有锁状态)。功能实际上比较底层,使用 condition 更方便。持有多个锁的时候注意防止死锁。
不要用 Thread 的 wait,因为 Thread 在退出时会自动调用 notify,这样会打乱自己原来的设计。
Sleep 方法可以让线程进入 Waiting 状态,不占用 cpu 资源,但不会释放锁(synchronized 和 lock),知道规定时间后再执行,如果休眠被中断,会抛出异常,清除中断状态。
wait/notify 和 sleep 的比较:相同在于阻塞、响应中断,不同在于同步方法、释放锁、指定时间、所属类。
synchronized (lock) {
lock.notify(); // 如果 直接 notify,会出现 IllegalMonitorStateException
lock.wait();
}
public synchronized void method(){
wait(); // 实际上是 this.wait();
notify();
}
public Class Test{
public static synchronized void method(){
Test.class.notify();
}
}
join:新的线程加入我们,所以我们要等待它执行完再出发。主线程等待子线程。主线程执行子线程.join时,catch 中加上子线程.interrupt。主线程执行join时处于 wait 状态。join的源码中是借助 wait 实现的,而 notify 是依赖 jvm 实现,即上面提到的每个 Thread 执行完都会调用 notifyAll 方法。
thread.join();
// 上下效果一样
synchronized (thread) {
thread.wait();
}
yield 释放 cpu 时间片,但不会释放锁,仍是 running,随时可能被再次执行。但 jvm 并不保证遵循这个原则,所以自己开发中一般不用 yield,但并发包里面用得不少。
编号:系统用、自增(从1开始,因为是 ++num)。除了 main 线程外,jvm还自动创建其他线程,所以马上建立的main外线程不是2
Thread Group(main, ...), Finalizer(执行对象finalize方法), Reference Handler(GC、引用相关线程), Signal Dispatcher(把操作系统信号发给程序)
名称:可以随时修改
是否为守护线程:为用户线程提供服务,如果只剩下守护线程,jvm就会停止。线程默认继承父线程(用户或守护),可以把用户线程改为守护线程,但没必要。通常守护线程都是由jvm启动的,jvm启动时会先启动 main。
对比守护和普通:整体无区别,唯一区别在于是否影响jvm退出
优先级:10个级别,默认5。子线程会继承。程序设计不应该依赖优先级,不同的操作系统对优先级的定义是不一样的。
操作系统优先级的级别和java的不一致,而且系统有可能有越过优先级的分配资源的功能,这样优先级就无效了。低优先级甚至有可能饿死。
子线程出现的异常,主线程难以感知(子线程抛异常,主线程照样执行),其异常在主线程无法用传统方法捕获。
处理方法:UncaughtExceptionHandler 来实现全局的子线程异常捕获
Thread.setDefaultUncaughtExceptionHandler(handler);
ThreadGroup本身实现了UncaughtExceptionHandler,子线程继承自它,所以子线程会调用它实现的uncaughtException,该方法不停地调用父类的uncaughtException,直到父类为null,然后看是否设置了uncaughtExceptionHandler,有的话调用,没有就输出栈信息。
run方法本身不能抛异常。
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。
本质是资源竞争,一些场景有
a++:两个线程各自给同一个变量做加法,结果会比预定的要小。因为某个线程a取数加1,但还没写回内存,其他线程就进行读取,获得还没加1的结果,而在此基础上加1就会漏掉线程a的。
活跃性问题:死锁、饥饿
对象发布和初始化
对象能够被发布的情况:public、方法返回、方法参数传入。
逸出:方法返回一个private对象;还没完成初始化。
解决方法
总结性提醒:
性能问题:
JVM优化包括指令重排、消除不必要的锁
jvm内存结构:和jvm的运行时区域有关
java内存模型:和java的并发编程有关
java对象模型:和java对象在jvm中的表现形式有关
详细看《New Job Interview Part 1》和《开发拾遗》
C语言不存在内存模型,依赖处理器,而处理器不同硬件有不同实现,结果相同代码有不同的运行效果。JMM就是为了解决这种不一致而产生的统一的一组规范,各JVM开发者都需要遵循。volatile、synchronized、Lock等原理都是JMM。有了JMM,java开发者就可以通过同步工具和关键字开发并发程序。
重排序
出现情况:jit翻译、jvm解释、cpu重排
对于小概率事件,可以通过死循环,直到出现小概率事件才退出,进而证明小概率的存在。
可见性
各线程的本地缓存是相互不可见的,只能通过主内存来共享信息,所以可见性的根本原因是多级缓存的存在。volatile 能够保证线程读取前,在其他线程本地缓存中修改的值flush到主内存。
JMM对多级缓存和内存进行了抽象,出现本地内存(working memory)、buffer、主内存。
Happens-before:前一个操作的结果能够被后一个操作看到,那就符合 happens-before。符合这个原则的情况有:
例题
# thread1
x=1
y_read=y
# thread2
y=1
x_read=x
# 有可能 t1 和 t2 得到 y_read=0、x_read=0,由于乱序和可见性。
通过 object == null 来判断是否加锁时,要用 volatile object,否则 object 在锁代码块中进行实例化,分为分配内存、初始化变量、变量覆盖到内存,这几步有可能让锁外面的线程获得没有构建好的object
数据库中有些有检测并放弃事务来解决死锁的功能,但JVM中并没有自动处理的功能,压力测试也不能找出所有潜在的死锁。
产生原因:竞争资源/调度顺序不当
找死锁的方法:jstack pid、MXBean
条件与解决:
对于线上死锁,防范于未然是必须的,之后一旦发现,先保存案发现场,重启服务,然后根据案发现场的信息查找原因,修改代码,重新发布。
开发中注意:
典型算法:银行家算法,现用「所需资源」-「已有资源」=「差距资源」,然后通过「剩余资源」去配对每个「差距资源」,「剩余资源」大于「差距资源」的,优先分配,待分配完的进程执行完后归还资源,然后再重新分配。
活锁:线程在运行,但是一直得不到进展,消耗cpu资源。解决方法:
饥饿:线程始终得不到cpu资源。解决方法:避免持有锁而一直不释放、不要设置优先级
好处:加快响应速度(避免不停的创建和销毁);合理利用CPU和内存(循环利用);统一管理(提供一些统计信息)
场景:服务器,或者5个线程以上的情况
原理解析
线程池的7个属性:
数量设置:一般IO密集型,core5倍以上,CPU密集型,core的一到两倍。一般公式
core_num * (1+avg_waiting_time/avg_working_time)
,更具体就需要压测。其他策略:
.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
.DiscardPolicy:也是丢弃任务,但是不抛出异常。
.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
.CallerRunsPolicy:由调用线程处理该任务
execute的执行步骤:
线程池如何实现线程复用?底层会创建 worker,其实现 runnable,但组合了 Thread,所以可以调用 run 方法,实际是调用 runWorker,里面先把 worker 的 task 取出,然后实现这个 task 的 run 方法
线程池状态
内置线程池
默认线程池的弊端:newSingleThredExecutor、newFixedThreadPool中的Queue是Integer最大值,newCachedThreadPool中的maximumPoolSize是Integer最大值,都可能OOM
ScheduledThreadPool 里面使用 DelayedWorkQueue,对于一些定时任务可以考虑使用。
使用
//runnable
ExecutorService pool = Executors.newxxxThreadPool(); //Scheduled可以指定执行的延迟,速率等。
Runnable command = new MyRunnable();
pool.execute(command);//.submit配合future使用
pool.shutdown();// 进入停止状态,即不再接收任务(新提交会抛异常),里面所有线程执行完才结束线程池。
pool.isShutdown(); // 是否进入 shutdown 状态
pool.isTerminated(); // 是否都执行完了
pool.awaitTermination(); // 阻塞,等待一段时间,一旦发现已经 terminated,就返回true,超过时间就返回 false,被中断也会返回 false
shutdownNow(); // interrupt正在执行的任务,队列中的任务返回出一个List<Runnable>,需要做相应处理
// 钩子:重写 beforeExecute 和 afterExecute 来增加功能,比如日志、统计
// 生产中,线程池最好还是自己定义,这里就涉及 任务队列、线程工厂 和 拒绝策略 的实现
//callable
ExecutorService pool = Executors.newxxxThreadPool();
List<Future> futureList = new ArrayList<>();
for(int i=0; i<20; i++){
Callable<Integer> task = new MyCallable();
Future future = pool.submit(task);
futureList.add(future);
}
for(Future<Integer> f: futureLIst){
Int res = future.get();//等线程执行完毕后才有,并继续执行。
}
pool.shutdown();
//监控方法
getTaskCount, getCompletedTaskCount, getPoolSize(当前线程数), getActiveCount()
场景
使用
原理
每个 Thread 都有一个 ThreadLocalMap,里面存储多个 Entry<ThreadLocal<?>, T>
对象。当调用 ThreadLocal 实例的 get 时,会先获取执行这个方法的线程的 ThreadLocalMap,没有的话执行初始化,把 initialValue 产生的 value 组装成 entry 加入 map。ThreadLocal<?>
作为 key,通过内存地址来判断是否重复。
开发注意
最常见是 ReentrantLock,只允许一个线程访问共享资源。
synchronized的不足
使用
lock:使用 lock 时,无论怎样都要用 try-finally。lock不能被中断,一旦陷入死锁就永久等待。
tryLock:可设置超时,lockInterruptibly 相当于 timeout 设置为无限。
分类
锁的优化
优劣(相比于锁):粒度更细,但高度竞争时效率不高
大体类别:primary、array、reference、fieldupdater、adder、accumulator
reference:将引用的替换变为原子
fieldupdater:某个类的某个属性升级为原子,而且可以选择是否进行原子加减。具体例子查看learning_thread的atomic包
adder:比primary的increment效果好。本质是空间换时间,把不同线程对应到不同的cell上进行修改,降低冲突概率,使用多段锁保证安全。内部有一个base变量和Cell数组,如果竞争不激烈,直接累加到base,如果竞争激烈,各个线程分散累加到自己的Cell[x]中(hash)。最后sum显示当前结果,即base和cell的总和,但由于没有加锁,实际上的结果并非是快照。
accumulator:比adder更通用,适合并行计算场景,结合线程池。不过jdk8的并行流或许更好。
底层原理CAS
compare and set:通过比较内存的值是否与预期的值相等来判断是否对内存的这个值进行更新。这个比较过程是CPU指令级别来保证原子性的。具体来说,Java 是通过它的 Unsafe 类来实现的,这个类包含不少 native 方法来调用操作系统的 api。
场景:乐观锁、并发容器(concurrentHashMap)、原子类
历史抛弃:Vector、Hashtable、Collections.synchronizedList()
HashMap线程不安全的原因:
ConcurrentHashMap原理:
put:
上面的「锁住该槽」指锁住数组中的 Node
get:计算hash,直接取值 or 红黑找 or 遍历链表找
不能用get、++、put,要直接用 replace
CopyOnWrite适合读多写少,例如黑名单、每日更新。和读写锁的区别是读和写不互斥,但读会有延迟。CopyOnWrite 可以在遍历的时候修改集合元素,因为修改的对象和遍历的对象是不一样的。每次写都会创建出一个新的对象。
并发队列:
重要方法:
put、take:满或者空会阻塞
add、remove、element:会抛异常,element返回头元素,空抛异常
offer、poll:offer返回boolean,poll会删除,都可阻塞
peek:peek不会删除
ArrayBlockingQueue:指定容量,可以选择是否公平。
LinkedBlockQueue:take和put分为两把锁
PriorityBlockingQueue:自然顺序(非先进先出)
SynchronousQueue:容量为0
ConcurrentLinkedQueue:非阻塞队列
选择:容量不变/节省内存 ArrayBlockingQueue,容量可变 LinkedBlockQueue,是否需要排序,并发性
下面类基本淘汰了Object的wait、notify方式
CountDownLatch:经典是多等一或者一等多,但多等多也是可以的。针对事件,countdown并不会阻塞线程,不能复用,需要新建。
Semaphore:适合资源有限的限流场景,比如网关。类似轻量级的CountDownLatch。这个类更加注意公平,因为基本都是应对任务堆积的场景。
Condition:一个lock可以对应多个条件,所以更加灵活。
CyclicBarrier:针对线程的,因为调用的是await,可以复用
AbstractQueuedSynchronizer 是 jdk 中很多并发工具类的框架类,例如 ReentrantLock、Semaphore、CountDownLatch 等。这些类的内部会有一个静态内部类继承自AQS。它负责线程状态的原子性管理、线程的阻塞和重启、线程队列管理。
AQS的三大核心:
https://juejin.im/post/5c11d6376fb9a049e82b6253
https://mp.weixin.qq.com/s/sA01gxC4EbgypCsQt5pVog
runnable 的 run 方法的返回值是 void 且没有定义能够抛出异常。正常来说,实现 run 方法的人更清楚这个任务会出什么异常,所以在 run 里面就把异常处理好是更好的选择。
使用 Future,实现一个 Callable 接口
future 接口的方法
boolean cancel(boolean mayInterruptIfRunning); // 如果清楚任务能够处理好 interrupt,可以用 true
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
批量使用,定义一个 List<Callable<U>>
集合,然后交给executor,调用invokeAll,然后 block,直到所有任务完成。executor.invokeAll则只对最先完成的一个任务做处理,其他取消。下面则不需要等所有完成,可以提前对已完成的做处理。
ExecutorCompletionService service
= new ExecutorCompletionService(executor);
for (Callable<T> task : tasks) service.submit(task);
for (int i = 0; i < tasks.size(); i++) {
Process service.take().get()
// Do something else
}
public static CompletableFuture<String> readPage() {...}
public static List<String> getLinks(String content) {...}
CompletableFuture<String> contents = readPage();
CompletableFuture<List<URL>> links = contents.thenApply(Parser::getLinks);
当 contents 完成时,getLinks 就会被另一个线程调用。thenCompose 用于组合 T -> CompletableFuture<U> 和 U-> CompletableFuture<V> 为 T -> CompletableFuture<V>
。其他一些方法可以让一组future执行,只要有一个执行成功就放弃其他的。
参考:
玩转Java并发工具,精通JUC,成为并发多面手
《码出高效:Java开发手册》
原文:https://www.cnblogs.com/code2one/p/12900645.html