Java并发编程札记-(一)基础-05线程安全问题

来源:互联网 发布:海森伯格矩阵图片 编辑:程序博客网 时间:2024/06/06 03:14

在多线程编程中,可能会出现多个线程访问一个资源的情况,资源可以是同一内存区(变量,数组,或对象)、系统(数据库,web services等)或文件等等。如果不对这样的访问做控制,就可能出现不可预知的结果。这就是线程安全问题,常见的情况是“丢失修改”、“不可重复读”、“读‘脏’数据”等等。

目录

  1. 线程安全问题
  2. 线程安全的实现

线程安全问题

上面简单介绍了什么是线程安全问题,下面具体说下什么是“丢失修改”,其他的问题有兴趣可以自己去了解。

丢失修改

两个事务T1和T2读入同一数据并修改,T2提交的结果破坏了T1提交的结果,导致T1的修改被丢失。

拿火车票订票系统举例:

  1. 一号窗口读出某班次的火车票余票A,设A=1;
  2. 二号窗口读出同一班次的火车票余票B,当然也为1;
  3. 一号窗口判断出余票A=1>0,卖出一张火车票,修改余票A←A-1,A为0,把A写回数据库;
  4. 二号窗口判断出余票B=1>0,也卖出一张火车票,修改余票B←B-1,B为-1;

余票只有一张,但最后卖出了两张火车票。在程序中,没有对两个窗口对余票的访问做控制,所以造成了这个错误。
例1:火车票订票系统-线程不安全版

public class SellTickets {    public static void main(String[] args) {        TicketsWindow tw = new TicketsWindow();        Thread t1 = new Thread(tw, "一号窗口");        Thread t2 = new Thread(tw, "二号窗口");        t1.start();        t2.start();    }}class TicketsWindow implements Runnable {    private int tickets = 1;    @Override    public void run() {        while (true) {            if (tickets > 0) {                System.out.println(Thread.currentThread().getName() + "还剩余票:" + tickets + "张");                tickets--;                System.out.println(Thread.currentThread().getName() + "卖出一张火车票,还剩" + tickets + "张");            } else {                System.out.println(Thread.currentThread().getName() + "余票不足,暂停出售!");                try {                    Thread.sleep(1000 * 60 * 5);                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        }    }}

运行结果为

一号窗口还剩余票:1张二号窗口还剩余票:1张一号窗口卖出一张火车票,还剩0张二号窗口卖出一张火车票,还剩-1张一号窗口余票不足,暂停出售!二号窗口余票不足,暂停出售!

这明显不是我们想要的结果。

线程安全问题解决方法

上面的问题归根结底是由于两个线程访问相同的资源造成的。对于并发编程,需要采取措施防止两个线程来访问相同的资源。

一种措施是当资源被一个线程访问时,为其加锁。第一个访问资源的线程必须锁定该资源,是其他任务在资源被解锁前不能访问该资源。

基本上所有的并发模式在解决线程安全问题时,都采用“序列化访问临界资源”的方案。即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问。通常来说,是在访问临界资源的代码前面加上一个锁,当访问完临界资源后释放锁,让其他线程继续访问。
 
在Java多线程编程当中,提供了以下几种方式来实现线程安全:

  • 内部锁(Synchronized)和显式锁(Lock)。这两种方式是重量级的多线程同步机制,可能会引起上下文切换和线程调度,它同时提供内存可见性、有序性和原子性。
  • volatile:轻量级多线程同步机制,不会引起上下文切换和线程调度。仅提供内存可见性、有序性保证,不提供原子性。
  • CAS原子指令:轻量级多线程同步机制,不会引起上下文切换和线程调度。它同时提供内存可见性、有序性和原子化更新保证。

本文就讲到这里,想了解更多内容请参考:

  • Java并发编程札记-目录

END.

原创粉丝点击