分布式锁

Posted by Xsp on November 22, 2018

死锁和活锁

死锁

多个线程相互占用对方的资源的锁,而又相互等对方释放锁,导致阻塞等待。

例如如下代码就会导致死锁:

public class Test {
    public static void main(String[] args) {
        final Object a = new Object();
        final Object b = new Object();

        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (a) {
                    try {
                        System.out.println("threadA lock a");
                        TimeUnit.SECONDS.sleep(10);
                        synchronized (b) {
                            System.out.println("threadA lock b");
                        }
                    } catch (Exception e) {
                    }
                }
            }
        });

        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (b) {
                    try {
                        System.out.println("threadB lock b");
                        TimeUnit.SECONDS.sleep(10);
                        synchronized (a) {
                            System.out.println("threadB lock a");
                        }
                    } catch (Exception e) {
                    }
                }
            }
        });

        threadA.start();
        threadB.start();
    }
}

死锁检测

首先,通过jps确定当前执行任务的进程号

$ jps
40040 Jps
18441 Bootstrap
39693 Launcher
39694 Test

确定任务进程号是39694,然后执行jstack命令查看当前进程堆栈信息:

$ jstack -F 39694
Attaching to process ID 39694, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.121-b13
Deadlock Detection:

Found one Java-level deadlock:
=============================

"Thread-0":
  waiting to lock Monitor@0x00007fac2101cea8 (Object@0x00000007956e7850, a java/lang/Object),
  which is held by "Thread-1"
"Thread-1":
  waiting to lock Monitor@0x00007fac2101a4b8 (Object@0x00000007956e7840, a java/lang/Object),
  which is held by "Thread-0"

Found a total of 1 deadlock.

可以看到,进程的确存在死锁,两个线程分别在等待对方持有的Object对象。

也可以通过jconsole来查看死锁:

通过以上截图可以看到,Thread-0 线程出于 blocked 状态。

死锁预防

  • 按照一定的顺序对资源加锁
  • 一次性获得所有的锁
  • 超时放弃

那么死锁的时候,CPU占用怎么样?取决于死锁的实现:

  • 自旋锁:拿不到锁的时候,忙等待,反复探测锁状态,直到拿到锁,进入临界区。这种情况会消耗CPU。

  • 休眠锁:拿不到锁的时候,放弃CPU,休眠,离开运行队列,这种情况不会消耗CPU。java的synchronized应该是休眠锁。

出于死锁状态的线程的状态是 BLOCKED ,所以是不占用CPU资源的?

线程池死锁。。。

final ExecutorService executorService = 
        Executors.newSingleThreadExecutor();
Future<Long> f1 = executorService.submit(new Callable<Long>() {

    public Long call() throws Exception {
        System.out.println("start f1");
        Thread.sleep(1000);//延时
        Future<Long> f2 = 
           executorService.submit(new Callable<Long>() {

            public Long call() throws Exception {
                System.out.println("start f2");
                return -1L;
            }
        });
        System.out.println("result" + f2.get());
        System.out.println("end f1");
        return -1L;
    }
});

jstack结果:

Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.121-b13 mixed mode):

"Attach Listener" #11 daemon prio=9 os_prio=31 tid=0x00007ff85180c000 nid=0x1f07 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"DestroyJavaVM" #10 prio=5 os_prio=31 tid=0x00007ff851876800 nid=0x1003 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"pool-1-thread-1" #9 prio=5 os_prio=31 tid=0x00007ff852833800 nid=0x4c03 waiting on condition [0x00007000010c6000]
   java.lang.Thread.State: WAITING (parking)
	at sun.misc.Unsafe.park(Native Method)
	- parking to wait for  <0x000000079577a010> (a java.util.concurrent.FutureTask)
	at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
	at java.util.concurrent.FutureTask.awaitDone(FutureTask.java:429)
	at java.util.concurrent.FutureTask.get(FutureTask.java:191)
	at Test$1.call(Test.java:28)
	at Test$1.call(Test.java:15)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	at java.lang.Thread.run(Thread.java:745)

这个例子中,线程池的任务1依赖任务2的执行结果,但是线程池是单线程的,也就是说任务1不执行完,任务2永远得不到执行,那么因此造成了死锁。

可以从jstack结果看到 当前线程wait在java.util.concurrent.FutureTask对象上。

活锁

获取部分资源,但是无法获取全部,所以释放资源,并重新获取,如此反复。

以哲学家问题为例:

如果所有的哲学家都先拿左手边的筷子后拿右手边的筷子,哲学家们就会死锁,因为他们右手边的筷子被另一个哲学家当作左手边的筷子拿走了。哲学家们发现如果他们“持有并等待”就会发生死锁,于是他们达成协议:如果申请第二根筷子没有成功,则放下第一根筷子,重新拿筷子,于是他们就形成了一个活锁。

示例代码:

class Philosopher implements Runnable {
    private int id;

    public Philosopher(int id) {
        this.id = id;
    }

    @Override
    public void run() {

        int leftCsIndex = id;
        int rightCsIndex = (id + 1) % 5;

        while (true) {
            LiveLockTest.chopsticks[leftCsIndex].lock();
            System.out.println("Philosopher" + id + ": I got left chopstick");
            try {
                Thread.sleep(100);
            } catch (Exception e) {
            }
            if (LiveLockTest.chopsticks[rightCsIndex].tryLock()) {
                System.out.println("Philosopher" + id + ": I got right chopstick");
                System.out.println("Philosopher" + id + ": eating");
                LiveLockTest.chopsticks[rightCsIndex].unlock();
                break;
            } else {
                try {
                    Thread.sleep(100);
                } catch (Exception e) {
                }
            }
            LiveLockTest.chopsticks[leftCsIndex].unlock();
        }
    }
}

public class LiveLockTest {
    public static Lock[] chopsticks = new Lock[5];

    public static void main(String[] args) {
        for (int i = 0; i < chopsticks.length; i++) {
            chopsticks[i] = new ReentrantLock();
        }
        ExecutorService exec = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            exec.execute(new Philosopher(i));
        }
        exec.shutdown();
    }
}

饥饿

多线程执行中有线程优先级这个东西,优先级高的线程能够插队并优先执行,这样如果优先级高的线程一直抢占优先级低线程的资源,导致低优先级线程无法得到执行,这就是饥饿。

分布式锁

分布式环境中,可能是不同语言对应的多个不同的进程,需要对某一个共享资源进行访问,会有不一致的情况,比如数据库的某张表。

参考

  • Java面试必问-死锁终极篇 - https://juejin.im/post/5aaf6ee76fb9a028d3753534
  • Java死锁排查和Java CPU 100% 排查的步骤整理和OOM FullGc案例 - https://blog.csdn.net/u010648555/article/details/80721815
  • 线程死锁是不是cpu资源一定会居高不下?
  • 并发进阶(十)饥饿与活锁 - https://zhuanlan.zhihu.com/p/40612679
  • 百度百科-活锁
  • java多线程中的死锁、活锁、饥饿、无锁 - https://blog.csdn.net/huangshulang1234/article/details/79158306