Java多线程开发中,为了避免多个线程对同一份数据的操作,我们需要对我们的线程做加锁的操作,只要加锁,就必然存在锁竞争的问题,如果锁竞争的问题处理不当就会出现死锁问题。死锁会让程序一直卡住,程序不再往下执行。我们只能通过中止并重启的方式来让程序重新执行。
这是我们非常不愿意看到的一种现象,我们要尽可能避免死锁的情况发生!
造成死锁的原因可以概括成三句话:
动态顺序锁导致死锁是最常见的死锁,例如我们在2个线程对2个账户进行转账操作,我们需要先锁定汇账账户减钱,然后再锁定入款账户加钱,一般来讲这个逻辑顺序是没有问题的,但是如果这2个线程并发同时处理,就会产生死锁。
public class DeadLock {
public static void main(String[] args) {
Account accountA = new Account(1L,"A",10000L);
Account accountB = new Account(1L,"B",10000L);
Thread a = new Thread(()->{
try {
transferMoney(accountA,accountB,100L);
} catch (Exception e) {
e.printStackTrace();
}
});
Thread b = new Thread(()->{
try {
transferMoney(accountB,accountA,100L);
} catch (Exception e) {
e.printStackTrace();
}
});
a.start();
b.start();
while (true){
try {
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
}
}
// 转账
public static void transferMoney(Account fromAccount,
Account toAccount,
Long amount) throws Exception {
// 锁定汇账账户
synchronized (fromAccount) {
System.out.println(Thread.currentThread().getName()+"获取账户"+fromAccount.getName()+"锁");
// 锁定来账账户
synchronized (toAccount) {
System.out.println(Thread.currentThread().getName()+"获取账户"+toAccount.getName()+"锁");
// 判余额是否大于0
if (fromAccount.getAmount().compareTo(amount) < 0) {
throw new Exception("No enough money");
} else {
// 汇账账户减钱
fromAccount.debit(amount);
// 来账账户增钱
toAccount.credit(amount);
System.out.println(Thread.currentThread().getName()+"完成转账");
}
}
}
}
}
@Data
class Account{
private Long id;
private String name;
private Long amount;
public Account(){}
public Account(long id,String name, long amount){
this.id = id;
this.name = name;
this.amount = amount;
}
public void debit(Long amount) {
this.amount -= amount;
}
public void credit(Long amount) {
this.amount += amount;
}
}
程序执行结果如下:
Thread-1获取账户B锁
Thread-0获取账户A锁
我们会发现我们的转账一直无法完成交易,程序一直卡住,程序不再往下执行。即发生了死锁。
Jconsole是JDK自带的图形化界面工具,使用JDK给我们的的工具JConsole,我们可以直接查看Java进程中出现的死锁。
控制台输入jconsole启动图形化界面工具
jconsole
选择连接我们的本地调试进程
选择线程栏,我们会看到我们的正在运存的测试代码的3个线程
选择我们的线程,检测死锁
很明显看出,Thread-0需要的资源被Thread-1占用,Thread-0被阻塞。
针对动态锁顺序导致的死锁,我们可以通过固定加锁的顺序来解决
public class FixedOrderDeadLock {
public static void main(String[] args) {
Account accountA = new Account(1L,"A",10000L);
Account accountB = new Account(1L,"B",10000L);
Thread a = new Thread(()->{
try {
transferMoney(accountA,accountB,100L);
} catch (Exception e) {
e.printStackTrace();
}
});
Thread b = new Thread(()->{
try {
transferMoney(accountB,accountA,100L);
} catch (Exception e) {
e.printStackTrace();
}
});
a.start();
b.start();
while (true){
try {
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
}
}
public static void transferMoney(final Account fromAcct,
final Account toAcct,
final Long amount)
throws Exception {
class Helper {
public void transfer() throws Exception {
if (fromAcct.getAmount().compareTo(amount) < 0)
throw new Exception("No enough money");
else {
fromAcct.debit(amount);
toAcct.credit(amount);
System.out.println(Thread.currentThread().getName()+"完成转账");
}
}
}
// 得到锁的hash值
int fromHash = System.identityHashCode(fromAcct);
int toHash = System.identityHashCode(toAcct);
// 根据hash值来上锁 不管是汇款账户还是入款账户,总是Hash值小的先锁,则对象锁的顺序是固定的
if (fromHash < toHash) {
synchronized (fromAcct) {
System.out.println(Thread.currentThread().getName()+"获取账户"+fromAcct.getName()+"锁");
synchronized (toAcct) {
System.out.println(Thread.currentThread().getName()+"获取账户"+toAcct.getName()+"锁");
new Helper().transfer();
}
}
} else if (fromHash > toHash) {// 根据hash值来上锁
synchronized (toAcct) {
System.out.println(Thread.currentThread().getName()+"获取账户"+toAcct.getName()+"锁");
synchronized (fromAcct) {
System.out.println(Thread.currentThread().getName()+"获取账户"+fromAcct.getName()+"锁");
new Helper().transfer();
}
}
} else {//如果是同对象,由于 synchronized 已经支持可重入锁,所以并发的同账户互相转账不会产生死锁,锁顺序不产生影响
synchronized (fromAcct) {
synchronized (toAcct) {
new Helper().transfer();
}
}
}
}
}
程序执行结果
Thread-0获取账户A锁
Thread-0获取账户B锁
Thread-0完成转账
Thread-1获取账户A锁
Thread-1获取账户B锁
Thread-1完成转账
如结果所示,并不会产生死锁。
除了动态顺序锁发生死锁的情况,还存在一些死锁的情况,但是不是顺序锁那么简单可以被发现。因为有可能并不是在同一个方法中显示请求两个锁,而是嵌套另一个方法去获取第二个锁。这就是隐式获取两个锁(对象之间协作)。
例如,ProductProducer生产者会不断的生产商品并且向仓库注册商品,ProductDepository商品仓库会不断的处理注册到仓库的商品加工,生成编号。
public class CooperatingDeadlock {
public static void main(String[] args) {
ProductProducer productProducer = new ProductProducer();
ProductDepository productDepository = new ProductDepository();
productProducer.setProductDepository(productDepository);
productProducer.start();
productDepository.start();
while (true){
try {
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
}
}
}
class ProductProducer extends Thread{
public void setProductDepository(ProductDepository productDepository) {
this.productDepository = productDepository;
}
private ProductDepository productDepository;
@Override
public void run(){
produce();
}
// produce()需要ProductProducer对象锁
private synchronized void produce(){
do {
Product p = new Product(this);
System.out.println(Thread.currentThread().getName()+"获取"+getClass().getName()+"对象锁,生产商品,并向仓库注册");
this.productDepository.addAvailable(p);
try{
Thread.sleep(100);
}catch (Exception e){
e.printStackTrace();
}
}while (true);
}
public synchronized void handle(Product product) {
System.out.println(Thread.currentThread().getName()+"获取"+getClass().getName()+"对象锁,加工商品,生成编号");
product.setNo(System.currentTimeMillis());
}
}
class ProductDepository extends Thread{
public ProductDepository(){
availableProductList = new ArrayList<>();
}
private ArrayList<Product> availableProductList;
@Override
public void run(){
handleAvailable();
}
public synchronized void addAvailable(Product product) {
System.out.println(Thread.currentThread().getName()+"获取"+getClass().getName()+"对象锁,注册到仓库");
availableProductList.add(product);
}
// handleAvailable()需要ProductDepository对象锁
public synchronized void handleAvailable() {
System.out.println(Thread.currentThread().getName()+"获取"+getClass().getName()+"对象锁,将注册的商品进行加工");
do{
for (Product t : availableProductList)
// 调用handle()需要ProductProducer对象锁
t.handle();
availableProductList.clear();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}while (true);
}
}
class Product{
private ProductProducer productProducer;
public Product(){}
public void setNo(Long no) {
this.no = no;
}
private Long no;
public Product(ProductProducer productProducer){
this.productProducer = productProducer;
}
public void handle(){
this.productProducer.handle(this);
}
}
此时我们执行程序:
Thread-0获取com.pubutech.multithread.example.deadlock.ProductProducer对象锁,生产商品,并向仓库注册
Thread-1获取com.pubutech.multithread.example.deadlock.ProductDepository对象锁,将注册的商品进行加工
很显然,我们的程序将陷入死锁无法继续执行下去。
在协作对象之间发生死锁的例子中,主要是因为在调用某个方法时就需要持有锁,并且在方法内部也调用了其他带锁的方法!
如果在调用某个方法时不再持有锁,而改为同步代码块仅用于保护那些涉及共享状态的操作!那么这种调用被称为开放调用!
我们可以这样来改造:
public class OpenMethodCooperationDeadLock {
public static void main(String[] args) {
OpenMethodProductProducer productProducer = new OpenMethodProductProducer();
OpenMethodProductDepository productDepository = new OpenMethodProductDepository();
productProducer.setProductDepository(productDepository);
productProducer.start();
productDepository.start();
while (true){
try {
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
}
}
}
class OpenMethodProductProducer extends Thread{
public void setProductDepository(OpenMethodProductDepository productDepository) {
this.productDepository = productDepository;
}
private OpenMethodProductDepository productDepository;
@Override
public void run(){
produce();
}
private void produce(){
do {
OpenMethodProduct p = null;
//需要ProductProducer对象锁,但是缩小锁的范围,不会同时去获取两个锁
synchronized (this){
System.out.println(Thread.currentThread().getName()+"获取"+getClass().getName()+"对象锁,生产商品,并向仓库注册");
p = new OpenMethodProduct(this);
}
if (null != p){
System.out.println(Thread.currentThread().getName()+"向仓库注册");
this.productDepository.addAvailable(p);
}
try{
Thread.sleep(100);
}catch (Exception e){
e.printStackTrace();
}
}while (true);
}
public synchronized void handle(OpenMethodProduct product) {
System.out.println(Thread.currentThread().getName()+"获取"+getClass().getName()+"对象锁,加工商品,生成编号");
product.setNo(System.currentTimeMillis());
}
}
class OpenMethodProductDepository extends Thread{
public OpenMethodProductDepository(){
availableProductList = new ArrayList<>();
}
private ArrayList<OpenMethodProduct> availableProductList;
@Override
public void run(){
handleAvailable();
}
public synchronized void addAvailable(OpenMethodProduct product) {
System.out.println(Thread.currentThread().getName()+"获取"+getClass().getName()+"对象锁,注册到仓库");
availableProductList.add(product);
}
public void handleAvailable() {
do{
ArrayList<OpenMethodProduct> availableProductListCopy;
//需要ProductDepository对象锁,但是缩小锁的范围,不会同时去获取两个锁
synchronized (this){
System.out.println(Thread.currentThread().getName()+"获取"+getClass().getName()+"对象锁,准备将注册的商品进行加工");
availableProductListCopy = new ArrayList<>(availableProductList);
availableProductList.clear();
}
for (OpenMethodProduct t : availableProductListCopy)
// 调用handle()需要ProductProducer对象锁
t.handle();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}while (true);
}
}
class OpenMethodProduct{
private OpenMethodProductProducer productProducer;
public OpenMethodProduct(){}
public void setNo(Long no) {
this.no = no;
}
private Long no;
public OpenMethodProduct(OpenMethodProductProducer productProducer){
this.productProducer = productProducer;
}
public void handle(){
this.productProducer.handle(this);
}
}
程序执行结果如下,将会不断的协作下去直至我们终端程序而不会产生死锁
Thread-0获取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer对象锁,生产商品,并向仓库注册
Thread-1获取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository对象锁,准备将注册的商品进行加工
Thread-0向仓库注册
Thread-0获取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository对象锁,注册到仓库
Thread-1获取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository对象锁,准备将注册的商品进行加工
Thread-1获取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer对象锁,加工商品,生成编号
Thread-0获取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer对象锁,生产商品,并向仓库注册
Thread-0向仓库注册
Thread-0获取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository对象锁,注册到仓库
Thread-1获取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository对象锁,准备将注册的商品进行加工
Thread-0获取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer对象锁,生产商品,并向仓库注册
Thread-0向仓库注册
Thread-0获取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository对象锁,注册到仓库
Thread-1获取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer对象锁,加工商品,生成编号
Thread-0获取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer对象锁,生产商品,并向仓库注册
Thread-0向仓库注册
Thread-0获取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository对象锁,注册到仓库
Thread-1获取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository对象锁,准备将注册的商品进行加工
Thread-1获取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer对象锁,加工商品,生成编号
Thread-1获取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer对象锁,加工商品,生成编号
Thread-1获取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository对象锁,准备将注册的商品进行加工
Thread-0获取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer对象锁,生产商品,并向仓库注册
Thread-0向仓库注册
Thread-0获取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository对象锁,注册到仓库
Thread-1获取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository对象锁,准备将注册的商品进行加工
Thread-0获取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer对象锁,生产商品,并向仓库注册
Thread-0向仓库注册
Thread-0获取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository对象锁,注册到仓库
Thread-1获取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer对象锁,加工商品,生成编号
Thread-0获取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer对象锁,生产商品,并向仓库注册
Thread-0向仓库注册
Thread-0获取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository对象锁,注册到仓库
synchronized 关键字的锁是由Java虚拟机实现的,它无法显示获取锁超时,但是Java5以后Java RetreenLock提供了tryLock()方法来实现获取锁超时,我们在获取锁时可以使用tryLock()方法。当等待超过时限的时候,tryLock()不会一直等待,而是返回错误信息。