多线程与死锁

多线程与死锁

  • 动态顺序锁死锁
  • Jconsole 查看死锁
  • 固定锁顺序避免死锁
  • 协作锁之间发生死锁
  • 开放调用避免死锁
  • RetreenLock锁超时解决死锁

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 查看死锁

Jconsole是JDK自带的图形化界面工具,使用JDK给我们的的工具JConsole,我们可以直接查看Java进程中出现的死锁。
控制台输入jconsole启动图形化界面工具

jconsole

多线程与死锁_第1张图片
选择连接我们的本地调试进程
多线程与死锁_第2张图片
选择线程栏,我们会看到我们的正在运存的测试代码的3个线程
多线程与死锁_第3张图片
选择我们的线程,检测死锁
多线程与死锁_第4张图片
多线程与死锁_第5张图片
很明显看出,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对象锁,注册到仓库

RetreenLock锁超时解决死锁

synchronized 关键字的锁是由Java虚拟机实现的,它无法显示获取锁超时,但是Java5以后Java RetreenLock提供了tryLock()方法来实现获取锁超时,我们在获取锁时可以使用tryLock()方法。当等待超过时限的时候,tryLock()不会一直等待,而是返回错误信息。

你可能感兴趣的:(多线程)