当多个线程对同一个资源进行操作时,有可能引发线程安全问题:如下图所示:
输出结果:
出现这种情况的原因是因为当一个线程还未执行扣款操作时,由于CPU的切换,另一个线程获得CPU资源,在上一个线程做出扣款操作之前也进入了循环。
java提供的多种保障线程安全的解决方案,常见的有一下方式:
(1)使用synchronized实现同步方法
每一个用synchronized关键字声明的方法都是临界区。在Java中,同一个对象的临界区,在同一时间只有一个允许被访问。
运行结果:
补充:
1、synchronized关键字会降低应用程序的性能,因此只能在并发场景中修改共享数据的方法上使用它。
2、临界区的访问应该尽可能的短。方法的其余部分保持在synchronized代码块之外,以获取更好的性能
(2)使用重入锁实现线程同步
在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁, 它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。
ReenreantLock类的常用方法有:
ReentrantLock() : 创建一个ReentrantLock实例
lock() : 获得锁
unlock() : 释放锁
补充:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用。
公平锁和非公平锁
公平锁是指多个线程等待同一个锁时,必须按照申请锁的先后顺序来一次获得锁
特点:等待锁的线程不会饿死,但整体效率相对低一些
非公平锁是指可以不按照顺序,可以抢占锁
特点:整体效率高,但有些线程会饿死或者说很早就在等待锁,但要等很久才会获得锁
重入锁有这样一个构造函数,对公平性进行设置。当fair为true时,表示此锁是公平的。
例如:
测试如下:
死锁
当一个线程永远地持有一个锁,并且其他线程都尝试去获得这个锁时,那么它们将永远被阻塞,这个我们都知道。如果线程1持有锁A并且想获得锁B,线程2持有锁B并且想获得锁A,那么这两个线程将永远等待下去,这种情况就是最简单的死锁形式。
当一组Java线程发生死锁时,这两个线程就永远不能再使用了,并且由于两个线程分别持有了两个锁,那么这两段同步代码/代码块也无法再运行了----除非终止并重启应用。
死锁是设计的BUG,问题比较隐晦。不过死锁造成的影响很少会立即显现出来,一个类可能发生死锁,并不意味着每次都会发生死锁,这只是表示有可能。当死锁出现时,往往是在最糟糕的情况----高负载的情况下。
测试方法:
输出结果:
等了n久也不见输出结果。
补充:如何定位死锁问题:
(1)输入jps命令,获取当前Java虚拟机进程的pid
(2)jstack+pid命令,打印堆栈:
这时候可以利用taskkill命令去终止没有被Terminate的进程:
如何避免死锁:
1、让程序每次至多只能获得一个锁。当然,在多线程环境下,这种情况通常并不现实
2、设计时考虑清楚锁的顺序,尽量减少嵌在的加锁交互数量
3、既然死锁的产生是两个线程无限等待对方持有的锁,那么只要等待时间有个上限不就好了。当然synchronized不具备这个功能,但是我们可以使用Lock类中的tryLock方法去尝试获取锁,这个方法可以指定一个超时时限,在等待超过该时限之后变回返回一个失败信息