Java并发包JUC的Lock锁讲解

概述

Java有两种锁,一种是使用关键字Synchronized对方法或者代码块进行加锁,一种是使用接口Lock(实际上其实现类)进行上锁和解锁。

区别:

  • Synchronized是java的一个关键字,而Lock是一个java类。
  • Synchronized是一个“自动”的“隐式”锁,也就是只要在方法或者代码块上加上该关键字,就会实现自动的上锁和解锁,而Lock是一个非自动的显式锁,上锁和解锁都需要用代码显示指定。如果不显式释放锁,就会造成死锁。
  • Synchronized是可重入、非公平锁。而Lock锁是可重入,公平/非公平锁,通过设置可以设置为公平锁或者非公平锁。
  • Synchronized修饰成员方法锁是这个对象本身,修饰静态方法的锁是类对象,修饰代码块时可以指定任意非null对象。而Lock锁的锁就是这个Lock对象。
使用

Lock接口有三个实现类:

  • ReentrantLock:可重入锁,可以代替上面synchronized关键字的使用。
  • ReentrantReadWriteLock.ReadLock :读锁。
  • ReentrantReadWriteLock.WriteLock:写锁。
    读锁与写锁总是成对存在的,以实现对资源的合理访问,如果使用普通的可重入锁的话,无论是对资源的读和写,都是排他的。如果使用读写锁,那么在资源处于读锁状态时,其他读操作也可以进来访问,写操作就不行,如果处于写锁状态时,那么其他读写操作都不能访问。可以实现并发度、排他写。
可重入锁ReentrantLock的使用:
public class LockDemo {

    //创建一个锁对象
    private static Lock lock = new ReentrantLock();

    public static void main(String[] args){
    	//代码就是开始是个线程,然后每个线程对以共享变量num循环自增十次。正确输出结果是100
        testReentrantLock();
    }


    public static void testReentrantLock(){
        Integer num = 0;
        Data data = new Data(num);
        for (int i = 0;i<10;i++){
            new Thread(data).start();
        }

    }

    static class Data implements Runnable{
        private Integer num;


        public Data(Integer num){
            this.num = num;
        }
        @Override
        public void run() {
            for (int i = 0;i<10;i++){
                //上锁
                lock.lock();
                try {
                    try {
                        //这里睡眠一毫米是为了让效果更佳明显
                        TimeUnit.MILLISECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    num ++;
                    System.out.println(num);
                }finally {
                    //释放锁
                    lock.unlock();
                }

            }
        }
    }
}

结果:正确
在这里插入图片描述
去掉上锁、解锁的代码。结果错误。
在这里插入图片描述

还可以给获取锁设置一个超时时间,时间过了还没获取到锁,就返回一个false并抛出一个中断异常。获取到锁就返回true。

//使用该方法获取锁,可以设置超时,使得线程不会永久阻塞。
boolean tryLock(long time, TimeUnit unit) 
使用Synchronized和ReentrantLock实现一个消费者生产者实例并做比较

使用Synchronized

public class SynchronizedConsumerProductDemo {


    public static void main(String[] args) {
        ConsumerProduct consumerProduct = new ConsumerProduct();
        new Thread(()->{
            try {
                for (int i = 0;i<10;i++){
                    consumerProduct.consumer();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"AAAAAAAAAAA").start();

        new Thread(()->{
            try {
                for (int i = 0;i<10;i++){
                    consumerProduct.product();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"BBBBBBBBBB").start();

        new Thread(()->{
            try {
                for (int i = 0;i<10;i++){
                    consumerProduct.consumer();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"CCCCCCCCCCCC").start();

        new Thread(()->{
            try {
                for (int i = 0;i<10;i++){
                    consumerProduct.product();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"DDDDDDDDDD").start();
    }



    static class ConsumerProduct{

        private int num;

        public synchronized void consumer() throws InterruptedException {
            while (num == 0){
                //如果num等于0,就把当前线程设置为等待状态。
                this.wait();
            }
            num --;
            System.out.println(num);
            //唤醒其他线程
            this.notifyAll();
        }

        public synchronized void product() throws InterruptedException {
            while (num == 1){
                //如果num
                this.wait();
            }
            num ++;
            System.out.println(num);
            this.notifyAll();
        }
    }
}

使用ReentrantLock

static class ConsumerProduct{

        private int num;

        private Lock lock = new ReentrantLock();

        private Condition condition = lock.newCondition();

        public  void consumer() throws InterruptedException {
            while (num == 0){
                //如果num等于0,就把当前线程设置为等待状态。
                condition.await();
            }
            num --;
            System.out.println(Thread.currentThread().getName() + "====> num = " + num);
            //唤醒其他线程
            condition.signalAll();
        }

        public  void product() throws InterruptedException {
            while (num == 1){
                //如果num
                condition.await();
            }
            num ++;
            System.out.println(Thread.currentThread().getName() + "====> num = " + num);
            condition.signalAll();
        }
    }

总结:
synchronized是使用wait和notifyAll或者notify配合实现线程间通信的。
ReentrantLock是使用condition的await和signalAll或者signal配合实现线程间通信的。await是等待。

condition可以实现精准的通知唤醒,也就是可以精准地通知唤醒哪些类型的线程。

public class Obj2 {

    public static void main(String[] args) {

        data1 data = new data1();

        new Thread(()->{
            try {
                for (int i = 0;i<10;i++){
                    data.print1();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"AAAAAAAAAAA").start();

        new Thread(()->{
            try {
                for (int i = 0;i<10;i++){
                    data.print2();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"BBBBBBBBBB").start();

        new Thread(()->{
            try {
                for (int i = 0;i<10;i++){
                    data.print3();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"CCCCCCCCCCCC").start();

        new Thread(()->{
            try {
                for (int i = 0;i<10;i++){
                    data.print4();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"DDDDDDDDDD").start();
    }


    public static class data1{
        private int num;

        Lock lock = new ReentrantLock();
        Condition condition1 = lock.newCondition();
        Condition condition2 = lock.newCondition();
        Condition condition3 = lock.newCondition();
        Condition condition4 = lock.newCondition();

        public void print1() throws InterruptedException {
            lock.lock();
            try {
                while (num != 0){
                    //使用condition1进行线程阻塞,要唤醒,就要使用condition1
                    condition1.await();
                }
                num = 1;
                System.out.println(Thread.currentThread().getName() + "=====> num = " + num);
                //唤醒condition2管理的线程
                condition2.signalAll();
            }finally {
                lock.unlock();
            }
        }

        public void print2() throws InterruptedException {
            lock.lock();
            try {
                while (num != 1){
                    condition2.await();
                }
                num = 2;
                System.out.println(Thread.currentThread().getName() + "=====> num = " + num);
                condition3.signalAll();
            }finally {
                lock.unlock();
            }
        }

        public void print3() throws InterruptedException {
            lock.lock();
            try {
                while (num != 2){
                    condition3.await();
                }
                num = 3;
                System.out.println(Thread.currentThread().getName() + "=====> num = " + num);
                condition4.signal();
            }finally {
                lock.unlock();
            }
        }

        public void print4() throws InterruptedException {
            lock.lock();
            try {
                while (num != 3){
                    condition4.await();
                }
                num = 0;
                System.out.println(Thread.currentThread().getName() + "=====> num = " + num);
                condition1.signal();
            }finally {
                lock.unlock();
            }
        }
    }
}

解释:同一个lock可以创建多个condition,使用某个condition来阻塞线程,就要使用这个condition来唤醒线程,所以就可以准确地设置要唤醒那些线程。比如上面代码AAAAAAAAAAA线程会唤醒BBBBBBB线程,BBBBBB线程会唤醒CCCCCCCC线程,CCCCCCCCCCC线程会唤醒DDDDDDDDDDD线程,DDDDDDDDDD线程会唤醒AAAAAAAAAAAAA线程。

在这里插入图片描述
线程的执行顺序都是有序的。

读写锁的使用

读写锁是为了区分读线程和写线程,使得对资源的控制更加地合理,因为读操作不会涉及到并发安全问题,所以多个读线程可以同时获得读锁,但是读线程占用锁时,写线程不能访问资源。写线程占用锁时,读线程和其他写线程不能访问资源,这就既保证了读线程总能读到最新数据。资源的方法控制也更加合理。


public class ReadWriteLockDemo {

    public static void main(String[] args) {
        ReadWriteLockD readWriteLockD = new ReadWriteLockD();
        CountDownLatch latch = new CountDownLatch(50);
        for (int i = 0;i<35;i++){
            new Thread(()->{
                //35个读线程
                try {
                    readWriteLockD.read();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                try {
                    latch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
            latch.countDown();
        }

        for (int i = 0;i<15;i++){
            new Thread(()->{
                //15个写线程
                try {
                    readWriteLockD.write();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                try {
                    latch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
            latch.countDown();
        }
    }


    static class ReadWriteLockD{
        private int num = 0;
        private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

        private Lock readLock =  readWriteLock.readLock();

        private Lock writeLock = readWriteLock.writeLock();

        public void read() throws InterruptedException {
            //睡眠十毫秒,使得结果更为明显
            TimeUnit.MILLISECONDS.sleep(10);

            readLock.lock();
            try {
                System.out.println("获取到了读锁 num = " + num);
            }finally {
                System.out.println("释放了读锁");
                TimeUnit.MILLISECONDS.sleep(10);
                readLock.unlock();
            }
        }


        public void write() throws InterruptedException {
            TimeUnit.MILLISECONDS.sleep(10);

            writeLock.lock();
            try {
                num ++ ;
                System.out.println("获取到了写锁使得num+1");
            }finally {
                System.out.println("释放了写锁");
                TimeUnit.MILLISECONDS.sleep(10);
                writeLock.unlock();
            }
        }
    }
}

结果:
在这里插入图片描述上图可以看出可以并发获取读锁,而不用等到上一个读锁释放。
在这里插入图片描述
但是写锁就非常有规律,必须等写锁释放,其他线程才能获取锁。


版权声明:本文为qq_40837310原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。