线程安全性问题

来源:互联网 发布:阿里域名优惠口令 编辑:程序博客网 时间:2024/06/01 19:07

线程安全性问题

线程安全性问题说白了就是多个线程在抢夺同一个资源的时候,出现数据不一致的现象。我们来模拟一个售票的情景。

Ticket代码

package com.hy.thread.t4;public class Ticket {    private Integer count;    public Ticket(Integer count) {        this.count = count;    }    public Integer getCount() {        return count;    }    public void setCount(Integer count) {        this.count = count;    }    public Integer sale() {        if(count >= 1) {            System.out.println(Thread.currentThread().getName() + " 卖出第 " + count + " 张票!");            return count --;        }        return -1;    }}

Windows代码

package com.hy.thread.t4;/** * 模拟售票大厅 * * @author 007 * */public class Windows implements Runnable {    private Ticket ticket;    public Windows(Ticket ticket) {        this.ticket = ticket;    }    @Override    public void run() {        saleTicket();    }    private void saleTicket() {        while(ticket.sale() > 0) {        }    }}

运行类

package com.hy.thread.t4;import java.util.concurrent.atomic.AtomicInteger;public class ThreadDemo {    public static void main(String[] args) {        Ticket ticket = new Ticket(100);        Windows w1 = new Windows(ticket);        Thread t1 = new Thread(w1);        Thread t2 = new Thread(w1);        Thread t3 = new Thread(w1);        Thread t4 = new Thread(w1);        t1.start();        t2.start();        t3.start();        t4.start();    }}

运行结果如下(截取部分):

Thread-2 卖出第 100 张票!Thread-0 卖出第 100 张票!Thread-3 卖出第 100 张票!Thread-1 卖出第 100 张票!Thread-3 卖出第 97 张票!Thread-0 卖出第 98 张票!Thread-3 卖出第 4 张票!Thread-1 卖出第 5 张票!Thread-0 卖出第 1 张票!Thread-2 卖出第 2 张票!

从运行结果中我们可以看到,同一张票可能会被销售多次,而且甚至可能会出现卖出负数票的现象,这和实际的场景是不符合的,这就出现了线程安全性问题。出现问题的原因就是由于多个线程一起来对tickets进行减操作。而tickets– 这一个操作是一个非原子性操作,因此出现了tickets数据的不一致,也就导致了一张票卖多次的情况。在解决这个问题之前,我们先来搞清楚出现线程安全性问题的原因。通过上面我们可以总结以下三点:

  • 首先要有多个线程
  • 其次要有共享资源
  • 还要对资源进行非原子性操作

以上三点必须同时全部满足才会出现线程安全性问题。缺少其中的任何一个都不会发生线程安全问题。以卖票的例子说明,首先我们创建了4个线程来进行同时卖票,票作为多个线程的共享资源,对票的减操作为非原子性操作。因此出现了线程安全性问题。

这里多次提到了原子操作。那么什么是原子操作呢,我们认为原子是不可再分割的最小单元。那么代码的原子操作也就是说这段代码一步就执行完毕了,不会分成多步来执行。比如我们上面遇到的tickets– 这个就不是原子性操作,为什么呢,因为tickets– 可以认为是先 减一,再赋值两步操作。

分析出了问题的原因,那么我们改如何解决这个问题呢?

  • 多线程改为单线程
  • 去掉共享资源
  • 将非原子性操作转变为原子性操作

以上三点我们发现出了第三点可以为我们所用以外,以上两点是以程序为代价的,一般来讲是不会使用的。那么我们所研究的重点也就到了把非原子性操作转为原子性操作。

如何把非原子性操作转为原子性操作呢,有很多的手段,这也是我们后面的内容要详细的来学习的。这里仅仅做一个简单的描述。首先就是我们所熟悉的同步监视器,也就是在出现线程安全问题的方法上添加synchronized关键字,这样标识有synchronized关键字的方法在同一时刻只能有一个线程进入,必须等待一个线程执行完毕其他线程才能开始执行,也就相当于原子性操作。另外可以使用jdk提供的原子操作,比如把Integer换成AtomicInteger,AtomicInteger就是一个原子类,那么把减操作就可以使用其提供的方法来处理。另外jdk也给我们提供了大量的锁,我们也可以使用各种的锁来保证方法的原子性操作。

1.使用synchronized关键字来解决线程安全性问题

public synchronized Integer sale() {    if(count >= 1) {        System.out.println(Thread.currentThread().getName() + " 卖出第 " + count + " 张票!");        return count --;    }    return -1;}

结果如下

Thread-0 卖出第 20 张票!Thread-2 卖出第 19 张票!Thread-2 卖出第 18 张票!Thread-2 卖出第 17 张票!Thread-2 卖出第 16 张票!Thread-2 卖出第 15 张票!Thread-2 卖出第 14 张票!Thread-2 卖出第 13 张票!Thread-2 卖出第 12 张票!Thread-2 卖出第 11 张票!Thread-2 卖出第 10 张票!Thread-2 卖出第 9 张票!Thread-2 卖出第 8 张票!Thread-2 卖出第 7 张票!Thread-2 卖出第 6 张票!Thread-2 卖出第 5 张票!Thread-2 卖出第 4 张票!Thread-2 卖出第 3 张票!Thread-3 卖出第 2 张票!Thread-1 卖出第 1 张票!

使用synchronized是如何保证线程安全性问题的呢?

当一个线程试图访问被synchronized修饰的方法时,它首先必须得到锁。退出或者抛出异常时必须释放锁。那么当线程进来之后会拿到锁,别的线程执行到这个方法之后就会等待,直到拿到锁的线程执行完毕之后,将锁释放,另一个线程才能进入。因此在同一时刻,被synchronized修饰的方法只能被同一个线程执行,这也就保证了线程执行的安全性。

  1. 使用AtomicInteger来代替Integer
package com.hy.thread.t4;import java.util.concurrent.atomic.AtomicInteger;public class Ticket {    private AtomicInteger count;    public Ticket(Integer count) {        this.count = new AtomicInteger(count);    }    public AtomicInteger getCount() {        return count;    }    public void setCount(AtomicInteger count) {        this.count = count;    }    public Integer sale() {        return count.getAndDecrement(); // 注意这里    }}

处理线程安全性问题是在多线程编程中需要时刻注意的问题,多线程的复杂之处其实也就在于性能与安全性之间的平衡。我们可以直接通过synchronized来解决线程安全问题,但性能却大大的降低。

原创粉丝点击