12.Lock

之前在使用synchronized解决线程安全问题时,经常提到用lock也可以实现synchronized的功能,现在我们就来看看lock的使用。

本篇文章少部分内容引用了Java并发编程:Lock,文章的作者15年毕业,这是他14年写的文章,再想想自己14年的水平,汗颜的同时,也为当时没有人提携指引深感遗憾,后面有空了,打算写写这几年的感悟和认识,对自己做一个总结,与君共勉吧。感慨结束,进入正题。

1.ReentrantLock

1.1ReentrantLock实现同步

Lock是一个接口,它有一个重要的实现类ReentrantLock,通过lock方法来进行加锁,reentrantLock.lock()实际上就相当于synchronized(reentrantLock),“锁”即为reentrantLock对象,也可以实现多线程的同步。我们来看对于之前的购票例子如何使用reentrantLock实现线程安全。

public class Ticket {
    public int total = 5;
    public ReentrantLock reentrantLock = new ReentrantLock();
    
    public void buy() throws InterruptedException{
        try{
            reentrantLock.lock();
            
            if(total > 0){
                //模拟买票过程
                Thread.sleep(100);
                --total;
            }   
        }finally{
            reentrantLock.unlock();
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        Ticket ticket = new Ticket();
        TicketThread ticketThread = new TicketThread(ticket);
        //10个人抢5张票
        for(int i=0;i<10;i++){
            new Thread(ticketThread).start();
        }
        //确保所有线程都执行完了
        Thread.sleep(3000);
        System.out.println("剩余:"+ticket.total);
    }
}
public class TicketThread implements Runnable{
    Ticket ticket = null;
    public TicketThread(Ticket ticket) {
        this.ticket = ticket;
    }
    
    @Override
    public void run() {
        try {
            ticket.buy();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

可以看到,在使用ReentrantLock的时候,需要自己手动调用lock()/unlock()方法,来实现加锁/释放锁,一般会把unlock放到finally中执行,避免因程序异常,导致锁一直无法释放。

1.2ReentrantLock之Condition实现等待/通知

之前我们有提到线程间通信的一种方式wait/notify,ReentrantLock可以通过Condition来实现类似的功能,相比之前的wait/notify,这种方式更加灵活,来看实际的例子。
MyMethod类定义了一个ReentrantLock对象,从中获取两个condition,method将会执行5次输出,在执行到第3次的时候,调用await(),阻塞当前线程,等待唤醒后,才能继续执行后2次输出。在main方法中,我们指定唤醒线程1。最终线程1可以进行5次输出,线程2执行三次输出后,由于未被唤醒,一直阻塞在那里。

public class MyMethod {
    //可重入锁 与synchronized(reentrantLock1)效果一致
    ReentrantLock reentrantLock1 = new ReentrantLock();
    
    //Condition实现wait/notify机制
    Condition condition1 = reentrantLock1.newCondition();
    Condition condition2 = reentrantLock1.newCondition();
    
    public void method1() throws InterruptedException{
        try{
            reentrantLock1.lock();
            for(int i = 0;i < 5;i++){
                if(i == 3){
                    condition1.await();
                }
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName()+"调用method1()");
            }   
        }finally{
            reentrantLock1.unlock();
        }
        
    }
    
    public void method2() throws InterruptedException{
        try{
            //注意这里得使用reentrantLock1,即和method1用的同一个锁,才能实现同步
            reentrantLock1.lock();
            for(int i = 0;i < 5;i++){
                if(i == 3){
                    condition2.await();
                }
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName()+"调用method2()");
            }
        }finally{
            reentrantLock1.unlock();
        }
    }
    
    public void signal1(){
        try{
            reentrantLock1.lock();
            condition1.signalAll();
        }finally{
            reentrantLock1.unlock();
        }
    }
}
public class MyThread1 extends Thread{
    MyMethod myMethod = null;
    public MyThread1(MyMethod myMethod) {
        this.myMethod = myMethod;
    }
    
    @Override
    public void run() {
        try {
            myMethod.method1();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class MyThread2 extends Thread{
    MyMethod myMethod = null;
    public MyThread2(MyMethod myMethod) {
        this.myMethod = myMethod;
    }
    
    @Override
    public void run() {
        try {
            myMethod.method2();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Main {
    public static void main(String[] args) throws InterruptedException {
        MyMethod myMethod = new MyMethod();
        
        MyThread1 myThread1 = new MyThread1(myMethod);
        myThread1.setName("myThread1");
        MyThread2 myThread2 = new MyThread2(myMethod);
        myThread2.setName("myThread2");
        
        myThread1.start();
        myThread2.start();
        
        Thread.sleep(5000);
        //这里只通知线程1
        myMethod.signal1();
    }
}

最终执行结果:

myThread1调用method1()
myThread1调用method1()
myThread1调用method1()
myThread2调用method2()
myThread2调用method2()
myThread2调用method2()
myThread1调用method1()
myThread1调用method1()

通过这个例子,我们可以看到Lock的Condition可以实现唤醒指定线程。而如果使用notify,唤醒哪个线程是随机的,完全取决于JVM。如果有兴趣,可以把上面的例子改成wait/notify实现,你会对这两者的区别有更清楚的认识。

1.3ReentrantLock其他特性

  1. 指定公平锁还是非公平锁
    ReentrantLock的构造方法可以传入一个boolean类型的参数,true即为公平锁,false即为非公平锁,默认为false。

public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

公平锁表示线程获得锁的顺序是按照加锁的顺序来分配的,就像食堂打饭要排队,先来先得。非公平锁则是一种抢占机制,可能造成某些线程一直拿不到锁,就像早高峰一起挤公交,有人可能过了好几趟车,也没挤上去。

  1. 获得锁的状态
  • getHoldCount(),获得当前线程保持此锁的个数,即调用lock()的次数。未调用lock()时,为0,调用lock(),为1,调用unlock(),为0。
  • getQueueLength,获得等待该锁的线程个数。假设有5个线程都start(),线程1执行了lock(),一直未执行unlock(),此时其他四个线程都在等着线程1释放该锁,getQueueLength将会返回4。
  • hasQueuedThread(Thread thread),查询指定线程是否在等待该锁
  • hasQueuedThreads(),查询是否有线程在等待该锁
  • isFair(),判断该锁是否为公平锁
  • isHeldByCurrentThread(),查询当前线程是否持有该锁
  • isLocked(),查询是否有线程持有该锁
  • tryLock(),尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回,在拿不到锁时不会一直在那等待
  • tryLock(long time, TimeUnit unit),与tryLock()类似,区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false;如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true
  1. 响应中断
    假设有这样一个场景,线程1获得锁,但在执行较为耗时的操作,此时线程2只能干等着,但我现在不想让线程2等了,我要中断线程2,如果使用synchronized关键字,线程2是无法中断的;如果使用reentrantLock.lockInterruptibly()则可以中断线程2。
    lockInterruptibly的作用为,如果当前线程未被中断,则获取锁,如果被中断,则抛出异常。
    来看代码示例:
    synchronized无法响应中断
public class TestSynchronized {
    public static void test() throws InterruptedException{
        synchronized(TestSynchronized.class){
            System.out.println(Thread.currentThread());
            Thread.sleep(10000);
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        //线程一 持有锁,并保持10s
        new Thread(new Runnable() {
            public void run() {
                try {
                    TestSynchronized.test();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        
        //由于锁被线程一持有,线程二进入阻塞状态
        Thread thread = new Thread(new Runnable() {
            public void run() {
                try {
                    TestSynchronized.test();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
        
        //1s后,尝试中断线程二,发现线程二无法响应中断,必须等够10s
        Thread.sleep(1000);
        thread.interrupt();
    }
}

ReentrantLock能够响应中断

public class TestReentrantLock {
    public static ReentrantLock reentrantLock = new ReentrantLock();
    
    public static void test() throws InterruptedException{
        reentrantLock.lockInterruptibly();
        
        System.out.println(Thread.currentThread());
        Thread.sleep(10000);
        
        reentrantLock.unlock();
    }
    
    public static void main(String[] args) throws InterruptedException {
        //线程一 得到锁,并占用10s
        new Thread(new Runnable() {
            public void run() {
                try {
                    TestReentrantLock.test();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        
        //线程二 因为锁被线程一持有,进入阻塞状态
        Thread thread = new Thread(new Runnable() {
            public void run() {
                try {
                    TestReentrantLock.test();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
        
        //1s后,尝试中断线程2,发现能够正常中断
        Thread.sleep(1000);
        thread.interrupt();
    }
}

2.ReentrantReadWriteLock

假设有这样一个场景,线程1和线程2都需要读写某一文件,为了保证线程安全,我们可以使用synchronized关键字或ReentrantLock,通过加锁来保证线程安全,即读读互斥,读写互斥,写读互斥,写写互斥。但实际上对于1 2两个线程同时读取的操作,是不会出现线程安全问题的,加锁反而影响效率。我们可以使用ReentrantReadWriteLock,来实现读读共享,读写互斥,写读互斥,写写互斥的效果。

代码示例:

public class ReadWriteService {
    ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    
    public void read() throws InterruptedException{
        try{
            readWriteLock.readLock().lock();
            System.out.println(Thread.currentThread().getName()+"拿到读锁"+System.currentTimeMillis());
            Thread.sleep(1000);
        }finally{
            readWriteLock.readLock().unlock();
        }
    }
    
    public void write() throws InterruptedException{
        try{
            readWriteLock.writeLock().lock();
            System.out.println(Thread.currentThread().getName()+"拿到写锁"+System.currentTimeMillis());
            Thread.sleep(1000);
        }finally{
            readWriteLock.writeLock().unlock();
        }
    }
}
public class ReadThread extends Thread{
    ReadWriteService service = null;
    
    public ReadThread(ReadWriteService service) {
        this.service = service;
    }
    
    @Override
    public void run() {
        try {
            service.read();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class WriteThread extends Thread{
    ReadWriteService service = null;
    
    public WriteThread(ReadWriteService service) {
        this.service = service;
    }
    
    @Override
    public void run() {
        try {
            service.write();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Main {
    public static void main(String[] args) throws InterruptedException {
        ReadWriteService service = new ReadWriteService();
        
        ReadThread readThread1 = new ReadThread(service);
        readThread1.setName("readThread1");
        ReadThread readThread2 = new ReadThread(service);
        readThread2.setName("readThread2");
        
        WriteThread writeThread1 = new WriteThread(service);
        writeThread1.setName("writeThread1");
        WriteThread writeThread2 = new WriteThread(service);
        writeThread2.setName("writeThread2");
        
        //读读共享
        /*readThread1.start();
         Thread.sleep(100);
        readThread2.start();*/
        
        //读写互斥
        /*readThread1.start();
        Thread.sleep(10);
        writeThread1.start();*/
        
        //写读互斥
        /*writeThread1.start();
        Thread.sleep(10);
        readThread1.start();*/
        
        //写写互斥
        writeThread1.start();
        Thread.sleep(10);
        writeThread2.start();
    }
}

3.synchronized与Lock的对比

通过上面的介绍,我们可以总结一下这两种锁的区别:

1)Lock是一个接口,通过Java代码来实现锁,而synchronized是Java中的关键字,是内置的实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行;
4)Lock提供了对锁的更多操作,比如可以指定公平锁还是非公平锁,获得锁的状态,是否成功获得了锁等;
5)ReentrantLock的Condition可以实现更灵活的wait/notify机制
6)ReentrantReadWriteLock可以提高多个线程进行读操作的效率

两者如何选用:
一般场景使用synchronized就够了,因为它更简单易用,而且经过不断优化,它的性能也已经得到了很大改善。建议复杂场景或者有更高级的要求的时候,再考虑使用lock,而且在使用时,一定要好好研究清楚了再用,否则到时候出现问题是很难排查的。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 摘要: 我们已经知道,synchronized 是Java的关键字,是Java的内置特性,在JVM层面实现了对临界...
    kingZXY2009阅读 1,883评论 0 20
  • 本文是我自己在秋招复习时的读书笔记,整理的知识点,也是为了防止忘记,尊重劳动成果,转载注明出处哦!如果你也喜欢,那...
    波波波先森阅读 11,699评论 4 56
  • Java8张图 11、字符串不变性 12、equals()方法、hashCode()方法的区别 13、...
    Miley_MOJIE阅读 3,922评论 0 11
  • 线程安全 当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或...
    闽越布衣阅读 812评论 0 6
  • 风走过须臾的路口 就像时间倏然而过 我们总在回头的瞬间 发现时光的孤单 风像流浪的孤儿 随意停歇的脚步 总踩着时间...
    一纸疯癫阅读 308评论 5 5

友情链接更多精彩内容