(3)Java多线程之安全问题-上

1.引言

      在本篇博客中,我主要记录一下在多线程编程中存在的线程安全问题,以及如何去解决这种问题。

2 现实实例(出现问题)

      首先我们举个例子:我们开发了一个售票系统,一共有100张票,然后我们用10个线程去买票,直到卖票结束位置。

  • 定义我们的Ticket类

public class Ticket {
    //一共有100张票
    private int num=100;
    public int getNum() {
        return num;
    }
    //用于卖票,调用一下这个方法,票少一张
    public boolean saleTicket() throws Exception
    {   
        //用于判断,票是否卖没
        boolean finish=false;
        if(this.num>0){
            //模拟卖票需要时间50ms
            Thread.sleep(50);
            num--;
        }else
        {
            finish=true;
            System.out.println("票卖结束了:剩余"+num);
        }
        return finish;
    }
}
  • 定义售票线程

public class MyThread extends Thread {
    private Ticket ticket;

    public MyThread(Ticket t) {
        this.ticket = t;
    }

    @Override
    public void run() {
        boolean finish=false;
        while (true) {
            try {
                finish=ticket.saleTicket();
            } catch (Exception e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            if(finish)
            {
                return;
            }
        }
    }

}
  • 启动2个线程来卖票


public class app {
    public static void main(String[] args) throws InterruptedException {
        Ticket t=new Ticket();
        for(int i=0;i<2;i++)
        {
            MyThread td=new MyThread(t);
            td.start();
        }
    }

}
  • 运行结果

这里写图片描述

我们的票的数量怎么会出现负数呢?
这在现实生活中是不合理的,一共只有100张票,卖出超过了100张。
这也是我们在这篇博客中应该解决的问题

3 问题的原因

接下来我们用一张图来看一下这种bug出现的原因:

这里写图片描述

4 解决多线程安全问题

      在解决线程安全问题之前,我们有比较了解一下:锁的概念。什么是锁?我们可以这么理解,在线程执行一个方法之前,首先要获得这个方法的锁,如果拿不到这个锁,该线程就只能等待,直到拿到该锁为止。在Java中,存在两种锁,一种是对象锁,一种是类锁。具体什么意思我们接下来看一下。

4.1 给售票方法直接加一个对象锁

  • 代码修改如下(只是在方法上添加了一个关键字synchronized,注意此方法还是非静态方法。)
public synchronized boolean  saleTicket() throws Exception
    {   
        boolean finish=false;
        if(this.num>0){
            Thread.sleep(50);
            num--;
        }else
        {
            finish=true;
            System.out.println("票卖结束了:剩余"+num);
        }
        return finish;
    }
  • 在此运行该方法:结果

这里写图片描述

解释:
    1.当线程1进入saleTicket方法前,先判断是否可以拿到该方法的锁(是对象锁,具体点说,也就是this这个对象,因为这个方法是非静态方法),
    如果拿到this中的锁,那么进入该方法,当线程1执行完saleTicket方法之后,释放this对象锁,此时其他线程才可以访问saleTicket方法。
2.当线程2想要进入saleTicket方法前,如果线程1正在访问该方法,线程2是无法得到this对象锁的,
    线程2只能等待,等到线程1执行完方法,线程2才可以进入saleTicket方法。

4.2 给售票方法直接加锁的弊端

      我们解决了上述的问题,发现了一个问题,异步线程编程了同步的方法,那么我们多线程还有什么意义呢?就好比搬砖,一个人搬砖需要2个小时,10个人搬砖还是需要两个小时(因为第二个人想要搬砖需要等待上一个搬砖结束。),这不是多此一举吗?这种弊端我们可以通过静态代码块来解决。

4.3 通过同步块给方法加锁

  • 修改之后的代码如下
public  boolean  saleTicket() throws Exception
    {   
        boolean finish=false;
        if(this.num>0){
            Thread.sleep(50);
            //注意这里:我们只是将判断这一个简单的过程,同步化
            synchronized (this) {
                if(this.num>0){
                    num--;
                }
            }   
        }else
        {
            finish=true;
            System.out.println("票卖结束了:剩余"+num);
        }
        return finish;
    }
  • 运行结果

这里写图片描述

修改之后的代码优点:
    我们并不是将整个方法加锁,只是将代码的重要部分加锁,一些复杂的(耗时间的)不加锁,
    这样既可以保证我们线程的安全,又可以提高我们的效率。这个大大提高了我们程序的效率

4.4 通过同步块给方法加锁补充

4.4.1 补充一

  • 看下面的代码
public  void  saleTicket1()
    {   
        synchronized (this) {
            System.out.println(Thread.currentThread().getName()+"saleTicket1");

        }

    }
    public synchronized void  saleTicket2()
    {   
        synchronized (this) {
            System.out.println(Thread.currentThread().getName()+"saleTicket2");

        }
    }
这里有两个方法可以售票,同步代码块加锁都是用this对象进行加锁,所以多个线程不可以同时访问代码块的内容,
例如:
    线程1访问方法saleTicket1的同时(也就是线程1未释放锁之前),
    线程2也不可以访问saleTicket2方法(因为线程2拿不到this对象锁),必须要等待线程1将this锁释放。

4.4.2补充二

同样是上面的代码:如果线程1拿到了this锁(在saleTicket1方法中拿到的锁),那么线程1可以访问saleTicket2方法。例子

    public  void  saleTicket1()
    {   
        synchronized (this) {
            saleTicket2();
        }   
    }
    public synchronized void  saleTicket2()
    {   
            System.out.println("在saleTicket1()方法中拿到锁,可以访问saleTicket2()方法");
    }
  • 运行结果

这里写图片描述

4.4.3 补充三

还是上面的代码:如果我们用不同的对象进行加锁,那么多个线程是可以同时访问的。


public class Ticket {
    private Object o1=new Object();
    private Object o2=new Object();
    public  void  saleTicket1()
    {   
        synchronized (o1) {
            System.out.println(Thread.currentThread().getName()+"saleTicket1");

        }

    }
    public synchronized void  saleTicket2()
    {   
        synchronized (o2) {
            System.out.println(Thread.currentThread().getName()+"saleTicket2");

        }
    }
}
我们将saleTicket1用o1对象加锁,将saleTicket2用o2对象加锁,
那么多线程的时候,当线程1访问saleTicket1方法的时候(拿到的是o1对象锁)
线程2如果想访问saleTicket2方法,那么线程2将要去拿o2对象锁,两者是没有影响的。

5 总结

      在本篇博客中,主要写了:

  • 线程安全出现的原因
  • 给方法加对象锁(没有讲解,如何加类锁)
  • 给同步块加锁的一些补充
  • 同步块加锁的一些优点
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值