用真挚的钥匙打开Java多线程之锁,从此Java中有我,我中有Java!


 关于锁的概念,需要读者首先对线程安全、死锁有一定的了解,在此前提下会对本篇文章有更加深入的了解,并能够很好的与 synchronized比较,进而灵活的使用;因此,附上几篇链接,望对您有帮助!


锁 Lock

简述

JDK5加入,与synchronized比较,显示定义,结构更灵活;

  • synchronized方法中锁的获取和释放都是不可预见的,而Lock是通过调用方法实现的,是显性的;
  • Lock实现提供比使用synchronized方法和语句可以获得的更广泛的锁定操作。 它们允许更灵活的结构化,可能具有完全不同的属性,并且可以支持多个相关联的对象Condition

方法

方法 描述
void lock() 获得锁。
void lockInterruptibly() 获取锁定,除非当前线程是 interrupted 。
Condition newCondition() 返回一个新Condition绑定到该实例Lock实例。
boolean tryLock() 只有在调用时锁为空闲状态才可以获得锁。
boolean tryLock(long time, TimeUnit unit) 如果在给定的等待时间内是空闲的,并且当前的线程尚未得到 而被中断,则获取该锁。
void unlock() 释放锁。

 相比之前而言,这里有一个比较有趣的方法就是tryLock();在没使用这个方法之前,通常是下面的情形:
 一般而言,对于线程来说,当访问临界资源对象而未拿到锁标记时,会进入阻塞状态,一直等待该锁标记释放为止;
在这里插入图片描述
 而对于tryLock()而言,在线程访问临界资源对象时会尝试去拿锁标记,拿到锁标记返回ture,并完整整个线程的执行;而拿不到锁标记返回false,并不进入阻塞状态,线程可以选择去做其他事情,过一个周期或几个周期再来访问,直到临界资源的时间片被释放为止;
 这个可以类比海底捞排号,你去海底捞吃饭,很多时候都需要排队,而排队并不是传统的排队,而是按照先来后到进行排号,用户拿到排号不必一直原地等待,可以在此期间去购物啊等做其他事情;
在这里插入图片描述
在这里插入图片描述

重入锁 ReentrantLock

简述

 **ReentrantLock:**Lock接口实现类,一个可重入互斥锁具有与使用synchronized方法和语句访问的隐式监视锁相同的基本行为和语义,但具有扩展功能。

使用:

详戳:深入理解Java线程安全——银行欠我400万!!!
 之前在此链接的代码案例中,没处理之前线程是不安全的,而当时的做法时通过对银行这个临界资源对象加锁(使用synchronized)解决,同样地,使用Lock来解决线程安全问题。格式如下:

  • 使用synchronized
synchronized (临界资源对象){ //对临界资源加锁
	//代码(原子操作)
}
  • 使用lock
    //创建一个重入锁对象
    Lock locker = new ReentrantLock();
     //开启锁
    locker.lock();
   		 //代码(原子操作)
    //释放锁
	locker.unlock();

 若代码中可能抛异常,为了是程序不因抛异常而导致释放锁无法,因此会用到try finally结构,使得程序再抛异常之前先把锁资源释放掉,这也是finally的特点和优势;

    //创建一个重入锁对象
    Lock locker = new ReentrantLock();
 	locker.lock();
	try{
		//可能出现异常的代码
	}finally{
	locker.unlock();
	//无论是否出现异常,都需要 执行的代码结构,用于释放锁资源
}

读写锁 ReentrantReadWriteLock

简述

 A ReadWriteLock维护一对关联的locks ,一个用于只读操作,一个用于写入。 read lock可以由多个阅读器线程同时进行,只要没有writer。 write lock是独占的。

ReentrantReadWriteLock:

  • 一种支持一写多读的同步锁。读写分离。可分别分配读锁、写锁。
  • 支持多次分配读锁。使多个读操作可以并发执行。

读写锁与互斥锁区别

 显然,读写锁有多把锁,一把锁用于分配给读线程,另一把分配给写线程,为什么要这样呢?原因在与读操作并不会改变数据,在一个读操作大于写操作的场合下,由于读操作并不会改变数据,多个线程并发执行并不会导致数据的不一致,因此读锁就不会产生互斥;而写操作会改变数据,因此写锁是互斥的;如此细分,读写锁在读操作大于写操作的环境中,在保证线程安全的情况下,读写锁效率远远高于互斥锁。

读写互斥规则

  • 写-写:互斥,阻塞;
  • 读-写:互斥,读阻塞写、写阻塞读
  • 读-读:不互斥、不阻塞

方法

方法 描述
Lock readLock() 返回用于阅读的锁。
Lock writeLock() 返回用于写入的锁。

读写锁代码案例

模拟读写操作

 新建一个Dog类,该对象有一个属性时value,创建setter、getter方法表示读写操作;使用读写锁,首先要创建读写锁,然后从ReentrantReadWriteLock分别拿出写锁和读锁,在set和get方法中使用,每个锁的使用都必须有开启锁和释放锁的两个步骤,且释放锁资源必须放在finally中,以保证程序的执行后释放锁资源;
 在使用读写锁加入Thread.sleep(1000);是线程执行睡眠,是因为线程执行的速度是很快的,依次减慢线程的执行速度(设定线程执行为1s),更容易看出其执行效率;

class Dog{
    String value;

    //创建读写锁
    ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    //内部类对象
    ReadLock r1 =  rwl.readLock();
    WriteLock w1 =  rwl.writeLock();
    //读
    public String getValue() {

        r1.lock();
        try {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return value;
        } finally {
            r1.unlock();
        }
    }
    //写
    public void setValue(String value) {
        w1.lock();
        try {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.value = value;
        } finally {
            w1.unlock();
        }
    }
}

在这里插入图片描述

创建读写任务

 手首先通过Callable接口创建两个任务即readTaskwriteTask,然后创建一个线程池,随即记录当前系统时间,然后依次分别循环执行读写任务,这里读任务是18次,写任务是2次;执行完毕后完毕当前线程池并记录当前系统时间,打印开始和结束时间,即得到执行时间,通过执行时间的快慢来衡量效率;

在这里插入图片描述

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

        final Dog dog = new Dog();

        Callable<Object> writeTask = new Callable<Object>() {
            @Override
            public Object call() throws Exception {
                dog.setValue("hello");
                return null;
            }
        };
        Callable<Object> readTask = new Callable<Object>() {
            @Override
            public Object call() throws Exception {
                dog.getValue();
                return null;
            }
        };
        //定义线程池
        ExecutorService executorService = Executors.newFixedThreadPool(20);

        //开始时间
        long start = System.currentTimeMillis();

        //写操作提交2次
        for (int i = 0; i <2 ; i++) {
            executorService.submit(writeTask);
        }
        //读操作提交18次
        for (int i = 0; i <18; i++) {
            executorService.submit(readTask);
        }
        //停止线程池,(不再接受新任务,将现有的任务全部执行完毕)
        executorService.shutdown();

        while(true){
            System.out.println("结束了吗?");
            if(executorService.isTerminated())
                break;
        }
        //结束时间
        long end = System.currentTimeMillis();
        System.out.println(end-start);
    }
}

执行结果

在这里插入图片描述

总结分析

程序的执行时间是4s左右,说明了什么?

  • 这说明对于读写操作来说,写锁是互斥的,不能并发执行,而读锁不是互斥的可以并发执行,因此对于互斥锁来说,不存在读写之别,因为互斥,所以都不能并发,相比于此,读写锁能够大大提高效率。
发布了86 篇原创文章 · 获赞 229 · 访问量 14万+

猜你喜欢

转载自blog.csdn.net/qq_44717317/article/details/104948595
今日推荐