Java多线程 第四章 ReentrantLock简介

一、ReentrantLock简介

jdk中独占锁的实现除了使用关键字synchronized外,还可以使用ReentrantLock。虽然在性能上ReentrantLock和synchronized没有什么区别,但ReentrantLock相比synchronized而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景。

二、ReentrantLock和Synchronized对比

类别SynchronizedReentrantLock
存在层次Java的关键字,jvm层面一个类
锁的释放1、以获取锁的线程执行完同步代码,释放锁;2、线程执行发生异常,jvm会释放锁在finally中必须释放锁,否则容易造成线程死锁
锁的获取假如A线程获的锁,B线程等待。如果A线程阻塞,B线程会一直等待分情况而定,Lock有多个锁获取的方式,大致就是可以尝试获得锁,线程可以不用一直等待
锁的状态无法判断可以判断
锁类型可重入,不可中断,非公平可冲入,可判断,可公平(两者皆可)
性能少量同步大量同步

三、ReentrantLock的简单使用,及使用效果

3.1、ReentrantLock锁例子;

import java.util.concurrent.locks.ReentrantLock;

public class ThreadTest2 {
    public static void main(String[] args){
        LockTest t = new LockTest();
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        t1.start();
        t2.start();
    }
}

class LockTest implements Runnable{
    ReentrantLock lock = new ReentrantLock();
    private static int j = 0;
    public  void run() {
        try {
            lock.lock();
            j++;
            try {
                Thread.sleep(110);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" 你是第" + j + "个线程");
        }finally {
            lock.unlock();
        }
    }
}

输出的结果

Thread-1 你是第1个线程
Thread-0 你是第2个线程

Process finished with exit code 0

3.2、Synchronized锁例子:

public class ThreadTest3 {
    public static void main(String[] args){
        Thread3 t = new Thread3();
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        t1.start();
        t2.start();
    }
}

class Thread3 implements Runnable{
    private static int j = 0;
    public void run() {
        synchronized (this){
            j++;
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" 你是第" + j + "个线程");
        }
    }
}

输出的结果

Thread-1 你是第1个线程
Thread-0 你是第2个线程

Process finished with exit code 0

通过上面的可以明显的看到,Synchronized是用关键字加锁,ReentrantLock则是一个类的引用。

四、ReentrantLock的使用场景

4.1、公平锁和非公平锁

公平锁是指当锁可用时,在锁上等待时间最长的线程将获得锁的使用权。
而非公平锁则随机分配这种使用权。和synchronized一样,默认的ReentrantLock实现是非公平锁,因为相比公平锁,非公平锁性能更好。
在创建ReentrantLock的时候通过传进参数true创建公平锁,如果传入的是false或没传参数则创建的是非公平锁
//非公平锁,默认为非公平锁
ReentrantLock lock = new ReentrantLock();
//公平锁在ReentrantLock中加入true即可, 相比非公平锁,公平锁性能要差些
ReentrantLock locktr = new ReentrantLock(true);

举个例子,开启五个线程,每个线程都会去获取和释放两次锁,我们来看看公平锁和非公平锁的使用情况

import java.util.concurrent.locks.ReentrantLock;

public class ReentTest1 {
    public static void main(String[] args) {
        //初始化锁
        //非公平锁,默认为非公平锁
        ReentrantLock lock = new ReentrantLock();
        //公平锁 相比非公平锁,公平锁性能要差些
        ReentrantLock locktr = new ReentrantLock(true);
        String name = "非公平线程";
        for(int i=0;i<5;i++){
            new Thread(new ThreadDemo(lock,name)).start();
        }
//        String nametr = "公平线程";
//        for(int i=0;i<5;i++){
//            new Thread(new ThreadDemo(locktr,nametr)).start();
//        }
    }
}

class ThreadDemo implements Runnable {
    ReentrantLock lock;
    String name;
    ThreadDemo(ReentrantLock lock,String name){
        this.lock = lock;
        this.name = name;
    }

    public void run() {
        try {
           Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for(int i=0;i<2;i++){
            try {
                lock.lock();
                System.out.println("获得锁的线程:"+ name + " " + Thread.currentThread().getName());
            }finally {
                lock.unlock();
            }
        }
    }
}

两者的结果如下:

公平锁的结果:基本上线程是交替获得锁的。

获得锁的线程:公平线程 Thread-3
获得锁的线程:公平线程 Thread-4
获得锁的线程:公平线程 Thread-2
获得锁的线程:公平线程 Thread-0
获得锁的线程:公平线程 Thread-1
获得锁的线程:公平线程 Thread-3
获得锁的线程:公平线程 Thread-2
获得锁的线程:公平线程 Thread-0
获得锁的线程:公平线程 Thread-1
获得锁的线程:公平线程 Thread-4

Process finished with exit code 0

非公平锁结果:线程会重复获取锁。如果申请获取锁的线程足够多,那么可能会造成某些线程长时间得不到锁。这就是非公平锁的“饥饿”问题。

获得锁的线程:非公平线程 Thread-2
获得锁的线程:非公平线程 Thread-2
获得锁的线程:非公平线程 Thread-0
获得锁的线程:非公平线程 Thread-0
获得锁的线程:非公平线程 Thread-1
获得锁的线程:非公平线程 Thread-1
获得锁的线程:非公平线程 Thread-3
获得锁的线程:非公平线程 Thread-3
获得锁的线程:非公平线程 Thread-4
获得锁的线程:非公平线程 Thread-4

Process finished with exit code 0

公平锁和非公平锁该如何选择
大部分情况下我们使用非公平锁,因为其性能比公平锁好很多。但是公平锁能够避免线程饥饿,某些情况下也很有用。

4.2获取锁时限时等待

ReentrantLock还给我们提供了获取锁限时等待的方法tryLock(),可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。我们可以使用该方法配合失败重试机制来更好的解决死锁问题。
tryLock如果获得就执行没有就不执行(当一个任务获得锁的时候,必须等他执行完,其他线程获得了锁才能执行,再此期间没有获得锁的线程将不能执行)

举个例子

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class ThreadTest {
    public static void main(String[] args) {

        ReentrantLock lock = new ReentrantLock();

        new Thread(new ThreadDemo(lock)).start();
        new Thread(new ThreadDemo2(lock)).start();
    }
}

class ThreadDemo implements Runnable {
    ReentrantLock lock;

    ThreadDemo(ReentrantLock lock){
        this.lock = lock;
    }
    public void run() {
        try {
            lock.lock();
            System.out.println("获得锁的线程:" + Thread.currentThread().getName());
            //4s后在释放lock
            Thread.sleep(4000);
            lock.unlock();
            System.out.println("释放锁的线程:" + Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class ThreadDemo2 implements Runnable {
    ReentrantLock lock;

    ThreadDemo2(ReentrantLock lock){
        this.lock = lock;
    }

    public void run() {
        try {
            Thread.sleep(100);
            System.out.println("尝试获取锁:" + Thread.currentThread().getName());
            if(lock.tryLock(5, TimeUnit.SECONDS)){///如果已经被lock,尝试等待5s,看是否可以获得锁,如果5s后仍然无法获得锁则返回false继续执行
                System.out.println("获得锁的线程:" + Thread.currentThread().getName());
                lock.unlock();
                System.out.println("释放锁的线程:" + Thread.currentThread().getName());
            }else {
                System.out.println("线程" + Thread.currentThread().getName()+ "没有获得锁:");
            }

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

4.3、ReentrantLock可响应中断

当使用synchronized实现锁时,阻塞在锁上的线程除非获得锁否则将一直等待下去,也就是说这种无限等待获取锁的行为无法被中断。而ReentrantLock给我们提供了一个可以响应中断的获取锁的方法lockInterruptibly()。该方法可以用来解决死锁问题。
lockInterruptibly()中断正在进行的操作立刻释放锁继续下一操作.比如 取消正在同步运行的操作,来防止不正常操作长时间占用造成的阻塞

import java.util.concurrent.locks.ReentrantLock;

public class ThreadTest6 {
    public static void main(String[] args) {

        ReentrantLock lock = new ReentrantLock();
        ThreadDemo td1 = new ThreadDemo(lock);
        ThreadDemo2 td2 = new ThreadDemo2(lock);
        Thread t1 = new Thread(td1);
        Thread t2 = new Thread(td2);
        t1.start();
        t2.start();
        t1.interrupt();
    }
}

class ThreadDemo implements Runnable {
    ReentrantLock lock;

    ThreadDemo(ReentrantLock lock){
        this.lock = lock;
    }
    public void run() {
        try {
            lock.lockInterruptibly();
            System.out.println( Thread.currentThread().getName()+ "获得锁");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class ThreadDemo2 implements Runnable {
    ReentrantLock lock;

    ThreadDemo2(ReentrantLock lock){
        this.lock = lock;
    }

    public void run() {
        try {
            Thread.sleep(100);
            lock.lockInterruptibly();
            System.out.println( Thread.currentThread().getName()+"获取锁");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
            System.out.println( Thread.currentThread().getName()+"释放锁");
        }
    }
}

结果
在这里插入图片描述
第一个线程获取了锁,但是一直没有释放,如果没有中断的化,第二个线程只能一直等待;我们通过使其中一个线程中断,来结束线程间毫无意义的等待。被中断的线程将抛出异常,而另一个线程将能获取锁后正常结束。

4.4、结合Condition实现等待通知机制

使用synchronized结合Object上的wait和notify方法可以实现线程间的等待通知机制。
ReentrantLock结合Condition接口同样可以实现这个功能。而且相比前者使用起来更清晰也更简单。
Condition由ReentrantLock对象创建,并且可以同时创建多个

ReentrantLock lock = new ReentrantLock();
Condition con1= lock.newCondition();
Condition con2= lock.newCondition();

Condition接口在使用前必须先调用ReentrantLock的lock()方法获得锁。之后调用Condition接口的await()将释放锁,并且在该Condition上等待,直到有其他线程调用Condition的signal()方法唤醒线程。使用方式和wait,notify类似

一个简单的使用:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ThreadTest6 {
    public static void main(String[] args) {

        ReentrantLock lock = new ReentrantLock();
        Condition con = lock.newCondition();

        ThreadDemo td1 = new ThreadDemo(lock,con);
        ThreadDemo2 td2 = new ThreadDemo2(lock,con);
        Thread t1 = new Thread(td1);
        Thread t2 = new Thread(td2);
        t1.start();
        t2.start();
    }
}

class ThreadDemo implements Runnable {
    ReentrantLock lock;
    Condition con;
    ThreadDemo(ReentrantLock lock,Condition con){
        this.lock = lock;
        this.con = con;
    }
    public void run() {
        try {
            lock.lock();
            System.out.println( Thread.currentThread().getName()+ "获得锁");
            con.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
            System.out.println( Thread.currentThread().getName()+ "释放锁");
        }
    }
}

class ThreadDemo2 implements Runnable {
    ReentrantLock lock;
    Condition con;

    ThreadDemo2(ReentrantLock lock,Condition con){
        this.lock = lock;
        this.con = con;
    }

    public void run() {
        try {
            Thread.sleep(100);
            lock.lock();
            System.out.println( Thread.currentThread().getName()+"获取锁");
            con.signal();
            System.out.println( Thread.currentThread().getName()+"唤起线程0");

        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
            System.out.println( Thread.currentThread().getName()+"释放锁");
        }
    }
}

运行结果:线程0通过con.await()进入等待,线程1通过con.signal()唤起线程0接着执行;

Thread-0获得锁
Thread-1获取锁
Thread-1唤起线程0
Thread-1释放锁
Thread-0释放锁

Process finished with exit code 0

我们用ReentrantLock和Condition来实现生产者和消费者模式:

import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ThreadTest6 {
    public static void main(String[] args) {

        ProCons pc = new ProCons(10);
        Producter p = new Producter(pc);
        Consumer c = new Consumer(pc);
        Thread t1 = new Thread(p);
        Thread t2 = new Thread(c);
        t1.start();
        t2.start();
    }
}

class Producter implements Runnable{
    ProCons pc;
    Producter(ProCons pc){
        this.pc = pc;
    }
    @Override
    public void run() {
        try {
            pc.pro();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class Consumer implements Runnable{
    ProCons pc;
    Consumer(ProCons pc) {
        this.pc = pc;
    }
    @Override
    public void run() {
        try {
            pc.cons();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class ProCons {

    int size;
    int i=0;

    ReentrantLock lock = new ReentrantLock();
    Condition conFull = lock.newCondition();
	Condition conEmpty = lock.newCondition();
	
    ProCons(int size){
        this.size = size;
    }

    public void pro() throws InterruptedException {
        try {
            lock.lock();
            while (true){
                if(i >= size){
                    conFull.await();
                }
                i++;
                System.out.println("生产了1个,现在有"+i+"个");
                conEmpty.signal();
            }
        }finally {
            lock.unlock();
        }
    }

    public void cons() throws InterruptedException {
        try{
            Thread.sleep(1000);
            lock.lock();
            while (true){
                if (i <= 0) {
                    conEmpty.await();
                }
                i--;
                System.out.println("消费1个,现在还有" + i + "个");
                conFull.signal();
            }
        }finally {
            lock.unlock();
        }
    }
}

五、ReentrantLock和Synchronized如何选择?

灵活性:synchronized与ReentrantLock相比较之后,感觉ReeantrantLock比synchronized有更强大的功能,更灵活的操作,在很多复杂场景下,ReentrantLock相比synchronized会更合适。
使用性:syschronzied使用门槛更低,简单粗暴,而且不会出现忘记解锁的状况。
性能:在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方建议用Synchronized

《Java并发编程实战》中建议,在synchronized无法满足需求的时候,再用ReeantrantLock,原因大致如下:
1、synchronized简单易用,容易上手,而且不容易出现忘记解锁的情况
2、未来更可能提高synchronized的性能,因为synchronized是JVM的内置属性

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值