线程状态改变方法解析

概述

都知道,在Object对象里有三个关于线程的方法,wait(底层实现后它为什么会释放锁呢)、notify(底层实现)、notifyall(底层实现后notify的区别)再者对象锁的获取锁和释放锁与JDK1.5之后的锁的显示获取和释放又有什么区别呢?已经线程的sleep(time)函数的理解和sleep(0)的意思,下面这篇博客将解析这些疑问。

锁的获取与释放锁解析

对象锁:即对于同步代码块或者说同步方法锁的获取或者释放,底层是依赖JVM的,其中主要是依赖一个“管程”的数据结构。
管程:monitor,一种数据结构,用于监视一段代码,确保该代码只能有一个线程执行,它会对等待获取锁的线程放入其同步队列中,在锁释放后会从同步队列中唤醒一个线程去获取锁、
在知道了管程的概念后,synchronized的原理其实也比较容易理解,就是对于synchronized修饰的代码块,在汇编时编译器会在代码块的起始处生成“moniterenter”的汇编指令和在代码块末尾处生成“moniterexit”的汇编指令。
moniterenter:该汇编指令会让线程执行同步代码块前去获取锁的宿主,即对象关联的监视器(monitor)的使用权,获取成功时会调用ObjectMonitor的enter方法改变monitor的_owner属性和count属性,_owner属性指向获取锁的线程,获取锁成功次数就是count记录。当但获取锁失败时,即锁已经别其他线程获取了,monitor就会时失败的线程直接阻塞进入它的同步队列中,等待释放锁的线程的唤醒。注意这一阻塞动作会造成线程的上下文的切换开销和由于JAVA多线程实现模型原理,会切换到内核态去阻塞线程,这也会造成开销。因此才有synchronized是重量级锁的称号。
monitereixt:该汇编指令就是释放锁,底层动作就是重置monitor的_owner和count的属性,和唤醒同步队列里的阻塞线程。
对于管程,monitor要用如下概念,ObjectMonitor中有如下两个变量:
_WaitSet(等待队列):
主要存放所有wait的线程的对象,也就是说如果有线程处于wait状态,将被挂入这个队列
_EntryList(阻塞队列):
所有在等待获取锁的线程的对象,也就是说如果有线程处于等待获取锁的状态的时候,将被挂入这个队列。

JDK1.5锁的获取与释放:
对于JDK1.5的锁的获取和释放,底层实现是依赖于同步组件(AOS)来完成的,即它的
获取:简单来说(之后会出一篇AQS的解析博客,所以这里先简单说下思路),就是通过线程去抢夺status的同步变量,成功的话就获得锁,失败的话如果在独占的模式下,那么该线程和一些失败信息就会被封装为一个Node节点,将其放入AQS内部维护的FIFO的阻塞队列中,注意并不是失败了就马上阻塞而是会自旋一段时间,如果条件满足(带AQS解析)会阻塞,否则就会自旋尝试获取锁(只有前驱节点是头节点才能去尝试去获取,所以这里体现了FIFO)
释放:即通过重置它对同步状态的改变来释放锁,即可以理解为还原同步状态变量初值。

wait方法解析

Object中的wait:
对于Object里的wait方法会,会让线程阻塞,其中依赖JVM里的ObjectMonitor这个监视器对象的wait方法实现的,而线程阻塞后会被封装为ObjectWaiter的节点,而ObjectWaiter里存放者线程(thread)和(ParkEvent)而ParkEvent里面会更具事件调用线程的thread里的Parker里的park方法,来阻塞。那么Parker的park方法于是怎么实现的呢,其实是依赖ParkPlatom里的counter、mutex、_crond来实现阻塞的,即按如下流程:
在调用park的时候如果counter是0则会去执行挂起的流程,否则返回,在挂起恢复后再将counter置为0。在unpark的时候如果counter是0则会执行唤醒的流程,否则不执行唤醒流程,并且不管什么情况始终将counter置为1

所以wait方法总结下来的流程如下:
1)void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS)
2)通过ObjectMonitor::AddWaiter调用把新建立的ObjectWaiter对象放入到 _WaitSet 的队列的末尾中
3)然后在ObjectMonitor::exit释放锁,接着 thread_ParkEvent->park 也就是wait
(所以这里说明了为什么调用了wait方法后线程会释放锁)

notify与notifyall方法解析

notify和notifyall方法都是将调用了wait方法的线程(ObjectWaiter)从等待队列移动到阻塞队列中开始竞争锁,而平时我们都知道notify和notifyall调用后线程并没释放锁,而被唤醒的线程也不能立即获得锁,那么这是为什么?

1)调用方法ObjectMonitor::notify
2)调用ObjectMonitor::DequeueWaiter 摘除第一个ObjectWaiter对象从_WaitSet 的队列中
3)并把这个ObjectWaiter对象放入_EntryList中,_EntryList 存放的是ObjectWaiter的对象列表,列表的大小就是那些所有在等待这个对象锁的线程数。
注意这里并没有调用ObjectMonitor::exit释放锁
(所以所线程调用了notify后还是持有锁而被唤醒的线程在锁没被释放的时候还是不能从wait醒来)

那么两者方法的区别?
notify:只会唤醒等待队列中的第一个阻塞队列
notifyall:会唤醒等待队列中的所有阻塞队列,是for循环吗?其实不是,是当前线程调用了notifyall后,当前线程释放锁,即退出同步块后当前线程会调用notify方法来调用最后一个入等待队列的线程,该线程被唤醒后再释放锁后又会调用notify唤醒倒数第二个线程(即采用了FILO,当然这个策越可以改变)

线程的sleep(time)和sleep(0)的区别

其实要说明这两个方法的区别和底层含义,就要理解操作系统中线程的调用(所有我们学计算机操作系统很重要)。
对于不同的系统当然有不同的线程调度程序,对于Window而言是采用抢占式的调度策越、而Linux采用时间片的调度策越。
抢占式调度策越:即优先级高的可以抢占优先级低的线程的CPU执行权,即高的会一直执行,所以可能会出现“饥饿”的情况,只有优先级高的线程不愿在继续执行了,操作系统才会重新计算所有线程的优先级,优先级高的会被执行。
时间片调度策越:即每个线程维护在一个就绪队列中,每个线程都被分配了一个运行执行的时间,每个线程的时间执行完后后续的线程才可以执行,执行完的线程又会重新插入到队列末尾。
那么现在来说下sleep(time)的底层含义,线程调用了这个函数后就是告诉操作系统,在未来的time时间段类我将不参与CPU执行权的竞争,即可以理解为操作系统此时忽视了这个线程。而当time时间到了后,线程会不会参与到CPU执行权的竞争中呢?我个人理解是看操作系统采用的调度策越,如果是时间片那么就不会,如果是抢占式调度,那么就要看优先级高的线程是否主动要求是否CPU执行权,要求重新计算优先级,如果是这种情况,那么就会参与到CPU的竞争,否则就不会。(仔细里操作系统线程调度策越)
那么thread.sleep(0)有是什么意思,在抢占式调度策越下,其实他就是说线程在时间=0这个时间段不参与CPU的竞争,OS接受到这个信息后就会重新计算线程的优先级,同时调用这个方法的线程也会参与到这个优先级重新计算中来,即该方法会在抢占式调度策越下会重新触发一个对CPU执行权的竞争。

猜你喜欢

转载自blog.csdn.net/CSDNadvancer/article/details/87971124