Java虚拟机结构及并发编程Condition-Lock的等待通知[图]

一、Java虚拟机结构
学习Java虚拟机,先要掌握其基本结构,了解各部分有什么作用,各部分之间是如何协调工作的。本文将介绍如下内容:
Java虚拟机结构
举例说明Java堆、Java栈、方法区关系
1.基本结构
图片来自:https://blog.csdn.net/aijiudu/article/details/72991993
Java虚拟机结构包括:类加载子系统、运行时数据区、垃圾回收系统、执行引擎。
类加载子系统:负责从文件系统或者网络中加载Class信息。
垃圾回收系统:对方法区、Java堆和直接内存进行回收,其中Java堆是垃圾收集的工作重点。对于不再使用垃圾对象,垃圾回收系统会在后台默默地查找、标记并释放垃圾对象内存,完成对方法区、Java堆和直接内存的自动化管理。
执行引擎:执行引擎是负责执行虚拟机的字节码。

Java虚拟机结构及并发编程Condition-Lock的等待通知[图]

运行时数据区包括:方法区、Java堆、Java栈、直接内存、本地方法栈、PC(ProgramCounter)寄存器。
方法区:存放两部分内容。①类加载子系统加载的类信息;②Java程序运行时的常量池信息,包括字符串字面量和数字常量。
方法区是线程共享的。
JDK6、JDK7永久代实现方法区,JDK8之后永久带被彻底移除,被元数据区取代。元数据区数堆外的直接内存
Java堆:Java程序最主要的内存工作区域,存放Java对象实例。Java堆空间是所有线程共享的。
Java堆分为新生代和老年代。新生代分为eden区、s0区、s1区,s0区和s1区是两块大小相等、可以互换角色的内存空间。
几乎所有的对象都存放在堆中。Java堆是自动化管理的,垃圾对象会被自动清理,不需要程序员手动释放内存。
新生代存放新生的和年龄不大的对象。对象首先分配在eden区,如果在一次新生代回收后还存活就会进入s0或s1,对象年龄增加1。当对象达到一定年龄后,就会进入老年代,老年代存放老年对象。(堆内存作为最主要的内存空间,会在之后的垃圾回收中详细讲解)
直接内存:Java堆外的、直接向系统申请的内存空间。可以这样理解,直接内存就是JVM以外的机器内存,比如,你有4G的内存,JVM占用了1G,则其余的3G就是直接内存。所以直接内存收到本机器内存的限制,也可能出现OutOfMemoryError的异常。访问直接内存的速度会优于访问Java堆内存的速度,读写频繁的场景会考虑使用直接内存。NIO使用的就是直接内存。
Java栈:线程被创建的时候Java栈被创建,Java栈是线程私有的内存空间,同Java方法的调用密切相关。Java栈中保存着栈帧信息,栈帧包括局部变量、操作数栈和帧数据。
本地方法栈:本地方法栈和Java栈类似,本地方法栈用于本地方法的调用。Java虚拟机允许Java直接调用本地方法,本地方法通常是使用C编写的,native修饰的代码。
PC(ProgramCounter)寄存器:Java虚拟机会为每个Java线程创建PC寄存器,所以PC寄存器也是每个线程的私有空间。Java线程正在执行的方法称为当前方法,如果当前方法是Java方法,PC寄存器就会执行当前正在被执行的指令;如果当前方法是本地方法,PC寄存器的值就是undefined。
2.堆、栈、方法区关系
publicclassJVMTest{privateintid;publicJVMTest(intid){this.id=id;}publicvoidshowID(){System.out.println("id="+id);}publicstaticvoidmain(String[]args){JVMTestobject1=newJVMTest(1);JVMTestobject2=newJVMTest(1);object1.showID();object2.showID();}}
创建的两个JVMTest实例分配在堆中。
main方法object1和object2两个局部变量存放在Java栈中,并指向堆中的两个实例。
描述JVMTest类的类信息存放在方法区。
二、Java并发编程Condition-Lock的等待通知
我们知道synchronized锁通过Object类的wait()和notify()方法实现线程间的等待通知机制,而比synchronized更灵活Lock锁同样也有实现等待通知机制的方式,那就是条件Condition。本文将从以下几个方面介绍Condition:
如何使用Condition
源码分析
Condition的应用场景
1.Condition的使用
1.1Condition类提供的方法
等待方法:
//当前线程进入等待状态,如果其他线程调用condition的signal或者signalAll方法并且当前线程获取Lock从await方法返回,如果在等待状态中被中断会抛出被中断异常voidawait()throwsInterruptedException//当前线程进入等待状态直到被通知,中断或者超时longawaitNanos(longnanosTimeout)//同第二个方法,支持自定义时间单位booleanawait(longtime,TimeUnitunit)throwsInterruptedException//当前线程进入等待状态直到被通知,中断或者到了某个时间booleanawaitUntil(Datedeadline)throwsInterruptedException
唤醒方法:
//唤醒一个等待在condition上的线程,将该线程从等待队列中转移到同步队列中,如果在同步队列中能够竞争到Lock则可以从等待方法中返回voidsignal()//与1的区别在于能够唤醒所有等待在condition上的线程voidsignalAll()
1.2使用举例
启动waiter和signaler两个线程。
waiter线程获取到锁,检查flag=false不满足条件,执行condition.await()方法将线程阻塞等待并释放锁。
signaler线程获取到锁之后更改条件,将flag变为true,执行condition.signalAll()通知唤醒等待线程,释放锁。
waiter线程被唤醒获取到锁,自旋检查flag=true满足条件,继续执行。
publicclassConditionTest{privatestaticReentrantLocklock=newReentrantLock();privatestaticConditioncondition=lock.newCondition();privatestaticvolatilebooleanflag=false;publicstaticvoidmain(String[]args){Threadwaiter=newThread(newwaiter());waiter.start();Threadsignaler=newThread(newsignaler());signaler.start();}staticclasswaiterimplementsRunnable{@Overridepublicvoidrun(){lock.lock();try{while(!flag){System.out.println(Thread.currentThread().getName()+"当前条件不满足等待");try{condition.await();}catch(InterruptedExceptione){e.printStackTrace();}}System.out.println(Thread.currentThread().getName()+"接收到通知条件满足");}finally{lock.unlock();}}}staticclasssignalerimplementsRunnable{@Overridepublicvoidrun(){lock.lock();try{flag=true;condition.signalAll();}finally{lock.unlock();}}}}
输出结果:
Thread-0当前条件不满足等待Thread-0接收到通知,条件满足
2.Condition与wait/notify
Object的wait和notify/notify是与synchronized配合完成线程间的等待/通知机制,是属于Java底层级别的。而Condition是语言级别的,具有更高的可控制性和扩展性。具体表现如下:
wait/notify方式是响应中断的,当线程处于Object.wait()的等待状态中,线程中断会抛出中断异常;Condition有响应中断和不响应中断模式可以选择。
wait/notify方式一个synchronized锁只有一个等待队列;一个Lock锁可以根据不同的条件,new多个Condition对象,每个对象包含一个等待队列。
需要注意的是,Condition同wait/notify一样,在等待与唤醒方法使用之前必须获取到该锁。
3.源码分析
Tips:需要在理解AQS及ReentrantLock基础上阅读本文源码,给出这两篇的链接:
【原创】14|AQS源码分析【原创】15|重入锁ReentrantLock
3.1条件队列
首先看Condition对象的创建:
ReentrantLocklock=newReentrantLock();Conditioncondition=lock.newCondition();publicConditionnewCondition(){returnsync.newCondition();}finalConditionObjectnewCondition(){returnnewConditionObject();}
创建的Condition对象其实就是ConditionObject对象,ConditionObject是AbstractQueuedSynchronizer(AQS)的内部类,实现了Condition接口。
每个ConditionObject对象都有一个条件等待队列,用于保存在该Condition对象上等待的线程
。条件等待队列是一个单向链表,结点用的AQS的Node类,每个结点包含线程、next结点、结点状态。ConditionObject通过持有头尾指针类管理条件队列。
注意区分AQS的同步队列和Condition的条件队列。

线程抢锁失败时进入AQS同步队列,AQS同步队列中的线程都是等待着随时准备抢锁的。线程因为没有满足某一条件而调用condition.await()方法之后进入Condition条件队列,Condition条件队列中的线程只能等着,读后感(https://www.yuananren.com)没有获取锁的机会。当条件满足后调用condition.signal()线程被唤醒,那么线程就从Condition条件队列移除,进入AQS同步队列,被赋予抢锁继续执行的机会。
条件队列源码:

publicclassConditionObjectimplementsCondition,java.io.Serializable{privatetransientNodefirstWaiter;//头结点privatetransientNodelastWaiter;//尾结点/***入队操作*/privateNodeaddConditionWaiter(){Nodet=lastWaiter;//如果尾结点取消等待了,将其清除出去,并检查整个条件队列将已取消的所有结点清除if(t!=null&&t.waitStatus!=Node.CONDITION){unlinkCancelledWaiters();//这个方法会遍历整个条件队列,然后会将已取消的所有结点清除出队列t=lastWaiter;}//将当前线程构造成结点,加入队尾Nodenode=newNode(Thread.currentThread(),Node.CONDITION);if(t==null)firstWaiter=node;elset.nextWaiter=node;lastWaiter=node;//维护尾结点指针returnnode;}/***遍历整个条件队列,清除已取消等待的结点*/privatevoidunlinkCancelledWaiters(){Nodet=firstWaiter;Nodetrail=null;//用于保存前一个结点while(t!=null){Nodenext=t.nextWaiter;if(t.waitStatus!=Node.CONDITION){//t结点状态不是Node.CONDITION,说明已经取消等待,删除t.nextWaiter=null;if(trail==null)firstWaiter=next;elsetrail.nextWaiter=next;if(next==null)lastWaiter=trail;}elsetrail=t;//下次循环中t结点的前一个结点t=next;}}}staticfinalclassNode{volatileThreadthread;//每一个节点对应一个线程NodenextWaiter;//next结点volatileintwaitStatus;//结点状态staticfinalintCONDITION=-2;//结点状态:当前节点进入等待队列中...}
3.2await()
当调用condition.await()方法后会使得线程进入到条件队列,此时线程将被阻塞。当调用condition.signal()方法后,线程从条件队列进入AQS同步队列排队等锁。线程在AQS中发生的事情这里就不介绍了,不明白的可以看下以前AQS的文章【原创】14|AQS源码分析。
await()方法源码:
/***当前线程被阻塞,并加入条件队列*线程在AQS同步队列中被唤醒后尝试获取锁*/publicfinalvoidawait()throwsInterruptedException{//响应打断if(Thread.interrupted())thrownewInterruptedException();//将当前线程构造成结点,加入条件队列队尾,上文详细分析了该方法Nodenode=addConditionWaiter();//释放锁,线程阻塞前必须将锁释放,下文详解fullyRelease()方法intsavedState=fullyRelease(node);intinterruptMode=0;/**1.isOnSyncQueue()检查node是否在AQS同步队列中,不在同步队列中返回false,下文详解isOnSyncQueue()方法*2.如果node不在AQS同步队列中,将当前线程阻塞*3.当其他代码调用signal()方法,线程进入AQS同步队列后被唤醒,继续从这里阻塞的地方开始执行*4.注意这里while循环的自旋,线程被唤醒以后还要再检查一下node是否在AQS同步队列中*/while(!isOnSyncQueue(node)){//检查node是否在AQS同步队列中LockSupport.park(this);//阻塞,线程被唤醒后从这里开始执行if((interruptMode=checkInterruptWhileWaiting(node))!=0)break;}/**到这里,是当前线程在AQS同步队列中被唤醒了,尝试获取锁*acquireQueued()方法抢锁,抢不到锁就在同步队列中阻塞*acquireQueued()方法是AQS文章中详细重点讲解过的这里不详细分析了*/if(acquireQueued(node,savedState)&&interruptMode!=THROW_IE)interruptMode=REINTERRUPT;if(node.nextWaiter!=null)//cleanupifcancelledunlinkCancelledWaiters();if(interruptMode!=0)reportInterruptAfterWait(interruptMode);}
fullyRelease()方法:
/***将node线程的锁全部释放*“全部”是指多次重入的情况,这里一次全部释放*/finalintfullyRelease(Nodenode){booleanfailed=true;try{intsavedState=getState();//锁状态if(release(savedState)){//释放锁failed=false;returnsavedState;}else{thrownewIllegalMonitorStateException();}}finally{if(failed)node.waitStatus=Node.CANCELLED;}}
isOnSyncQueue()方法:
/***检查node是否在AQS同步队列中,在同步队列中返回true*/finalbooleanisOnSyncQueue(Nodenode){//状态为Node.CONDITION条件等待状态,肯定是在条件队列中,而不在同步队列中if(node.waitStatus==Node.CONDITION||node.prev==null)returnfalse;//如果node已经有后继节点next,那肯定是在同步队列了if(node.next!=null)returntrue;//遍历同步队列,查看是否有与node相等的结点returnfindNodeFromTail(node);}/***从同步队列的队尾开始从后往前遍历找,如果找到相等的,说明在同步队列,否则就是不在同步队列*/privatebooleanfindNodeFromTail(Nodenode){Nodet=tail;for(;;){if(t==node)returntrue;if(t==null)returnfalse;t=t.prev;}}
3.3signal()
调用condition.signal()方法后,线程从Condition条件队列移除,进入AQS同步队列排队等锁。
注意:正常情况下signal只是将线程从Condition条件队列转移到AQS同步队列,并没有唤醒线程。线程的唤醒时机是AQS中线程的前驱节点释放锁之后。
publicfinalvoidsignal(){//验证当前线程持有锁才能调用该方法if(!isHeldExclusively())thrownewIllegalMonitorStateException();Nodefirst=firstWaiter;if(first!=null)doSignal(first);}/***从条件队列队头往后遍历,找出第一个需要转移的结点node,将node从条件队列转移到AQS同步队列*为什么需要遍历找?因为前有些线程会取消等待,但是可能还在条件队列中*/privatevoiddoSignal(Nodefirst){do{//将first中条件队列中移除,将first的next结点作为头结点赋值给firstWaiterif((firstWaiter=first.nextWaiter)==null)lastWaiter=null;first.nextWaiter=null;/**transferForSignal()将first结点加入AQS同步队列*如果first结点加入同步队列失败,是因为first结点取消了Node.CONDITION状态,原因在下面transferForSignal()的讲解中说明*如果first结点加入同步队列失败,那么选择first后面的第一个结点进行转移,依此类推*/}while(!transferForSignal(first)&&//将first结点加入AQS同步队列(first=firstWaiter)!=null);//first结点加入同步队列失败,选择first后面的结点进行转移}/***将结点转移到同步队列*@returntrue-代表成功转移;false-代表在signal之前,节点已经取消等待了*/finalbooleantransferForSignal(Nodenode){/**CAS设置结点状态*CAS失败说明此node的waitStatus已不是Node.CONDITION,说明节点已经取消。既然已经取消,也就不需要转移了,方法返回,转移后面一个节点*CAS失败为什么不是其他线程抢先操作了呢?因为这里还持有lock独占锁,只有当前线程可以访问。*/if(!compareAndSetWaitStatus(node,Node.CONDITION,0))returnfalse;Nodep=enq(node);//自旋进入同步队列的队尾intws=p.waitStatus;//正常情况下不会走这里,这里是前驱节点取消或者CAS失败的情况if(ws>0||!compareAndSetWaitStatus(p,ws,Node.SIGNAL))LockSupport.unpark(node.thread);returntrue;}staticfinalclassNode{volatileThreadthread;//每一个结点对应一个线程NodenextWaiter;//next结点volatileintwaitStatus;//结点状态staticfinalintCONDITION=-2;//结点状态:当前结点进入等待队列中}
3.4源码过程总结
ReentrantLocklock=newReentrantLock();创建lock锁,对应生成AQS同步队列,一个ReentrantLock锁对应一个AQS同步队列。
Conditioncondition=lock.newCondition();创建condition,对应生成condition条件队列。
线程A调用condition.await();,线程A阻塞并加入condition同步队列。
线程B调用condition.signal();,线程A阻塞从condition1同步队列转移到AQS同步队列的队尾。
当AQS队列中线程A的前驱节点线程执行完并释放锁时,将线程A唤醒。
线程A被唤醒之后抢锁,执行逻辑代码。
4.应用
Condition实现的生产者消费者问题。
classBoundedBuffer{finalReentrantLocklock=newReentrantLock();finalConditionnotFull=lock.newCondition();finalConditionnotEmpty=lock.newCondition();finalObject[]items=newObject[100];intputptr,takeptr,count;//生产publicvoidput(Objectx)throwsInterruptedException{lock.lock();try{while(count==items.length)notFull.await();//队列已满,等待,直到notfull才能继续生产items[putptr]=x;if(++putptr==items.length)putptr=0;++count;notEmpty.signal();//生产成功,队列已经notempty了,发个通知出去}finally{lock.unlock();}}//消费publicObjecttake()throwsInterruptedException{lock.lock();try{while(count==0)notEmpty.await();//队列为空,等待,直到队列notempty,才能继续消费Objectx=items[takeptr];if(++takeptr==items.length)takeptr=0;--count;notFull.signal();//被我消费掉一个,队列notfull了,发个通知出去returnx;}finally{lock.unlock();}}}
生产者线程调用put()方法向队列中添加对象,当队列满时,生产者线程就阻塞等待。
消费者线程调用take()方法取出队列中的对象,取出对象后队列可以添加对象了,通知被阻塞的生产者线程。
生产者线程被唤醒后,从阻塞的位置开始执行,继续向队列中添加对象。
同样,消费者取出队列中对象时,发现队列为空了也会阻塞等待,生产者线程添加对象之后会通知消费者线程。
总结
Object的wait和notify/notify是与synchronized配合完成线程间的等待/通知机制,而Condition与Lock配合完成等待通知机制。
Condition比wait和notify具有更高的可控制性和扩展性,一个Lock锁可以有多个Condition条件,此外Condition还有响应中断和不响应中断模式可以选择。Condition的使用与wait/notify一样,在等待与唤醒方法使用之前必须获取到锁。
Condition的实现原理:每个condition都有一个条件队列,调用condition.await()方法将线程阻塞后线程就进入了条件队列,调用condition.sigal()方法后线程从condition条件队列转移到AQS同步队列等锁,该线程的前一节点释放锁之后会唤醒该线程抢锁执行。
Condition多用于实现的生产者消费者问题。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值