什么是线程安全问题?如何解决线程安全问题?
现在就进入正题,解答上述问题:
当多个线程同时操作同一个共享全局变量的时候,就容易出现线程安全问题,线程安全问题只会影响到线程对同一个共享的全局变量的写操作。
接下来就为大家演示一个线程安全问题:
package com.java.threadSecurity;
/**
* @Author:Mr.Liu
* @Description:线程安全问题再现:火车站买票
* @Date:Created in 11:20 2017/12/5
* @Modified By:
*/
public class ThreadProblem implements Runnable{
private int ticketCount = 100;//火车站总票数
private int ticketNo = 100;//未卖票数量
@Override
public void run() {
while (ticketNo>0){
try {
Thread.sleep(100);//一些其他操作耗时100ms
} catch (InterruptedException e) {
e.printStackTrace();
}
buyTicket();
}
System.out.println(Thread.currentThread().getName()+"火车票已售罄");
}
public void buyTicket(){
System.out.println(Thread.currentThread().getName()+"买票成功,当前是第几张票?:"+(ticketCount-ticketNo+1));
ticketNo--;
}
public static void main(String[] args) {
System.out.println("火车站开始售票--------------start");
Runnable runnable = new ThreadProblem();
Thread t01 = new Thread(runnable,"售票窗口No.1");
Thread t02 = new Thread(runnable,"售票窗口No.2");
t01.start();
t02.start();
}
}
上述代码运行的结果会出现如下情况:
火车站开始售票--------------start
.
.
.
售票窗口No.2买票成功,当前是第几张票?:95
售票窗口No.1买票成功,当前是第几张票?:96
售票窗口No.2买票成功,当前是第几张票?:97
售票窗口No.1买票成功,当前是第几张票?:98
售票窗口No.2买票成功,当前是第几张票?:99
售票窗口No.1买票成功,当前是第几张票?:100
售票窗口No.1火车票已售罄
售票窗口No.2买票成功,当前是第几张票?:101
售票窗口No.2火车票已售罄
从结果可以看出,火车站的已卖票数量比火车站的总票数还要大,这肯定是不合理的,原因就是因为两个线程对共享的变量ticketNo同时做减票的操作,当ticketNo =1的时候,两个线程同时满足了while循环的条件,其中一个线程已经把ticketNo更新到了0,而另一条线程直接走到了buyTicket(),故此,出现了卖出第101张票。
那么我们该如何解决呢?
对于这种现成安全问题我们通常采用锁的机制,即:当一条线程在操作ticketNo的时候为这一段写操作加上一把锁,这样其他线程就不能对这个ticketNo也做写的操作,只有当这段写操作的锁被释放之后才能由其他线程继续做写操作。接下来为大家讲解synchronized同步锁来解决上述线程安全问题,更改后的代码如下:
public void buyTicket(){
synchronized (ticketCount){
if(ticketNo>0){
System.out.println(Thread.currentThread().getName()+"买票成功,当前是第几张票?:"+(ticketCount-ticketNo+1));
ticketNo--;
}
}
}
再次运行代码,可以看到:
火车站开始售票--------------start
.
.
.
售票窗口No.2买票成功,当前是第几张票?:94
售票窗口No.1买票成功,当前是第几张票?:95
售票窗口No.2买票成功,当前是第几张票?:96
售票窗口No.1买票成功,当前是第几张票?:97
售票窗口No.2买票成功,当前是第几张票?:98
售票窗口No.1买票成功,当前是第几张票?:99
售票窗口No.2买票成功,当前是第几张票?:100
售票窗口No.2火车票已售罄
售票窗口No.1火车票已售罄
此时很明显,已经不会出现最初的安全问题了。
synchronized除了上述用法之外还有如下两种用法来达到共享全局变量的同步保证同一时间只会有一个线程对这个共享全局变量做操作,保证线程安全:
//同步函数1
public synchronized void buyTicket(){
if(ticketNo>0){
System.out.println(Thread.currentThread().getName()+"买票成功,当前是第几张票?:"+(ticketCount-ticketNo+1));
ticketNo--;
}
}
//同步函数2
public static synchronized void buyTicket(){
if(ticketNo>0){
System.out.println(Thread.currentThread().getName()+"买票成功,当前是第几张票?:"+(ticketCount-ticketNo+1));
ticketNo--;
}
}
上述用法同样可以保证不会出现线程安全问题,这三种用法的区别在于:
1. synchronized包裹的代码块所用的锁可以是一个普通的共享对象;
2. 同步函数(public synchronized void buyTicket())所用的是this锁;
3. 静态同步函数(public static synchronized void buyTicket())所持有的是当前类加载的时候的字节码文件对象(即上述代码的ThreadProblem.class)
同步锁一般试用在传统项目,且至少会有两个线程同时操作数据,其性能不是特别好。
解决线程安全问题的另一种同步方法还有Lock锁,Lock锁与synchronized锁的区别:
Lock锁类似于手动挡汽车,获取锁和释放锁需要我们自己用代码操作,synchronized锁类似自动挡汽车,锁的获取和释放都是自动的,不需要人为去控制。相比之下Lock锁更灵活。
Lock锁常用API:
tryLock(long time, TimeUnit unit):尝试在一定的时间内获取锁,如果超时未获得锁返回false,可能会抛 出InterruptedException异常;
tryLock():尝试获取锁,如果无法获取则返回false;
unlock():释放所持有的锁。
用法如下:
package com.java.threadSecurity;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author:Mr.Liu
* @Description:线程安全问题再现:火车站买票
* @Date:Created in 11:20 2017/12/5
* @Modified By:
*/
public class ThreadLock implements Runnable{
private int ticketCount = 100;//火车站总票数
private int ticketNo = 100;//未卖票数量
private Lock lock = new ReentrantLock();
@Override
public void run() {
while (ticketNo>0){
try {
Thread.sleep(100);//一些其他操作耗时100ms
} catch (InterruptedException e) {
e.printStackTrace();
}
buyTicketWithTimeLock();
//buyTicketWithNoTimeLock();
}
System.out.println(Thread.currentThread().getName()+"火车票已售罄");
}
//tryLock(long time, TimeUnit unit)
public void buyTicketWithTimeLock(){
boolean lockFalg = true;
try {
System.out.println(Thread.currentThread().getName()+"获取锁");
lockFalg = lock.tryLock(5, TimeUnit.SECONDS);//尝试在5s内获取锁
if(lockFalg){//尝试获取锁
if(ticketNo>0){
Thread.sleep(10000);//线程休眠10s
System.out.println(Thread.currentThread().getName()+"买票成功,当前是第几张票?:"+(ticketCount-ticketNo+1));
ticketNo--;
}
}else{
System.out.println(Thread.currentThread().getName()+"获取锁失败");
}
} catch (InterruptedException e) {
System.out.println(e.getCause());
} finally {
if(lockFalg){
System.out.println(Thread.currentThread().getName()+"释放锁");
lock.unlock();//释放锁
}
}
}
//tryLock()
public void buyTicketWithNoTimeLock(){
boolean flag = lock.tryLock();
if(flag){
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(ticketNo>0){
System.out.println(Thread.currentThread().getName()+"买票成功,当前是第几张票?:"+(ticketCount-ticketNo+1));
ticketNo--;
}
System.out.println(Thread.currentThread().getName()+"释放锁");
lock.unlock();
}else{
System.out.println(Thread.currentThread().getName()+"获取锁失败");
}
}
public static void main(String[] args) {
System.out.println("ThreadLock火车站开始售票--------------start");
Runnable runnable = new ThreadLock();
Thread t01 = new Thread(runnable,"售票窗口No.1");
Thread t02 = new Thread(runnable,"售票窗口No.2");
t01.start();
t02.start();
}
}
至此,Lock锁和synchronized锁解决线程安全问题已讲解完毕。
下一篇将为大家讲解多线程的三大特性。