JAVA多线程(三)

在之前运用多线程练习时,写了一个模拟三个窗口售票的简单程序,但其实它还存在许多小问题,也就是我们所说的多线程安全问题,我们先来看看之前的:

首先我们有100张票要卖:
在这里插入图片描述
卖票的线程类这样写:
加粗样式
为了使问题更加明显,我们加上睡眠方法

接下来用一个测试类新建三个子线程,并且启动:
在这里插入图片描述
查看运行结果:
在这里插入图片描述
在这里插入图片描述
在这两处我们发现,卖出的票存在剩余票无序、重复剩余,甚至还有余票为负数的情况,那么这样的问题是怎么来的?分析一下:

在之前有说过,JAVA的多线程是抢占式策略,系统每次会给每个正在进行的线程分配一段时间,时间一到,不管该线程有没有完成任务,都会被剥夺cpu资源,等待下一次重新分配。由于我们无法对系统分配的时间进行调整,在这其中就可能会出现问题。

先用一张图来描述该程序:
在这里插入图片描述
整个售票程序我们可以大概描述成这样:三个窗口线程同时运行,接着进行if判断余票tick是否大于0,大于则打印售票语句并且对tick进行一次递减。假设窗口1抢占到CPU资源后,在顺利走完tick>0的判断并且完成了计算余票后,系统给其分配的时间用尽,还没来得及打印语句就被回收了CPU使用权,而此时其实余票为99,但窗口1只能排队等待下一次分配。同样此时窗口2与窗口1发生了同样的状况,此时余票98,但没有打印,窗口2等待下一次分配。而窗口3顺利的走完了所有流程,此时打印出来tick剩余97张票。在某个时间,窗口1和窗口2重新得到分配,打印出了剩余98和剩余99,但此时已经顺序错乱了。

同样地,3个窗口其一也有可能在进行tick>0判断时,时间就用尽,被系统剥夺CPU使用权,等待下一次分配,当某一个时间余票为0时,被剥夺使用权的窗口重新得到分配,顺利进行了整个流程,由于余票为0,打印出的就是负数等等。这样的问题就是多线程安全问题。

归根到底,问题出现的原因就围绕在对tick这一共享数据的操作上,如果我们能控制住每次只有一个线程执行完对tick的操作再进行下一个线程,这样一个一个的线程有序执行。问题就能得到解决。不论是判断tick>0还是对tick进行递减,都是对tick的我们不妨将对tick操作的所有方法包装到一起,只用对入口,即判断tick>0时就进行线程的控制,那么问题也会更容易解决。这就是我们说的线程同步。

说了这么多,就看看这个例子使用线程同步怎么优化:

方法1:同步代码块
使用synchronized方法:

  int tick=1000;

    Object obj=new Object();

    @Override
    public void run() {

        while (true){
            synchronized (obj){
       
                System.out.println(Thread.currentThread().getName()+"完成售票,余票为:"+--tick);
            }

        }

其中括号中的参数叫做同步监视器,俗称锁,它可以使用任意对象充当,但必须确保每个线程都是用的同一个对象,在这里我新建了一个全局的Object对象,当然我们也可以直接传入this,为了观察方便我去除了sleep方法,并将tick调整为1000,反复运行:
在这里插入图片描述
在这里插入图片描述
发现该问题已经得到解决。

方法2:同步方法

我们也可以将售票的代码单独写成一个方法,使用synchronized关键字修饰:

public synchronized void show(){
        if (tick > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {

            }
            System.out.println(Thread.currentThread().getName()+"完成售票,余票为:"+--tick);
        }
    }

接着在run方法中调用即可,该方法不能声明锁,但存在一把隐式锁,即this

方法3:同步锁

该方法比较特殊,首先我们声明一个锁Lock接口的实现类:

Lock l=new ReentrantLock();

在访问共享资源前上锁,在访问完后解锁:

l.lock();
            try {
                if (tick > 0) {
               /*  try {
                     Thread.sleep(100);
                 } catch (InterruptedException e) {

                 }*/
                    System.out.println(Thread.currentThread().getName()+"完成售票,余票为:"+--tick);
                }
            }  finally {
                l.unlock();
            }

如果不解锁,程序会一直运行下去,所以我们将解锁方法放在finally语句块中,保证它一定会执行。

使用以上3种方法,就可以实现线程同步,当然,在一些情况下,我们也会出现死锁的情况,比如有这样一个例子:

public class TestDeadLock {
    public static void main(String[] args) {
        DeadLock d1=new DeadLock();
        Thread t1=new Thread(d1);
        t1.start();

        DeadLock d2=new DeadLock();
        d2.flag=false;
        Thread t2=new Thread(d2);
        t2.start();
    }
}

class DeadLock implements Runnable{

    static Object obj1=new Object();//共享资源1
    static Object obj2=new Object();//共享资源2

    boolean flag=true;

    @Override
    public void run() {
        if (flag){
            synchronized (obj1){
                for (int i=0;i<5;i++){


                System.out.println("获取资源1,等待资源2");

                synchronized (obj2){
                    System.out.println("**************");
                }
                }
            }

        }else {
            synchronized (obj2){
                for (int i=0;i<5;i++) {
                    System.out.println("获取资源2,等待资源1");
                    synchronized (obj1) {
                        System.out.println("******************");
                    }
                }
            }
        }
    }
}

运行后发现,有时候该程序不会停止:
在这里插入图片描述
在这里插入图片描述
分析原因:
线程t1在走if语句块时,占用了共享资源obj1,而同时线程t2由于flag值进行了改变,走的是else语句块,占用了共享资源obj2,当线程1继续往下走时需要占用obj2,线程2则要占用obj1,但都被对方握住了不放,这样程序无法结束,也就形成了死锁。

针对此种情况,我们解决的方法无非就是要么对共享资源的定义尽量减少、要么有自己写下的一套算法对其进行避免。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值