天天看点

并发编程4 - 线程状态、死锁及ReentrantLock

文章目录

    • 一. 再述线程状态转换
    • 二. 多把锁与线程活跃性问题
      • 1. 多把锁
      • 2. 活跃性
    • 三. ReEntrantLock
      • 1. 基本用法
      • 2. 可重入
      • 3. 可打断
      • 4. 锁超时
      • 5. 公平锁
      • 6. 条件变量

一. 再述线程状态转换

并发编程4 - 线程状态、死锁及ReentrantLock

情况1:New ——》RUNNABLE

当调用 t.start() 方法时

情况2:RUNNABLE 《——》WAITING

线程 synchronized(obj) 获得锁以后,调用 obj.wait 方法,状态将从 RUNNABLE ——》WAITING;调用 notify、notifyAll、interrupt 方法,如果:

- 竞争锁失败,线程从 WAITING ——》 BLOCKED

- 竞争锁成功,线程从 WAITING ——》RUNNABLE

注意 BLOCKED 状态与 WAITING 状态的区别:

waiting:主动为之,wait()方法释放cpu执行权和释放锁进入等待队列,需要notify()唤醒进入同步队列竞争锁;

blocked:被动的,在竞争锁的时候失败,被阻塞,在同步队列里继续竞争锁。

情况3:RUNNABLE 《——》WAITING

  • 当当前线程调用 t.join() 方法时,当前线程从 RUNNABLE ——》WAITING。
  • t 线程运行结束,或调用了当前线程的 interrupt(),当前线程从 WAITING ——》RUNNABLE 。

情况4:RUNNABLE 《——》WAITING

  • 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE ——》WAITING;
  • 调用 LockSupport.unpark(t) 或调用了线程的 interrupt(),会让目标线程从 WAITING ——》RUNNABLE 。

情况5/6/7/8:RUNNABLE 《——》TIMED_WAITING

调用 obj.wait(n)、t.join(n)、Thread.sleep(n)、LockSupport.parkNanos(n)、LockSupport.parkUntil(n) 时,线程从 RUNNABLE ——》TIMED_WAITING。

注意:调用sleep(long)方法不会释放锁,时间到了自己返回原状态。

情况9:RUNNABLE 《——》BLOCKED

  • t 线程用 synchronized(obj) 竞争对象锁失败时,从 RUNNABLE ——》BLOCKED;
  • 获得锁的线程执行完毕后,会唤醒所有阻塞的线程重新竞争,竞争成功的从 BLOCKED ——》RUNNABLE

情况10:RUNNABLE ——》TERMINATED

线程中的代码全部执行完毕,RUNNABLE ——》TERMINATED。

二. 多把锁与线程活跃性问题

1. 多把锁

当两个业务完全不相干时,可以选择更加细粒度的锁,增加程序的并发性。

class MultiLock {
    //两个对象锁
    private Object bedRoom = new Object();
    private Object studyRoom = new Object();
    
    public void sleep() {
        //不再锁住 this 对象,而是更加细粒度的锁
        synchronized (bedRoom) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    public void study() {
        synchronized (studyRoom) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
           

2. 活跃性

(1)死锁

但是当一个程序中存在多把锁,而一个线程需要同时获得多把锁时,就容易发生死锁。

public class MultiLockTest {
    final static Logger logger = LoggerFactory.getLogger(MultiLockTest.class);
    public static void main(String[] args) {
        Object lock1 = new Object();
        Object lock2 = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (lock1) {
                try {
                    logger.info("lock 1 ……");
                    Thread.sleep(1000);
                    synchronized (lock2) {
                        logger.info("lock 2 ……");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            synchronized (lock2) {
                try {
                    logger.info("lock 2 ……");
                    Thread.sleep(2000);
                    synchronized (lock1) {
                        logger.info("lock 1 ……");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t2");

        t1.start();
        t2.start();
    }
}
           
并发编程4 - 线程状态、死锁及ReentrantLock

(2) 定位死锁

方式1:jps + jstack

并发编程4 - 线程状态、死锁及ReentrantLock
并发编程4 - 线程状态、死锁及ReentrantLock

方式2:jconsole

连接——》线程——》检测死锁

并发编程4 - 线程状态、死锁及ReentrantLock

(3)哲学家就餐问题

并发编程4 - 线程状态、死锁及ReentrantLock

程序模拟死锁问题:

筷子类

class Chopstick {
    private String name;
    public Chopstick(String name) {
        this.name = name;
    }
}
           

哲学家类

class Philosopher extends Thread {
    Logger logger = LoggerFactory.getLogger(Philosopher.class);
    //模拟左右两根筷子
    private Chopstick left;
    private Chopstick right;

    public Philosopher(String name, Chopstick left, Chopstick right) {
        super(name);
        this.left = left;
        this.right = right;
    }

    @Override
    public void run() {
        while(true) {
        	//需要两根筷子才能吃饭
            synchronized (left) {
                synchronized (right) {
                    logger.info("eating ……");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}
           

测试类

public class PhilosopherTest {
    public static void main(String[] args) {
        Chopstick c1 = new Chopstick("c1");
        Chopstick c2 = new Chopstick("c2");
        Chopstick c3 = new Chopstick("c3");
        Chopstick c4 = new Chopstick("c4");
        Chopstick c5 = new Chopstick("c5");

        new Philosopher("p1", c1, c2).start();
        new Philosopher("p2", c2, c3).start();
        new Philosopher("p3", c3, c4).start();
        new Philosopher("p4", c4, c5).start();
        new Philosopher("p5", c5, c1).start();

    }
}
           
并发编程4 - 线程状态、死锁及ReentrantLock

jconsole检测结果,五个线程均陷入死锁:

并发编程4 - 线程状态、死锁及ReentrantLock

(4)活锁

当两个线程都在改变对方的运行结束条件时会发生活锁。

示例:

public class LiveLockTest {
    static int count = 10;
    private static final Logger logger = LoggerFactory.getLogger(LiveLockTest.class);

    public static void main(String[] args) {
        new Thread(() -> {
            while (count > 0) {
                count--;
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                logger.info("count: " + count);
            }
        }, "t1").start();
        new Thread(() -> {
            while (count < 20) {
                count++;
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                logger.info("count: " + count);
            }
        }, "t2").start();

    }
}
           
并发编程4 - 线程状态、死锁及ReentrantLock

与死锁的不同:死锁是两个线程都陷入了阻塞状态,而活锁是两个线程都在运行,只是无法结束。

解决活锁问题,可以使两个线程交错运行,或者增加随机睡眠时间。

(5)饥饿

饥饿:一个线程由于优先级太低,始终得不到CPU调度执行,也不能够结束。

比如之前的哲学家就餐问题,为避免死锁问题,我们可以通过采用顺序获取锁的方式解决:

new Philosopher("p1", c1, c2).start();
new Philosopher("p2", c2, c3).start();
new Philosopher("p3", c3, c4).start();
new Philosopher("p4", c4, c5).start();
new Philosopher("p5", c1, c5).start(); //获取锁的方式改为 c1, c5
           

但是日志显示,P4和P3线程获取锁的几率较高,而P5线程几乎获取不到锁,这就是饥饿现象:

并发编程4 - 线程状态、死锁及ReentrantLock

三. ReEntrantLock

1. 基本用法

//获取锁,只有获取到了锁后面的代码才会继续执行,否则将一直阻塞在这里
reentrantLock.lock();
try {
	// 临界区 
} finally {
	// 释放锁
	reentrantLock.unlock();
}
           

2. 可重入

可重入是指同一个线程如果已经获得了一把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁。

如果是不可重入锁,那么第二次获取锁时,自己也会被锁挡住。

synchronized 也是可重入锁。

public class ReEntrantLockTest {
    private static final Logger logger = LoggerFactory.getLogger(ParkTest.class);
    public static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        lock.lock();
        try {
            logger.info("enter main……");
            m1();
        } finally {
            lock.unlock();
        }
    }

    public static void m1() {
        lock.lock(); //可重入
        try {
            logger.info("enter m1……");
        } finally {
            lock.unlock();
        }
    }
}
           

3. 可打断

可打断表明可以打断一直处于等待锁状态的线程,避免死锁。

synchronized 是不可打断锁,因为interrupt() 方法只是将打断标记设置为 true,当线程未获取到锁处于阻塞状态下时,并没有显式抛出异常。而处于WAITING 状态下的线程可被打断是因为 wait、sleep、join 这些方法会检查打断标记,并抛出InterruptedException异常。为什么synchronized不可打断

ReentrantLock的 lock 方法也是不可打断的,而 lockInterruptibly() 方法 是可打断的。

示例:

public class ReEntrantLockTest {
    private static final Logger logger = LoggerFactory.getLogger(ParkTest.class);
    public static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            try {
                logger.info("t1线程尝试获取锁……");
                lock.lockInterruptibly();
                logger.info("t1获取到锁……");
                lock.unlock();
            } catch (InterruptedException e) {
                logger.info("t1线程没有获取到锁……");
                e.printStackTrace();
            }
        });

        lock.lock();
        logger.info("主线程获取到锁……");
        t1.start();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t1.interrupt();
    }
}
           
并发编程4 - 线程状态、死锁及ReentrantLock

查看 lockInterruptibly() 方法的源码发现,它其实也是通过检查打断标记抛出InterruptedException异常,来实现可打断的功能:

lockInterruptibly 源码:

并发编程4 - 线程状态、死锁及ReentrantLock
并发编程4 - 线程状态、死锁及ReentrantLock

4. 锁超时

tryLock() 与 tryLock(long timeout, TimeUnit unit) 方法允许线程自己设置锁等待时间,前者没有获取到锁立即返回,后者会等待一定的时间,超过时间没有获取到锁则返回。

public class TryLockTest {
    private static final Logger logger = LoggerFactory.getLogger(TryLockTest.class);
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        Thread t1 = new Thread(() -> {
            logger.info("尝试获取锁");
            try {
                if (! lock.tryLock(2, TimeUnit.SECONDS)) {
                    logger.info("没有获取到锁");
                    return; //没获取到锁需要返回
                }
            } catch (InterruptedException e) { //带参数的tryLock方法也允许被打断
                e.printStackTrace();
                logger.info("没有获取到锁");
                return;
            }
            //获取到了锁需要执行的代码
            try {
                logger.info("获取到了锁");
            } finally {
                lock.unlock();
            }
        }, "t1");
        logger.info("获取了锁");
        lock.lock();
        t1.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        logger.info("释放了锁");
        lock.unlock();
    }
}
           

使用 tryLock() 解决哲学家就餐问题:

由于不能再使用 synchronized 锁住筷子对象,则筷子对象本身就需要具有锁特性。因此,这里将筷子类继承 ReentrantLock 类。

class Chopstick extends ReentrantLock {
    private String name;
    public Chopstick(String name) {
        this.name = name;
    }
}
           

将 synchronized 换成 tryLock 或者 tryLock(long timeout, TimeUnit unit) :

class Philosopher extends Thread {
    Logger logger = LoggerFactory.getLogger(Philosopher.class);
    private Chopstick left;
    private Chopstick right;

    public Philosopher(String name, Chopstick left, Chopstick right) {
        super(name);
        this.left = left;
        this.right = right;
    }

    @Override
    public void run() {
        while(true) {
        	//将synchronized换成 tryLock
            if (left.tryLock()) {
                try {
                    if (right.tryLock()) {
                        try {
                            logger.info("eating ……");
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        } finally {
                            right.unlock();
                        }
                    }
                } finally {
                	//解决死锁的关键就在于右筷子拿不到时会释放左筷子
                    left.unlock();
                }
            }
        }
    }
}
           
并发编程4 - 线程状态、死锁及ReentrantLock

5. 公平锁

synchronized 锁是不公平锁,即当锁被释放时,阻塞队列里的线程会同时去争抢锁,而不是按照进入阻塞队列的顺序获取锁。

ReentrantLock 默认是不公平锁,可以通过改变其构造方法将其设置为公平锁:

公平锁可以用来解决饥饿问题,但是会降低并发度,一般无需设置。

6. 条件变量

与 synchronized 的 wait/notify 类似的概念,当条件不满足时,为了不阻塞其他线程用锁,可以进入 waitSet 等待。

ReentrantLock 的条件变量更强大的地方在于,它支持多个条件变量,即等待不同条件的线程可以进入不同的等待室。

使用流程:

  • await 前需要获得锁;
  • await 执行后会释放锁,进入 conditionObject 等待;
  • await 的线程被唤醒(或打断、或超时)后,需要重新竞争锁;
  • 竞争锁成功后,从await 后继续执行。

await / signal 使用示例:

public class ReEntrantLockTest {
    private static final Logger logger = LoggerFactory.getLogger(ParkTest.class);
    public static ReentrantLock lock = new ReentrantLock();
    static boolean condition1 = false;
    static boolean condition2 = false;

    public static void main(String[] args) throws Exception {

        Condition waitSet1 = lock.newCondition();
        Condition waitSet2 = lock.newCondition();

        Thread t1 = new Thread(() -> {
            lock.lock();
            try {
                while (!condition1) {
                    try {
                        logger.info("条件1不满足,进入等待");
                        waitSet1.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                logger.info("t1 开始执行");
            } finally {
                lock.unlock();
            }

        }, "t1");

        Thread t2 = new Thread(() -> {
            lock.lock();
            try {
                while (!condition2) {
                    try {
                        logger.info("条件2不满足,进入等待");
                        waitSet2.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                logger.info("t2 开始执行");
            } finally {
                lock.unlock();
            }
        }, "t2");

        t1.start();
        t2.start();
        Thread.sleep(2000);
        lock.lock();
        try {
            logger.info("通知t1执行……");
            condition1 = true;
            waitSet1.signal();

            logger.info("通知t2执行……");
            condition2 = true;
            waitSet2.signal();
        } finally {
            lock.unlock();
        }
    }
}

           
并发编程4 - 线程状态、死锁及ReentrantLock

继续阅读