专栏首页IT笔记分享Java多线程学习(六)——Lock的使用

Java多线程学习(六)——Lock的使用

锁是用于通过多个线程控制对共享资源的访问的工具。通常,锁提供对共享资源的独占访问:一次只能有一个线程可以获取锁,并且对共享资源的所有访问都要求首先获取锁。 但是,一些锁可能允许并发访问共享资源,如ReadWriteLock的读写锁。Java5之后并发包中新增了Lock接口以及相关实现类来实现锁功能。

虽然synchronized方法和语句的范围机制使得使用监视器锁更容易编程,并且有助于避免涉及锁的许多常见编程错误,但是有时您需要以更灵活的方式处理锁。例如,用于遍历并发访问的数据结构的一些算法需要使用“手动”或“链锁定”:您获取节点A的锁定,然后获取节点B,然后释放A并获取C,然后释放B并获得D等。在这种场景中synchronized关键字就不那么容易实现了,使用Lock接口容易很多。

Lock接口的特性

  • 尝试非阻塞地获取锁:当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁;
  • 能被中断地获取锁:获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放;
  • 超时获取锁:在指定的截止时间之前获取锁, 超过截止时间后仍旧无法获取则返回。

ReentrantLock

ReentrantLock实现了Lock接口,并提供和synchronized相同的互斥性和内存可见性,与synchronized相比,ReentrantLock也有进入/退出同步代码块相同的内存语义,也同样的提供了可重入加锁语义。ReentrantLock还为处理锁的不可用性问题提供更高的灵活性。

public class LockTest {    private Lock lock = new ReentrantLock();
    public void test(){        lock.lock();        try {            for (int i = 0; i < 5; i++) {                System.out.println("ThreadName=" + Thread.currentThread().getName() + (" " + (i + 1)));            }        }finally {            lock.unlock();        }    }}public class Main {
    public static void main(String[] args) {        LockTest lockTest = new LockTest();
        for (int i=0; i<5; i++){            new Thread(lockTest::test, "Thread-"+i).start();        }    }}

Condition实现等待/通知机制

synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。

在使用notify/notifyAll()方法进行通知时,被通知的线程是有JVM选择的,使用ReentrantLock类结合Condition实例可以实现“选择性通知”,这个功能非常重要,而且是Condition接口默认提供的。

而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。

import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;
/** * @author xiaosen * @date 2019/6/24 8:20 * @description */public class Myservice {    private Lock lock = new ReentrantLock();    public Condition condition = lock.newCondition();
    public void await() {        lock.lock();        try {            System.out.println(" await时间为" + System.currentTimeMillis());            condition.await();            System.out.println("这是condition.await()方法之后的语句,condition.signal()方法之后我才被执行");        } catch (InterruptedException e) {            e.printStackTrace();        } finally {            lock.unlock();        }    }
    public void signal() throws InterruptedException {        lock.lock();        try {            System.out.println("signal时间为" + System.currentTimeMillis());            condition.signal();            Thread.sleep(3000);            System.out.println("这是condition.signal()方法之后的语句");        } finally {            lock.unlock();        }    }
}
public class MyserviceTest {
    public static void main(String[] args) throws InterruptedException{        Myservice myservice = new Myservice();        new Thread(() -> myservice.await()).start();        Thread.sleep(3000);        myservice.signal();    }}
// 输出结果 await时间为1561335861608signal时间为1561335864608这是condition.signal()方法之后的语句这是condition.await()方法之后的语句,condition.signal()方法之后我才被执行

在使用wait/notify实现等待通知机制的时候我们知道必须执行完notify()方法所在的synchronized代码块后才释放锁。在这里也差不多,必须执行完signal所在的try语句块之后才释放锁,condition.await()后的语句才能被执行。

注意:必须在condition.await()方法调用之前调用lock.lock()代码获得同步监视器,不然会报错。

多个Condition实现等待/通知机制

import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;
/** * @author xiaosen * @date 2019/6/24 8:29 * @description */public class MoreConditionNotify {
    private Lock lock = new ReentrantLock();    public Condition conditionA = lock.newCondition();    public Condition conditionB = lock.newCondition();
    public void awaitA() {        lock.lock();        try {            System.out.println("begin awaitA时间为" + System.currentTimeMillis()                    + " ThreadName=" + Thread.currentThread().getName());            conditionA.await();            System.out.println("  end awaitA时间为" + System.currentTimeMillis()                    + " ThreadName=" + Thread.currentThread().getName());        } catch (InterruptedException e) {            e.printStackTrace();        } finally {            lock.unlock();        }    }
    public void awaitB() {        lock.lock();        try {            System.out.println("begin awaitB时间为" + System.currentTimeMillis()                    + " ThreadName=" + Thread.currentThread().getName());            conditionB.await();            System.out.println("  end awaitB时间为" + System.currentTimeMillis()                    + " ThreadName=" + Thread.currentThread().getName());        } catch (InterruptedException e) {            e.printStackTrace();        } finally {            lock.unlock();        }    }
    public void signalAll_A() {        lock.lock();        try {            System.out.println("  signalAll_A时间为" + System.currentTimeMillis()                    + " ThreadName=" + Thread.currentThread().getName());            conditionA.signalAll();        } finally {            lock.unlock();        }    }
    public void signalAll_B() {        lock.lock();        try {            System.out.println("  signalAll_B时间为" + System.currentTimeMillis()                    + " ThreadName=" + Thread.currentThread().getName());            conditionB.signalAll();        } finally {            lock.unlock();        }    }}

public class MoreConditionNotifyMain {
    public static void main(String[] args) throws InterruptedException{        MoreConditionNotify conditionNotify = new MoreConditionNotify();        new Thread(() -> conditionNotify.awaitA(), "A").start();        new Thread(() -> conditionNotify.awaitB(), "B").start();        Thread.sleep(3000);        conditionNotify.signalAll_A();    }}
// 输出begin awaitA时间为1561336446748 ThreadName=Abegin awaitB时间为1561336446748 ThreadName=B  signalAll_A时间为1561336449748 ThreadName=main  end awaitA时间为1561336449748 ThreadName=A

此时线程一直处于挂起状态,只有A线程被唤醒了。

实现生产者/消费者模式:一对一交替打印

package alternately;
import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.ReentrantLock;
/** * @author xiaosen * @date 2019/6/24 8:40 * @description */public class ConditionService {    private ReentrantLock lock = new ReentrantLock();    private Condition condition = lock.newCondition();    private boolean hasValue = false;
    public void set(){        try {            lock.lock();            while (hasValue == true){                condition.await();            }            System.out.println("打印☆");            hasValue = true;            condition.signal();        }catch (InterruptedException e){            e.printStackTrace();        }finally {            lock.unlock();        }    }
    public void get(){        try {            lock.lock();            while (hasValue == false){                condition.await();            }            System.out.println("打印★");            hasValue = false;            condition.signal();        }catch (InterruptedException e){            e.printStackTrace();        }finally {            lock.unlock();        }    }
}
public static void main(String[] args){        ConditionService service = new ConditionService();        new Thread(() -> {            for (int i=0; i<100; i++){                service.set();            }        }).start();        new Thread(() -> {            for (int i=0; i<100; i++){                service.get();            }        }).start();
    }
// 输出打印☆打印★打印☆打印★打印☆打印★打印☆。。。

公平锁与非公平锁

Lock锁分为:公平锁 和 非公平锁。公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO先进先出顺序。而非公平锁就是一种获取锁的抢占机制,是随机获取锁的,和公平锁不一样的就是先来的不一定先得到锁,这样可能造成某些线程一直拿不到锁,结果也就是不公平的了。

ReentrantReadWriteLock

ReentrantLock(排他锁)具有完全互斥排他的效果,即同一时刻只允许一个线程访问,这样做虽然虽然保证了实例变量的线程安全性,但效率非常低下。ReadWriteLock接口的实现类-ReentrantReadWriteLock读写锁就是为了解决这个问题。

读写锁维护了两个锁,一个是读操作相关的锁也成为共享锁,一个是写操作相关的锁 也称为排他锁。通过分离读锁和写锁,其并发性比一般排他锁有了很大提升。

多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥(只要出现写操作的过程就是互斥的。)。在没有线程Thread进行写入操作时,进行读取操作的多个Thread都可以获取读锁,而进行写入操作的Thread只有在获取写锁后才能进行写入操作。即多个Thread可以同时进行读取操作,但是同一时刻只允许一个Thread进行写入操作。

Github: https://github.com/lgsxiaosen/notes-code/tree/master/lock-test

本文分享自微信公众号 - IT笔记分享(xiaosen_javashare),作者:xiaosen L

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-09-10

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java8新增方法使用

    Java8允许我们使用default关键字为接口添加非抽象的方法。这个特点也被称为扩展方法,下面是例子:

    小森啦啦啦
  • SpringBoot连接Elasticsearch实战总结

    第一次使用elasticsearch,于是从网上找轮子复制粘贴。早好轮子测试完毕,上线。可是几天下来发现接口响应时间一直都偏高(默认的超时时间是500ms),所...

    小森啦啦啦
  • Java多线程学习(三)——synchronized(上)

    在前两节的《Java多线程学习(一)——多线程基础》和《Java多线程学习(二)——Thread类的方法介绍》中我们接触了线程安全和非线程安全的概念,这节就来学...

    小森啦啦啦
  • asp.net中的比较完美的验证码

    本文转载:http://blog.csdn.net/zjk20108023/article/details/7836174

    跟着阿笨一起玩NET
  • POJ 刷题系列:2151. Check the difficulty of problems

    版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/...

    用户1147447
  • 线程篇2:[- sleep、wait、notify、join、yield -]

    张风捷特烈
  • 《Monkey Java》课程3.4之练习课

    GitOPEN
  • 3274. Gold Balanced Lineup

    所以:求每一位置的累加和,可以快速求得任意区间内某个属性的个数,一个暴力的做法是遍历每个区间以及每个位置,判断每个属性个数是否相等。

    用户1147447
  • 【蓝桥杯】ALGO-11 瓷砖铺放

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

    喜欢ctrl的cxk
  • Java 基础-LocalDate相关

    那么在写具体的LocalDate前,我们先来看下为什么要在Java8中搞一套新的API呢,因为旧的Date类非常的难用,比如,其中的几个构造方法都被标注为@De...

    haoming1100

扫码关注云+社区

领取腾讯云代金券