例如下面是用多个线程(窗口)售票的代码。
class Ticket implements Runnable { private int num; //票的数量 Ticket(int num){ this.num = num; } //售票 public void sale() { if(num>0) { num--; System.out.println(Thread.currentThread().getName()+"-------"+remain()); } } //获取剩余票数 public int remain() { return num; } public void run(){ while(true) { sale(); } } } public class ConcurrentDemo { public static void main(String[] args) { Ticket t = new Ticket(100); //创建多个线程对象 Thread t1 = new Thread(t); Thread t2 = new Thread(t); Thread t3 = new Thread(t); Thread t4 = new Thread(t); //开启多个线程使其执行任务 t1.start(); t2.start(); t3.start(); t4.start(); } }执行结果大致如下:
以上代码的执行过程大致如下图:
共开启了4个线程执行任务(不考虑main主线程),每一个线程都有4个任务:
①判断if条件if(num>0);
②票数自减num--;
③获取剩余票数return num;
④打印返回的num数量System.out.println(Thread.currentThread().getName()+"-------"+remain())。
这四个任务的共同点也是关键点在于它们都操作同一个资源Ticket对象中的num,这是多线程出现安全问题的本质,也是分析多线程执行过程的切入点。
当main线程开启t1-t4这4个线程时,它们首先进入就绪队列等待被CPU随机选中。(1).假如t1被先选中,分配的时间片执行到任务②就结束了,于是t1进入就绪队列等待被CPU随机选中,此时票数num自减后为99;(2).当t3被CPU选中时,t3所读取到的num也为99,假如t3分配到的时间片在执行到任务②也结束了,此时票数num自减后为98;(3).同理t2被选中执行到任务②结束后,num为97;(4).此时t3又被选中了,于是可以执行任务③,甚至是任务④,假设执行完任务④时间片才结束,于是t3的打印语句打印出来的num结果为97;(5).t1又被选中了,于是任务④打印出来的num也为97。
显然,上面的代码有几个问题:(1)有些票没有卖出去了但是没有记录;(2)有的票重复卖了。这就是线程安全问题。
4.2 线程同步java中解决线程安全问题的方法是使用互斥锁,也可称之为"同步"。解决思路如下:
(1).为待执行的任务设定给定一把锁,拥有相同锁对象的线程在wait()时会进入同一个线程池睡眠。
(2).线程在执行这个设了锁的任务时,首先判断锁是否空闲(即锁处于释放状态),如果空闲则去持有这把锁,只有持有这把锁的线程才能执行这个任务。即使时间片到了,它也不是释放锁,只有wait()或线程结束时才会安全地释放锁。
(3).这样一来,锁被某个线程持有时,其他线程在锁判断后就继续会线程池睡眠去了(或就绪队列)。最终导致的结果是,(设计合理的情况下)某个线程一定完整地执行完一个任务,其他线程才有机会去持有锁并执行任务。
换句话说,使用同步线程,可以保证线程执行的任务具有原子性,只要某个同步任务开始执行了就一定执行结束,且不允许其他线程参与。