欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

通过一个详细例子引入重入锁ReentrantLock、ReadWriteLock读写锁

程序员文章站 2022-05-04 20:05:54
...

1.概述

ReentrantLock是可重入的独占锁。比起synchronized功能更加丰富,支持公平锁实现,支持中断响应以及限时等待等等。可以配合一个或多个Condition条件方便的实现等待通知机制。

2.代码示例

Demo1:

并发安全会存在抢占问题

package com.company.lock;

public class MyLockDemo {
    static int i = 0;
    public static void main(String[] args) throws InterruptedException {
        new Thread(new Add()).start();
        new Thread(new Add()).start();
        
        //Thread.sleep(3000);
        System.out.println(i);
    }
}

class Add implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 100000; i++) {
            LockDemo.i++;
        }
    }
}

 上面程序的执行结果是0,没有加Thread.sleep(3000)时:

 

通过一个详细例子引入重入锁ReentrantLock、ReadWriteLock读写锁

因为:

主函数所在的类默认是一个线程类,在执行过程中启动两个add线程,线程被start的时候并不是立即执行,而是会经历一系列的准备过程,这就意味着add线程还在启动中,主线程可以抢占执行权继续执行,从而输出0。如果上面的线程没有执行完,那么主线程即使抢到执行权,也应该阻塞,所以添加sleep3秒,先让线程计算完成,此时输出的结果如下,并不是20W说明出现了线程抢占,出现了线程并发安全问题。

通过一个详细例子引入重入锁ReentrantLock、ReadWriteLock读写锁

Demo2:调整上面代码加入锁synchronized,确定锁对象就为Add.class,代码如下

package com.company.lock;

public class MyLockDemo {
    static int i = 0;
    public static void main(String[] args) throws InterruptedException {
        new Thread(new Add()).start();
        new Thread(new Add()).start();

        Thread.sleep(3000);
        System.out.println(i);
    }
}

class Add implements Runnable{

    @Override
    public void run() {
        synchronized (Add.class) {
            for (int i = 0; i < 100000; i++) {
                MyLockDemo.i++;
            }
        }
    }
}

此时执行的结果就是20W

通过一个详细例子引入重入锁ReentrantLock、ReadWriteLock读写锁

Demo3:

上面的Demo2中将Add.class作为了锁对象,但是还是有问题,当有2个类--A类和B类,他们中都有2个同样的方法(代码如下)此时的A和B是2个线程的情况下此时启动A和B类中4个线程,那么他们之间构成了互斥,因为锁对象是一致的都是Add.class。如果想要A中的2个线程互斥,B中的2个线程互斥,各干各的,互不影响---这种情况下synchronized不好实现,由此可以发现synchronized锁的一个特点。

synchronized在使用过程中需要去确定锁对象。如果锁对象确定错误,容易导致死锁。

synchronized不够灵活

package com.company.lock;

public class MyLockDemo {
    static int i = 0;
    public static void main(String[] args) throws InterruptedException {
        new Thread(new Add()).start();
        new Thread(new Add()).start();

        Thread.sleep(3000);
        System.out.println(i);
    }
}

class Add implements Runnable{

    @Override
    public void run() {
        synchronized (Add.class) {
            for (int i = 0; i < 100000; i++) {
                MyLockDemo.i++;
            }
        }
    }
}

class A {
    public static void main(String[] args) {
        new Thread(new Add()).start();
        new Thread(new Add()).start();
    }
}

class B {
    public static void main(String[] args) {
        new Thread(new Add()).start();
        new Thread(new Add()).start();
    }
}

Demo4

上述Demo3中synchronized还需要去确定锁对象,且存在不灵活等问题,所以在jdk5中提供了Lock 接口,它的一个实现类是ReentrantLock--重入锁。所以先构建一个锁对象,并传入线程中,然后在通过有参构造接收锁对象。

此时因为锁对象是自己传递进来的,所以此时加锁,只需要调用lock()方法加锁即可(也就不用再去寻找锁对象),结束后调用方法解锁。

此时验证执行结果正确

通过一个详细例子引入重入锁ReentrantLock、ReadWriteLock读写锁

package com.company.lock;

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

public class MyLockDemo {
    static int i = 0;

    public static void main(String[] args) throws InterruptedException {
        //构建锁对象
        Lock lock = new ReentrantLock();
        //传入锁对象
        new Thread(new Add(lock)).start();
        new Thread(new Add(lock)).start();

        Thread.sleep(3000);
        System.out.println(i);
    }
}

class Add implements Runnable {
    //通过有参构造接收锁对象
    private Lock lock;

    public Add(Lock lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        //加锁
        lock.lock();
        for (int i = 0; i < 100000; i++) {
            MyLockDemo.i++;
        }
        //解锁
        lock.unlock();
    }
}

有了重入锁,此时A和B 类如果想要内部互斥,外部不影响,那么分别创建自己的锁就行。

class A {
    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        new Thread(new Add(lock)).start();
        new Thread(new Add(lock)).start();
    }
}

class B {
    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        new Thread(new Add(lock)).start();
        new Thread(new Add(lock)).start();
    }
}

ReentrantLock - 重入锁:当线程将锁资源释放之后,其他线程依然可以抢占这个锁资源来继续使用;

非重入锁 - 当线程将锁资源释放之后,这个锁就不能被再次使用(Lock中只提供了重入,没有提供非重入)

读写锁的添加,需要根据场景进行判断添加,如下面的例子,是要进行数据求和,所以添加写锁

package com.company.lock;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class MyLockDemo {
    static int i = 0;

    public static void main(String[] args) throws InterruptedException {
        //获取一个写锁
        ReadWriteLock rwl = new ReentrantReadWriteLock();
        Lock lock = rwl.writeLock();
        //传入锁对象
        new Thread(new Add(lock)).start();
        new Thread(new Add(lock)).start();

        Thread.sleep(3000);
        System.out.println(i);
    }
}

class Add implements Runnable {
    //通过有参构造接收锁对象
    private Lock lock;

    public Add(Lock lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        //加锁
        lock.lock();
        for (int i = 0; i < 100000; i++) {
            MyLockDemo.i++;
        }
        //解锁
        lock.unlock();
    }
}

运行结果正常

通过一个详细例子引入重入锁ReentrantLock、ReadWriteLock读写锁

查阅ReentrantReadWriteLock的源码,可以看到里面的描述提到了公平策略

通过一个详细例子引入重入锁ReentrantLock、ReadWriteLock读写锁

读写锁和公平、非公平策略归纳如下:

ReadWriteLock - 读写锁

  • 读锁:允许多个线程同时读但是不允许线程写
  • 写锁:只允许1个线程写但是不允许线程读

公平和非公平策略

  • 在资源有限的情况下,每一个线程实际抢占执行的次数并不均等,这种方式称之为非公平策略
  • 在公平策略的前提下,线程并不是直接去抢占资源而是抢占入队顺序。在这种策略下,每一个线程的执行次数是大致相等
  • 相对而言,非公平策略的效率更高一些
  • 如果不指定,则默认使用非公平策略
  • synchronized也是非公平的

 

相关标签: 高并发 Lock锁