作者归档:admin

Java中死锁的定位与修复

  死锁应该可以说是并发编程中比较常见的一种情况,可以说如果程序产生了死锁那将会对程序带来致命的影响;所以排查定位、修复死锁至关重要;
  我们都知道死锁是由于多个对象或多个线程之间相互需要对方锁持有的锁而又没有释放对方所持有的锁,导致双方都永久处于阻塞状态

enter image description here

  如上图所示,线程1持有对象1的锁、线程2持有对象2的锁,持此线程1又想去获取对象2对象锁、线程2想获取对象1对象锁,此时由于双方都没有获取到想要的锁,任务没完成所以也没释放锁,导致一直僵持呢,于是阻塞、产生死锁;

死锁检测

  需要检测死锁肯定要先有死锁出现,下面的demo模拟了一个死锁的产生;

 public class DeadlockDemo extends Thread {
private BaseObj first;
private BaseObj second;

public DeadlockDemo(String name, BaseObj first, BaseObj second) {
    super(name);
    this.first = first;
    this.second = second;
}

public void reentrantLock() throws InterruptedException {
    first.lock();
    System.out.println(String.format("%s 持有:%s 对象锁,等待获取:%s对象锁", this.getName(), first, second));
    second.lock();
    first.unlock();
    second.unlock();
}
@Override
public void run() {
    try {
        reentrantLock();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

public static void main(String[] args) throws InterruptedException {
    ObjOne one = new ObjOne();
    ObjTwo two = new ObjTwo();

    DeadlockDemo thread1 = new DeadlockDemo("Thread1", one, two);
    DeadlockDemo thread2 = new DeadlockDemo("Thread2", two, one);

    thread1.start();
    thread2.start();

    thread1.join();
    thread2.join();
}
 }

  运行上面的demo将看到程序被阻塞了,没法结束运行;只看到如下运行结果:
Thread1 持有:objOne 对象锁,等待获取:objTwo对象锁 Thread2 持有:objTwo 对象锁,等待获取:objOne对象锁

enter image description here

  这demo没法结束运行就是由于产生了死锁,两个线程都在相互对待获取对方所持有的对象锁;
  这时候要解决问题就需要找出哪里出现了死锁,通过代码走查通常不容易发现死锁,当然我们这程序很容易发现,因为我们刻意产生的死锁;所以就需要工具来检测死锁,这里可用的工具主要有:jconsole、jvisualvm、jstack等,这些工具其实都是jdk自带的,用法都很类似;

  这里使用jvisualvm来检测当前的demo程序是否产生了死锁;打开jvisualvm连接到当前的应用程序即可看到程序的监控信息,如内存、CPU、性能、GC等等;打开进入线程的tab项查看程序的线程信息,这里很明显的就看到了提示该程序被检测除了死锁!

enter image description here
  点击 线程Dump可以看到线程的堆栈信息,从中可以看到线程的详细信息,并定位死锁;
enter image description here
  从上图可以看到线程产生死锁的原因,Thrad2是等待Thread1、Thread1是等待Thread1, 从下图的堆栈信息即可定位死锁产生的位置;
enter image description here

死锁扫描

  除了发现程序出现问题后我们去扫描死锁外,我们还可以实时的去扫描程序用于发现程序中是否存在死锁;
  JDK提供了MXBean Api可用于扫描程序是否存在死锁,ThreadMXBean提供了findDeadlockedThreads()方法,可以用于找到产生死锁的线程;这里在上面的demo程序中添加一个方法用于扫描死锁,虽然这种方法可以扫描到死锁但是由于每次都对线程打快照对程序性能会有比较大的影响,所以慎用;

 public static void scanDeadLock() {
    ThreadMXBean mxBean = ManagementFactory.getThreadMXBean();
    Runnable runnable = () -> {
        long[] ids = mxBean.findDeadlockedThreads();
        System.out.println("扫描死锁...");
        if (ids != null) {
            ThreadInfo[] threadInfos = mxBean.getThreadInfo(ids);
            for (ThreadInfo threadInfo : threadInfos) {
                System.out.println(threadInfo);
            }
        }
    };

    ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(5, Executors.defaultThreadFactory());
    executorService.scheduleAtFixedRate(runnable, 1, 5, TimeUnit.SECONDS);
}

enter image description here

避免死锁

  解决死锁最好的方法就是避免死锁了,比如上面的demo我们可以把直接使用无参数的lock()方法换为使用tryLock方法,tryLock还可以指定获取锁超时时间,到了超时时间还没获得到锁就会放弃获取锁,当然还有其它方法可以避免死锁;
  1、避免使用多个锁、长时间持有锁;
  2、设计好多个锁的获取顺序
  3、使用带超时的获取锁方法

enter image description here

微积分——自动微分

  梯度下降法(Gradient Descendent)是机器学习的核心算法之一,自动微分则是梯度下降法的核心;
  梯度下降法用于求损失函数的最优值,前面的文章中我们说过梯度下降是通过计算参数与损失函数的梯度并在梯度的方向不断迭代求得极值;但是在机器学习、深度学习中很多求导往往是很复杂的,手动使用链式法则求导很容易出错,借助于计算机也只能是硬编码;
  这时候就需要借助于自动微分了,求导主要有这么四种:手动微分法数值微分法符号微分法自动微分法,这里分别说说这几种求导方法;

手动微分法(Manual Differentiation)
  手动微分法需要我们手动编写出代价函数激活函数的求导代码,硬编码这些函数的求导方法,如果这些函数后面有调整该函数的求导方法又要重新实现,可以说是又麻烦又容易出错;
数值微分法(Numerical Differentiation)
  通过使用函数值来估计函数的导数,该方法主要是计算速度慢,精度差等问题;
符号微分法(Symbolic Differentiation)
  符号微分广泛用在各种数学软件中如Matlab、Octave等,它通关过使用符号表达式进行求导,符号微分是基于求导法则进行的;

  如表达式:f(x) = 2y + x^2
  表达式树为:
    enter image description here

  通过符号微分法求得:

  enter image description here

  符号微分有个缺陷就是得到的导数不一定是最简的,函数较为复杂时表达式树会很复杂,可能会出现表达式爆炸的情况出现;

自动微分法(Autodiff)

  自动微分法介于数值微分符号微分 之间,数值微分是直接代入数值近似求解而符号微分为直接通过表达式树对表达式进行求解;自动微分先将符号微分用于基本的算子,带入数值并保存中间结果,后应用于整个函数;自动微分本质上就是图计算,容易做很多优化所以广泛应用于各种机器学习深度学习框架中;
  自动微分又分为前向模式(Forward mode Autodiff)反向模式(Reverse-Mode Atuodiff)求导;

前向模式(Forward mode Autodiff)

  前向模式引入二元数(dual number),同时会先将表达式转换为计算图然后会依次从下往上计算每一步的导数,由于每步都使用了上一步的导数所以不会导致重复计算不会出现像符号微分一样的表达式膨胀问题,但由于深度学习的参数比较多所以前向模式的效率还是有些差;一个前向过程就可以求出其函数值与导数,下面简单举个例子:

  二元数:a+bꜫ
  a与b都是实数,ꜫ为无穷小的数,且ꜫ^2=0,并满足加分与乘法法则,且还有:
    enter image description here
  这样是要求出f( a+ꜫ)就可以得出f(a)与f(a)的导数;
  还是上面的表达式:f(x) = 2y + x^2

    enter image description here

  如上图当x=2,y=3时,我们可以得出:二元数 10+4ꜫ,即函数f(x)关于x的偏导数为:4,函数值为10;

反向模式(Reverse mode autodiff)

  反向模式为先通过正向遍历计算图求出每个节点的值,然后通过反向遍历整个图,计算出每个节点的偏导,其原理为微积分链式法则,这里所说的反向模式其实也就是我们在深度学习中所说的BP算法(反向传播算法),只需要一个前向传播、一个反向传播就可以求得所有参数的导数,所以性能很高,非常适用于深度学习中的自动求导;

          enter image description here

  上图为经过反向传播的计算图,根据链式法则:
enter image description here
enter image description here

  正如上面所说的,经过一次正向传播求出所有的节点值后再经过一次反向传播就求得了所有输入参数的导数效率很高,而且避免符号微分、数值微分所带来的问题;目前Tensoflow、MXNet等深度学习框架中也都使用了反向模式实现自动微分只是各种具体算法还是有不少差异;

参考资料:
https://blog.csdn.net/aws3217150/article/details/70214422
https://arxiv.org/pdf/1502.05767.pdf