前言
- 知识大部分摘录于《Thinking In Java》(Java编程思想)和《Core Java Volume I-Fundamentals》(Java核心技术卷I)
- 个人理解和来源于网上资料由于权威性问题,会额外标出
- 遇到不懂得的建议查看原书籍,毕竟这是我的个人理解后的产物,是会存在偏差的
线程
线程状态
- New(新创建):线程刚创建,尚未完成某些初始化动作(如将其设置为deamon),尚未运行线程中的代码,一个线程一旦开始运行,状态改为Runnable,就再也无法变回New
- Runnable(可运行):可能在运行,也可能没运行,它不必始终保持运行,当它被中断,目的是为了给其他线程提供运行机会(大部分操作系统使用的都是抢占式调度)
- Blocked(被阻塞):如果某个程序因为该程序控制范围之外的某些条件(通常是I/O)导致不能继续执行,称为任务或线程阻塞。常见情况就是当线程尝试获取一个内部的对象锁,锁被其他线程持有,则该线程被动进入阻塞状态
- Waiting(等待):线程等待另一线程通知调度器一个条件时,它自己主动进入等待状态
- Timed waiting(计时等待):同等待,不过等待条件额外增加一个,即超过时间限制(个人理解)
- Terminated(被终止):因为run方法正常退出自然死亡,或因为一个没有捕获的异常终止run方法而意外死亡
源码如下
//这是Thread类中的一个内部枚举类
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
线程机制
- 抢断式(Java):调度机制会周期性地中断进程,将上下文切换到另一个进程,从而为每个进程都提供时间片
- 协作式:每个任务都会自动放弃控制,这要求程序员要有意识地在每个任务中插入让步代码,上下文切换开销小,并且同时执行地线程数量在理论上没有任何限制
线程属性
线程优先级
- 每个线程都有一个优先级,默认继承父线程的优先级
- 可以通过
public final int getPriority()
获取优先级 - 可以通过
public final void setPriority(int newPriority)
设置优先级,1~10,数值越大,优先级越高,Thread类定义了几个常量用来表示优先级public final static int MIN_PRIORITY = 1; public final static int NORM_PRIORITY = 5; public final static int MAX_PRIORITY = 10;
- 线程优先级高度依赖于系统,当虚拟机依赖于宿主机平台的线程实现机制时,Java线程优先级被映射到宿主机平台的优先级上,优先级个数也许更多,也许更少,比如Windows只有7个优先级别,而Linux直接忽略优先级,所有线程具有相同优先级
守护线程
- 后台线程是程序运行时,在后台提供一种通用服务的线程,并不属于程序中不可或缺的一部分
- 当所有非后台线程都结束时,程序就会终止,并杀死所有后台线程(此时后台线程执行的任务会突然结束run方法,而且如果存在finally子句,该子句是不会被执行的)
- 后台线程创建的所有线程都是后台线程
- 可以通过调用
(Thread对象).setDeamon(true)
来将线程转换为守护线程,该方法必须在线程启动前调用 - 可以用过
boolean isDaemon()
确定该线程是否为守护线程
未捕获异常处理器
- 线程run方法不能抛出任务受查异常,但是非受查异常会导致线程终止,此时,线程死亡,但是,在线程死亡之前,异常会被传递到一个用于未处理捕获异常的处理器!
- 该处理器所属类必须实现Thread.UncaughtExceptionHandler接口,重写
void uncaughtException(Thread t,Throwable e)
- 可以使用Thread类的
void setUncaughtExceptionHandler(UncaughtExceptionHandler eh)
为任何线程安装一个处理器 - 可以使用Thread类的静态方法
static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh)
为所有线程安装一个默认处理器 - 如果不安装默认处理器,默认处理器为空
- 如果不为独立的线程安装默认处理器,此时的处理器就是该线程的ThreadGroup对象,ThreadGroup重写了uncaughtException方法,源码如下
public void uncaughtException(Thread t, Throwable e) {
//首先判断父线程组是否存在,存在直接调用父线程组的方法
if (parent != null) {
parent.uncaughtException(t, e);
} else {
//父线程组不存在,直接调用默认处理器
Thread.UncaughtExceptionHandler ueh =
Thread.getDefaultUncaughtExceptionHandler();
if (ueh != null) {
ueh.uncaughtException(t, e);
} else if (!(e instanceof ThreadDeath)) {
//默认处理器也不存在,直接打印栈轨迹(就是我们看到的报错信息)
System.err.print("Exception in thread \""
+ t.getName() + "\" ");
e.printStackTrace(System.err);
}
}
}
定制线程属性
可以通过给Excutor传入一个ThreadFactory的实现类对象,该实现类需要重写方法
Thread newThread(Runnable r){
Thread t = new Thread(r);
//对线程进行处理,比如设置优先级,设置是否为后台进程
return t;
}
线程与任务
- 线程就像铁道,而任务就是上面的火车,没有火车的铁道,仅仅就是铁道,不能发挥任何作用。每一个线程创建出来,就是为了执行任务的(个人理解)
任务描述方式(线程驱动任务)
- Runnable接口(不具备返回值):实现Runnable接口并重写run()方法,run()里即我们需要执行的命令(通常方法中会有某种模式的循环,使得任务一直运行下去直到不再需要,此时就需要跳出循环的条件)
public interface Runnable {
public abstract void run();
}
- Callable<V>接口(具备类型为V的返回值):必须使用ExecutorService.submit()调用它
public interface Callable<V> {
V call() throws Exception;
}
Thread类
run()&start()(个人理解):
- run()方法
- 创建线程时,如果不传入Runnable对象,则执行run()方法时,什么也不做(有轨道没火车)
- 创建线程时,如果传入Runnable对象,则执行run()方法时,将执行传入的Runnable对象的run()方法(有轨道有火车)
/* What will be run. */
private Runnable target;
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
...
public void run() {
if (target != null) {
target.run();
}
}
- start()方法
- Thread的start()方法,首先判断线程的状态是否为0(对应Thread.State类的NEW状态,即新创建),否则抛出异常
- 其次将该线程加入当前线程组(一个线程创建时,如果没有主动指定线程组,将会默认加入父线程的线程组)
- 尝试调用native方法启动线程,如果发生异常,将该线程记录为启动失败线程
/* The group of this thread */
private ThreadGroup group;
private volatile int threadStatus = 0;
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
- 注意:不要直接调用run()方法,这样只会执行一个线程中的任务,而不会启动新线程,应该调用start()方法,该方法将创建一个新线程执行run()方法
interrupt()
- 用于请求终止线程:当线程调用该方法时,该线程的中断状态将被置位,这是每一个线程都具有的boolean标志,每个线程都会不时地检查该标志,以判断该线程是否被中断
interrupted()&isInterrupted()
- interrupted()
1.用于判断线程地中断状态是否被置位,会清除中断状态
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
- isInterrupted()
1.用于判断线程地中断状态是否被置位,不会清除中断状态
public boolean isInterrupted() {
return isInterrupted(false);
}
private native boolean isInterrupted(boolean ClearInterrupted);
- 当线程被阻塞,便无法检测中断状态,会产生InterruptedException
- 在中断状态调用sleep,不但不会休眠,反而会清除中断状态并抛出InterruptedException
join()
- void join():在当前线程A中加入调用该方法的线程B,此时线程A被挂起,直到线程B执行完毕,线程A才会继续执行
- void join(long t): 在当前线程A中加入调用该方法的线程B,此时线程A被挂起t毫秒,执行线程B,然后两个线程交互执行
yield()
- static void yield():给线程调度器(java线程机制的一部分,可以将CPU从一个线程转移给另一个线程)暗示可以把CPU让给其他线程(但是只是一个暗示,没有任何机制保证它将被采纳,所以对于任何重要的控制或在调整应用时,都不能依赖于yield())
并发
基础概念梳理
- 并发与并行(百度百科摘录) :
– 并发(两个或多个事件在同一时间间隔内发生):指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行
– 并行(两个或者多个事件在同一时刻发生):指若干个程序段同时在系统中运行,这些程序的执行在时间上是重叠的,一个程序段的执行尚未结束,另一个程序段的执行已经开始,无论从微观还是宏观,程序都是一起执行的 - 进程与线程
– 进程(资源不共享):进程是运行在它自己的空间地址的自包容程序,多任务操作系统可以周期性地将CPU从一个进程切换到另一个进程,来实现多个进程(程序)运行,操作系统通常会将进程相互隔离开,使他们不会互相干涉
– 线程(资源共享):共享诸如内存和I/O这样的资源,协调不同线程驱动的任务之间对这些资源的使用,使得这些资源不会被多个任务同时访问 - 多线程程序最基本困难:解决资源共享问题
使用目的
- 大幅度提高程序执行速度:利用额外的CPU,将程序切分成多个片段,在单独的CPU上运行每个片段
- 提高容错率,适应更多使用场景:单线程运行时,当出现阻塞,整个程序都要停止,但是多线程运行时,某一线程停止,其他线程还能继续运行,程序还能继续运行
产生具有可响应页面的程序:假如有一个程序,需要长期地运行某个操作如果程序是单线程的,那么用户输入的动作作为另一个任务,就会一直被阻塞。除非处理方法就是在打印文字的任务中增加周期性检查用户输入动作的代码。这时候就可以创建一个单独的线程来响应用户输入(因为等待用户输入,所以该线程大部分时间是阻塞的)
线程池
- CachedThreadPool(首选):使用过程中会创建与所需数量相同的线程,然后回收旧线程时,停止创建新线程
- FixedThreadPool:使用了有限的线程集来执行所提交的任务,可以一次性预先进行线程分配
- SingleThreadExecutor:就像是线程数为1的FixedThreadPool。如果向其提交了多任务,那么这些任务将会排队,所有的任务都会使用相同的线程,按照它们被提交的顺序进行执行,SingleThreadExecutor会序列化所有提交给它的任务,并会维护自己(隐藏)的悬挂任务队列
接口java.util.concurrent.Executor(执行器)
1 作用:用于管理Thread对象,简化并发编程
2 原理:Executor在客户端和执行层之间提供了一个间接层(与客户端直接执行任务不同,这个中介对象将执行任务。Executor允许你管理异步任务的执行,而无需显式地管理线程的生命周期)
3 ExecutorService(接口)——具有服务生命周期的执行器
类ExecutorService
1.submit()方法用于提交任务,可以是
①Runnable,无返回值得任务
②Callable<V>,有返回值的任务,此时用Future<V>接收
2.shutdown()方法可以防止新任务被提交给这个Executor
类Future<V>
①boolean isDone() 用于查询Future是否完成
②V get() 用于获取完成结果
当不调用isDone方法就直接使用get方法也是可行的,但是此时get将阻塞,直到结果准备就绪
Lock接口与Condition接口(高度订制的锁控制)
- 基础使用(实现类ReentrantLock为例)
//Lock对象必须被显式地创建、锁定、释放
private Lock lock=new ReentrantLock();
//使用lock()和unlock()在use方法内创建了临界资源
public Source use(){
lock.lock();
try{
//处理资源
return //return必须放在try块中,防止unlock()过早发生,从而将数据暴露给第二个任务;
}catch(){……}finally{
lock.unlock();
}
}
- 公平策略
构造一个带有公平策略(一个公平锁偏爱等待时间最长的线程)的锁
ReentrantLock(boolean fair)
- 条件对象(条件变量)Condition接口
public static Lock lock = new ReentrantLock();
public static Condition condition= lock.newCondition();
线程一
public void method{
lock.lock()
try{
while(...){
//wait,持有lock锁,进入等待阶段
}
}
}
1. 当线程一执行到wait处时,线程一持有lock锁,此时由于等待某些条件达成的原因,线程进入等待阶段
2. 其他线程因为锁被持有,进入阻塞阶段,只能一直等待,但达成条件需要其他线程获得lock锁,于是进入死锁状态
解决方案
public static Lock lock = new ReentrantLock();
public static Condition condition= lock.newCondition();
线程一
public void method{
lock.lock();
try{
while(...){
condition.wait();//放弃lock锁,进入阻塞阶段
...
}
}finally{
lock.unlock();
}
}
线程二
public void method1{
lock.lock()
try{
while(...){
//做了某些事使得条件达成
condition.signalAll();//唤醒所有因此条件进入等待的线程,但是此时该线程仍然持有lock锁,其他线程仍然需要等待锁的释放
}
}finally{
lock.unlock();//待该线程释放锁后,其他因为signalAll()被唤醒的线程需要抢到lock锁,才能继续执行
}
}
关键字synchronized(独占锁、悲观锁)
- 嵌入Java语言内部的锁机制
public synchronized void method{
[method body]
}
等价于
public void method(){
this.intrinsicLock.lock();
try{
[method body]
}finally{
this.intrinsicLock.unlock();
}
}
- 内部对象锁有且只有一个默认相关条件(假设为intrinsicCondition)
作为Obecjt类final方法,Condition接口的方法名必须不与其冲突
public interface Condition {
void await() throws InterruptedException;
void signalAll();
void signal();
}
public class Object {
public final native void notify();
public final native void notifyAll();
public final native void wait(long timeout) throws InterruptedException;
}
从Object继承的以上三个方法,实际上等价于对该内部对象锁的默认相关条件调用
this.wait()————intrinsicCondition.await()
this.notify()————intrinsicCondition.signal()
this.notifyAll()————intrinsicCondition.signalAll()
- 注意:以上三个方法必须在持有内部对象锁时调用,否则会抛出IllegalMonitorStateException异常
- 将资源(文件、输入/输出端口、打印机等)包装进一个对象,然后把所有要访问这个对象的方法标记为synchronized。
- 如果某个任务处于对一个标记为synchronized方法进行调用,那么在这个线程从该方法返回前,其他所有要调用类中任何标记为synchronized方法的线程都会被阻塞
- 所有的对象都拥有单一的锁(也称为监视器),当在对象上调用其任意synchronized方法时,此对象都被加锁,所有synchronized方法共享一把锁
- 一个任务可以多次获得对象的锁,当一个任务执行了一个synchronized方法,锁计数+1,该方法内部又调用了这个类的另一个synchronized方法,锁计数+1(此时等于2),每当任务离开一个synchronized方法,锁计数-1,直到锁计数=0,才代表锁被完全释放,此时别的任务才能使用该资源
- 针对类也存在一个锁(作为Class对象的一部分),所以synchronized static方法可以在类范围内防止对static数据的并发访问
监视器
-
概念
- 监视器是只包含私有域的类
- 每个监视器类的对象有一个相关的锁
- 使用该锁对所有方法进行加锁
- 该锁可以拥有任意多个相关条件
-
java对象与监视器的区别
- 域不要求必须private
- 方法不要求必须是synchronized
- 内部锁对客户是可用的
解决共享资源竞争
解决方式概述:基本上所有的并发模式在解决线程冲突时,都是采用序列化访问共享资源的方案,这意味着在给定时刻,只允许一个任务访问共享资源
相关基础概念
①Brian的同步规则:如果正在编写一个变量,它可能接下来被另一个线程读取,或者正在读取上一个已经被另一个线程写过的变量,那么必须同步,并且,读写线程必须用相同的监视器锁同步
②临界区:如果希望阻止多个线程同时访问方法内部的部分代码而不是整个方法,通过这种方式分离出来的代码段被称为临界区
原子性:
①原子操作:不能被线程调度机制中断的操作,一旦操作开始,那么它一定可以在可能发生的“上下文切换”之前(切换到其他线程)执行完毕,如:对域中的值进行赋值和返回操作
②原子性可以应用于除long和double外的所有基本类型上的操作,JVM可以将64位(long和double变量)的读写当作两个分离的32位操作来执行,这就产生了一个读写过程中发生上下文切换的可能(称为字撕裂),但是如果当定义long和double类型变量时,使用volatile关键字,就可以获得原子性
可视性:
描述:多个线程访问同一个共享变量,一旦某一线程修改了这个变量的值,其他线程能够立即看到修改的值
缓存一致性问题:计算机在计算时,每条指令都是放在CPU中执行的,程序运行时,临时数据存放在主存(物理内存中),CPU执行速度极快,而从主存中读写数据与之相比较慢,所以CPU里存在自己的高速缓存。当高速缓存的数据已经发生更改,还未同步到主存,别的数据在主存 中读到的数据将不是最新数据,基于此旧数据的操作将会产生错误
缓存一致性解决方案(硬件层面):
A 在CPU总线加LOCK锁方式
B 高速缓存一致性协议,Intel的MESI协议
M(Modified)行数据有效,数据被修改了,和内存中数据不一致,数据只存在于本Cache中
E(Exclusive)行数据有效,数据和内存一致,只存在于本Cache中
S(Shared)行数据有效,数据和内存一致,存在于很多Cache中
I(Invalid)行数据无效
原理:当CPU写数据时,如果发现操作的变量是共享变量,会发出信息通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存中重新读取
3 关键字volatile:保证了可见性,但无法保证原子性,让变量的读取从主存直接到CPU,而不经过 cpu Cache
A 在非volatile的域的原子操作不必刷新到主存中(如果多个任务访问同一个域,那么这个域就应该是volatile,否则该域就只能由同步来访问)
B 当一个域的值依赖于它之前的值,volatile就无法工作
C 当一个域的值受到其他域的值的限制,volatile也无法工作