第十八章 JAVA多线程交互

第一节 JAVA线程停止的错误方法

stop方法,no stop 这不是正确的方法,会让我们的程序戛然而止,会使我们不知道哪些工作没做,完成了什么任务以及没有机会去做清理工作。使用的结果会造成程序突然停止,强行关闭,有时一个循环可能都没做完。
JAVA停止线程的正确做法—设置退出旗标,使用退出标志来停止线程,如之前的程序先设置一个布尔类型的值,volatile类型来保证每次都能读取到它的值,赋值false来退出线程。
JAVA停止线程广为流传的错误方法—interrupt方法,interrupt初衷并不是停止我们的线程。
查询JAVA API文档,在java.lang包下,找到Thread,Ctrl+F找到interrupt(),找到三个。
interrupt() 中断线程
interrupted() 测试当前线程是否已经中断,注意这个方法是静态方法。
isInterrupted() 测试线程是否已经中断。后者两个方法返回的值都是布尔值。
在API中我们看到:如果线程在调用Object类的wait()、wait(long)或wait(long, int)方法,或者该类的join()、join(long)、join(long, int)、sleep(long)或 sleep(long, int)方法过程中受阻,则其中断状态将被清除,它还将收到一个 InterruptedException。
在这里首先我们看到API中interrupt()方法中断线程是有条件的,在API中提示如果以前的条件都没有保存,才会将该线程的中断状态设置。此时调用后面的interrupted()或者isInterrupted()将返回一个布尔值的变量true来表示线程被中断。
如果使用了join方法或者sleep方法使得线程阻塞中断的情况下,使用interrupet会使得线程的中断状态被清除,并且当前的线程将会收到一个InterruptedException,这代表如后面再调用interrupted或者isInterrupted方法将不会得到一个正确的值。这就是为什么我们在使用join方法或者sleep方法需要用try-catch语句包围来捕获这个InterruptedException异常的原因。在使用join或者sleep方法时,一旦其它或当前线程调用了interrupted()方法,它将会收到一个异常。这些被阻塞的线程因为某些原因需要被唤醒,比如外部发生了中断,它需要响应,这时它就通过抛出异常的方式来使我们有机会做出一些响应。所以interrupt并不能正确的停止我们的线程。

下面使用代码来实际演示一下:

/**
 * Created by Administrator on 2017/4/7.
 */
public class WrongWayStopThread extends Thread {
    public void run(){
        while(!this.isInterrupted()){
            //!this.isInterrupted() 一开始使用的是true,线程停止不了,更换后
            //正确退出线程,此方法和设置退出标志法一样,只是退出旗标比较特殊
            //线程是否被中断的一个状态
            System.out.println("Thread is running...");
            //获取当前系统时间的一个毫秒值
            long time=System.currentTimeMillis();
            //long time1=System.nanoTime();这个精度到纳秒,
            //比上面currentTimeMillis()毫秒精度高
            //while((System.currentTimeMillis()-time)<1000){
                //减少我们的屏幕输出,大概等于sleep休眠1000毫秒
                //也就是每秒中输出一条信息("Thread is running..."
                // 这里为什么没有使用sleep呢?
            //}
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //使用sleep方法运行结果,抛出异常,程序没有正确结束,
            // sleep方法使线程进入一种阻塞状态之时,此时如果这个//
            // 线程再被调用interrupt方法,它会产生两个结果,
            // 1是它的中断状态被清除,线程的isInterrupted就不能
            // 返回一个表示是否被中断的正确状态,那么这里的while
            // 就不能正确的退出了。
            //2第二个结果就是收到一个InterruptedException异常表明
            // 它被中断了
        }
    }
    public static void main(String[] args) {
        WrongWayStopThread thread=new WrongWayStopThread();
        //新建一个线程 thread。这里父类子类引用效果应该是一样的吧?先照
        //示例演示一遍之后再更换成 Thread类型尝试下
        System.out.println("Starting thread...");
        //启动线程
        thread.start();
        //休眠3秒钟并且用try-catch语句块包围
        try {
           Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Interrupting thread...");
        //用 interrupt方法中断线程,注意前面调用了sleep方法阻塞中断线程3秒钟
        thread.interrupt();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Stopping application");
    }
}
第二节 JAVA线程交互之汽车人之忧:消失的能量

程序的任务是建立一个能量盒子的数组,初始值里数组里每个盒子是一个固定的初始值总能量也固定,然后互相传递能量。在线程实现后有部分情况下能量出现丢失。
先建立一个宇宙的能量系统
1.定义了一个私有的数组常量,并且在初始化构造方法中,先用一个参数n表示数组的长度,再利用for循环把数组每个值都设置为统一固定的参数值。
2.定义了一个能量传递的方法,参数为(传递初的位置下标from,传递后的终点位置的下标to,传递的能量值),方法体第一句表示如果最初的这个盒子能量不满足本次传递则终止本次转出,if(条件),return;然后是方法体内的公式,数组[from]-=传递的能量值,能量转出后对方要数组[to]+=传递的能量值。中间插入输出语句,用printf格式占位输出。注意只是传递了一次。
System.out.printf(“从%d转移到%10.2f单位能量到%d”,from,amount,to);%d对应我们参数中的from,%d代表输出一个整数值,%10.2f代表我们输出一个浮点数,并且它的小数部分是两位,整数点之前的部分应该有十位。System.out.printf(“能量总和:%10.2f%n”,getTotalEnergies());我们这里的%n则类似我们的转义字符反斜杠n /n 代表了一个回车换行符。
3.定义一个方法这里我们需要获取这个能量世界的总和。利用for循环遍历其中的数组元素并设立一个变量初始值为0,累计相加最后得出并输出一个总和
4.设置一个方法直接返回当前数组的长度.length

/**
 * Created by Administrator on 2017/4/7.
 * 宇宙的能量系统
 * 遵循能量守恒的定律
 * 能量不会凭空消失,只会从一处转移到另一处
 */
public class EnergySystem {
    //能量盒子的总和,贮存能量的地方,每个盒子存储了一定的能量
    //所有的盒子能量之和代表宇宙的总能量
    private final double[] energyBoxes;

    /**
     *
     * @param n 代表了能量盒子的数量
     * @param initialEnergy 每个能量盒子初始含有的能量值
     */
    public EnergySystem(int n,double initialEnergy) {
        this.energyBoxes = new double[n];
        for(int i=0;i<energyBoxes.length;i++){
            energyBoxes[i]=initialEnergy;
        }
    }
    //这个构造方法会使这个double数组一开始所有的元素值相等,
    // 总和=n*initialEnergy,数组构造还是第一次见

    /**
     *能量的转移,从一个盒子到另一个盒子
     * @param from 能量源
     * @param to 能量终点
     * @param amount 转移的能量值
     */
    public void transfer(int from,int to,double amount){
        //能量转出的值不足以满足本次能量转出时,我们则会终止本次转出
        if(energyBoxes[from]<amount)
            return;
        //这个return指的是符合条件则跳出当前方法
        System.out.print(Thread.currentThread().getName());
        energyBoxes[from]-=amount;
        System.out.printf("从%d转移到%10.2f单位能量到%d",from,amount,to);
        //printf格式输出,它利用特别的字符来占位,然后在我们后面的输出列表
        // 之中,对应其真实的值,例如%d对应我们参数中的from,%d代表输出一个
        // 整数值,%10.2f代表我们输出一个浮点数,并且它的小数部分是两位,
        // 整数点之前的部分应该有十位。
        energyBoxes[to]+=amount;
        System.out.printf("能量总和:%10.2f%n",getTotalEnergies());
        //我们这里的%n则类似我们的转义字符反斜杠n /n 代表了一个回车换行符
    }
    /**
     * 获取能量世界的能量总和
     */
    public double getTotalEnergies(){
        double sum=0;
        for (double amount:energyBoxes){
            sum+=amount;
        }
        return sum;
    }
    /**
     * 返回能量合子的长度
     */
    public int getBoxAmount(){
        return energyBoxes.length;
    }
}

上一个类只是一个宇宙能量系统的类, 提供了能量传递的方法和计算总能量和返回数组长度的方法,定义了能量盒子数组。第二个类是我们的线程任务类,能量传递任务,一共设置了四个属性,第一个是EnergySsytem,因为只使用一个能量世界,定义了一个EnergySystem能量系统的属性,并且把对象当做构造方法参数传递进去。
定义了一个能量转移的盒子下标 fromBox,这个应该不能超过数组的长度,因为下标从0开始,0-长度-1,还有每次可以转移的最大能量值,设置了一个休眠时间,输入一个带参构造方法,复写run方法,能量传递的值和toBox,目的盒子的下标,两者都是随机数。但不能超过最大值,故选择最大值*Math.random()方法,等于或大于0.0,小于1.0之间的随机数,调取传递方法后休眠*Math.random()随机时常,whiletrue循环,注意try块把try放在循环体外

package energysystem;

/**
 * Created by Administrator on 2017/4/7.
 */
public class EnergyTransferTask implements Runnable{
    //共享的能量世界
    private EnergySystem energySystem;
    //能量转移的源能量盒子下标
    private int fromBox;
    //单词能量转移最大单元
    private double maxAmount;
    //最大休眠时间毫秒
    private int DELAY=10;

    public EnergyTransferTask(EnergySystem energySystem,int from,double max){
        this.energySystem=energySystem;
        this.fromBox=from;
        this.maxAmount=max;
    }
    @Override
    public void run() {
        try {
            while(true){
                int toBox=(int)(energySystem.getBoxAmount()*Math.random());
                //Math.random()是令系统随机选取大于等于 0.0 
且小于 1.0 的伪随机 double//长度假如说是10*0.9=9,因为不会等于1,所以无论如何也不会超过限度。
                double amount=maxAmount*Math.random();
                energySystem.transfer(fromBox,toBox,amount);
                Thread.sleep((int)(DELAY*Math.random()));
                }
            }catch (InterruptedException e) {
            //注意把try块try的开头移到循环体之外,在内部的话如果有异常会在循环后来回
            // 抛出,容易造成内存溢出,在外面美观好用
                e.printStackTrace();
            }
        }
    }

运行类

package energysystem;

/**
 * Created by Administrator on 2017/4/7.
 */
public class EnergySystemTest {
    //将要构建的能量世界中能量盒子数量
    public static final int BOX_AMOUNT=100;
    //每个盒子的初始能量
    public static final double INITIAL_ENERGY=1000;

    public static void main(String[] args) {
        EnergySystem eng=new EnergySystem(BOX_AMOUNT,INITIAL_ENERGY);
        for(int i=0;i<BOX_AMOUNT;i++){
            EnergyTransferTask task=new 
EnergyTransferTask(eng,i,INITIAL_ENERGY);
            Thread t=new Thread(task,"TransferThread_"+i);
            t.start();
        }
    }
}
第三节 JAVA线程之争用条件

上一节程序最后的结果是在不断的传递能量过程中,总能量出现减少,而出现这种情况就是由于我们的争用条件问题造成的。
什么是争用条件呢?
利用一个生活化的例子,女神往往受到很多追求者青睐,而女神最后只能选择一个,当她同时答应多个人的追求时,将不免发生流血事件。同理,把女神比作数据(内存区域)
当多个线程同时共享访问同一数据(内存区域)时,每个线程都尝试操作该数据,从而导致数据被破坏,这种现象我们就称之为争用条件。
以上面的代码为例,线程1和线程2共享了同一个能量转移目标,同时我们知道,在同一时间只能有一个线程在cpu上运行,而线程之间的调度则是通过分时和抢度完成的。线程一转入500,线程二转入900,由于争用条件最后赋值还是线程1转入的5500

第四节 能量守恒:互斥和同步

互斥:相互排斥,在同一时间里只能有一条线程去对我们的关键数据或者临界区进行操作,同步:归根结底,就是线程之间的一种通信机制。一条线程执行完了某项任务,它会以某种方式告知其它线程,我做完了。在这里我们要提到关键字 synchronized ,还有对象的.wait 和notifyAll()。新增添一个常量类型是Object对象,作为我们的线程锁

private final Object lockObj=new Object();
//定义了一个常量,这个是我们的锁对象

再去程序中修改一下能量转移的方法

   public void transfer(int from,int to,double amount){
        //能量转出的值不足以满足本次能量转出时,我们则会终止本次转出
        //synchronized既可以出现于我们的方法体之上也可以出现在我们的方法体之中
        //把这个不满足能量退出的方法加入synchronized修改一下。
        //if(energyBoxes[from]<amount)
        // return;
        //这个return指的是符合条件则跳出当前方法
        synchronized(lockObj){
            //这里的this一开始填的lockObj,通过对对象的加锁来实现我们的互斥行为,
            //已知加锁必须是一个共享且唯一的,后面我们把该对象作为参数传递
            //入线程,此时是共享一个对象了
            //if(energyBoxes[from]<amount)
                //return;
            //上面这个方法表示不满足转出则程序退出,现在想想退出之后我们的这段
            // 线程能然有机会去获取CPU资源,从而再次要求进行加锁,而我们的加锁
            // 操作是有开销的,(之前的话不需要加锁,大不了多试几次退出几次)
            // 这样会降低我们系统的性能。那么好的办法是什么呢?
            // 当我们发现条件不满足时,这时我们应该让线程去等待某些条件的发生
            // 从而降低这个线程去获取锁的开销,提高我们整体的性能。
            //while循环,保证条件不满足时任务都会被条件阻挡,而不是去竞争我们
            // 的CPU资源
            while(energyBoxes[from]<amount){
                //当我们发现线程不满足某些条件时,应该将线程阻挡在我们业务逻辑
                // 之前
                try {
                    lockObj.wait();
                    //wait Set等待集合
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //这会使我们的线程进入等待状态,而避免了我们的线程去持续的申请锁
            }

            System.out.print(Thread.currentThread().getName());
            energyBoxes[from]-=amount;
            System.out.printf("从%d转移到%10.2f单位能量到%d",from,amount,to);
        //printf格式输出,它利用特别的字符来占位,然后在我们后面的输出列表
        // 之中,对应其真实的值,例如%d对应我们参数中的from,%d代表输出一个
        // 整数值,%10.2f代表我们输出一个浮点数,并且它的小数部分是两位,
        // 整数点之前的部分应该有十位。
            energyBoxes[to]+=amount;
            System.out.printf("能量总和:%10.2f%n",getTotalEnergies());
            lockObj.notifyAll();
        //我们这里的%n则类似我们的转义字符反斜杠n /n 代表了一个回车换行符

        //这里我们应该通知被阻挡的线程,告诉被等待的线程,条件发生了变化,他们
        //有可能能被执行了
        //唤醒所有lockObj对象上等待的线程。
        }
    }

此时再运行我们的程序,能量就守恒了。

第五节 深度剖析

什么是互斥,互斥是怎么实现的?互斥也就是说关键数据在同一时间只能被一个线程所访问。互斥的实现 synchronized(intrinsic lock) 单词含义固有的锁,synchronized(this)相当于给我们的代码加一把锁,使得其他线程不能进我们这个关键区域去访问关键资源。只有获得了(this)锁的线程能够进入这个核心区域。java的语法保证了同一时间,只能有一条线程获得我们的线程锁。上面的例子就是新建了一个Object对象作为锁,感觉换成this也完全可以实行,因为共享了一个能量世界,锁住的方法也是这个类的方法。
同步的实现 wait() notify() notifyAll();这三个方法属于object对象而不是thread对象,object对象的成员函数。如之前的代码:

lockObj.wait();
lockObj.notifyAll();

当能量不满足不能转移时,调用对象的wait方法让该线程进入wait set等待的集合,然后继续下一个线程,当其他线程执行完毕唤醒所有等待线程。(当有线程执行完,使之前的线程能量得到满足,拥有足够的能量可以满足本次转移时,继续执行。)注意这里的wait和notifyAll不是同一个线程哦。同步是两个线程之间的交互。wait set,是线程休息室。critical section 我们的共享资源共享数据又被称为临界区,当有一个线程访问我们的资源时,首先它需要获得一把锁,当它获得了锁它将进入我们的临界区操作,操作过程中它发现某些情况不被满足,比如能量不满足转移。它将调用我们锁对象上的wait方法,此时这个线程首先释放掉我们的锁资源,然后进入我们的锁对象上的wait set。由于这个线程释放掉了我们的锁资源,可以让其他线程来竞争我们的锁资源。所以我们看到其他线程获得锁并进入了我们的临界区,同时我们看到waitset中有多条线程在等待条件的满足。当我们当前线程执行完某些操作,需要通知等待的线程时,调用我们的notify()方法 将会唤醒我们锁资源所持有的等待区域中的1条线程,使这条线程有机会去竞争cpu资源。notifyAll()将唤醒所有等待的线程,去竞争临界区的锁对象。

第六节 总结及展望

在这里我们学习了:
1.如何创建线程及线程的基本操作
2.可见性及volatile关键字实现线程的可见性编程
3.争用条件
4.线程的互斥synchronized
5.线程的同步wait/notifyAll
扩展建议
如何扩展java并发的知识:
1.Java Memory Mode
JMM描述了java线程如何通过内存进行交互,通过这部分知识我们可以了解什么是happens-before原则,为什么要使用happens-before原则,java是如何通过synchronized,volatile&final关键字来实现这一原则的。
2.另外可以看看Lockc&Condition这两个对象
这两个是java5.0后引入的是对我们java锁机制和等待条件的高层实现,通过它我们可以了解我们如何对程序实现加锁以及同步的通信,结合我们的synchronized和wait以及notifyAll方法,大家可以更好的理解我们的互斥与同步是如何实现的。 java.util.concurrent.locks
3.另一部分我们可以了解下线程的安全性
什么是原子性与可见性的问题,如何通过atomic包来避免我们原子性编程的问题,java.util.concurrent.atomic,同时当我们的一个原子操作由多个操作语句构成时,我们又是如何通过synchronized的方法来实现我们的原子型操作,类似我们如何通过synchronized&volatile 来实现我们的可见性编程,最后大家需要了解什么是死锁,死锁产生的条件是什么,进而可以书写出一些避免死锁发生的线程DeadLocks
4.另外一方面多线程编程常用的交互模型
Producer-Consumer模型 经典的生产者消费者模型
Read-write Lock模型 读写锁模型
Future模型
Worker Thread模型
在了解这些模型的基础之上,大家可以考虑在我们的java并发实现当中,有哪些类是实现了这些模型可以供我们直接调用的。
5.最后一方面就是java5引入的一些并发编程工具
java.util.concurrent,都在这个包之下
线程池 ExecutorService
Callable&Future对象
BlockingQueue对象
大大简化我们我们之前的线程编程模型,使我们可以更加方便的使用一种面向于任务,更加接近于实际应用需求的一种抽象方式来书写我们的线程,使我们线程有了更大的利用空间。
最后推荐两本书 core-java 第九版。
java编程的圣经 java concurrency in practice

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值