12.Lock

作者: xialedoucaicai | 来源:发表于2018-08-28 21:05 被阅读0次

    之前在使用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,而且在使用时,一定要好好研究清楚了再用,否则到时候出现问题是很难排查的。

    相关文章

      网友评论

          本文标题:12.Lock

          本文链接:https://www.haomeiwen.com/subject/bwypiftx.html