【Java语言】Ja.1.3--浅谈多线程机制(三)之互斥与同步

来源:互联网 发布:北京网络策划公司 编辑:程序博客网 时间:2024/05/01 19:18

一、概述

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

       一个基本事实:在任意时刻,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  盒子数量     * @param initialBallNum  盒子初始小球的数量     */    public BallSystem(int n,int initialBallNum)    {        ballBoxs=new int[n];        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[n];        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问题(读者-写者问题、哲学家就餐问题)等。

0 0
原创粉丝点击