创建和运行线程
Thread类
继承Thread类,重写run方法,或者使用匿名内部类。
1 |
|
可以通过setName给线程设置名字
Runnable接口
实现Runnable接口,可用匿名内部类,也可使用lambda表达式。
将runnable实现对象传入Thread的构造器中,使用thread开启线程
1 |
|
1 |
|
@FunctionalInterface 函数式接口,可以使用lambda表达式创建其实现对象
Thread和Runnable的关系
使用Runnable时,将runnable对象传入Thread,会将这个runnable对象赋给Thread中的一个target成员变量,当target不为空时,将会调用run重写方法。
使用Thread时,在子类对象重写了run()方法,最终会执行子类中的run方法。
使用FutureTask
FuturenTask实现了RunableFuture接口,RunnableFuture接口实现了Runnable,Future接口。Future接口中有get方法可以返回任务执行结果。
FuturenTask能够接受Callable类型的参数,用来处理有返回结果的情况。
1 |
|
线程运行的原理
栈与栈帧
每个线程启动后,虚拟机就为其分配一块栈内存。每个栈有多个栈帧组成,对应着每次方法调用时所占用的内存,每一个栈帧只能有一个活动栈帧,对应着当前正在执行的那个方法。
线程上下文切换
以下一些原因导致CPU不再执行当前的线程,转而执行另一个线程的代码
- 线程的CPU时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了sleep,yield、wait、join、park、synchronized、lock等方法
当线程上下文切换发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java中对应的就是程序计数器,它的作用是记住下一条JVM指令的执行地址,是线程私有的。
状态包括:程序计数器,栈中每个栈帧的信息;线程上下文切换频繁会影响性能。
常见方法
- start() 启动一个新线程,在新的线程中运行run方法中的代码;start方法只是让线程进入就绪,具体执行需要任务调度器。每个线程对象的start方法只能调用一次,不能多次调用,否则出现异常
- run() 新线程启动后就会调用run方法
- join() 等待线程运行结束(一直等待线程结束)
- join(long n) 等待线程运行结束,最多等待n毫秒
- getId() 获取线程唯一id
- getName() 获取线程名称
- setName(String) 修改线程名称
- getPriority() 获取线程优先级
- setPriority(int) 修改线程优先级,Java中1~10
- getState() 获取线程状态 Java中有6个线程的状态
- isInterrupted() 判断是否被打断 不会清除打断标记
- isAlive() 线程是否存活
- interrupt() 打断线程
- interrupted() static 判断当前线程是否被打断 会清除打断标记
- currentThread() static 获取当前正在执行的线程
- sleep(long n) static 让当前执行的线程休眠b毫秒,休眠时让出cpu的时间片给其他线程
- yield() static 提示线程调度器让出当前线程对CPU的使用
start和run
若直接调用对象的run方法,则仍然是在主线程中运行。
sleep和yield
sleep
- 调用sleep会让线程从Running进入TimedWaiting状态
- 其他线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出一个异常InterruptedException
- 睡眠结束后的线程未必会立刻得到执行
- 建议用TimeUnit的sleep代替Thread的sleep来获得更好的可读性
1 |
|
interrupt
1 |
|
TimeUnit内部也是调用sleep方法,做了单位的换算
1 |
|
yield
- 调用yield会让当前线程从Running进入Runnable就绪状态,然后调度执行其他线程
- 具体实现依赖于操作系统的任务调度器
线程优先级
- 线程优先级会提示调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
- 如果cpu比较忙,那么优先级高的线程会获得更多的时间片,但CPU闲时,优先级几乎没作用
案例 防止CPU占用100%
在没有利用cpu来计算时,不要让while(true)空转浪费cpu,这时可以使用yield或sleep来让出cpu的使用权给其他程序
1 | while(true) |
- 可以用wait或条件变量达到类似的效果
- 不同的是,后两种需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景
- sleep适用于无需锁同步的场景
join 方法详解
1 | public class Test{ |
上述代码打印r是什么?r=0
分析:因为主线程和线程1是并行执行的,t1线程需要1秒之后才能算出r=10;而主线程一开始就要打印r的结果,所以只能打印出r=0
解决方法:
- 用sleep行不行?为什么 不知道到底sleep多久
- 用join,加在t1.start()之后即可
应用之同步
以调用方角度来讲,如果
- 需要等待结果返回,才能继续运行就是同步
- 不需要等待结果返回,就能继续运行就是异步
有时效的join
join(long n)会等待n毫秒,线程提前结束,则直接不等待。
interrupt方法详解
打断sleep、wait、join进程
打断阻塞状态的线程
1 |
|
对于sleep、wait、join被打断后会被清除打断,它们以异常的形式来展示被打断了。
打断正常运行的线程
1 | public class InterruptNormalTest { |
这样无法打断正常执行的程序,需要在其中的线程中添加判断条件。
1 | public class InterruptNormalTest { |
两阶段终止模式
在一个线程T1优雅的终止线程T2,这里的优雅并不是直接杀死,而是给T2一个终止的准备,最后终止。
错误思路:使用线程对象的stop方法停止线程;stop方法杀死线程后,如果这时线程锁住了共享资源,那么就无法释放锁,其他线程永远也没有机会释放锁。使用System.exit方法停止线程:将会把整个进程都停止。
1 | package cn.xiaohupao.juc; |
1 | package cn.xiaohupao.juc; |
打断park线程
1 | public class ParkTest { |
若线程中的打断标记已经是true,则park会失效。
不推荐的方法
- stop() 停止线程运行
- suspend() 挂起线程运行
- resume() 恢复线程运行
主线程和守护线程
默认情况下,Java进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即时守护线程的代码没有执行完,也会强制结束。
垃圾回收器就是一种守护线程;Tomcat中的Acceptor和Poller线程都是守护线程,所以Tomcat接收到shutdown命令后,不会等待它们处理完当前请求。
1 | package cn.xiaohupao.juc; |
线程的状态
从操作系统层面上分:
- 初始状态:仅在语言层面创建了线程对象,还未与操作系统线程关联
- 可运行状态:线程已被创建,可以由CPU调度执行
- 运行状态:指获取CPU时间片运行中的状态
- 阻塞状态:调用了阻塞API,如BIO读写文件、这时该线程实际不会用到CPU,会导致线程上下文切换,进入阻塞状态
- 需要操作系统唤醒阻塞的线程,转至可运行状态
- 终止状态:表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
从Java API中Thread.State枚举,分为六种状态:
- NEW 线程刚被创建出来,还没有start
- RUNNABLE 当调用start()方法之后,Java API 层面的RUNNABLE状态涵盖了操作系统层面的【可运行状态】、【运行状态】、【阻塞状态】
- BLOCKED、WAITING、TIMED_WAITING都是Java API层面对阻塞状态的细节。
- TERMINATED 当线程代码运行结束
1 | package cn.xiaohupao.juc; |
共享带来的问题
1 | package cn.xiaohupao.juc; |
问题分析:
以上的结果可能是正数、负数、零。自增、自减操作并不是原子操作。多个线程在上下文切换中,因为指令交错而导致线程安全问题。
临界区
- 一个程序运行多个线程本身是没有问题的
- 问题出现在多个线程访问共享资源
- 多个线程读共享资源其实也没有问题
- 在多个线程对共享资源读写操作时发生交错,就会出现问题
- 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
静态条件
多个线程在临界区内执行,由于代码得执行序列不同而导致结果无法预测,称之为发生了竞态条件
synchronized解决方案
为避免临界区的竞态条件发生,有多种手段可以达到目的
- 阻塞式解决方案:synchronized、Lock
- 非阻塞式的解决方案:原子变量
synchronized俗称对象锁,它采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其他线程再想获取这个对象锁时就会阻塞。这样就能保证拥有锁的线程可以安全的执行临界区的代码,不用担心线程上下文切换
1 | synchronized(对象) |
1 | package cn.xiaohupao.juc; |
思考:
synchronized实际上是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断的
- 如果把synchronized(obj)放在for循环的外面,如何理解?–原子性
- 如果把t1 synchronized(obj1)而t2 synchronized(obj2)会怎么运作?–锁对象
- 如果t1 synchronized(obj)而t2没有加会怎么样?如何理解?–锁对象
把需要保护的共享变量放入一个类
1 | package synchronizedTest; |
1 | package synchronizedTest; |
synchronized加在方法上
1 | class Test |
1 | class Test |
线程八锁
1 | //情况1 |
1 | //情况2 |
1 | //情况3 |
1 | //情况4 |
1 | //情况5 |
1 | //情况6 |
1 | //情况7 |
1 | //情况8 |
变量的线程安全分析
成员变量和静态变量是否线程安全
- 如果他们没有共享,则线程安全
- 如果它们被共享了,根据它们的状态是否能够被改变,又分两种情况
- 如果只有读取操作,则线程安全
- 如果有读有写操作,则这段代码是临界区,需要考虑线程安全
局部变量是否线程安全
- 局部变量是线程安全的
- 但局部变量引用的对象则未必
- 如果该对象没有逃离方法的作用访问,它是线程安全的
- 如果该对象逃离方法的作用范围,需要考虑线程安全
1 | public static void test1() |
每个线程调用test1()方法时局部变量u,会在每个线程的栈帧内存中被创建多份,因此不存在共享。
1 | class ThreadUnsafe |
1 | static final int THREAD_NUMBER = 2; |
1 | class ThreadSafe() |
1 | class ThreadSafe() |
线程安全类
String、Integer、StringBuffer、Random、Vector、Hashtable、java.util.concurrent包下的类。
1 | Hashtable table = new Hashtable(); |
- 它们的每个方法是原子的
- 但注意它们多个方法的组合不是原子的
1 | Hashtable table = new Hashtable(); |
不安全的
不可变类线程安全性
String、Integer等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的。
1 | //例1 |
1 | //例2 |
1 | public class MyAspect |
Monitor概念
Java对象头
普通对象
- Object Header
- Mark Word
- hashcode | age | biased_lock(偏向锁) :0 | 01(加锁状态) Normal(状态)
- thread
- Klass Word
- Mark Word
数组对象
- Object Header
- Mark Word
- Klcass Word
- array length
Monitor(锁)
Monitor:监视器或管程
每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁之后,该对象的Mark Word中就被设置指向Monitor对象的指针
- 刚开始Monitor中的Owner为nul
- 当Thread-2执行synchronized(obj)就会将Monitor的所有者Owner置为Thread-2,Monitor中只能有一个Owner
- 在Thread-2上锁的过程中,如果Thread-3,Thread-4、Thread-5也来执行synchronized(obj),就会进入EntryList BLOCKED
- Thread-2执行完同步代码块的内容,然后唤醒EntryList中等待的线程来竞争锁,竞争的时是非常公平的
- WaitSet中的Thread-0,Thread-1是之前获得过锁,但条件不满足进入WAITING状态的进程
注意:
- synchronized必须是进入同一个对象的monitor才有上述的效果
- 不加synchronized的对象不会关联监视器,不遵从以上规则
字节码角度
轻量级锁
轻量级锁的使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是synchronized。
- 创建锁记录对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word
- 让锁记录中的Object reference指向锁对象,并尝试用cas替换Object的Mark Word
- 如果cas替换成功,对象头中存储了锁记录地址和状态00,表示由该线程给对象加锁
- 如果cas失败,有两种
- 如果是其它线程已经持有了该Object的轻量级锁,这时表明有竞争,进入锁膨胀过程
- 如果是自己执行了synchronized锁重入,那么再添加一条Lock Record
- 当退出synchronized代码块锁记录的值为null,表示有重入,这时重置锁记录,表示重入计数减一
- 当退出synchronized代码块锁记录的值不为null,这时使用cas将Mark Word的值恢复给对象头
- 成功,则解锁成功
- 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级解锁流程。
锁膨胀
如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
- 当Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁
- 这时Thread-1加轻量级锁失败,进入锁膨胀流程
- 即为Object对象申请Monitor锁,让Object指向重量级锁的地址
- 然后自己进入Monitor的EntryList的BLOCKED
- 当Thread-0退出同步块解锁时,使用cas将Mark Word的值恢复给头对象,失败。这时会进入重量级解锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList中的BLOCKED线程
自旋优化
重量级锁竞争的时候,还可以使用自选来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放锁),这时当前线程就可以避免阻塞。
自旋重试成功的情况
- 在Java6之后自旋锁是自适应的
- 自旋锁占用CPU时间,单核CPU自旋就是浪费,多核自旋才能发挥优势
- Java7之后不能控制是否开启自旋功能
偏向锁
轻量级锁在没有竞争时,每次重入仍然需要执行CAS操作
Java6中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不同重新CAS。以后只要不发生竞争,这个对象就归该线程所有
偏向状态
对象头格式
Mark Word | State | |
---|---|---|
biased_lock:0 \ | 01 | Normal |
biased_lock:1 \ | 01 | Biased |
\ | 00 | Lightweight Locked |
\ | 10 | Heavyweight Locked |
一个对象创建时:
- 如果开启了偏向锁,那么对象创建后,markword值为0x05即最后三位是101,它的thread、epoch、age都为0
- 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以添加vm参数-XX:BiasedLockingStartupDelay = 0来禁用延迟
- 如果没有开启偏向锁,那么对象创建后,markword值为0x01即最后3位为001,这时它的hashcode、age都为0,第一次用到hashcoe时才会赋值
-xx:-UseBiasedLocking禁用偏向锁
撤销-调用对象 hashCode
调用了对象的hashCode,但偏向锁的对象MarkWord中存储的线程id,如果调用hashCode会导致偏向锁被撤销。
- 轻量级锁会在锁记录中记录hashcode
- 重量级锁会在Monitor中记录hashcode
撤销-其他线程使用对象
当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
批量重偏向
如果对象被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍然有机会重新偏向T2,重偏向会重置对象的Thread ID
当撤销偏向锁阈值超过20次后,JVM会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至枷锁线程
批量撤销
当撤销偏向锁超过40次后,JVM会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向,新建的对象也是不可偏向的。
锁消除
wait/notify原理
- Owner线程发现条件不满足,调用wait方法,即可进入WaitSet变为WAITING状态
- BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片
- BLOCKED线程会在Owner线程释放锁时唤醒
- WAITING线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后并不意味着立刻获得锁,仍然进入EntryList重新竞争
API介绍
- obj.wait()让进入object监视器的线程到waitSet等待
- obj.notify()在object上正在waitSet等待的线程中挑一个唤醒
- obj.notifyAll()让object上正在waitSet等待的线程全部唤醒
它们都是线程之间进行协作的手段,都属于Object对象的方法。必须获得此对象的锁,才能调用这几个方法
1 | package cn.xiaohupao.juc; |
wait(long n)有时限的等待,到n毫秒结束等待或是被notify
wait(long timeout, int nanos) 假的纳秒,让timeout+1
wait VS sleep
- sleep是Thread静态方法,wait是Object的方法
- sleep不需要强制和synchronized配合使用;但wait需要和synchronized一起用
- sleep在睡眠的同时,不会释放对象锁,但wait在等待的时候会释放对象锁
- 它们的状态都是TIMEWATING
1 | synchronized(look){ |
同步模式之保护性暂停
用一个线程等待另一个线程的执行结果
要点:
- 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个GuardedObject
- 如果有结果不断从一个线程到另一个线程那么可以使用消息队列
- JDK中,join的实现,Future的实现,采用的就是此模式
- 因为要等待另一方的结果,因此归类于同步模式
1 | package cn.xiaohupao.juc; |
1 | package cn.xiaohupao.juc; |
1 | package cn.xiaohupao.juc; |
保护暂停-增加超时
1 | public Object getResponse(long timeOut){ |
join原理
1 | public final synchronized void join(long millis) |
Park & Unpark
是LockSupport类中的方法
1 | LockSupport.park();//暂停当前线程 |
1 | package cn.xiaohupao.juc; |
与Object中的wait和notify相比:
- wait,notify和notifyAll必须配合Object Monitor一起使用,而unpark不必
- park & unpark是以线程为单位来【阻塞】和【唤醒】线程,而notify只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】
- park & unpark可以先unpark,而wait & notify不能先notify
park & unpark原理
每个线程都有自己的一个Parker对象,由三部分组成_counter, _cond和_mutex。
- 调用park就是要看需不需要停下来歇息
- 如果备用干粮耗尽,那么就休息
- 如果备用干粮充足,那么就继续前进
- 调用unpark,就好比令干粮充足
- 如果线程在休息,就唤醒它继续前进
- 如果这时线程还在运行,那么下次他调用park时,消耗掉备用干粮,不需要休息继续前进
- 多次调用unpark仅会补充一份备用干粮
线程状态转换
NEW ——> RUNNABLE
当调用t.start()方法时,由NEW - - - > RUNNABLE
RUNNABLE ———> WAITING
t线程用synchronized(obj)获取了对象锁后;
- 调用了obj.wait()方法时,t线程从runable- - -> waiting
- 调用obj.notify(),obj.notifyAll(),t.interrupt()时
- 竞争成功,t线程从waiting - - - > runnable
- 竞争锁失败,t线程从waiting - - - > blocked
1 | package cn.xiaohupao.juc; |
- 当前线程调用t.join()方法时,当前线程从runnable - - - > waiting
- 注意是当前线程在t线程对象的监视器上等待
- t线程运行结束,或调用了当前线程的interrupt(),当前线程从waiting - - -> runnable
当前线程调用LockSupport.park()方法会让当前线程从runnable - - -> waiting
调用LockSupport.unpark或调用了线程的interrupt(),会让目标线程从 waiting - - > runnable
RUNNABLE ——> TIME_WAITING
t线程用synchronized(obj)获取了对象锁后:
- 调用obj.wait(long n)方法时,t线程从runnable - - - > time_waiting
- t线程等待时间超过了n毫秒,或调用obj.notify(),obj.notifyAll(), t.interrupt()时
- 竞争锁成功,t线程从time_waiting - - - > runnable
- 竞争锁失败,t线程从time_waiting - - - > blocked
当前线程调用t.join(long n)方法时,当前线程从runnable - - - > time_waiting
当前线程时间超过了n毫秒,或t线程运行结束,或调用了当前线程的interrupt()时,当前线程从time_waiting - - - > runnable
当前线程调用了Thread.sleep(long n),当前线程从runnable - - - > time_waiting
当前线程等待时间超过了n毫秒,当前线程从time_waiting - - - > runnable
当前线程调用LockSupport.parkNanos(long nanos)或LockSupport.parkUntil(long millis)时,当前线程从Runnable - - - > time_waiting
当调用LockSupport.unpark(目标线程)或调用了线程的interrupt()或是等待超时,会让目标线程从time_waiting - - -> runnable
RUNNABLE < - - - > BLOCKED
线程用synchronized(obj)获取了对象锁如果竞争失败,从runnable - - - > blocked
持obj锁线程的同步代码块执行完毕,会唤醒该对象上所有blocked的线程重新竞争,如果其中t线程竞争成功,从block - - - > runnable ,其它失败的线程仍然blocked
RUNNABLE < - - > TERMINATED
当前线程所有代码运行完毕,进入TERMINATED
线程安全集合类概述
遗留的安全集合
- Hashtable
- Vector
修饰的安全集合类
Collections修饰的线程安全集合
JUC安全集合
- Blocking类
- 大部分实现基于锁,并提供用来阻塞的方法
- CopyOnWrite类
- 写|修改的开销相对较重
- Concurrent类
- 内部很多操作使用cas优化,可以提高吞吐量
- 弱一致性
- 遍历时弱一致性,如果容器发生修改,迭代器仍然可以继续进行遍历,这时内容是旧的
- 求集合大小也不一定正确
- 读取时弱一致性
concurrentHashMap
HashMap并发死链
get流程
1 | public V get(Object key) { |
put方法
1 | final V putVal(K key, V value, boolean onlyIfAbsent) { |
数组初始化
1 | private final Node<K,V>[] initTable() { |