您的位置:首页 > 编程语言 > Java开发

【Java多线程】浅谈多线程机制(三)之互斥与同步

2017-03-15 21:42 585 查看
一、概述

在说明线程的互斥和同步之前,先看一个叫做竞争条件的名词。和操作系统中进程间的通信一样,竞争条件是指:两个或者两个以上线程同时读写某些共享数据时,最后的执行结果取决于线程运行的精确时序的情况。 竞争条件的出现可能导致共享数据被破坏,即可能出现数据不一致性的情况。

一个基本事实:在任意时刻,CPU上最多只能运行一个线程(进程)实例。有了这样一个基本事实,可以举这样一个小的例子来说明:假设有一片共享数据区域存储了一个整数50,在某个时刻,线程A尝试读取出该共享区域的数据并使其增加1,但是还没等到A把加1后的数据写回,它的执行时间已经到了,这时调度程序选择线程B执行,B读出共享数据区域数据,仍然为整数50,这时调度程序又转而选择A执行,A把51写回共享数据区,A线程执行完毕,停止。这时B又执行,当它再从共享数据区域读出数据用于验证时,发现51≠50,数据不一致了。对于B线程来说,它感觉自己是连续不断执行的,可事实上中断了一次。

二、实例演示

下面用一个实际例子来说明这种不一致情况:

一个封闭的空间中有若干盒子,每个盒子中装有若干个小球

小球可以从一个盒子转移到另外一个盒子,但是不能凭空产生或者消失

利用多线程来模拟小球在不同盒子间转移的情景。

基于上述条件,可以用如下程序实现:

(1)BallSystem.java:表示这个小球的封闭系统

package com.bebdong.ipc;

public class BallSystem
{
private final int[] ballBoxs;   //表示装小球的盒子

/**
* @param n  盒子数量
4000
* @param initialBallNum  盒子初始小球的数量
*/
public BallSystem(int n,int initialBallNum)
{
ballBoxs=new int
;
for(int i=0;i<ballBoxs.length;i++)
ballBoxs[i]=initialBallNum;
}

//返回当前小球总数
public int getTotalBalls()
{
int temp=0;
for(int count:ballBoxs)
temp+=count;
return temp;
}

//获取盒子数量
public int getBoxAmount()
{
return ballBoxs.length;
}

/**
* @param from   转出盒子
* @param to     转入盒子
* @param count  转移数量
*/
public void transfer(int from,int to,int count)
{
if(ballBoxs[from]<count)   //此盒子已不足以转出count数量小球
return;
System.out.print(Thread.currentThread().getName());
ballBoxs[from]-=count;   //转出count数量的小球

System.out.printf("从%d转移%d个小球到%d", from,count,to);
ballBoxs[to]+=count;     //转入count数量的小球

System.out.print("  当前小球总数:"+getTotalBalls());
System.out.println();
}
}


(2)BallTransferTask.java:小球转移的线程

package com.bebdong.ipc;

public class BallTranferTask implements Runnable
{
//共享的小球系统
private BallSystem ballSystem;
//小球转移的源下标
private int fromBox;
//单次小球转移最大数目
private int maxCount;
//最大休眠时间(毫秒)
private int DELAY=10;

public BallTranferTask(BallSystem ballSystem,int from,int max)
{
this.ballSystem=ballSystem;
this.fromBox=from;
this.maxCount=max;
}

@Override
public void run()
{
while(true)
{
int toBox=(int)(ballSystem.getBoxAmount()*Math.random());  //随机指定转移目的地
int count=(int)(maxCount*Math.random());
ballSystem.transfer(fromBox, toBox, count);
try {
Thread.sleep((long) ((int)(DELAY)*Math.random()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}

}

}


(3)BallSystemTest.java:主方法在此类中定义

package com.bebdong.ipc;

public class BallSystemTest
{
//总数为100*1000个小球
public static final int BOX_AMOUNT=100;
public static final int INITIAL_BALL_NUM=1000;

public static void main(String[] args)
{
BallSystem ball=new BallSystem(BOX_AMOUNT, INITIAL_BALL_NUM);

//对每个盒子构造一个转移线程,使其成为小球转出方
for(int i=0;i<BOX_AMOUNT;i++)
{
BallTranferTask task=new BallTranferTask(ball, i, INITIAL_BALL_NUM);
Thread thread=new Thread(task,"转移线程_"+i);
thread.start();
}
}

}


运行上述程序可以得到结果:



结果分析:可以看到,程序中并没有设置生成小球或者丢失小球的相关操作,但是不同时刻,小球的总数不总是等于100,000的。这就是因为前文所说的竞争条件引起的。

三、解决办法

那么通过什么样的方法来避免出现竞争条件呢?这就引出了本篇文章的主题,互斥与同步。简单来说,互斥与同步有如下意义:

互斥表示控制不允许两个或多个线程同时进入临界区。(把对共享内存进行访问的程序片段称之为临界区

同步表示线程间的一种通信机制。好比在同一家公司的两个项目搭档,其中某个人完成了他负责的工作,应该告诉另外一个人他已经完成的事实。

1、实现互斥:增加一个锁变量(仅当某个线程获得锁对象后才能进入临界区,即操作共享数据区域数据)。在Java中可以通过synchronized块儿来实现操作锁变量从而实现线程间的互斥。我们将BallSystem.java做如下修改:

package com.bebdong.ipc;

public class BallSystem
{
private final int[] ballBoxs;   //表示装小球的盒子

private final Object lock=new Object(); //锁变量

/**
* @param n  盒子数量
* @param initialBallNum  盒子初始小球的数量
*/
public BallSystem(int n,int initialBallNum)
{
ballBoxs=new int
;
for(int i=0;i<ballBoxs.length;i++)
ballBoxs[i]=initialBallNum;
}

//返回当前小球总数
public int getTotalBalls()
{
int temp=0;
for(int count:ballBoxs)
temp+=count;
return temp;
}

//获取盒子数量
public int getBoxAmount()
{
return ballBoxs.length;
}

/**
* @param from   转出盒子
* @param to     转入盒子
* @param count  转移数量
*/
public void transfer(int from,int to,int count)
{
//对lock加锁实现互斥行为
synchronized (lock)
{
//if(ballBoxs[from]<count)   //此盒子已不足以转出count数量小球
//return;
//为了避免出现此种不合理情况的线程仍然申请锁资源,降低系统效率的情况,我们做如下改进
//如下循环保证了不满足条件的情况下不会再次竞争CPU资源
while(ballBoxs[from]<count)
{
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

System.out.print(Thread.currentThread().getName());
ballBoxs[from]-=count;   //转出count数量的小球

System.out.printf("从%d转移%d个小球到%d", from,count,to);
ballBoxs[to]+=count;     //转入count数量的小球

System.out.print("  当前小球总数:"+getTotalBalls());
System.out.println();

//转移后,条件变化,唤醒lock上等待的线程,即线程同步过程
lock.notifyAll();
}

}
}


2、线程同步:如上所示,在某个转移操作执行完毕之后,盒子中小球的数量将发生变化,我们通过notifyAll()这个函数唤醒所有等待的线程,它们或许将再下一次执行中满足条件而可以顺利执行完毕,即实现了同步。

此时运行结果如下:



结果分析: (上述仅截取运行结果的一部分,有兴趣的读者可以运行程序来亲自观察)这时我们发现小球总数一直维持在100,000不再改变,当然这也是我们期望的结果。

以上关于通过加锁实现线程互斥,通过线程等待与唤醒实现同步,以及synchronized块的具体实现,由于篇幅有限这里不再详细介绍,这里仅介绍在Java中如何通过编程实现互斥与同步。

关于Java的并发编程、线程(进程)间的通信、线程(进程)的调度是个庞大的知识体系,涉及到Java语言机制、死锁、线程(进程)的几种状态及转换、互斥与同步等等方方面面的知识,如果对此理解有困难的读者可以去查阅有关方面的资料,可以先从实例的入手,比如几个经典的IPC问题(读者-写者问题、哲学家就餐问题)等。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: