本文讲述一下线程的安全问题及解决方法。
一 线程安全问题举例说明
在电影院售票的场景,假使有100张票待售,有3个窗口在售票,如图所示:
三个窗口都卖出票1,一个票被卖了3次,多线程访问共享数据“票”,出现了线程安全问题。
问题代码见下:
public class SellTicketImpl1 implements Runnable {
private int ticket=100;
@Override
public void run() {
while (true){
if(ticket>0){
System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票");
ticket--;
}
}
}
}
public class ThreadSafeDemo1 {
public static void main(String[] args) {
SellTicketImpl1 sellTicket=new SellTicketImpl1();
Thread t1=new Thread(sellTicket);
Thread t2=new Thread(sellTicket);
Thread t3=new Thread(sellTicket);
t1.start();
t2.start();
t3.start();
}
}
运行后出现下图所示:
二 解决线程安全问题三种方式
为了保证每个线程都能正常执行原子操作,java引入了线程同步机制。
有三种方式完成同步操作:
- 同步代码块
- 同步方法
- 锁机制
下面依次讲述三种同步操作:
1 同步代码块
1)说明:
解决线程安全问题的第一种方案是使用同步代码块,同步代码块的格式见下:
synchronized(锁对象){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
注意:
通过代码块中的锁对象,可以使用任意的对象,例如Object类对象;
但是必须保证多个线程使用的锁对象是同一个锁对象:把同步代码块锁住,只让一个线程在同步代码块中执行。
2)代码
public class SellTicketImpl2 implements Runnable {
private int ticket=100;
//创建一个Object类对象,当做锁对象
Object obj=new Object();
@Override
public void run() {
while (true){
//使用synchronized同步代码块
synchronized (obj) {
if (ticket > 0) {
try {
//让它睡眠1秒
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
ticket--;
}
}
}
}
}
public class ThreadSafeDemo2 {
public static void main(String[] args) {
SellTicketImpl2 sellTicket=new SellTicketImpl2();
Thread t1=new Thread(sellTicket);
Thread t2=new Thread(sellTicket);
Thread t3=new Thread(sellTicket);
t1.start();
t2.start();
t3.start();
}
}
- 同步方法
1)说明
解决线程安全问题的第二种方案是使用同步方法,同步方法的格式见下:
修饰符 synchronized 返回值类型 方法名(参数列表){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
2)代码
public class SellTicketImpl3 implements Runnable {
//定义一个多个线程共享的票源
private int ticket=100;
//设置线程任务:卖票
@Override
public void run() {
while (true) {
sellTicket();
}
}
//定义同步方法
public synchronized void sellTicket(){
//使用死循环,让卖票重复操作执行
while(true){
//先判断票是否存在
if(ticket>0){
try {
//提高安全问题出现的概率,让程序睡眠
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//如果票存在,卖票ticket
System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票");
ticket--;
}
}
}
}
public class ThreadSafeDemo3 {
public static void main(String[] args) {
SellTicketImpl3 sellTicket=new SellTicketImpl3();
Thread t1=new Thread(sellTicket);
Thread t2=new Thread(sellTicket);
Thread t3=new Thread(sellTicket);
t1.start();
t2.start();
t3.start();
}
}
3 Lock锁
1)说明:解决线程安全问题的第三种方案是使用Lock锁,即java.util.concurrent.locks.Lock接口。
Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。
Lock 接口中的方法有:
void lock() 获取锁
void unlock() 释放锁
2)使用步骤:
(1)在成员位置创建一个ReentrantLock 对象;
(2)在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁;
(3)在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁;
3)代码
public class SellTicketImpl4 implements Runnable {
//定义一个多个线程共享的票源
private int ticket=100;
//1.在成员位置创建一个ReentrantLock对象
//使用多态
Lock lk=new ReentrantLock();
//设置线程任务:卖票
@Override
public void run() {
while (true){
//使用死循环,让卖票重复操作执行
while(true){
//2.在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁
lk.lock();
//先判断票是否存在
if(ticket>0){
try {
//提高安全问题出现的概率,让程序睡眠
Thread.sleep(10);
//如果票存在,卖票ticket
System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票");
ticket--;
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//3.在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁
lk.unlock(); //无论是否出现异常,都释放锁
}
}
}
}
}
}
public class ThreadSafeDemo4 {
public static void main(String[] args) {
SellTicketImpl4 sellTicket=new SellTicketImpl4();
Thread t1=new Thread(sellTicket);
Thread t2=new Thread(sellTicket);
Thread t3=new Thread(sellTicket);
t1.start();
t2.start();
t3.start();
}
}
运行后,如下图所示:
三 同步技术原理:
n个线程t1,t2,t3,…,tn,一起抢夺cpu的执行权,谁抢到了谁执行run方法进行卖票。假设t1抢到了cpu的执行权,执行run方法,遇到synchronized代码块,这时t1会检查synchronized代码块是否有锁对象(上述的obj),如果有,就会获取到锁对象,进入到同步执行;这时候t2也抢到了cpu执行权但是没有检查到锁对象,那么t2就会进入阻塞状态,会一直等待t1线程归还锁对象,一直到t1线程执行完同步中的代码,会把对象归还给synchronized代码块,t2才能获取到锁对象进入到同步中执行。
总结:同步中的线程,没有执行完毕不会释放锁,同步外的线程没有锁进不去同步。同步保证了只能有一个线程在同步中执行共享数据。