ReentrantLock基础知识

翻译成中文,ReentrantLock表示可重入锁,与synchronized一样,都是属于可重入锁。
与synchronized相比具有如下特点:

  • 可中断:synchronized只能等待同步代码块执行结束,不可以中断,而reentrantlock可以调用线程的interrupt方法来中断等待,继续执行下面的代码。
  • 可以设置超时时间:调用lock.trylock(),如果没有设置等待时间的话,没获取到锁,将返回false
  • 可以设置为公平锁:公平锁其实是为了解决饥饿问题,当一个线程由于优先级太低的时候,就可能没有办法获取到时间片
  • 可以支持多个变量:类似于调用wait方法时,不满足条件的线程进入waitset队列等待CPU随机调度,支持多个变量表示支持多个类似自定义waitset,这样就可以指定对象来唤醒了。

基本语法:

//获取锁
reentrantLock.lock();

try{ 
	//临界区
}finally{ 
	//释放锁
	reentrantLock.unlock();
}

1. ReentrantLock可重入

<mark>同一个线程</mark> 如果首次获取到该锁资源,则它就有权力再次获取到该锁,这就是锁的重入!

public class LockInterruptibly { 

    private static ReentrantLock reentrantLock = new ReentrantLock();
    public static void main(String[] args) { 
        m1();
    }

    public static void m1(){ 
        reentrantLock.lock();
        try { 
            log.debug("m1 lock");
            m2();
        }finally { 
            reentrantLock.unlock();
        }

    }
    public static void m2(){ 
        reentrantLock.lock();
        try { 
            log.debug("m2 lock");
            m3();
        }finally { 
            reentrantLock.unlock();
        }
    }

    public static void m3(){ 
        reentrantLock.lock();
        try { 
            log.debug("m3 lock");
        }finally { 
            reentrantLock.unlock();
        }
    }
}

输出:

2. ReentrantLock可中断

可中断指的是在尝试获取锁的过程中,可以中断该过程,并且执行相关业务。

public class LockInterruptibly { 

    static ReentrantLock reentrantLock = new ReentrantLock();
    public static void main(String[] args) { 

        Thread t1 = new Thread(() -> { 
            try { 
                log.debug("t1尝试获取锁");
                reentrantLock.lockInterruptibly();
            } catch (InterruptedException e) { 
                log.debug("获取锁过程被打断");
                return;
            }

            try { 
                log.debug("t1 获取到锁");
            }finally { 
                reentrantLock.unlock();
            }

        }, "t1");

        //优先t1上锁
        reentrantLock.lock();
        log.debug("main获取到锁");
        t1.start();

        try { 
            Thread.sleep(1000);
            t1.interrupt();
        } catch (InterruptedException e) { 
            e.printStackTrace();
        }finally { 
            reentrantLock.unlock();
        }
    }

}

输出:

可见在获取锁的过中被打断后,程序return停止

注意我们这边使用的是可中断上锁lockInterruptibly,如果使用了普通的lock()

public class LockInterruptibly { 

    static ReentrantLock reentrantLock = new ReentrantLock();
    public static void main(String[] args) { 

        Thread t1 = new Thread(() -> { 
// try { 
                log.debug("t1尝试获取锁");
                reentrantLock.lock();
// } catch (InterruptedException e) { 
// log.debug("获取锁过程被打断");
// return;
// }

            try { 
                log.debug("t1 获取到锁");
            }finally { 
                reentrantLock.unlock();
            }

        }, "t1");

        //优先t1上锁
        reentrantLock.lock();
        log.debug("main 获取到锁");
        t1.start();

        try { 
            Thread.sleep(1000);
            t1.interrupt();
        } catch (InterruptedException e) { 
            e.printStackTrace();
        }finally { 
            reentrantLock.unlock();
        }
    }

}

程序阻塞,输出:

3. ReentrantLock超时时间

如果没有指定时间,则立即失败(没有获取到锁)

public class LockInterruptibly { 

    static ReentrantLock reentrantLock = new ReentrantLock();
    public static void main(String[] args) { 

        Thread t1 = new Thread(() -> { 
            log.debug("t1尝试获取锁");
            if(!reentrantLock.tryLock()){ 
                log.debug("没有获取到锁");
                return;
            }

            try { 
                log.debug("t1 获取到锁");
            }finally { 
                reentrantLock.unlock();
            }

        }, "t1");

        //主线程上锁
        reentrantLock.lock();
        log.debug("main 获取到锁");
        t1.start();

        try { 
            Thread.sleep(3000);
        } catch (InterruptedException e) { 
            e.printStackTrace();
        }finally { 
            reentrantLock.unlock();
        }
    }

}

输出:

在主线程还没释放到锁,t1线程是无法获取到锁的,因此reentrantLock.tryLock()返回false,执行没有锁的相关业务。

如果有指定时间,在指定最大时间内没有获取到锁则失败

@Slf4j(topic = "c.LockInterruptibly")
public class LockInterruptibly { 

    static ReentrantLock reentrantLock = new ReentrantLock();
    public static void main(String[] args) { 

        Thread t1 = new Thread(() -> { 
            log.debug("t1尝试获取锁");
            try { 
                if(!reentrantLock.tryLock(2,TimeUnit.SECONDS)){ 
                    log.debug("没有获取到锁");
                    return;
                }
            } catch (InterruptedException e) { 
                e.printStackTrace();
            }

            try { 
                log.debug("t1 获取到锁");
            }finally { 
                reentrantLock.unlock();
            }

        }, "t1");

        //主线程上锁
        reentrantLock.lock();
        log.debug("main 获取到锁");
        t1.start();

        try { 
            Thread.sleep(1000);
        } catch (InterruptedException e) { 
            e.printStackTrace();
        }finally { 
            reentrantLock.unlock();
        }
    }

}

当等待获取锁的时间大于主线程释放锁的时间,则肯定可以获取到锁!!

用所学的tryLock知识,解决一个死锁问题:哲学家就餐问题

有五位哲学家,围坐在圆桌旁。
他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
如果筷子被身边的人拿着,自己就得等待

public class TestDeadLock { 
    public static void main(String[] args) { 
        Chopstick c1 = new Chopstick("1");
        Chopstick c2 = new Chopstick("2");
        Chopstick c3 = new Chopstick("3");
        Chopstick c4 = new Chopstick("4");
        Chopstick c5 = new Chopstick("5");
        new Philosopher("苏格拉底", c1, c2).start();
        new Philosopher("柏拉图", c2, c3).start();
        new Philosopher("亚里士多德", c3, c4).start();
        new Philosopher("赫拉克利特", c4, c5).start();
        new Philosopher("阿基米德", c5, c1).start();
    }
}

@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread { 
    Chopstick left;
    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) { 
                    eat();
                }
            }
        }
    }

    Random random = new Random();
    private void eat() { 
        log.debug("eating...");
        Sleeper.sleep(0.5);
    }
}

class Chopstick { 
    String name;

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

    @Override
    public String toString() { 
        return "筷子{" + name + '}';
    }
}

程序运行后一会儿会卡住。

使用我们之前的synchronized,就会出现死锁,即每个人某一时刻都只拥有一只筷子,又想要另外一只,此刻都无法得到另外一只,彼此都又不放开自己拥有的筷子,这种现象就是死锁。

使用ReentrantLock解决死锁问题

@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread { 
    Chopstick left;
    Chopstick right;

    private ReentrantLock reentrantLock = new ReentrantLock();

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

    @Override
    public void run() { 
        while (true) { 
            if(left.tryLock()){ 
                try { 
                    if(right.tryLock()){ 
                        try { 
                            eat();
                        }finally { 
                            right.unlock();
                        }

                    }
                }finally { 
                    left.unlock();
                }
            }


        }
    }

    Random random = new Random();
    private void eat() { 
        log.debug("eating...");
        Sleeper.sleep(0.5);
    }
}

class Chopstick extends ReentrantLock { 
    String name;

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

    @Override
    public String toString() { 
        return "筷子{" + name + '}';
    }

注意

            //while循环中的一种错误写法
            if(left.tryLock()){ 
                if(right.tryLock()){ 
                    try { 
                        eat();
                    }finally { 
                        left.unlock();
                        right.unlock();
                    }
                }

            }

这种错误的写***造成一些哲学家一直哪些一些筷子不放,也就是锁没有释放,又一直加锁,会导致上锁的次数超过预期的

4. ReentrantLock支持多个变量

支持多个变量类似于synchronized的waitset休息室,当条件不满足时,进入waitset队列等待,ReentrantLock的强大之处在于支持多个自定义的waitset,这样就可以灵活的应对和唤醒指定的线程。

使用要点:

  • await前需要获取到锁
  • await执行后会释放锁,进入conditionObject等待
  • await线程被唤醒(超时或打断后)重新竞争ReentrantLock锁
  • 竞争到ReentrantLock后,继续执行await后的代码

看一个例子:

public class TestWaitNotify { 
    final static Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;


    public static void main(String[] args) { 

        //小南
        new Thread(() -> { 
            synchronized (room) { 
                log.debug("有烟没?[{}]", hasCigarette);
                if (!hasCigarette) { 
                    log.debug("没烟,先歇会!");
                    try { 
                        room.wait();
                    } catch (InterruptedException e) { 
                        e.printStackTrace();
                    }
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) { 
                    log.debug("可以开始干活了");
                }
            }
        }, "小南").start();

        //小女等外卖
        new Thread(() -> { 
            synchronized (room) { 
                Thread thread = Thread.currentThread();
                log.debug("外卖送到没?[{}]", hasTakeout);
                if (!hasTakeout) { 
                    log.debug("没外卖,先歇会!");
                    try { 
                        room.wait();
                    } catch (InterruptedException e) { 
                        e.printStackTrace();
                    }
                }
                log.debug("外卖送到没?[{}]", hasTakeout);
                if (hasTakeout) { 
                    log.debug("可以开始干活了");
                } else { 
                    log.debug("没干成活...");
                }
            }
        }, "小女").start();

        //送外卖
        sleep(1);
        new Thread(() -> { 
            synchronized (room) { 
                hasTakeout = true;
                log.debug("外卖到了噢!");
                room.notify();
            }
        }, "送外卖的").start();
    }
}

输出:

synchronized 的弊端也很明显了,只能有一个休息室,所以唤醒的目标无法明确。

使用ReentrantLock 改进

public class Test17 { 

    private static ReentrantLock lock = new ReentrantLock();
    //等烟休息室
    static Condition cigaretteRoom = lock.newCondition();
    //等外卖休息室
    static Condition eattingRoom = lock.newCondition();

    static boolean hasCigarette = false;
    static boolean hasTakeout = false;


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

        //小南
        new Thread(() -> { 
            lock.lock();
            try { 
                log.debug("有烟没?[{}]", hasCigarette);
                while (!hasCigarette) { 
                    log.debug("没烟,先歇会!");
                    try { 
                        cigaretteRoom.await();
                    } catch (InterruptedException e) { 
                        e.printStackTrace();
                    }
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) { 
                    log.debug("可以开始干活了");
                }
            }finally { 
                lock.unlock();
            }

        }, "小南").start();

        //小女等外卖
        new Thread(() -> { 
            lock.lock();
            try { 
                Thread thread = Thread.currentThread();
                log.debug("外卖送到没?[{}]", hasTakeout);
                while (!hasTakeout) { 
                    log.debug("没外卖,先歇会!");
                    try { 
                        eattingRoom.await();
                    } catch (InterruptedException e) { 
                        e.printStackTrace();
                    }
                }
                log.debug("外卖送到没?[{}]", hasTakeout);
                if (hasTakeout) { 
                    log.debug("可以开始干活了");
                } else { 
                    log.debug("没干成活...");
                }
            }finally { 
                lock.unlock();
            }
        }, "小女").start();

        //送烟的来了
        Thread.sleep(1000);
        new Thread(() -> { 
            lock.lock();
            try { 
                hasCigarette = true;
                cigaretteRoom.signal();
            }finally { 
                lock.unlock();
            }

        }, "送烟的").start();

        //送外卖的来了
        Thread.sleep(1000);
        new Thread(() -> { 
            lock.lock();
            try { 
                hasTakeout = true;
                eattingRoom.signal();
            }finally { 
                lock.unlock();
            }


        }, "送外卖的").start();
    }
}

输出:

需要注意的是

Thread.sleep(1000);
        new Thread(() -> { 
            lock.lock();
            try { 
                hasCigarette = true;
                cigaretteRoom.signal();
            }finally { 
                lock.unlock();
            }

        }, "送烟的").start();

其中

hasCigarette = true;
cigaretteRoom.signal();

必须在获取锁之后才能执行,同时也要注意unlock,这也是相比于synchronized 较为繁琐的。

多个线程的lock会出现堵塞,但是await会暂时让出锁资源,所以可以开断电调试便以观察程序的具体运行过程。

5. ReentrantLock公平性

多线程情况下部分线程无法获取到CPU的时间片,导致线程饥饿问题,ReentrantLock默认是不开启公平规则的,公平性就是为了解决该问题而产生的。

启动公平性

ReentrantLock unFair = new ReentrantLock(true);

看一个例子:

@Slf4j(topic = "c.TestFair")
public class TestFair implements Runnable { 

    private  ReentrantLock reentrantLock;  //锁
    private static Integer num = 0;

    public TestFair(ReentrantLock reentrantLock){ 
        this.reentrantLock = reentrantLock;
    }

    @Override
    public void run() { 
        while (true){ 
            reentrantLock.lock();
            try { 
                num ++;
                log.debug(Thread.currentThread().getName() + num);
            }finally { 
                reentrantLock.unlock();
            }

        }
    }

    public static void main(String[] args) { 

        //公平锁
        ReentrantLock fair = new ReentrantLock(true);
        new Thread(new TestFair(fair),"t1").start();
        new Thread(new TestFair(fair),"t2").start();

        //不公平锁
// ReentrantLock unFair = new ReentrantLock(false);
// new Thread(new TestFair(unFair),"t1").start();
// new Thread(new TestFair(unFair),"t2").start();
    }
}

公平性下的输出:

线程之间都交替运行。

不公平下的输出:

可见,这就是饥饿问题。

学习资料:
https://www.bilibili.com/video/BV16J411h7Rd?p=127