Java同步机制
为解决共享数据的访问问题,java的同步机制包括两个方面
1.共享数据的线程互斥锁定
2.传送数据的线程同步运行
第一节 互斥
多个线程可能会访问相同的资源,此时不能保证程序运行结果的正确性。解决办法是“互斥”。
以银行账户存取作为模型来讲述互斥的意义。参见《Java程序设计》宋中山严山钧的P277。
账户Account的存款、取款操作分别设计为线程类Save、Fetch。
class Account //账户类
{
privateintvalue;
void put(int i){value=value+i;}
int get(int i){
if (value>=i){
value=value-i;
return i;
}
elsereturn 0;
}
int howmatch(){returnvalue;}
}
class Save extends Thread //存款线程类
{
private Account a;
privateintamount;
public Save(Account a,int amount){this.a=a;this.amount= amount;}
publicvoid run()
{
System.out.println("现有"+a.howmatch());
//此处可接着调用sleep()方法以更方便演示
a.put(amount);
System.out.println("存入"+ amount);
System.out.println("余额"+a.howmatch());
}
}
class Fetch extends Thread //取款线程类
{
private Account a;
privateintamount;
public Fetch (Account a,int amount){this.a=a;this.amount= amount;}
publicvoid run()
{
System.out.println("现有"+a.howmatch());
//此处可接着调用sleep()方法以更方便演示
System.out.println("取出"+a.get(amount));
System.out.println("余额"+a.howmatch());
}
}
class Test{ //主程序
publicstaticvoid main(String []args){
Account a = new Account();
Save s1= new Save(a,100);s1.start();
Save s2= new Save(a,200);s2.start();
Fetch f1=new Fetch(a,250);f1.start();
}
}
在本例中,main()线程创造了3个线程对象(2个Save和1个Fetch),它们共享账户a。由于这3个线程的run()方法在同时执行时都能够操作账户a,3个线程的run()方法中的语句执行进度是不确定的,导致每次运行结果都不一样,而且运行结果可能与任何串行运行结果都不相同,所以上述代码不完善。
比如,一个运行结果输出如下表,经分析,这个输出与任何串行运行结果都不相同,不符情理。
现有0 存入200 余额200 现有0 存入100 余额300 现有300 取出250 余额50 |
解决的办法是,用synchronized关键字在临界区对共享的资源(这里是“账户”)进行加锁。本例即是对run()方法块进行加锁。修改后的Save类、Fetch类如下:
class Save extends Thread //存款线程类
{
private Account a;
privateintamount;
public Save(Account a,int amount){this.a=a;this.amount= amount;}
publicvoid run()
{
synchronized(a){
System.out.println("现有"+a.howmatch());
//此处可接着调用sleep()方法以更方便演示
a.put(amount);
System.out.println("存入"+ amount);
System.out.println("余额"+a.howmatch());
}
}
}
class Fetch extends Thread //取款线程类
{
private Account a;
privateintamount;
public Fetch (Account a,int amount){this.a=a;this.amount= amount;}
publicvoid run()
{
synchronized(a){
System.out.println("现有"+a.howmatch());
//此处可接着调用sleep()方法以更方便演示
System.out.println("取出"+a.get(amount));
System.out.println("余额"+a.howmatch());
}
}
}
这样,对于某一个账户a,某时刻最多只会有一个线程能够进入到被synchronized(a)标示的代码块中。运行结果虽然也有很多种,3个线程进入临界区的先后是不确定的。但运行结果均与某种串行运行结果相同。
思考:
另外,需要注意的是,可能你会考虑能否仅对Account类的put()、get(方法施加synchronized,而不对Save、Fetch类的run()的代码块施加synchronized,即
class Account //账户类
{
privateintvalue;
synchronized void put(int i){value = value +i;}
synchronized int get(int i){
if (value >=i){
value = value -i;
return i;
}
elsereturn 0;
}
int howmatch(){returnvalue;}
}
但是,这样就仅能保证账户中的最终金额不会错乱,但Save、Fetch类中的run()方法中的语句的执行进度仍然是随机的,运行结果不符合串行化要求。
第二节 同步
多个线程在访问相同的资源时,我们可能要求线程以及线程之间必须满足一定的条件。表面上,这些条件导致了线程访问过程具有“步调”,即一切均在严密的控制当中,这种控制实际上在程序设计中被称为“同步”。
以生产者-消费者问题作为模型来讲述同步的意义。参见《Java程序设计》宋中山严山钧的P284。
生产者Producer,消费者Consumer,仓库Cubbyhole。生产者向仓库放入产品,消费者从仓库拿出产品。
生产者、消费者代表线程,仓库代表资源。
条件constraint:1.当仓库满时,生产者将不能继续向仓库放入产品;2.当仓库空时,消费者将不能继续从仓库拿出产品。
为简单起见,假设仓库的容量为1,即只能容纳1个产品。并用向仓库赋一个值模拟放入产品,用从仓库读出该值模拟拿出产品。
假设我们不考虑上述条件,则设计出的程序代码如下:
//生产者
class Producer extends Thread{
private CubbyHole cubbyhole;
privateintnumber; //生产者编号
public Producer(CubbyHole cubbyhole, int number) {
this.cubbyhole = cubbyhole;
this.number = number;
}
publicvoid run() {
for(int i=0;i<10;i++){
cubbyhole.put(i);
System.out.println("Producer#"+this.number+"put:"+i);
try{
sleep((int)(Math.random()*100)); //使生产者休息一下
}
catch(InterruptedException e){}
}
}
}
//消费者
class Consumer extends Thread{
private CubbyHole cubbyhole;
privateintnumber; //消费者编号
public Consumer(CubbyHole cubbyhole, int number) {
this.cubbyhole = cubbyhole;
this.number = number;
}
publicvoid run() {
int value=0;
for(int i=0;i<10;i++){
value=cubbyhole.get();
System.out.println("Consumer#"+this.number+"get:"+value);
}
}
}
//仓库
class CubbyHole{
privateintseq;
publicsynchronizedvoid put(int value){seq=value;}
publicsynchronizedint get(){returnseq;}
}
//主程序
class test{
publicstaticvoid main(String[] args){
CubbyHole c=new CubbyHole();
Producer p1=new Producer(c, 1);
Consumer c1=new Consumer(c, 1);
p1.start();
c1.start();
}
}
本例中,生产者不断的向cubbyhole对象写入整数(依次从1到10),消费者不断的从此cubbyhole对象读出其中的数据。
该代码不能保证生产者每写入一个数据,消费者就及时读出一个数据并且只读一次。该代码的CubbyHole类的put()、get()方法用synchronized修饰,这只保证了同一时刻要么put()方法正在运行要么get()方法正在运行,但没有保证put()、get()方法交替运行,即不能控制“步调”。如果生产者比消费者快,那么消费者来不及取一个数据前,生产者就又产生了新的数据;如果消费者比生产者快,消费者可能两次取同一个数据。
比如,一个运行结果输出如下表,经分析,这个输出不满足条件constraint,是消费者比生产者快的情况。
Producer#1put:0 Consumer#1get:0 Consumer#1get:0 Consumer#1get:0 Consumer#1get:0 Consumer#1get:0 Consumer#1get:0 Consumer#1get:0 Consumer#1get:0 Consumer#1get:0 Consumer#1get:0 Producer#1put:1 Producer#1put:2 Producer#1put:3 Producer#1put:4 Producer#1put:5 Producer#1put:6 Producer#1put:7 Producer#1put:8 Producer#1put:9 |
为了能满足条件constraint(1.当仓库满时,生产者将不能继续向仓库放入产品;2.当仓库空时,消费者将不能继续从仓库拿出产品),需要用到一个所谓的“监视器”,即对条件的形式化、程序语言化的描述,以控制线程运行。
修改CubbyHole类的代码如下:
//仓库
class CubbyHole{
privateintseq;
privatebooleanavailable =false; //监视器,该例只用一个简单的测试变量
publicsynchronizedvoid put(int value){
while(available==true){ //判断条件
try{
wait();
}
catch(InterruptedException e){}
}
seq=value; //做具体处理
available=true;//修改状态
notifyAll(); //唤醒等待队列中的线程
}
publicsynchronized int get(){
while(available ==false){ //判断条件
try{
wait();
}
catch(InterruptedException e){}
}
int value=seq; //做具体处理
available =false;//修改状态
notifyAll(); //唤醒等待队列中的线程
return value;
}
}
说明:
为了实现同步控制,java提供了wait()、notifyAll()、notify()方法。
wait()方法使当前线程变为阻塞状态,线程主动释放了当前线程锁定的任何对象(而sleep()方法不会),进入等到队列。
notifyAll()方法唤醒等待队列中的全部线程,notify()方法唤醒等待队列中的某个线程。
wait()、notifyAll()、notify()方法是Object类的方法,因此所有的类都继承了这些方法。但只能在synchronized代码块(方法)中调用这些方法,否则会抛出IllegalMonitorStateException异常;
总结:同步机制的代码设计如下
(1)如果多个线程修改(访问)同一个对象(资源),那么将执行修改(访问)的方法,用synchronized修饰。
(2)如果一个线程必须等到某个状态条件成立时才能继续运行的话,那么此线程应在对象(资源)队列中等待,这要通过进入synchronized方法然后调用wait()方法来做到。直到状态条件成立,才能接着做处理。
(3)每当某一个(线程的)方法对资源做完具体处理并影响、修改了状态后,这个方法就应该接着调用notify()或者notifyAll()方法,(这会使其它线程的wait()方法立即返回),这样给那些等待队列中的线程一个机会,这些线程会检测运行环境是否已满足自己重新运行的需要,如果不满足则继续调用wait()来等待,否则有可能会被调度程序选定而运行。
上述(1)(2)(3)点的形式化描述如下:
public synchronized void method()
{
while(状态条件){
try{
wait();
}
catch(InterruptedException e){}
}
具体处理;
修改状态;
notifyAll()或notify();
}
思考:上述例子的仓库(资源)是简化版的,即容量为1,放产品和取产品是用设置一个整数的值来模拟的。这里考虑仓库是一个有着容量上限的资源池(用集合HashSet表示),其中存放了大量对象Object。代码如下:
//仓库
class CubbyHole{
private HashSet pool;
//其它属性
publicsynchronizedvoid put(Object o){
while(pool计数器指示满了){ //判断条件
try{
wait();
}
catch(InterruptedException e){}
}
向pool中放资源o//做具体处理
pool计数器加1; //修改状态
notifyAll()或notify(); //唤醒等待队列中的线程
}
publicsynchronized Object get(){
while(pool计数器指示为空){ //判断条件
try{
wait();
}
catch(InterruptedException e){}
}
从pool中取资源o//做具体处理
pool计数器减1; //修改状态
notifyAll()或notify(); //唤醒等待队列中的线程
return o;
}
}
第三节 死锁
以哲学家吃饭问题(Dijkstra发现)作为模型来讲述死锁的情况。参见《Java程序设计》宋中山严山钧的P286。
Java语言没有提供能够预防、解除死锁的机制,所以这应该由程序员来实现。解决死锁有两种方案:1:预防死锁:即采用技术使程序永远不会进入死锁的状态;2:解除死锁:当程序进入死锁状态时,能检测到然后采用技术解除死锁状态。
法1:一次性锁定所有要用到的资源,而不是先锁定一部分资源,再锁定另一部分资源。
则修改Philosopher类如下