目录
1.线程安全问题
通常情况下,一个进程中的比较耗时的操作(如长循环、文件上传下载、网络资源获取等),往往会采用多线程来解决。比如现实生活中,银行取钱问题、火车票多个售票窗口的问题,都需要多线程并发执行实现。当进程中有多个线程同时访问临界资源,也就是同时进入临界区时,很有可能引发线程安全问题,造成数据异常。正常逻辑下,同一张火车票只能售出一次,却由于线程安全问题而被多次售出,从而引起系统业务异常。
下面用一个案例来模拟多窗口售票所引起的线程安全问题:
① 声明一个Runable 接口的实现类,规定票的数量以及各个窗口卖票的动作。
public class RunableImpl implements Runnable {
//临界资源:100张票
int ticket = 100;
//卖票过程
@Override
public void run() {
while (true){
//临界区代码------------------------------------------
if(ticket>0){
//每卖一张票需要10毫秒
try {
sleep(10);
} catch (InterruptedException e) {}
//卖票
System.out.println(getName()+" 卖出了"+ticket+"号票");
ticket--;
}
//---------------------------------------------------
}
}
}
② 三个窗口同时开始卖票。
public static void main(String[] args) {
Runnable run = new RunableImpl();
//三个窗口开始卖票
new Thread(run,"窗口1").start();
new Thread(run,"窗口2").start();
new Thread(run,"窗口3").start();
}
③ 选取一部分不正常的结果进行分析:
从图中我们可以很清楚的看到,100 号、97 号、94 号票都被卖了多次,但 99 号、96 号、93 号、92 号票却莫名其妙的 “消失” 了。拿 97 号票来说,窗口 1 和窗口 2 几乎同时查看了票数,此时有 97 张票,因此它们都在控制台显示卖了 97 号票;之后窗口 3 开始卖票,因为窗口 1 和 2 均把票减少了 1 ,窗口 3 只能显示卖了 95 号票;之后三个窗口又同时查看票数再进行减 1 操作,致使票数一下子减少到 91 张。
2.实现互斥的访问临界资源
上面所出现的问题,究其原因就是三个线程在同一时间访问临界资源导致线程不安全。因此我们只要让这几个线程互斥的进入临界区,便可以避免此类问题的发生。虽然互斥的进入临界区会因为线程等待降低效率,但是为了确保程序的正确执行,这样的等待是值得的。在 Java 中互斥的实现方式有三种,分别是:同步代码块、同步方法以及同步锁机制,下面就来一一介绍这几种方式。
(1)同步代码块
public class RunableImpl implements Runnable {
//临界资源:100张票
int ticket = 100;
//同步代码块括号中的参数:同步锁对象
//要保证三个线程使用同一个锁
Object obj = new Object();
//卖票过程
@Override
public void run() {
while (true){
//同步代码块
synchronized (obj){
//临界区代码------------------------------------------
if(ticket>0){
//每卖一张票需要10毫秒
try {
sleep(10);
} catch (InterruptedException e) {}
//卖票
System.out.println(getName()+" 卖出了"+ticket+"号票");
ticket--;
}
//---------------------------------------------------
}
}
}
}
可以把同步代码块看作是一个房间,把 obj 看作是一把锁。当一个线程进入这个房间之后,就会把门锁上,此时若有另一个线程想进入这个房间,就必须等待房间里的线程在访问完临界资源之后开锁。只要保证锁是同一把锁,那么无论何时,此房间中至多只会有一个线程存在。
(2)同步方法
同步方法与同步锁原理相同,但同步方法省略的对同步锁的声明,而是默认锁为方法当前所处的实例化对象。若该方法是静态方法,默认锁为该类的 Class 对象,不过一般很少遇到需要静态方法的情形,因为在 Runable 实现类中的属性和方法本身就是被所有通过此实现类创建的线程所共享的。
public class RunableImpl implements Runnable {
//临界资源:100张票
int ticket = 100;
//卖票过程
@Override
public void run() {
while (true){
sellTicket();
}
}
//同步方法
private synchronized void sellTicket(){
//临界区代码------------------------------------------
if(ticket>0){
//每卖一张票需要10毫秒
try {
sleep(10);
} catch (InterruptedException e) {}
//卖票
System.out.println(getName()+" 卖出了"+ticket+"号票");
ticket--;
}
//---------------------------------------------------
}
}
(3)同步锁机制
虽然 synchronized 块和方法的范围机制使得锁编程方便了很多,而且还避免了很多涉及到锁的常见编程错误,但有时也需要以更为灵活的方式使用锁。Lock 接口的实现类允许锁在不同的作用范围内获取和释放,并允许以任何顺序获取和释放多个锁。随着灵活性的增加,也带来了更多的风险。不使用块结构锁就失去了使用 synchronized 块和方法时会出现的锁自动释放功能,因此在代码要离开临界区时就必须手动解锁,而且要尽量把解锁写在 finally 代码块中,避免程序异常导致忙等。
public class RunableImpl implements Runnable {
//临界资源:100张票
int ticket = 100;
//实例化一个锁对象
Lock lock = new ReentrantLock();
//卖票过程
@Override
public void run() {
while (true) {
//上锁
lock.lock();
try {
//临界区代码------------------------------------------
if (ticket > 0) {
//每卖一张票需要10毫秒
sleep(10);
//卖票
System.out.println(getName() + " 卖出了" + ticket + "号票");
ticket--;
}
//---------------------------------------------------
} catch (Exception e) {
} finally {
//解锁
lock.unlock();
}
}
}
}