本文讲述一下线程的安全问题及解决方法。
一 线程安全问题举例说明
在电影院售票的场景,假使有100张票待售,有3个窗口在售票,如图所示:
三个窗口都卖出票1,一个票被卖了3次,多线程访问共享数据“票”,出现了线程安全问题。
问题代码见下:
publicclassSellTicketImpl1implementsRunnable{privateint ticket=100;@Overridepublicvoidrun(){while(true){if(ticket>0){System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票");
ticket--;}}}}
publicclassThreadSafeDemo1{publicstaticvoidmain(String[] args){SellTicketImpl1 sellTicket=newSellTicketImpl1();Thread t1=newThread(sellTicket);Thread t2=newThread(sellTicket);Thread t3=newThread(sellTicket);
t1.start();
t2.start();
t3.start();}}
运行后出现下图所示:
二 解决线程安全问题三种方式
为了保证每个线程都能正常执行原子操作,java引入了线程同步机制。
有三种方式完成同步操作:
- 同步代码块
- 同步方法
- 锁机制
下面依次讲述三种同步操作:
1 同步代码块
1)说明:
解决线程安全问题的第一种方案是使用同步代码块,同步代码块的格式见下:
synchronized(锁对象){
可能会出现线程安全问题的代码(访问了共享数据的代码)}
注意:
通过代码块中的锁对象,可以使用任意的对象,例如Object类对象;
但是必须保证多个线程使用的锁对象是同一个锁对象:把同步代码块锁住,只让一个线程在同步代码块中执行。
2)代码
publicclassSellTicketImpl2implementsRunnable{privateint ticket=100;//创建一个Object类对象,当做锁对象Object obj=newObject();@Overridepublicvoidrun(){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--;}}}}}
publicclassThreadSafeDemo2{publicstaticvoidmain(String[] args){SellTicketImpl2 sellTicket=newSellTicketImpl2();Thread t1=newThread(sellTicket);Thread t2=newThread(sellTicket);Thread t3=newThread(sellTicket);
t1.start();
t2.start();
t3.start();}}
- 同步方法
1)说明
解决线程安全问题的第二种方案是使用同步方法,同步方法的格式见下:
修饰符 synchronized 返回值类型 方法名(参数列表){
可能会出现线程安全问题的代码(访问了共享数据的代码)}
2)代码
publicclassSellTicketImpl3implementsRunnable{//定义一个多个线程共享的票源privateint ticket=100;//设置线程任务:卖票@Overridepublicvoidrun(){while(true){sellTicket();}}//定义同步方法publicsynchronizedvoidsellTicket(){//使用死循环,让卖票重复操作执行while(true){//先判断票是否存在if(ticket>0){try{//提高安全问题出现的概率,让程序睡眠Thread.sleep(1);}catch(InterruptedException e){
e.printStackTrace();}//如果票存在,卖票ticketSystem.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票");
ticket--;}}}}
publicclassThreadSafeDemo3{publicstaticvoidmain(String[] args){SellTicketImpl3 sellTicket=newSellTicketImpl3();Thread t1=newThread(sellTicket);Thread t2=newThread(sellTicket);Thread t3=newThread(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)代码
publicclassSellTicketImpl4implementsRunnable{//定义一个多个线程共享的票源privateint ticket=100;//1.在成员位置创建一个ReentrantLock对象//使用多态Lock lk=newReentrantLock();//设置线程任务:卖票@Overridepublicvoidrun(){while(true){//使用死循环,让卖票重复操作执行while(true){//2.在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁
lk.lock();//先判断票是否存在if(ticket>0){try{//提高安全问题出现的概率,让程序睡眠Thread.sleep(10);//如果票存在,卖票ticketSystem.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票");
ticket--;}catch(InterruptedException e){
e.printStackTrace();}finally{//3.在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁
lk.unlock();//无论是否出现异常,都释放锁}}}}}}
publicclassThreadSafeDemo4{publicstaticvoidmain(String[] args){SellTicketImpl4 sellTicket=newSellTicketImpl4();Thread t1=newThread(sellTicket);Thread t2=newThread(sellTicket);Thread t3=newThread(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才能获取到锁对象进入到同步中执行。
总结:同步中的线程,没有执行完毕不会释放锁,同步外的线程没有锁进不去同步。同步保证了只能有一个线程在同步中执行共享数据。
版权归原作者 神州永泰 所有, 如有侵权,请联系我们删除。