java基础之线程同步机制(适合纯小白)
上文我们聊到了如何创建多线程的两种方式,现在有如下场景。
假设存在一个电影院,现在有100张票等待出售,同时有三个窗口出售电影票。
代码如下:
为了让效果更明显,这里让线程等待了10millis。
接下来我们在main方法中创建三个线程来“买票”
运行代码,得到如下结果:
售票竟然出现了重复的票,还出现了不存在的票。这是为什么呢?
票 我们可以看作是一个公共资源。为了让问题简化,我们假设现在只有A线程和B线程访问它。
--------------------------------------以下场景可能出现。
当A线程抢夺到CPU资源时运行run方法,当运行完
System.out.println(Thread.currentThread().getName() + "当前正在出售第" + ticket + "张票");
这一句代码后,这时候可能A线程失去了CPU资源(注意:此时ticket--这一句代码未运行,此时ticket的值仍然是100,控制台打印的值为"Thread-A当前正在出售第100张票"),B线程成功抢夺到CPU资源,运行run方法。当运行到
System.out.println(Thread.currentThread().getName() + "当前正在出售第" + ticket + "张票");
这一句代码时,控制台成功输出"Thread-B当前正在出售第100张票"(出现重复的票),然后此时B线程同样可能失去CPU资源,由A再次成功抢夺到执行权,运行A线程的
ticket--;
ticket此时的值为99,A线程失去资源,B线程获得资源,B线程中再次运行
ticket--;
ticket此时的值为98。经历多次运行后,此时假设票还剩余1张。
A线程获得CPU资源,运行到if(ticket>0)这一句后,由于1>0为true,成功进入循环,此时A线程失去资源,由B成功上位。ticket此时等于1,B线程也成功进入while循环。
然后B失去CPU资源,A重新上位。A打印:Thread-A当前正在出售第1张票,A结束,ticket此时的值为0。B重新获得运行资源,B由于if条件已经为true成功进入分支,直接打印:Thread-A当前正在出售第0张票。(不存在的票)A线程结束。
那么问题来了。在正常的生产环境中,我们一般不会允许这样的情况出现,如何才能避免线程安全问题呢?
以下给出解决方案。
方案1:使用synchronized关键字同步代码块
废话不多数,直接上代码:
现在加入synchronize关键字后,再次运行main方法:
可以看出此次运行,票数连续且正常(未出现重复票和不存在的票)。
原理:现在有两个线程A和B
当线程A获取CPU资源后进入run方法,遇到了synchronize修饰的代码块,此时会A会检查synchronize代码块中是否存在锁对象。如果存在锁对象,就会获取锁对象并成功进入同步中执行。
此时A失去了CPU资源,B成功上位。B遇到synchronize修饰的代码块。此时B同样也会检查synchronize代码块中是否有锁对象。当然没有,因为锁对象现在被A线程持有。这时,B线程会进入到阻塞状态,会一直等待A线程归还锁对象,然后B才能获取锁对象并进入同步中执行。
当A线程运行出被synchronize修饰的代码块后,锁对象会被归还。此时B线程如果成功抢夺到资源,就能成功进入同步。
同步保证了只能有一个线程在同步中操作共享数据。但是程序需要频繁的判断锁,获得锁,释放锁。程序的效率会下降。
方案2:使用synchronized关键字同步方法
运行main方法买票:
同样运行正确。
这个办法的原理和上种方法是一样的。同样是通过控制锁对象来实现代码的同步。这里的锁对象其实就是Runnable的实现类(即this)。
如果同步方法使用的static关键字修饰(静态同步方法),那么此时的锁对象就是本类的class属性(class文件对象)。
方案3:使用Lock锁(推荐)
强烈建议把解锁的代码放入到finally块中去。