thread
java多线程
线程的基础
线程进程区别
- 进程是操作系统的分配和调度系统内存资源、cpu时间片等<font color="red">资源</font>的基本单位,为正在运行的应用程序提供运行环境。
- 线程程序内部有并发性的顺序代码流,是cpu<font color="red">调度</font>资源的最小单元
Java线程模型
20160506143812820.jpgLinux,windows 操作系统下都是使用内核线程 - Kernel Thread
内核线程
内核线程就是内核的分身,一个分身可以处理一件特定事情。内核线程的使用是廉价的,唯一使用的资源就是内核栈和上下文切换时保存寄存器的空间。支持多线程的内核叫做多线程内核(Multi-Threads kernel )。
- 内核态: CPU可以访问内存所有数据, 包括外围设备, 例如硬盘, 网卡. CPU也可以将自己从一个程序切换到另一个程序
- 用户态: 只能受限的访问内存, 且不允许访问外围设备. 占用CPU的能力被剥夺, CPU资源可以被其他程序获取
线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作
轻量级进程(LWP)是建立在内核之上并由内核支持的用户线程,它是内核线程的高度抽象,每一个轻量级进程都与一个特定的内核线程关联。内核线程只能由内核管理并像普通进程一样被调度。
线程的状态
教科书上线程的状态
[图片上传失败...(image-7717e8-1547719360944)]
- Ready 代表当前的调度实例在可执行队列中,随时可以被切换到占用处理器的运行状态。
- Running代表当前的调度实例正在占用处理器运行中。
- Blocked(waiting)代表当前的调度实例在等待相应的资源。
linux线程的状态:
- 就绪:线程分配了CPU以外的全部资源,等待获得CPU调度
- 执行:线程获得CPU,正在执行
- 阻塞:线程由于发生I/O或者其他的操作导致无法继续执行,就放弃处理机,转入线程就绪队列
- 挂起:由于终端请求,操作系统的要求等原因,导致挂起。
java 线程的状态
public enum State {
/**
* 线程被new出来
*/
NEW,
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
* 线程的runnable状态是指线程正在被虚拟机执行,但是它可能正在等待操作系统的资源比如处理器
*/
RUNNABLE,
BLOCKED,
/**
* 线程处于WAITING状态是在调用Object.wait,Thread.join,LockSupport#park()后
*/
WAITING,
/**
* Thread.sleep,Object.wait带时间参数,Thread.join 带时间参数,
* LockSupport.parkNanos,LockSupport.parkUntil时处于TIMED_WAITING
*/
TIMED_WAITING,
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;
}
How does the thread state of Java map to linux or windows ? If the state of Java is runnable, what is on Linux or windows ?
A thread can be in only one state at a given point in time.
These states are virtual machine states which do not reflect
any operating system thread states.java线程的状态和操作系统线程没有映射关系(来自java doc)
NEW
线程的创建方式
- 线程的创建与运行
-
java中有三种线程创建方式,实现Runnable接口的run方法,继承Thread类并重写run方法,使用FutureTask方式,JAVA 8可以使用CompletableFuture
-
Thread
- 好处:获取当前线程方便,直接this
- 坏处:不能继承其他类了,任务与代码没区分,无返回值
-
Runnable接口
- 好处:可继承其他类,多任务
- 坏处:无返回值
-
callable接口
- 好处:有返回值
-
CompletableFuture的优势
提供了异步程序执行的另一种方式:回调,不需要像future.get()通过阻塞线程来获取异步结果或者通过isDone来检测异步线程是否完成来执行后续程序。
能够管理多个异步流程,并根据需要选择已经结束的异步流程返回结果。
-
RUNNABLE
/**
* 线程在等待监视器锁的状态,线程进入同步代码块或同步方法,或者调用wait()方 法后再次进
* 入同步代码块或同步方法
*/
为什么只有runnable状态 没有区分running 和 ready状态
现在的时分(time-sharing)多任务(multi-task)操作系统架构通常都是用所谓的“时间分片(time quantum or time slice)”方式进行抢占式(preemptive)轮转调度(round-robin式)。
这个时间分片通常是很小的,一个线程一次最多只能在 CPU上运行比如10ms-20ms 的时间(此时处于 running 状态),也即大概只有 0.01 秒这一量级,时间片用后就要被切换下来放入调度队列的末尾等待再次调度。(也即回到 ready 状态)
由于线程切换的如此的快,因此把这两个统一为runnable状态
传统概念中的阻塞状态也可以映射到runnable状态
线程的阻塞
隐式锁(Synchronized)
相对JDK提供的concurrent包中的实现Lock接口的锁工具类,是jvm所实现的锁
隐式锁的使用
//对普通方法同步
public synchronized void sayGoodbye() {
System.out.println("say good bye");
}
//对静态方法同步
public synchronized static void sayHi() {
System.out.println("say hi");
}
//对方法块同步
public void sayHello() {
synchronized (LockTest.class) {
System.out.println("say hello");
}
}
synchronized的实现简单说明
- 从字节码角度分析
- 方法块同步
0 ldc #6 <thread/ByteCodeDemo>
2 dup
3 astore_1
4 monitorenter
5 getstatic #2 <java/lang/System.out>
8 ldc #7 <say hello>
10 invokevirtual #4 <java/io/PrintStream.println>
13 aload_1
14 monitorexit
15 goto 23 (+8)
18 astore_2
19 aload_1
20 monitorexit
21 aload_2
22 athrow
23 return
synchronized代码块是由monitorenter和monitorexit两个指令实现的。关于这两个字节码虚拟机规范是这么说的
monitorenter:任何对象都有一个 monitor(这里 monitor 指的就是锁) 与之关联(规范上说,对象与其 monitor 之间的关系有很多实现,如 monitor 可以和对象一起创建销毁,也可以线程尝试获取对象的所有权时自动生成)。当且仅当一个 monitor 被持有后,它将处于锁定状态。线程执行到 monitorenter 指令时,将会尝试获取 objectref 所对应的 monitor 的所有权,那么:如果 objectref 的 monitor 的进入计数器为 0,那线程可以成功进入 monitor,以及将计数器值设置为 1。当前线程就是 monitor 的所有者。如果当前线程已经拥有 objectref 的 monitor 的所有权,那它可以重入这个 monitor,重入时需将进入计数器的值加 1。如果其他线程已经拥有 objectref 的 monitor 的所有权,那当前线程将被阻塞,直到 monitor 的进入计数器值变为 0 时,重新尝试获取 monitor 的所有权。
monitorexit:objectref必须为reference类型数据。执行monitorexit指令的线程必须是objectref对应的monitor的所有者。指令执行时,线程把monitor的进入计数器值减1,如果减1后计数器值为0,那线程退出monitor,不再是这个monitor的拥有者。其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。
- 方法同步
0 getstatic #2 <java/lang/System.out>
3 ldc #5 <say hi>
5 invokevirtual #4 <java/io/PrintStream.println>
8 return
对静态方法同步和方法块同步并没有 monitor 相关指令,而是多了 invokevirtual 指令。 invokevirtual 指令是用 来调用实例方法,依据实例的类型进行分派
Java 虚拟机规范上描述该指令:如果调用的是同步方法,那么与 objectref 相关的同步锁将会进入或者重入,就如同当前线程中执行了 monitorenter 指令一般。
虚拟机可以从方法常量池中的方法表结构(method_info structure)中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否是同步方法。当调用方法时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否设置,如果设置了,执行线程先持有同步锁,然后执行方法,最后在方法完成时释放锁。
-
synchronized锁的位置
- 锁存放在对象头中
对象实例由对象头、实例数据组成,其中对象头包括markword和类型指针,如果是数组,还包括数组长度。
markword的结构,定义在markOop.hpp文件:
// 64 bits: // -------- // unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object) // JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object) // PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object) // size:64 ----------------------------------------------------->| (CMS free block) // // unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object) // JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object) // narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object) // unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
-
hotSpot 如何具体实现
- 源码中锁的入口
在HotSpot的中有两处地方对
monitorenter
指令进行解析:一个是在bytecodeInterpreter.cpp#1816 ,另一个是在templateTable_x86_64.cpp#3667。JVM中的字节码解释器(
bytecodeInterpreter
),用C++实现了每条JVM指令(如monitorenter
、invokevirtual
等),其优点是实现相对简单且容易理解,缺点是执行慢。后者是模板解释器(templateInterpreter
),其对每个指令都写了一段对应的汇编代码,启动时将每个指令与对应汇编代码入口绑定,可以说是效率做到了极致。两者的原理是一致的,大家分析的时候可以基于字节码解释器的源码进行分析。-
为什么要做优化
monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高
-
有哪些优化
Java SE1.6为了减少获得锁和释放锁所带来的性能消耗,引入了“偏向锁”和“轻量级锁”,所以在Java SE1.6里锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。
锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
锁粗化(Lock Coarsening):将多个连续的锁扩展成一个范围更大的锁,用以减少频繁互斥同步导致的性能损耗。 锁消除(Lock Elimination):JVM及时编译器在运行时,通过逃逸分析,如果判断一段代码中,堆上的所有数据不会逃逸出去从来被其他线程访问到,就可以去除这些锁。 轻量级锁(Lightweight Locking):JDK1.6引入。在没有多线程竞争的情况下避免重量级互斥锁,只需要依靠一条CAS原子指令就可以完成锁的获取及释放。 偏向锁(Biased Locking):JDK1.6引入。目的是消除数据再无竞争情况下的同步原语。使用CAS记录获取它的线程。下一次同一个线程进入则偏向该线程,无需任何同步操作。 适应性自旋(Adaptive Spinning):为了避免线程频繁挂起、恢复的状态切换消耗。产生了忙循环(循环时间固定),即自旋。JDK1.6引入了自适应自旋。自旋时间根据之前锁自旋时间和线程状态,动态变化,用以期望能减少阻塞的时间。
-
加锁的流程
第一步,检查MarkWord里面是不是放的自己的ThreadId ,如果是,表示当前线程是处于 “偏向锁”.跳过轻量级锁直接执行同步体。
584866-20170419194339446-1408410540.png
第二步,如果MarkWord不是自己的ThreadId,锁升级,这时候,用CAS来执行切换,新的线程根据MarkWord里面现有的ThreadId,通知之前线程暂停,之前线程将Markword的内容置为空。
第三步,两个线程都把对象的HashCode复制到自己新建的用于存储锁的记录空间,接着开始通过CAS操作,把共享对象的MarKword的内容修改为自己新建的记录空间的地址的方式竞争MarkWord.
第四步,第三步中成功执行CAS的获得资源,失败的则进入自旋.
第五步,自旋的线程在自旋过程中,成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于轻量级锁的状态,如果自旋失败 第六步,进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己
584866-20170419191951321-2145960409.png
WAITING
/**
* Thread.sleep,Object.wait带时间参数,Thread.join 带时间参数,
* LockSupport.parkNanos,LockSupport.parkUntil时处于TIMED_WAITING
*/
- wait notify的实现
- wait notify 必须在同步代码中使用
ObjectMonitor() {
_header = NULL;//markOop对象头
_count = 0;
_waiters = 0,//等待线程数
_recursions = 0;//重入次数
_object = NULL;//监视器锁寄生的对象。锁不是平白出现的,而是寄托存储于对象中。
_owner = NULL;//指向获得ObjectMonitor对象的线程或基础锁
_WaitSet = NULL;//处于wait状态的线程,会被加入到wait set ObjectWaiter 类型;
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ;//处于锁block状态的线程,会被加入到entry set;
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;// _owner is (Thread *) vs SP/BasicLock
_previous_owner_tid = 0;// 监视器前一个拥有者线程的ID
}
class ObjectWaiter : public StackObj {
public:
enum TStates { TS_UNDEF, TS_READY, TS_RUN, TS_WAIT, TS_ENTER, TS_CXQ } ;
enum Sorted { PREPEND, APPEND, SORTED } ;
ObjectWaiter * volatile _next;
ObjectWaiter * volatile _prev;
Thread* _thread; // ObjectWaiter 对应的线程的
ParkEvent * _event; // 线程的ParkEvent
volatile int _notified ;
volatile TStates TState ;
Sorted _Sorted ; // List placement disposition
bool _active ; // Contention monitoring is enabled
};
在HotSpot虚拟机中,最终采用ObjectMonitor类实现monitor
ObjectMonitor的获取方法
ObjectMonitor * m = omAlloc (Self) ;//获取一个可用的ObjectMonitor
_WaitSet:
主要存放所有wait的线程的对象,也就是说如果有线程处于wait状态,将被挂入这个队列
_EntryList:
所有在等待获取锁的线程的对象,也就是说如果有线程处于等待获取锁的状态的时候,将被挂入这个队列。
-
wait的实现
ObjectSynchronizer::wait方法
通过object的对象中找到ObjectMonitor对象 调用方法
void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS)
通过ObjectMonitor::AddWaiter调用把新建立的ObjectWaiter对象放入到 _WaitSet 的队列的末尾中
然后在ObjectMonitor::exit释放锁,接着 thread_ParkEvent->park 也就是wait -
Notify方法的实现:
找到ObjectMonitor对象调用ObjectMonitor::notify 摘除第一个ObjectWaiter对象从_WaitSet 的队列中
并把这个ObjectWaiter对象放入_EntryList中,_EntryList 存放的是ObjectWaiter的对象列表,列表的大小就是那些所有在等待这个对象锁的线程数。**注意不管NotifyALL和Notify 并没有释放锁。锁的释放是在同步代码块结束的时候释放的这种可以从字节码看出
synchronized (objectLock) {
while (list.size() == 1) {
try {
objectLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("生产下");
list.add(1);
objectLock.notifyAll();// 通知所有在此对象上等待的线程
}
81 invokevirtual #15 <java/lang/Object.notifyAll>
84 aload_1
85 monitorexit
-
NotifyALL和Notify 的区别
NotifyALL 会把所有的_WaitSet中的对象放入_EntryList,Notify是随机选取一个 -
join的实现
public static void main(String[] args) {
Thread t2 = new Thread();
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
- 实现的原理以当前线程的为锁对象,jvm调用当前线程对象的notify方法释放锁通知
TIMED_WAITING
待续