概念
含义 | 地址空间 | 数据操作 | 通信开销 | 任务控制 | |
进程 | 进程是系统运行程序的基本单位。 正在运行的一个程序称为一个进程。 | 各进程独享系统分配的地址空间 | 数据共享复杂但同步简单 | 进程之间的通信开销大 | 操作系统 |
线程 | 线程是进程的最小执行单元。 一个线程代表一条代码执行流水线。 | 各线程共享所属进程的地址空间 | 数据共享简单但同步复杂 | 线程之间的通信开销小 | Java |
并发和并行的区别:
并发(Concurrent)就是单个CPU轮换执行多个线程,总体上看多个线程被“同时”执行。(宏观上并行,微观上串行)
并行(Parallel)就是多个CPU真正地同时执行多个线程。
对于单核CPU,多线程只能并发运行;对于多核CPU,多线程既可并发,也可并行执行。
同步和异步的区别:
同步:你做完我做。
异步:你走你的阳关道,我走我的独木桥。
多线程是为了最大限度榨干CPU资源。
上下文切换
上下文切换是指 CPU 从一个进程(或线程)切换到另一个进程(或线程)。
上下文是指某一时间点 CPU 寄存器和程序计数器的内容。
寄存器是CPU内部的少量的速度很快的闪存,通常存储和访问计算过程的中间值以提高计算机程序的运行速度。
程序计数器是一个专用的寄存器,存储的是正在执行的指令的位置或者下一个将要被执行的指令的位置,具体实现依赖于特定的系统。
举例说明 线程A - B:
1.先挂起线程A,将其在CPU中的状态保存在内存中。
2.在内存中检索下一个线程B的上下文并将其在 CPU 的寄存器中恢复,执行B线程。
3.当B执行完,根据程序计数器中指向的位置恢复线程A。
上下文切换通常是计算密集型的,意味着此操作会消耗大量的 CPU 时间,故线程也不是越多越好。
多线程的优缺点:
优点:在一个进程中可同时执行多个任务,提高了资源利用率(不是提高效率)。
缺点:增加了CPU和内存的负担,降低了每个线程的执行概率;引发线程安全问题、死锁现象。
读取脏数据:过期的数据
丢失更新:某些线程所做的更新被其他线程所做的更新覆盖
死锁(Deadlock):互相试图获取对方占有的锁又不放弃自己的锁导致一直互相等待。
活锁(Livelock):
线程饥饿(Starvation):永远无法获取处理器执行的机会而永远处于RUNNABLE状态的READY子状态。
上下文切换(Context Switch):增加了系统的消耗。
内存泄漏:
多进程的方式也可以实现并发,为什么我们要使用多线程?
- 进程间的通信比较复杂,而线程间的通信比较简单。
- 进程是重量级的,而线程是轻量级的,故多线程方式的系统开销更小。
推荐第二种,因为Java是单继承的,尽量用实现!
1、继承Thread类(该类是线程类,Thread类及其子类都是线程类)
自定义类继承Thread类并重写其run()方法(写自定义线程的执行代码),在需要的地方创建(new)并启动(start())自定义线程类实例
示例 | 说明 | |
public class MyThread extends Thread { @Override public void run() { | Thread类实现了Runnable接口并维持了一个Runnable接口的引用target 子类启动后,start()会调用子类重写后的run()方法。如果子类不重写run()方法,那么将调用父类Thread的run()方法,而父类Thread的run()方法什么都不执行,因为无参构造导致target为null,参考JDK源代码:
|
2、实现Runnable接口(该类不是线程类,线程类是Thread类及其子类)
自定义类实现Runnable接口并实现其run()方法,在需要的地方以该实现类实例为构造器参数创建(new)Thread类线程实例,然后启动(start())Thread线程类实例
示例 | 说明 | ||||
public class MyRunnable extends OtherClass implements Runnable { | 1)简写方式:
2)线程启动后,Thread类线程实例的run()方法就会调用传入的Runnable实现类实例的run()方法,参考JDK源代码(见上例源码)。 MyThread和Thread都是Runnable接口的实现类,Thread是线程类,MyThread不是线程类。 |
3、实现Callable接口(该类不是线程类,线程类是Thread类及其子类)
- 创建一个类实现Callable接口,实现call()方法。然后创建实现类对象。
- 创建一个FutureTask对象做为线程任务,构造器中传入上步创建的Callable对象。
- 创建Thread线程对象,构造器中传入上步创建的FutureTask对象。
- 启动线程(start()),然后调用FutureTask对象的get()方法等待获取Callable对象call()方法的返回值
示例 | 说明 | |
import lombok.SneakyThrows; | Callable接口 Callable接口(@since 1.5)类似于Runnable接口(@since 1.0),但比Runnable接口更加强大,增加了异常和返回值。 Future接口 Future接口只有几个比较简单的方法:
cancel方法是试图取消线程的执行,并不一定能取消成功(比如已完成、已取消的任务就谈不上取消)。返回值表示是否取消成功。参数表示是否采用中断的方式取消线程执行。 get方法是获取Callable任务返回的Object,如果Callable任务还没完成,get方法会阻塞直到任务完成。 FutureTask类 Thread类的构造方法并不能传入Callable接口,可使用Runnable接口的实现类FutrueTask作为Callable接口的装饰者。 FutureTask实现了RunableFuture接口,RunableFuture接口同时继承了Runnable接口和Future接口,FutureTask的构造方法可以传入Callable或Runnable,所以FutureTask可以装饰Callable作为Runnable被线程执行,又可以作为Future得到Callable的返回值。 |
Thread类
Thread线程类属性
属性 | 类型 | 说明 |
tid(编号) | long |
|
name(名称) | String | 1)默认值格式:“Thread-编号”,如:“Thread-0” |
priority(优先级) | int |
|
daemon(线程级别) | boolean |
|
Thread线程类常用方法
方法 | 含义 | 说明 |
Thread() | 构造方法 | |
Thread(String name) | 构造方法 | |
Thread(Runnable target) | 构造方法 | |
Thread(Runnable target, String name) | 构造方法 | |
static Thread currentThread() | 返回当前方法的执行线程 | 同一段代码对Thread currentThread()的调用,可能返回不同的线程,例如: main方法调用线程的start方法,则该线程run方法中调用Thread currentThread()将放回该线程; main方法调用线程的run方法,则该线程run方法中调用Thread currentThread()将返回main线程。 |
void start() | 线程启动 | 同一个thread只能start()一次,否则threadStatus != 0,抛出IllegalThreadStateException异常 |
void run() | 线程的任务处理逻辑 | 该方法由虚拟机直接调用,应用程序不应调用该方法 |
static sleep(long millis) | 使执行该方法的线程睡眠指定毫秒数 | 是执行当前方法的线程,不是调用当前方法的线程 |
void join() | 使当前执行线程等待调用该方法的线程执行完毕之后再继续执行 | 内部通过Object.wait()实现 |
static void yield() | 使当前执行线程让出并重新争夺CPU | 该方法不可靠,调用该方法后当前执行线程可能仍然获得执行机会 |
interrupt() | 设置调用线程的中断状态为true(默认是false) | 清除由sleep、join、wait等方法导致的线程阻塞状态 |
isInterrupted() | 判断调用线程是否中断 |
停止线程的方法:
- 停止一个线程一般使用boolean成员变量如flag(默认为true)去控制,例如将自定义线程的任务代码用while(flag){...}循环包起来,在合适的地方(其他方法中)执行flag=false去停止它。
- 另一种方式是直接在自定义线程的任务代码中设置return或break判断机制。
- 如果要停止的是一个阻塞状态下的线程,那么就要先用interrupt()方法清除阻塞状态,再停止它!
Java平台非用户创建的线程
main线程:Java虚拟机启动的时候会创建一个main线程,该线程负责执行Java程序的入口方法(main方法)
Web服务器请求处理线程:Web应用中的Servlet类的doGet、doPost等方法也总是由确定的线程负责执行的(具体与所使用的Web容器有关)
垃圾回收线程:Java虚拟机垃圾回收器(Garbage Collector)回收的动作是通过垃圾回收线程实现的,这些线程由Java虚拟机自行创建
JIT编译线程:为了提高Java代码的执行效率,Java虚拟机中的JIT(Just In Time)编译器会动态地将Java字节码(Byte Code)编译为Java虚拟机宿主机处理器可直接执行的机器码(本地代码)。这个动态编译的过程实际上是由Java虚拟机创建的专门的线程负责执行的。
一个Java应用程序在运行时至少有2个线程:main线程和垃圾回收线程。
线程的层级关系
父子线程
如果线程A所执行的代码创建了线程B,那么线程A就是线程B的父线程,线程B就是线程A的子线程。
一个线程是否为守护线程默认同父线程一致,一个线程的优先级默认同父线程的优先级。
Java平台中并没有API用于获取一个线程的父线程,或者获取一个线程的所有子线程。
父线程和子线程之间的生命周期也没有必然联系。父线程运行结束后,子线程可以继续运行,子线程运行结束也不妨碍其父线程继续运行。
习惯上,我们也称某些子线程为工作者线程(Worker Thread)或者后台线程(Background Thread)。工作者线程通常是其父线程创建来用于专门负责某项特定任务的执行的。例如Servlet的doGet方法对请求进行处理的线程通常被称为Web服务器的工作者线程。Java虚拟机中对内存进行回收的线程通常被称为GC(Garbage Collection)工作者线程。
线程组
获取当前的线程组名字
Thread.currentThread().getThreadGroup().getName()
复制线程组
// 获取当前的线程组 |
线程组统一异常处理
public static void main(String[] args) { |
线程组的数据结构
线程组是一个树状的结构,每个线程组下面可以有多个线程或者线程组。线程组可以起到统一控制线程的优先级和检查线程的权限的作用。
ThreadGroup成员变量
public class ThreadGroup implements Thread.UncaughtExceptionHandler { private final ThreadGroup parent; // 父亲ThreadGroup String name; // ThreadGroupr 的名称 int maxPriority; // 线程最大优先级 boolean destroyed; // 是否被销毁 boolean daemon; // 是否守护线程 boolean vmAllowSuspension; // 是否可以中断 int nUnstartedThreads = 0; // 还未启动的线程 int nthreads; // ThreadGroup中线程数目 Thread threads[]; // ThreadGroup中的线程 int ngroups; // 线程组数目 ThreadGroup groups[]; // 线程组数组 } |
ThreadGroup构造函数:
// 私有构造函数 private ThreadGroup() { this.name = "system"; this.maxPriority = Thread.MAX_PRIORITY; this.parent = null; } // 默认是以当前ThreadGroup传入作为parent ThreadGroup,新线程组的父线程组是目前正在运行线程的线程组。 public ThreadGroup(String name) { this(Thread.currentThread().getThreadGroup(), name); } // 构造函数 public ThreadGroup(ThreadGroup parent, String name) { this(checkParentAccess(parent), parent, name); } // 私有构造函数,主要的构造函数 private ThreadGroup(Void unused, ThreadGroup parent, String name) { this.name = name; this.maxPriority = parent.maxPriority; this.daemon = parent.daemon; this.vmAllowSuspension = parent.vmAllowSuspension; this.parent = parent; parent.add(this); } |
第三个构造函数里调用了checkParentAccess方法,这里看看这个方法的源码:
// 检查parent ThreadGroup private static Void checkParentAccess(ThreadGroup parent) { parent.checkAccess(); return null; } // 判断当前运行的线程是否具有修改线程组的权限 public final void checkAccess() { SecurityManager security = System.getSecurityManager(); if (security != null) { security.checkAccess(this); } } |
这里涉及到SecurityManager这个类,它是Java的安全管理器,它允许应用程序在执行一个可能不安全或敏感的操作前确定该操作是什么,以及是否是在允许执行该操作的安全上下文中执行它。应用程序可以允许或不允许该操作。
比如引入了第三方类库,但是并不能保证它的安全性。
其实Thread类也有一个checkAccess()方法,不过是用来当前运行的线程是否有权限修改被调用的这个线程实例。(Determines if the currently running thread has permission to modify this thread.)
线程池
线程池的优点
- 降低资源消耗:使用线程池,通过重复利用已创建的线程降低线程创建、启动和销毁造成的消耗。
- 提高系统稳定性:如果线程无限制地创建,容易造成系统不稳定,使用线程池可以对线程进行统一分配、调优和监控,
Executor框架(接口&类)
Executor | ExecutorService | AbstractExecutorService | ForkJoinPool | |
ThreadPoolExecutor | ScheduledThreadPoolExecutorService | |||
ScheduledExecutorService | ||||
Executors | ThreadFactory |
Executor:线程任务提交器。这个接口将任务提交与每个任务将如何运行实现了分离,包括线程使用、调度等细节。该接口只定义了一个execute()方法。execute():将任务提交给线程池,由线程池为该任务创建线程并启动。注意这个方法没有返回值,获取不到线程执行结果
ExecutorService:
shutDown()
shutDownNow()
isShutdown()
isTerminated()
invokeAll()
submit()
ThreadPoolExecutor
ctl:AtomicInteger类型,用于控制线程池的运行状态(runState,高3位)和线程池中有效线程的数量(workerCount,低29位)
线程池主的实现类主要有两个类ThreadPoolExecutor和ForkJoinPool。
ForkJoinPool是Fork/Join框架下使用的一个线程池,一般情况下,我们使用的比较多的就是ThreadPoolExecutor。
创建线程池ThreadPoolExecutor
通过ThreadPoolExecutor的构造方法创建线程池
ThreadPoolExecutor有多个不同参数的构造方法,以下是参数最全的ThreadPoolExecutor构造方法:
public ThreadPoolExecutor(int corePoolSize, |
ThreadFactory和RejectedExecutionHandler为非必传参数。
参数的含义:
corePoolSize:核心线程数量
核心线程空闲时也不会被销毁,除非设置了allowCoreThreadTimeOut
默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程;
在实际中如果需要线程池创建之后立即创建线程,可以通过以下两种方式:
prestartCoreThread():boolean prestartCoreThread(),初始化一个核心线程;
prestartAllCoreThreads():int prestartAllCoreThreads(),初始化所有核心线程;
maximumPoolSize:最大线程数量(不得小于核心线程数量)
keepAliveTime:核心线程池满后线程的空闲存活时间。
TimeUnit:keepAliveTime的时间单位。
workQueue:保存等待执行的任务的阻塞队列,可以选择以下几个:
ArrayBlockingQueue:基于数组的有界阻塞队列(FIFO),使用该队列时线程池的最大线程数将被限制为maximumPoolSize
LinkedBlockingQueue:基于链表的阻塞队列(FIFO)
使用该队列时线程池的最大线程数将被限制为corePoolSize(maximumPoolSize将无效)
吞吐量高于ArrayBlockingQueue。Executors.newFixedThreadPool()使用了这个队列。
SynchronousQueue:一个不储存元素的阻塞队列
每一个插入操作必须等到另外一个线程调用移除操作,否则插入操作一直处于阻塞状态。
吞吐量高于LinkedBlockingQueue,Executors.newCachedThreadPool使用了这个队列。
PriorityBlockingQueue:一个具有优先级的无限阻塞队列
RejectedExecutionHandler:拒绝策略(饱和策略)。当队列和线程池都满了,需要一种策略处理新的任务,此接口提供了四种内置的实现类:
AbortPolicy:直接抛出RejectedExecutionException异常(默认策略)。
CallerRunsPolicy:用调用者所在的线程(提交任务的线程)来运行任务。
DiscardOldestPolicy:丢弃队列里靠最前的任务,并执行当前任务。
DiscardPolicy:直接丢弃任务。建议一些无关紧要的业务采用此策略。
自定义策略:实现RejectedExecutionHandler接口,自定义策略(日志记录线程池参数、当前状态;输出线程堆栈dump文件;发送事件)。
ThreadFactory:线程工厂,用于创建线程
默认使用Executors.defaultThreadFactory(){return new DefaultThreadFactory();}来创建线程
可以按照DefaultThreadFactory来自己写一个ThreadFactory实现类
或者使用Google guava的ThreadFactoryBuilder来创建一个ThreadFactory,例如:
new ThreadFactoryBuilder().setNameFormat("retryClient-pool-").build();
或者使用Apache commons-lang3的BasicThreadFactory来创建一个ThreadFactory,例如:
new BasicThreadFactory.Builder().namingPattern("basicThreadFactory-").build();
(不推荐)通过Executors类的几个静态方法创建线程池ThreadPoolExecutor:
newFixedThreadPool(...): 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。 缺点:队列大小为int最大值,可能会堆积大量请求造成内存溢出。 | public static ExecutorService newFixedThreadPool(int nThreads[, ThreadFactory threadFactory]) { |
newSingleThreadExecutor(...): 创建一个单线程的线程池,可保证任务按照指定顺序(FIFO)执行。 缺点:队列大小为int最大值,可能会堆积大量请求造成内存溢出。 | public static ExecutorService newSingleThreadExecutor([ThreadFactory threadFactory]) { |
newCachedThreadPool(...): 创建一个可缓存线程池,无线程数量限制,回收空闲超过60秒的线程。 适用于短期异步任务的操作,或者是负载比较轻的服务器。 缺点:最大线程数量为int最大值,可能会创建大量线程造成内存溢出。 | public static ExecutorService newCachedThreadPool([ThreadFactory threadFactory]) { |
newScheduledThreadPool(...): 创建一个固定线程数的线程池,支持定时及周期性执行后台任务。 缺点:最大线程数量为int最大值,可能会创建大量线程造成内存溢出。 | public static ScheduledExecutorService newScheduledThreadPool( public ScheduledThreadPoolExecutor(int corePoolSize[, ThreadFactory threadFactory]) { |
任务提交:ThreadPoolExecutor#execute(...)/AbstractExecutorService#submit(...)
ThreadPoolExecutor的execute方法:
public void execute(Runnable command) { | 它的执行实际上分了三步:
线程池把每个线程都封装成一个对象Worker,有一个关键的函数: addWorker(Runnable firstTask, boolean core) firstTask代表这个线程池首先要执行的任务,core代表是否使用corePoolSize来做为线程池线程的最大标记。 |
ThreadPoolExecutor的成员变量ctl
ctl 是线程池中的非常重要的成员变量,它代表两个含义;
1、表示线程池的运行状态;
2、表示线程池的工作线程数;
为什么线程池要用一个ctl变量代表两个含义呢?
在多线程的环境下,运行状态和有效线程数量往往需要保证统一,不能出现一个修改而另一个没有修改的情况,如果将他们放在同一个AtomicInteger中,利用AtomicInteger的原子操作,就可以保证这两个值始终是统一的;
线程执行流程
提交新任务 │ ↓ 线程池核心是否已满──是─→队列是否已满──是─→线程池是否已满──是─→按照饱和策略处理任务 │ │ │ 否 否 否 ↓ ↓ ↓ 创建新线程执行任务 存储任务到队列 创建新线程执行任务 |
当新任务提交时(execute/submit):
- 核心线程数未满,则创建新线程执行
- 核心线程数满了,则任务入队列
- 队列满了,未达到最大线程数,则创建新线程执行
- 队列满了,且达到最大线程数,则使用拒绝策略
ThreadPoolExecutor采取上述步骤的总体设计思路,是为了在执行execute()方法时,尽可能地避免获取全局锁(那将会是一个严重的可伸缩瓶颈)。在ThreadPoolExecutor完成预热之后当前运行的线程数大于等于corePoolSize),几乎所有的execute()方法调用都是执行步骤2,而步骤2不需要获取全局锁。
线程池总体流程
线程池线程数计算
网友建议:CPU密集型建议线程数等于N+1,IO密集型建议线程数等于2N+1。N为CPU核心数
《Java Concurrency in Practice》(java并发编程实战)建议:
Nthreads=Ncpu*Ucpu*(1+w/c)
线程数=CPU核心数*CPU期望使用率*(1+等待时间/计算时间)
CPU期望使用率建议70%
《Programming Concurrency on the JVM Mastering》(Java 虚拟机并发编程)建议:
线程数=CPU核心数/(1-阻塞系数)
其中阻塞系数的取值在0和1之间,CPU密集型的为0,IO密集型的接近1。
若上述两个公式组成方程组,则Ncpu/(1-阻塞系数)=Ncpu*(1+w/c),===》阻塞系数=w/(w+c)
线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。
查看linux 逻辑cpu核数:cat /proc/cpuinfo| grep "processor"| wc -l
线程池大小的设置没有固定公式,不能设置过大也不能设置过小,我们可以根据业务场景,从上面三个公式中选择一个得到一个大概的线程数量,然后通过压测,逐渐“增大线程数量”和“减小线程数量”,然后观察整体的处理时间变化,最终确定一个具体的线程数量,让任务处理速度最快。
线程池的生命周期
java.util.concurrent.ThreadPoolExecutor中给出的五种线程池状态:
线程状态 | 导致状态发生条件 |
RUNNING | 能接受新提交的任务,并且也能处理阻塞队列中的任务。 |
SHUTDOWN | 关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。在线程池处于 RUNNING 状态时,调用shutdown()方法会使线程池进入到该状态(finalize()方法在执行过程中也会调用shutdown()方法进入该状态)。 |
STOP | 不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。在线程池处于RUNNING或SHUTDOWN状态时,调用shutdownNow()方法会使线程池进入到该状态 |
TIDYING | 如果所有的任务都已终止了,workerCount(有效线程数)为0,线程池进入该状态后会调用terminated()方法进入TERMINATED状态 |
TERMINATED | 在terminated() 方法执行完后进入该状态,默认terminated()方法中什么也没有做 |
一个线程在其整个生命周期中,只可能有一次处于NEW状态和TERMINATED状态。
---shutdown()-------SHUTDOWN----阻塞队列为空,线程池中工作线程数量为0------ | | RUNNING--| |---TIDYING---terminated()----TERMINATED | | ---shutdownNow()----STOP--------线程池中工作线程数量为0-------------------- |
局部线程池
根据要执行的任务的特点,使用多个业务线程池,把相同类型的任务放到同一个线程池中,可以起到隔离的作用,避免一个线程池中的线程阻塞或占满时影响到其它线程池执行;
因为不同业务的特征不一样,有的业务是CPU密集型的,有的业务是IO密集型的,有的任务执行耗时比较长,有的任务执行耗时比较短,有的任务可能是连数据库,有的任务可能是调第三方接口等等,分开使用不同的线程池。
创建局部线程池可以使用Executors类中的方法来实现,本例演示创建为一个固定长度的线程池
package com.tongwx.demo.thread; corePoolSize, corePoolSize * 2, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(100),
|
全局线程池(工具类)
import com.google.common.util.concurrent.ThreadFactoryBuilder; /** |
全局线程池(SpringBoot)
线程池配置类
package com.tongwx.demo.config; |
线程任务类
package com.tongwx.demo.biz; |
调用
@Autowired |
@Async使用注意
static失效:static修饰异步方法(spring boot不能扫描到这个方法)
相互调用失效:一个类中的方法相互调用不会触发异步
Transactional失效:@Async方法上使用@Transactional
线程的生命周期
java.lang.Thread.State枚举中给出的六种线程状态:
线程状态 | 导致状态发生条件 |
NEW(新建) | 线程刚被创建,但是并未启动。还没调用start方法。 |
RUNNABLE(可运行) | 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。 Java线程的RUNNABLE状态是包括传统操作系统线程的ready和running两个状态的。 |
BLOCKED(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。 |
WAITING(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。 |
TIMED WAITING(计时等待) | 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。 |
Teminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。 |
一个线程在其整个生命周期中,只可能有一次处于NEW状态和TERMINATED状态。
Thread.sleep(long)时间到 Thread.sleep(long) 锁.wait(long)时间到 锁.wait(long) thread.join(long)时间到 thread.join(long) LockSupport.unpark(thread) LockSupport.parkNanos(thread) 锁.notify()/锁.notifyAll) LockSupport.parkUntil() ----------------------------计时等待 TIMED_WAITING<------------------------ | | | 锁.notify()/锁.notifyAll) 锁.wait() | | thread.join()结束 thread.join() | | LockSupport.unpark(thread) LockSupport.park() | -------------------------------无限等待 WAITING<--------------------------- | | | 获取锁 等待锁 | | IO完成 IO阻塞 | ---------------------------------阻塞 BLOCKED<----------------------------- | | | | run()结束 | ┌─────可运行 RUNNABLE───┐ | interrupt()设置中断为true new() start() | │ 就绪 READY │ | 异常 创建 NEW ---------------------------> │ 获得CPU |↑ 失去CPU/ │------------------------------->终止 TERMINATED │ ↓| Thread.yield() │ │ 运行 RUNNING │ └─────────────────┚ |
线程的监控
平台 | 获取途径 | |
平台无关 | 方法1 | 执行命令:jstack -l Java进程ID |
方法2 | 使用图形化工具jvisualvm | |
方法3 | 使用图形化工具jmc(Java Mission Control) | |
Linux | 方法4 | 执行命令:kill -3 Java进程ID |
方法5 | 在启动Java程序的控制台中按下“Ctrl+\”组合键 | |
Windows | 方法6 | 在启动Java程序的CMD中按下“Ctrl+Break”组合键 |
注:Java进程ID可使用%JAVA_HOME%/bin/jps命令或Linux的ps命令获取
线程通信
等待/通知机制:wait和notify(重点)
(必须在同步代码块或同步函数中由锁对象调用)(Thread.sleep不会释放锁,obj.wait会释放锁)
void 锁对象.wait() 使在此对象监视器上的当前线程等待(执行该方法的线程会进入以锁对象为标识的等待池中等待)
void 锁对象.wait(long timeout) 在其他线程调用此对象的notify...()方法或者超过指定的时间量前,导致当前线程等待。
void 锁对象.wait(long timeout, int nanos) 在其他线程调用此对象的notify...()方法或者其他某个线程中断当前线程,或者已超过某个实际时间量前,导致当前线程等待。
void 锁对象.notify() 唤醒在此对象监视器上等待的单个线程(执行该方法的线程会唤醒在以锁对象为标识的等待池中等待的随机一个线程)
void 锁对象.notifyAll() 唤醒在此对象监视器上等待的所有线程(执行该方法的线程会唤醒在以锁对象为标识的等待池中等待的所有线程)
线程通信案例:模拟生产和消费,生产一个消费一个
public class WaitAndNotify { public static void main(String[] args) { Produce p = new Produce(); Thread t1 = new Thread(new Producer(p), "生产者"); Thread t2 = new Thread(new Customer(p), "消费者"); t1.start(); t2.start(); } } | class Produce { String name;// 产品名称 double price;// 产品价格 boolean flag;// 产品状态,true为有产品,false为无产品 } |
class Producer implements Runnable { Produce p; int num; public Producer(Produce p) { this.p = p; } @Override public void run() { while (true) { synchronized (p) {// 加锁防止价格混乱 if (!p.flag) {// 如果没有产品则生产产品 if (num++ % 2 == 0) {// 取余、自增以生产多种产品 p.name = "摩托车"; p.price = 3000; } else { p.name = "自行车"; p.price = 400; } System.out.println(Thread.currentThread().getName() + "生产了一辆" + p.name + ",价格为" + p.price); p.notify();// 生产完通知消费,如果有多个生产者或消费者,使用notifyAll() p.flag = true; } else { try { p.wait();// 如果有产品,则等待消费后再生产 } catch (InterruptedException e) { e.printStackTrace(); } } } } } } | class Customer implements Runnable { Produce p; public Customer(Produce p) { this.p = p; } @Override public void run() { while (true) { synchronized (p) {// 加锁防止价格混乱 if (p.flag) {// 如果有产品则消费产品 System.out.println(Thread.currentThread().getName() + "消费了一辆" + p.name + ",价格为" + p.price); p.flag = false; p.notify();// 消费完通知生产,如果有多个生产者或消费者,使用notifyAll() } else { try { p.wait();// 如果没有产品,则等待生产后再消费 } catch (InterruptedException e) { e.printStackTrace(); } } } } } } |
线程通信案例:四线程使某类对象成员变量在0和1之间随机变化
package com.tongwx.thread; public class Forever0or1 { public static void main(String[] args) { Sample sample = new Sample(); Thread t1 = new IncreaseThread(sample); Thread t2 = new DecreaseThread(sample); Thread t3 = new IncreaseThread(sample); Thread t4 = new DecreaseThread(sample); t1.start(); t2.start(); t3.start(); t4.start(); } } | class Sample { private int number; public synchronized void increase() { while (0 != number) {//不可用if,因为wait被唤醒后要重新判断number的值 try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } number++; System.out.println(number); notify(); } public synchronized void decrease() { while (0 == number) {//不可用if,因为wait被唤醒后要重新判断number的值 try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } number--; System.out.println(number); notify(); } } |
class IncreaseThread extends Thread { private Sample sample; public IncreaseThread(Sample sample) { this.sample = sample; } @Override public void run() { for (int i = 0; i < 20; i++) { try { Thread.sleep((long) (Math.random() * 1000)); } catch (InterruptedException e) { e.printStackTrace(); } sample.increase(); } } } | class DecreaseThread extends Thread { private Sample sample; public DecreaseThread(Sample sample) { this.sample = sample; } @Override public void run() { for (int i = 0; i < 20; i++) { try { Thread.sleep((long) (Math.random() * 1000)); } catch (InterruptedException e) { e.printStackTrace(); } sample.decrease(); } } } |
线程通信案例:轮流打印
public class WaitAndNotify { | |
class ThreadA implements Runnable { | class ThreadB implements Runnable { |
管道通信:PipedWriter、PipedReader、PipedOutputStream、PipedInputStream
线程通信案例:发送信息
import java.io.IOException; | |
@Data | @Data |
线程安全问题
产生线程安全问题是因为多个线程访问内存中的共享资源,从而产生了冲突、产生了脏数据、产生了意外的结果。
线程安全问题分析
出现线程安全问题的表现:竞态
对于同样的输入,程序的输出有时候是正确的而有时候却是错误的。这种一个计算结果的正确性与时间有关的现象就被称为竞态(Race Condition)
出现线程安全问题的原因:多个线程在没有采取任何措施的情况下并发更新、读取同一个共享变量。
脏读(Dirty Read):线程读取到一个过时的数据
丢失更新(Lost Update):某些线程所做的更新被其他线程所做的更新覆盖
线程安全的条件
原子性(Atomicity)
什么是原子性:
多线程的原子性是指一组指令被执行时,不受其他指令的干扰,比如CAS(Compare & Set)是原子的,java.util.concurrent.atomic下的原子操作类等,这里的原子性其实指的是隔离性,也就是一组操作不能被别的线程干扰。
例如:
非原子操作:user.setName(name); user.setAge(age)
原子操作:User newUser = new User(name, age); User user = newUser;
long型和double型的非原子性(32位系统下)
在32位系统下,long型和double型以外的任何类型的变量的写操作都是原子操作。
long和double的长度都是8个字节,32位(4字节)处理器对其读写操作无法一次完成。
若要long和double的写操作为原子性,使用volatile修饰long和double。
实现原子性的两种方式
要保证原子性,就要使共享变量在任意一个时刻只能够被一个线程访问。
原子性实现方式 | 适用场景 | 实现层次 |
锁(Lock) | 写多读少 | 软件层 |
CAS(Compare-and-Swap) | 写少读多 | 硬件层(硬件锁) |
synchronized同步锁的作用域:
使用synchronized修饰同步代码块或同步方法。(被同步保护的资源应当是私有的,否则同步可能无效)
对象锁:非静态同步方法的锁对象是this(当前函数调用者,当前方法所属当前对象)。
多线程同步执行相同对象的任何非静态同步方法(是同一个对象的对象锁);
多线程异步执行不同对象的任何非静态同步方法(是不同对象的对象锁)。
类锁:静态同步方法的锁对象是当前方法所属类的class字节码对象。
多线程同步执行同一类的任何静态同步方法,即使用对象调用静态同步方法,锁仍然是类锁。
同步代码块的锁对象:锁对象可以是任意对象,但需要有唯一性,通常用this,可以为全局唯一的字符串。
总结:只要锁对象相同,就同步执行(排队),否则就异步执行(并发或并行)。
推荐使用同步代码块:锁对象可自定义;同步范围可自定义,同步范围比同步方法小,并发性能高。
synchronized同步锁的类型
JDK1.6之前,synchronized只有重量级锁,JDK1.6之后新增了偏向锁、轻量级锁升;
Lock同步锁
使用Lock同步锁来同步代码块,方法如下:
- 声明一个同步锁:Lock lock = new ReentrantLock();
- 在需要同步的代码块开始前加同步锁:public void lock.lock();
- 在同步代码块结束后释放同步锁:lock.unlock();
可见性(Visibility)
可见性是指线程对共享变量的更新对其他线程可见。若不可见,则容易出现脏读。
可见性出问题的原因:
JIT编译器优化导致不可见:
如果代码没有给JIT编译器足够的提示而使得其认为共享变量只有一个线程对其进行访问,从而导致JIT编译器为了避免重复读取共享变量以提高代码的运行效率,而将共享变量替换为字面量,这导致其他线程更新共享变量对当前线程不可见。
计算机的存储系统导致可见性问题:
程序中的变量可能会被分配到寄存器(Register)而不是主内存中进行存储。每个处理器都有其寄存器(Register),而一个处理器无法读取另外一个处理器上的寄存器中的内容。因此,如果两个线程分别运行在不同的处理器上,而这两个线程所共享的变量却被分配到寄存器上进行存储,那么可见性问题就会产生。
另外,即便某个共享变量是被分配到主内存中进行存储的,也不能保证该变量的可见性。这是因为处理器对主内存的访问并不是直接访问,而是通过其高速缓存(Cache)子系统进行的。一个处理器上运行的线程对变量的更新可能只是更新到该处理器的写缓冲器(Store Buffer)中,还没有到达该处理器的高速缓存中,更不用说到主内存中了。而一个处理器的写缓冲器中的内容无法被另外一个处理器读取,因此运行在另外一个处理器上的线程无法看到这个线程对某个共享变量的更新。
即便一个处理器上运行的线程对共享变量的更新结果被写入该处理器的高速缓存,由于该处理器将这个变量更新的结果通知给其他处理器的时候,其他处理器可能仅仅将这个更新通知的内容存入无效化队列(Invalidate Queue)中,而没有直接根据更新通知的内容更新其高速缓存的相应内容,这就导致了其他处理器上运行的其他线程后续再读取相应共享变量时,从相应处理器的高速缓存中读取到的变量值是一个过时的值。
有序性
指令重排序:计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排。
指令重排一般分为三种:编译器优化重排、指令并行重排、内存系统重排。
指令重排可以保证串行语义一致,但是没有义务保证多线程间的语义也一致。所以在多线程下,指令重排序可能会导致一些问题。
Thread死锁问题(哲学家就餐问题)
当多处同步代码块使用同一个锁对象的情况发生多起时,容易出现死锁现象(线程之间互相等待对方释放锁)。所以应尽量避免多处同步代码块使用同一个锁对象! 死锁现象示例如右: | public class DeadLockThread extends Thread { public static void main(String[] args) { DeadLockThread t1 = new DeadLockThread("张三"); DeadLockThread t2 = new DeadLockThread("李四"); t1.start(); t2.start(); } public DeadLockThread(String threadName) { super(threadName); } @Override public void run() { if ("张三".equals(this.getName())) { synchronized ("遥控器") { System.out.println("取遥控器!"); synchronized ("电池") { System.out.println("取电池!"); } } } if ("李四".equals(this.getName())) { synchronized ("电池") { System.out.println("取电池!"); synchronized ("遥控器") { System.out.println("取遥控器!"); } } } } } |
多线程案例:模拟多窗口售票
继承Thread类的案例 | 实现Runnable的案例 |
public class SellTicketsThread extends Thread { public static void main(String[] args) { // 创建线程对象 SellTicketsThread s1 = new SellTicketsThread("窗口一"); SellTicketsThread s2 = new SellTicketsThread("窗口二"); SellTicketsThread s3 = new SellTicketsThread("窗口三"); // 启动线程对象 s1.start(); s2.start(); s3.start(); } // 总票数,静态的(所有线程共享) private static int num = 50; // 调用父类一个参数的构造函数, 初始化线程的名字。 public SellTicketsThread(String threadName) { super(threadName); } // 线程的任务代码 @Override public void run() { while (true) { synchronized ("锁1") { if (num > 0) { // 当前对象就是当前线程,可用this指代当前线程 System.out.println("当前线程所属类是" + this.currentThread().getClass());// SellTicketsThread System.out.println("当前对象所属类是" + this.getClass());// SellTicketsRunnable System.out.println(this.getName() + "卖出了" + num-- + "号票"); } else { System.out.println("售罄!"); break; } } } } } | public class SellTicketsRunnable implements Runnable { public static void main(String[] args) { // 创建Runnable实现类对象 SellTicketsRunnable s = new SellTicketsRunnable(); // 创建线程对象,传入Runnable实现类对象 Thread t1 = new Thread(s, "窗口一"); Thread t2 = new Thread(s, "窗口二"); Thread t3 = new Thread(s, "窗口三"); // 启动线程对象 t1.start(); t2.start(); t3.start(); } // 总票数,静态的(所有Runnable实现类共享),如果Runnable实现类只创建了一个,可不加static private static int num = 50; // 实现类的任务代码 @Override public void run() { while (true) { synchronized ("锁2") { if (num > 0) { // 当前对象不是当前线程,不可用this指代当前线程 System.out.println("当前线程所属类是" + Thread.currentThread().getClass());// Thread System.out.println("当前对象所属类是" + this.getClass());// SellTicketsRunnable System.out.println(Thread.currentThread().getName() + "卖出了" + num-- + "号票"); } else { System.out.println("售罄!"); break; } } } } } |
多线程和类变量、局部变量的关系
package com.tongwx.thread; public class TestThread { public static void main(String[] args) throws InterruptedException { // //同时输出两串0-9,因为不管num是成员变量还是局部变量,两个线程都各自拥有互不相干的num变量 // new MyThread1().start(); // new MyThread1().start(); // //同时输出两串0-9,因为不管num是成员变量还是局部变量,两个线程都各自拥有一个互不相干的MyThread2实例及其num变量 // new Thread(new MyThread2()).start(); // new Thread(new MyThread2()).start(); //如果num是成员变量,无序输出0-19,因为两个线程共享同一个对象的成员变量; //如果num是局部变量,同时输出两串0-9,因为两个线程各自拥有一份同一个对象之局部变量的拷贝。 MyThread2 myThread2 = new MyThread2(); new Thread(myThread2).start(); new Thread(myThread2).start(); } } class MyThread1 extends Thread { private int num; @Override public void run() { int num = 0; for (int i = 0; i < 10; i++) { System.out.println(num++); // System.out.println(this.num++); } } } class MyThread2 implements Runnable { private int num; @Override public void run() { int num = 0; for (int i = 0; i < 10; i++) { // System.out.println(num++); System.out.println(this.num++); } } } |
ThreadLocal类
ThreadLocal是一个线程局部变量工具类,一个线程局部变量在多个线程中,分别有独立的值(副本)。
ThreadLocal内部是一个弱引用的ThreadLocalMap来维护。ThreadLocalMap是一个哈希表。
ThreadLocal主要API:
ThreadLocal() 构造函数
initialValue() 值初始化方法。(未set()而get(),remove()后get(),皆得initialValue()。initialValue()未重写时得null)
withInitial(supplier) 自定义初始值的实例化静态方法
get()/set(...) 取/存
remove() 回收
自定义初始值示例:public static final ThreadLocal<String> THREAD_LOCAL = ThreadLocal.withInitial(() -> "default value");
使用场景
线程资源持有
比如我们有三个不同的类。在一次Web请求中,会在不同的地方,不同的时候,调用这三个类的实例。但用户是同一个,用户数据可以保存在一个线程里。这个时候,我们可以在程序1把用户数据放进ThreadLocalMap里,然后在程序2和程序3里面去用它。
这样做的优势在于:持有线程资源供线程的各个部分使用,全局获取,降低编程难度。
线程资源一致性
这里以JDBC为例。我们经常会用到事务,它是怎么实现的呢?
原来,我们每次对数据库操作,都会走JDBC getConnection,JDBC保证只要你是同一个线程过来的请求,不管是在哪个part,都返回的是同一个连接。这个就是使用ThreadLocal来做的。
当一个part过来的时候,JDBC会去看ThreadLocal里是不是已经有这个线程的连接了,如果有,就直接返回;如果没有,就从连接池请求分配一个连接,然后放进ThreadLocal里。
这样就可以保证一个事务的所有part都在一个连接里。TheadLocal可以帮助它维护这种一致性,降低编程难度。
线程安全
假设我们一个线程的调用链路比较长。在中途中出现异常怎么做?我们可以在出错的时候,把错误信息放到ThreadLocal里面,然后在后续的链路去使用这个值。使用TheadLocal可以保证多个线程在处理这个场景的时候保证线程安全。
并发计算
如果我们有一个大的任务,可以把它拆分成很多小任务,分别计算,然后最终把结果汇总起来。如果是分布式计算,可能是先存储在自己的节点里。而如果是单机下的多线程计算,可以把每个线程的计算结果放进ThreadLocal里面,最后取出来汇总。
InheritableThreadLocal
InheritableThreadLocal类与ThreadLocal类稍有不同,Inheritable是继承的意思。它不仅仅是当前线程可以存取副本值,而且它的子线程也可以存取这个副本值。
40个Java多线程问题总结
1、多线程有什么用?
一个可能在很多人看来很扯淡的一个问题:我会用多线程就好了,还管它有什么用?在我看来,这个回答更扯淡。所谓"知其然知其所以然","会用"只是"知其然","为什么用"才是"知其所以然",只有达到"知其然知其所以然"的程度才可以说是把一个知识点运用自如。OK,下面说说我对这个问题的看法:
(1)发挥多核CPU的优势
随着工业的进步,现在的笔记本、台式机乃至商用的应用服务器至少也都是双核的,4核、8核甚至16核的也都不少见,如果是单线程的程序,那么在双核CPU上就浪费了50%,在4核CPU上就浪费了75%。单核CPU上所谓的"多线程"那是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得比较快,看着像多个线程"同时"运行罢了。多核CPU上的多线程才是真正的多线程,它能让你的多段逻辑同时工作,多线程,可以真正发挥出多核CPU的优势来,达到充分利用CPU的目的。
(2)防止阻塞
从程序运行效率的角度来看,单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核CPU我们还是要应用多线程,就是为了防止阻塞。试想,如果单核CPU使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。
(3)便于建模
这是另外一个没有这么明显的优点了。假设有一个大的任务A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务A分解成几个小任务,任务B、任务C、任务D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。
2、创建线程的方式
比较常见的一个问题了,一般就是两种:
(1)继承Thread类
(2)实现Runnable接口
至于哪个好,不用说肯定是后者好,因为实现接口的方式比继承类的方式更灵活,也能减少程序之间的耦合度,面向接口编程也是设计模式6大原则的核心。
3、start()方法和run()方法的区别
只有调用了start()方法,才会表现出多线程的特性,不同线程的run()方法里面的代码交替执行。如果只是调用run()方法,那么代码还是同步执行的,必须等待一个线程的run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其run()方法里面的代码。
4、Runnable接口和Callable接口的区别
有点深的问题了,也看出一个Java程序员学习知识的广度。
Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
这其实是很有用的一个特性,因为多线程相比单线程更难、更复杂的一个重要原因就是因为多线程充满着未知性,某条线程是否执行了?某条线程执行了多久?某条线程执行的时候我们期望的数据是否已经赋值完毕?无法得知,我们能做的只是等待这条多线程的任务执行完毕而已。而Callable+Future/FutureTask却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务,真的是非常有用。
5、CyclicBarrier和CountDownLatch的区别
两个看上去有点像的类,都在java.util.concurrent下,都可以用来表示代码运行到某个点上,二者的区别在于:
(1)CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch则不是,某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行
(2)CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务
(3)CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了
6、volatile关键字的作用
一个非常重要的问题,是每个学习、应用多线程的Java程序员都必须掌握的。理解volatile关键字的作用的前提是要理解Java内存模型,这里就不讲Java内存模型了,可以参见第31点,volatile关键字的作用主要有两个:
(1)多线程主要围绕可见性和原子性两个特性而展开,使用volatile关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到volatile变量,一定是最新的数据
(2)代码底层执行不像我们看到的高级语言----Java程序这么简单,它的执行是Java代码-->字节码-->根据字节码执行对应的C/C++代码-->C/C++代码被编译成汇编语言-->和硬件电路交互,现实中,为了获取更好的性能JVM可能会对指令进行重排序,多线程下可能会出现一些意想不到的问题。使用volatile则会对禁止语义重排序,当然这也一定程度上降低了代码执行效率
从实践角度而言,volatile的一个重要作用就是和CAS结合,保证了原子性,详细的可以参见java.util.concurrent.atomic包下的类,比如AtomicInteger。
7、什么是线程安全
又是一个理论的问题,各式各样的答案有很多,我给出一个个人认为解释地最好的:如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。
这个问题有值得一提的地方,就是线程安全也是有几个级别的:
(1)不可变
像String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用
(2)绝对线程安全
不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的类,Java中也有,比方说CopyOnWriteArrayList、CopyOnWriteArraySet
(3)相对线程安全
相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove方法都是原子操作,不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的情况下都会出现ConcurrentModificationException,也就是fail-fast机制。
(4)线程非安全
这个就没什么好说的了,ArrayList、LinkedList、HashMap等都是线程非安全的类
8、Java中如何获取到线程dump文件
死循环、死锁、阻塞、页面打开慢等问题,打线程dump是最好的解决问题的途径。所谓线程dump也就是线程堆栈,获取到线程堆栈有两步:
(1)获取到线程的pid,可以通过使用jps命令,在Linux环境下还可以使用ps -ef|grep java
(2)打印线程堆栈,可以通过使用jstack pid命令,在Linux环境下还可以使用kill -3 pid
另外提一点,Thread类提供了一个getStackTrace()方法也可以用于获取线程堆栈。这是一个实例方法,因此此方法是和具体线程实例绑定的,每次获取获取到的是具体某个线程当前运行的堆栈,
9、一个线程如果出现了运行时异常会怎么样
如果这个异常没有被捕获的话,这个线程就停止执行了。另外重要的一点是:如果这个线程持有某个某个对象的监视器,那么这个对象监视器会被立即释放
10、如何在两个线程之间共享数据
通过在线程之间共享对象就可以了,然后通过wait/notify/notifyAll、await/signal/signalAll进行唤起和等待,比方说阻塞队列BlockingQueue就是为线程之间共享数据而设计的
11、sleep方法和wait方法有什么区别
这个问题常问,sleep方法和wait方法都可以用来放弃CPU一定的时间,不同点在于如果线程持有某个对象的监视器,sleep方法不会放弃这个对象的监视器,wait方法会放弃这个对象的监视器
12、生产者消费者模型的作用是什么
这个问题很理论,但是很重要:
(1)通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率,这是生产者消费者模型最重要的作用
(2)解耦,这是生产者消费者模型附带的作用,解耦意味着生产者和消费者之间的联系少,联系越少越可以独自发展而不需要收到相互的制约
13、ThreadLocal有什么用
简单说ThreadLocal就是一种以空间换时间的做法,在每个Thread里面维护了一个以开地址法实现的ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了
14、为什么wait()方法和notify()/notifyAll()方法要在同步块中被调用
这是JDK强制的,wait()方法和notify()/notifyAll()方法在调用前都必须先获得对象的锁
15、wait()方法和notify()/notifyAll()方法在放弃对象监视器时有什么区别
wait()方法和notify()/notifyAll()方法在放弃对象监视器的时候的区别在于:wait()方法立即释放对象监视器,notify()/notifyAll()方法则会等待线程剩余代码执行完毕才会放弃对象监视器。
16、为什么要使用线程池
避免频繁地创建和销毁线程,达到线程对象的重用。另外,使用线程池还可以根据项目灵活地控制并发的数目。
17、怎么检测一个线程是否持有对象监视器
我也是在网上看到一道多线程面试题才知道有方法可以判断某个线程是否持有对象监视器:Thread类提供了一个holdsLock(Object obj)方法,当且仅当对象obj的监视器被某条线程持有的时候才会返回true,注意这是一个static方法,这意味着"某条线程"指的是当前线程。
18、synchronized和ReentrantLock的区别
1)本质不同:synchronized是关键字(内置锁),ReentrantLock是类(显式锁),这是二者的本质区别。
2)锁类型不同:synchronized是非公平锁,ReentrantLock默认是非公平锁,也可以是公平锁。
3)作用域不同:synchronized可用来修饰普通方法、静态方法和代码块,而ReentrantLock只能用在代码块上。
4)用法不同:synchronized自动加锁解锁,ReentrantLock要先创建自身实例作为锁对象,然后使用lock方法加锁,使用unlock方法解锁。
5)响应中断不同:synchronized 不能响应中断,如果发生死锁,synchronized会一直等待下去;ReentrantLock可以使用lockInterruptibly获取锁并响应中断指令以释放锁
6)底层实现不同:synchronized是JVM层面通过监视器(Monitor)实现的,ReentrantLock是通过AQS(AbstractQueuedSynchronizer)程序级别的API实现的。
既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上:
(1)ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁
(2)ReentrantLock可以获取各种锁的信息
(3)ReentrantLock可以灵活地实现多路通知
另外,二者的锁机制其实也是不一样的。ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中mark word,这点我不能确定。
19、ConcurrentHashMap的并发度是什么
ConcurrentHashMap的并发度就是segment的大小,默认为16,这意味着最多同时可以有16条线程操作ConcurrentHashMap,这也是ConcurrentHashMap对Hashtable的最大优势,任何情况下,Hashtable能同时有两条线程获取Hashtable中的数据吗?
20、ReadWriteLock是什么
首先明确一下,不是说ReentrantLock不好,只是ReentrantLock某些时候有局限。如果使用ReentrantLock,可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不一致,但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。
因为这个,才诞生了读写锁ReadWriteLock。ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。
21、FutureTask是什么
这个其实前面有提到过,FutureTask表示一个异步运算的任务。FutureTask里面可以传入一个Callable的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。当然,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。
22、Linux环境下如何查找哪个线程使用CPU最长
这是一个比较偏实践的问题,这种问题我觉得挺有意义的。可以这么做:
(1)获取项目的pid,jps或者ps -ef|grep java,这个前面有讲过
(2)top -H -p pid,顺序不能改变
这样就可以打印出当前的项目,每条线程占用CPU时间的百分比。注意这里打出的是LWP,也就是操作系统原生线程的线程号,我笔记本山没有部署Linux环境下的Java工程,因此没有办法截图演示,网友朋友们如果公司是使用Linux环境部署项目的话,可以尝试一下。
使用"top -H -p pid"+"jps pid"可以很容易地找到某条占用CPU高的线程的线程堆栈,从而定位占用CPU高的原因,一般是因为不当的代码操作导致了死循环。
最后提一点,"top -H -p pid"打出来的LWP是十进制的,"jps pid"打出来的本地线程号是十六进制的,转换一下,就能定位到占用CPU高的线程的当前线程堆栈了。
23、Java编程写一个会导致死锁的程序
第一次看到这个题目,觉得这是一个非常好的问题。很多人都知道死锁是怎么一回事儿:线程A和线程B相互等待对方持有的锁导致程序无限死循环下去。当然也仅限于此了,问一下怎么写一个死锁的程序就不知道了,这种情况说白了就是不懂什么是死锁,懂一个理论就完事儿了,实践中碰到死锁的问题基本上是看不出来的。
真正理解什么是死锁,这个问题其实不难,几个步骤:
(1)两个线程里面分别持有两个Object对象:lock1和lock2。这两个lock作为同步代码块的锁;
(2)线程1的run()方法中同步代码块先获取lock1的对象锁,Thread.sleep(xxx),时间不需要太多,50毫秒差不多了,然后接着获取lock2的对象锁。这么做主要是为了防止线程1启动一下子就连续获得了lock1和lock2两个对象的对象锁
(3)线程2的run)(方法中同步代码块先获取lock2的对象锁,接着获取lock1的对象锁,当然这时lock1的对象锁已经被线程1锁持有,线程2肯定是要等待线程1释放lock1的对象锁的
这样,线程1"睡觉"睡完,线程2已经获取了lock2的对象锁了,线程1此时尝试获取lock2的对象锁,便被阻塞,此时一个死锁就形成了。代码就不写了,占的篇幅有点多,Java多线程7:死锁这篇文章里面有,就是上面步骤的代码实现。
24、怎么唤醒一个阻塞的线程
如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。
25、不可变对象对多线程有什么帮助
前面有提到过的一个问题,不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。
26、什么是多线程的上下文切换
多线程的上下文切换是指CPU控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取CPU执行权的线程的过程。
27、如果你提交任务时,线程池队列已满,这时会发生什么
如果你使用的LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务;如果你使用的是有界队列比方说ArrayBlockingQueue的话,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy。
28、Java中用到的线程调度算法是什么
抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。
29、Thread.sleep(0)的作用是什么
这个问题和上面那个问题是相关的,我就连在一起了。由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。
30、什么是自旋
很多synchronized里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然synchronized里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在synchronized的边界做忙循环,这就是自旋。如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。
31、什么是Java内存模型
Java内存模型定义了一种多线程访问Java内存的规范。Java内存模型要完整讲不是这里几句话能说清楚的,我简单总结一下Java内存模型的几部分内容:
(1)Java内存模型将内存分为了主内存和工作内存。类的状态,也就是类之间共享的变量,是存储在主内存中的,每次Java线程用到这些主内存中的变量的时候,会读一次主内存中的变量,并让这些内存在自己的工作内存中有一份拷贝,运行自己线程代码的时候,用到这些变量,操作的都是自己工作内存中的那一份。在线程代码执行完毕之后,会将最新的值更新到主内存中去
(2)定义了几个原子操作,用于操作主内存和工作内存中的变量
(3)定义了volatile变量的使用规则
(4)happens-before,即先行发生原则,定义了操作A必然先行发生于操作B的一些规则,比如在同一个线程内控制流前面的代码一定先行发生于控制流后面的代码、一个释放锁unlock的动作一定先行发生于后面对于同一个锁进行锁定lock的动作等等,只要符合这些规则,则不需要额外做同步措施,如果某段代码不符合所有的happens-before规则,则这段代码一定是线程非安全的
32、什么是CAS
CAS,全称为Compare and Swap,即比较-替换。假设有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true,否则什么都不做并返回false。当然CAS一定要volatile变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值A对某条线程来说,永远是一个不会变的值A,只要某次CAS操作失败,永远都不可能成功。
33、什么是乐观锁和悲观锁
(1)乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。
(2)悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。
34、什么是AQS
简单说一下AQS,AQS全称为AbstractQueuedSychronizer,翻译过来应该是抽象队列同步器。
如果说java.util.concurrent的基础是CAS的话,那么AQS就是整个Java并发包的核心了,ReentrantLock、CountDownLatch、Semaphore等等都用到了它。AQS实际上以双向队列的形式连接所有的Entry,比方说ReentrantLock,所有等待的线程都被放在一个Entry中并连成双向队列,前面一个线程使用ReentrantLock好了,则双向队列实际上的第一个Entry开始运行。
AQS定义了对双向队列所有的操作,而只开放了tryLock和tryRelease方法给开发者使用,开发者可以根据自己的实现重写tryLock和tryRelease方法,以实现自己的并发功能。
35、单例模式的线程安全性
老生常谈的问题了,首先要说的是单例模式的线程安全意味着:某个类的实例在多线程环境下只会被创建一次出来。单例模式有很多种的写法,我总结一下:
(1)饿汉式单例模式的写法:线程安全
(2)懒汉式单例模式的写法:非线程安全
(3)双检锁单例模式的写法:线程安全
36、Semaphore有什么作用
Semaphore就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore有一个构造函数,可以传入一个int型整数n,表示某段代码最多只有n个线程可以访问,如果超出了n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果Semaphore构造函数中传入的int型整数n=1,相当于变成了一个synchronized了。
37、Hashtable的size()方法中明明只有一条语句"return count",为什么还要做同步?
这是我之前的一个困惑,不知道大家有没有想过这个问题。某个方法中如果有多条语句,并且都在操作同一个类变量,那么在多线程环境下不加锁,势必会引发线程安全问题,这很好理解,但是size()方法明明只有一条语句,为什么还要加锁?
关于这个问题,在慢慢地工作、学习中,有了理解,主要原因有两点:
(1)同一时间只能有一条线程执行固定类的同步方法,但是对于类的非同步方法,可以多条线程同时访问。所以,这样就有问题了,可能线程A在执行Hashtable的put方法添加数据,线程B则可以正常调用size()方法读取Hashtable中当前元素的个数,那读取到的值可能不是最新的,可能线程A添加了完了数据,但是没有对size++,线程B就已经读取size了,那么对于线程B来说读取到的size一定是不准确的。而给size()方法加了同步之后,意味着线程B调用size()方法只有在线程A调用put方法完毕之后才可以调用,这样就保证了线程安全性
(2)CPU执行代码,执行的不是Java代码,这点很关键,一定得记住。Java代码最终是被翻译成汇编代码执行的,汇编代码才是真正可以和硬件电路交互的代码。即使你看到Java代码只有一行,甚至你看到Java代码编译之后生成的字节码也只有一行,也不意味着对于底层来说这句语句的操作只有一个。一句"return count"假设被翻译成了三句汇编语句执行,完全可能执行完第一句,线程就切换了。
38、线程类的构造方法、静态块是被哪个线程调用的
这是一个非常刁钻和狡猾的问题。请记住:线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。
如果说上面的说法让你感到困惑,那么我举个例子,假设Thread2中new了Thread1,main函数中new了Thread2,那么:
(1)Thread2的构造方法、静态块是main线程调用的,Thread2的run()方法是Thread2自己调用的
(2)Thread1的构造方法、静态块是Thread2调用的,Thread1的run()方法是Thread1自己调用的
39、同步方法和同步块,哪个是更好的选择
同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。请知道一条原则:同步的范围越小越好。
借着这一条,我额外提一点,虽说同步的范围越少越好,但是在Java虚拟机中还是存在着一种叫做锁粗化的优化方法,这种方法就是把同步范围变大。这是有用的,比方说StringBuffer,它是一个线程安全的类,自然最常用的append()方法是一个同步方法,我们写代码的时候会反复append字符串,这意味着要进行反复的加锁->解锁,这对性能不利,因为这意味着Java虚拟机在这条线程上要反复地在内核态和用户态之间进行切换,因此Java虚拟机会将多次append方法调用的代码进行一个锁粗化的操作,将多次的append的操作扩展到append方法的头尾,变成一个大的同步块,这样就减少了加锁-->解锁的次数,有效地提升了代码执行的效率。
40、高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?
这是我在并发编程网上看到的一个问题,把这个问题放在最后一个,希望每个人都能看到并且思考一下,因为这个问题非常好、非常实际、非常专业。关于这个问题,个人看法是:
(1)高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换
(2)并发不高、任务执行时间长的业务要区分开看:
a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务
b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换
(3)并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。