1.火车站售票系统仿真
某火车站目前正在出售火车票,共有50张票,而它有3个售票窗口同时售票,下面设计了一个程序模拟该火车站售票,通过实现Runnable接口实现(模拟网络延迟)。
伪代码:
java
Ticket类:
Ticket = 50;//总共有张票
Run(){
While(true){
If ticket>0;//判断是否有余票
输出"正在出售第ticket- -张票";
Else break;
Sleep(1000ms);//模拟网络延迟
}
}
MyTicket类:
Main(){
New Thread1 窗口1;//创建线程1
New Thread2 窗口2;//创建线程2
New Thread3 窗口3;//创建线程3
Start Thread1,Thread2,Thread3;//开启三个线程
}
运行结果(仅截取一部分):
上面的结果出现了相同票数的窗口,原因是ticket--不具有原子性,当窗口1休眠时,窗口3进入之后也休眠,这时窗口1苏醒了,进行到输出i的值这一步时,窗口3苏醒了并抢到了执行权,它也进行了输出i,由于上一步还没进行到i-1的步骤,因此窗口3和窗口1输出的值相同。因此需要限制修改票数的多并发。
下面一部分将介绍JVM中处理多线程并发时所用到的synchronized关键字,并介绍其是如何实现线程锁。
2.Java中synchronized关键字
2.1 synchronized关键字控制进程锁的实现结果
根据上一部分在售票过程中发现相同票数的窗口,为保证票数一致,针对多线程将才用synchronized关键字,对程序修改票数时上线程锁,在程序完成修改票数后释放线程锁。
伪代码:
java
Ticket类:
Ticket = 50;
Run(){
While(true){
Synchronized{//线程锁
If ticket>0;
输出"正在出售第ticket- -张票";
Else break;
}
Sleep(1000ms);
}
}
MyTicket类:
Main(){
New Thread1 窗口1;
New Thread2 窗口2;
New Thread3 窗口3;
Start Thread1,Thread2,Thread3;
}
运行结果(仅截取一部分):
根据结果发现使用synchronized关键字后,售票数据正常,程序运行结果无误。
2.2 synchronized关键字
2.2.1 synchronized关键字的用法
① 当synchronized作用在实例方法时,监视器锁(monitor)便是对象实例(this);
② 当synchronized作用在静态方法时,监视器锁(monitor)便是对象的Class实例,因为Class数据存在于永久代,因此静态方法锁相当于该类的一个全局锁;
③ 当synchronized作用在某一个对象实例时,监视器锁(monitor)便是括号括起来的对象实例;
2.2.2 synchronized的同步原理
查看上述代码反汇编后的结果:
实现原理:
1.monitorenter:每个对象都是一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
① 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者;
② 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;
③ 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;
2.monitorexit:执行monitorexit的线程必须是objectref所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。(monitorexit指令出现了两次,第1次为同步正常退出释放锁;第2次为发生异步退出释放锁;)
3.监视器(Monitor)
任何一个对象都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。Synchronized在JVM里的实现都是基于进入和退出Monitor对象来实现方法同步和代码块同步,都可以通过成对的MonitorEnter和MonitorExit指令来实现。
1.MonitorEnter指令:插入在同步代码块的开始位置,当代码执行到该指令时,将会尝试获取该对象Monitor的所有权,即尝试获得该对象的锁;
2.MonitorExit指令:插入在方法结束处和异常处,JVM保证每个MonitorEnter必须有对应的MonitorExit;
####3.1 Monitor实现原理
Monitor可以把它理解为一个同步工具,也可以描述为一种同步机制,它通常被描述为一个对象。与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象被创建初就带了一把看不见的锁,它叫做内部锁或者Monitor锁。也就是通常说Synchronized的对象锁,MarkWord锁标识位为10,其中指针指向的是Monitor对象的起始地址。在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的):
java
ObjectMonitor() {
_header = NULL;
_count = 0; // 记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象 ),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时:
1.首先会进入 _EntryList 集合,当线程获取到对象的monitor后,进入 _Owner区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1;
2.若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒;
3.若当前线程执行完毕,也将释放monitor(锁)并复位count的值,以便其他线程进入获取monitor(锁);
4.Java模拟PV原语
4.1信号量
count,如果是正值则表示当前资源的个数,如果是0,表示有一个进程在执行临界区的代码(也就是说这个进程位于临界区);并且没有进程处于阻塞队列中。如果是负值,这个值的绝对值(abs(count))表示阻塞队列中进程的个数。
queue,即为阻塞进程队列。当进程不能申请相应的资源是,则使用P操作,将自己插入阻塞队列中。当运行的进程执行完临界区代码时,就执行V操作,唤醒一个阻塞队列中的进程。
4.2功能实现
定义一个PV操作类:syn。在这个类中通过构造函数设置count的值。这个类中并没有阻塞进程所在的queue,是通过java的this.wait()与this.notifyAll()来实现的。
java
public class syn { //PV操作类
int count=0;//信号量
syn(){}
syn(int a){count=a;}
public synchronized void Wait(){ //关键字 synchronized 保证了此操作是一条【原语】
count--;
if(count<0){//等于0 :有一个进程进入了临界区
try { //小于0:abs(count)=阻塞的进程数目
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void Signal(){ //关键字 synchronized 保证了此操作是一条【原语】
count++;
if(count<=0){//如果有进程阻塞
this.notify();
}
}
}
由此可以简单实现jvm中synchronized关键字的基本操作