在多线程编程中,可能会出现多个线程访问一个资源的情况,资源可以是同一内存区(变量,数组,或对象)、系统(数据库,web services等)或文件等等。如果不对这样的访问做控制,就可能出现不可预知的结果。这就是线程安全问题,常见的情况是“丢失修改”、“不可重复读”、“读‘脏’数据”等等。
目录
- 线程安全问题
- 线程安全的实现
线程安全问题
上面简单介绍了什么是线程安全问题,下面具体说下什么是“丢失修改”,其他的问题有兴趣可以自己去了解。
丢失修改
两个事务T1和T2读入同一数据并修改,T2提交的结果破坏了T1提交的结果,导致T1的修改被丢失。
拿火车票订票系统举例:
- 一号窗口读出某班次的火车票余票A,设A=1;
- 二号窗口读出同一班次的火车票余票B,当然也为1;
- 一号窗口判断出余票A=1>0,卖出一张火车票,修改余票A←A-1,A为0,把A写回数据库;
- 二号窗口判断出余票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.