【线程安全问题例子】
模拟售票案例,4个窗口售票,总共100张票。
public class Demo { public static void main(String[] args) { SaleThread sa=new SaleThread(); Thread t1=new Thread(sa); Thread t2=new Thread(sa); Thread t3=new Thread(sa); Thread t4=new Thread(sa); t1.start(); t2.start(); t3.start(); t4.start(); }}class SaleThread implements Runnable{ private int tickets=100; //总共100张票 @Override public void run() { while(tickets>0){ try { Thread.sleep(10); //此处休眠10ms } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"----卖出的票号码:"+tickets--); } }}
【运行结果】
【分析】
当只剩下一张票时,某个线程判断满足while(tickets>0)进入循环,然后休眠10ms,此时其它线程在这10ms内依次进入判断while(tickets>0),然后都休眠10ms,最后大家都执行最后的System.out.println(Thread.currentThread().getName()+"----卖出的票号码:"+tickets--);,直接导致出现了结果中出错的几种情况。
【同步的基础】
Java中的每一个对象都可以作为锁。
同步方法 :锁是当前实例对象。
静态同步方法:锁是当前对象的Class对象。
同步代码块 :锁是synchronized括号里配置的对象。
【同步代码块】
想要解决上面的线程安全问题,必须保证处理共享资源的代码在任何时刻只能有一个线程访问。
当多个线程使用同一个共享资源的时候,可以将处理共享资源的代码放在一个代码块中,使用synchronized关键字来修饰,被称作同步代码块。
synchronized(lock){ //操作共享资源的代码块}
lock:是一个锁对象,是执行同步代码块的关键,当线程执行同步代码块时,首先会检查锁对象的标志位,默认情况下标志位为1,此时线程会执行同步代码块,同时将锁对象置为0。当一个新的线程执行到这段同步代码块时,由于锁对象的标志位为0,该新线程会发生阻塞,等待当前线程执行完同步代码块时,锁对象的标志位重新置为1,新线程才能进入同步代码块执行其中的代码。循环往复,直到共享资源被处理完为止。
【加同步代码块解决线程安全问题案例】
public class Demo { public static void main(String[] args) { SaleThread sa=new SaleThread(); Thread t1=new Thread(sa); Thread t2=new Thread(sa); Thread t3=new Thread(sa); Thread t4=new Thread(sa); t1.start(); t2.start(); t3.start(); t4.start(); }}class SaleThread implements Runnable{ private int tickets=100; //总共100张票 @Override public void run() { while(true){ synchronized (this) { try { Thread.sleep(10); //此处休眠10ms } catch (InterruptedException e) { e.printStackTrace(); } if(tickets>0){ System.out.println(Thread.currentThread().getName()+"----卖出的票号码:"+tickets--); }else{ break; } } } }}
【运行结果】
...............................
【同步方法】
在方法前面加上synchronized关键字修饰,被修饰的方法称为同步方法,它能使实现和同步代码快同样的功能。
synchronized 返回值类型 方法名([参数1,......]){}
被synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其它线程都会发生阻塞,直到当前线程访问完毕后,其它线程才有机会执行方法。
【同步方法的案例】
public class Demo { public static void main(String[] args) { TicketsThread tt=new TicketsThread(); Thread t1=new Thread(tt); Thread t2=new Thread(tt); Thread t3=new Thread(tt); Thread t4=new Thread(tt); t1.start(); t2.start(); t3.start(); t4.start(); }}class TicketsThread implements Runnable{ private int tickets =100; @Override public void run() { while(true){ saleTickets(); //调用同步方法 if(tickets<=0){ break; } } } //定义一个同步方法 saleTickets() private synchronized void saleTickets(){ if(tickets>0){ try { Thread.sleep(10); //所有的线程在这里都要休眠10ms } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"----卖出的票号码:"+tickets--); } }}
【运行结果】
......
【分析】
同步方法其实也有自己的锁,它的锁就是当前调用该方法的对象,也就是this指向的对象。
这样做的好处是:同步方法被所有线程共享,方法所在的对象相对于所有线程来说都是唯一的,从而保证了锁方法的唯一性。
【扩展:静态方法怎么处理?】
静态方法可以使用"类名.方法名()"方式直接被调用,调用静态方法无需创建对象,如果不创建对象,静态方法的锁就不会是this,JAVA中静态方法的锁是该方法所在类的class对象,该对象可以直接使用“类名.class”的方式获取。
【同步代码块、同步方法的优缺点】
[ 优点 ]
解决了多个线程同时访问共享数据时的安全问题,只要加上一个锁,在同一个时间内只能有一条线程执行。
[ 缺点 ]
线程执行同步代码块的时都要判断锁的状态,非常消耗资源,效率非常低。