Lock锁接口实现

上次主要说的就是锁,锁的应用,梳理了锁的原理,其实你会发现,没有太多技巧性的内容点,这个不容易看明白,先明白原理在看代码。这个更方便理解。学习的过程就是:采坑,入坑,回头看看风景不错。本次主要围绕JUC开发,里面有很多类。

(一)Lock的核心API

  • ① 介绍

LOCK也提供同步的特性,但是为什么必须使用锁,想比较同步而言,它的灵活性更加高,提供了一系列的API。同步关键字synchronized,要么是执行完,如果没执行完的话需要一直等待。Lock类里面有个trylock() ,就是试一下,如果拿到拿不到锁,就立刻返回,还可以设置等待时间,lock.tryLock(10000L, TimeUnit.MILLISECONDS)。使用它的主要原因,就是灵活。并不是因为性能高。

  • ② 常用方法
    1.lock

获取锁的方法,若锁被其他线程获取,则等待(阻塞)

2.lockInterruptbly

在锁的获取过程中可以中断当前线程

3.tryLock

尝试非阻塞地获取锁,立即返回

4.unlock

释放锁

  • ③ 代码演示
import java.util.concurrent.locks.ReentrantLock;

//  演示可重入
public class ReentrantDemo1 {
    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        lock.lock();  // block until condition holds
        try {
            System.out.println("第一次获取锁");
            System.out.println("当前线程获取锁的次数" + lock.getHoldCount());
            lock.lock();
            System.out.println("第二次获取锁了");
            System.out.println("当前线程获取锁的次数" + lock.getHoldCount());
        } finally {
            lock.unlock();
            lock.unlock();
        }
        System.out.println("当前线程获取锁的次数" + lock.getHoldCount());

        // 如果不释放,此时其他线程是拿不到锁的
        new Thread(() -> {
            System.out.println(Thread.currentThread() + " 期望抢到锁");
            lock.lock();
            System.out.println(Thread.currentThread() + " 线程拿到了锁");
        }).start();


    }
}

第一个线程拿到锁,第二个线程执行要做的下面的事情,如果第一个线程不释放锁的话,其他线程是拿不到锁的,可重入就是同一把锁被锁了多次,都释放后这个锁就没被使用了,可重入是之同一个线程的意思。

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

// 可响应中断
public class LockInterruptiblyDemo1 {
    private Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        LockInterruptiblyDemo1 demo1 = new LockInterruptiblyDemo1();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    demo1.test(Thread.currentThread());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        Thread.sleep(500); // 等待0.5秒,让thread1先执行

        thread2.start();
        Thread.sleep(2000); // 两秒后,中断thread2

        thread2.interrupt();
    }

    public void test(Thread thread) throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + ", 想获取锁");
        lock.lockInterruptibly();   //注意,如果需要正确中断等待锁的线程,必须将获取锁放在外面,然后将InterruptedException抛出
        try {
            System.out.println(thread.getName() + "得到了锁");
            Thread.sleep(10000); // 抢到锁,10秒不释放
        } finally {
            System.out.println(Thread.currentThread().getName() + "执行finally");
            lock.unlock();
            System.out.println(thread.getName() + "释放了锁");
        }
    }
}

响应中断的lockInterruptibly,就是响应了不继续等待。

(二) 读写锁的实现

  • ① 介绍

维护一对关联锁,一个用于只读操作,一个用于写入,读锁可以由多个读线程同时持有,写锁是排他的。适合读取线程比写入线程多的场景,改进互斥锁的性能。

  • ② 场景

缓存组件,集合的并发线程安全性改造。

  • ③ 示例

不用读写锁

public class ReentrantReadWriteLockDemo1 {
    public static void main(String[] args)  {
        final ReentrantReadWriteLockDemo1 readWriteLockDemo1 = new ReentrantReadWriteLockDemo1();
        // 多线程同时读/写
        new Thread(() -> {
            readWriteLockDemo1.read(Thread.currentThread());
        }).start();

        new Thread(() -> {
            readWriteLockDemo1.write(Thread.currentThread());
        }).start();

        new Thread(() -> {
            readWriteLockDemo1.read(Thread.currentThread());
        }).start();
    }

    // 不管读写,只有一个线程能用, 独享锁
    public synchronized void read(Thread thread) { // 2秒
        long start = System.currentTimeMillis();
        while(System.currentTimeMillis() - start <= 1) {
            System.out.println(thread.getName()+"正在进行“读”操作");
        }
        System.out.println(thread.getName()+"“读”操作完毕");
    }

    /** 写 */
    public synchronized void write(Thread thread) {
        long start = System.currentTimeMillis();
        while(System.currentTimeMillis() - start <= 1) {
            System.out.println(thread.getName()+"正在进行“写”操作");
        }
        System.out.println(thread.getName()+"“写”操作完毕");
    }
}

通过同步代码块来完成, 独占的话,性能问题,原本修改数据加锁还算可以,但是读数据你也加锁,就说不过去了,线程1-2 就是读数据。一般都是读多写少,为了偶然的一次写导致加锁性能消耗这是不应该的。

读写锁(既保证了读数据的效率,也保证数据的一致性),一个读锁,一个写锁。

看了这个图 应该基本明白了吧

import java.util.concurrent.locks.ReentrantReadWriteLock;

// 读写锁(既保证了读数据的效率,也保证数据的一致性)
public class ReentrantReadWriteLockDemo2 {
    ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    public static void main(String[] args) {
        final ReentrantReadWriteLockDemo2 readWriteLockDemo2 = new ReentrantReadWriteLockDemo2();
        // 多线程同时读/写
        new Thread(() -> {
            readWriteLockDemo2.read(Thread.currentThread());
        }).start();

        new Thread(() -> {
            readWriteLockDemo2.read(Thread.currentThread());
        }).start();

        new Thread(() -> {
            readWriteLockDemo2.write(Thread.currentThread());
        }).start();
    }

    // 多线程读,共享锁
    public void read(Thread thread) {
        readWriteLock.readLock().lock();
        try {
            long start = System.currentTimeMillis();
            while (System.currentTimeMillis() - start <= 1) {
                System.out.println(thread.getName() + "正在进行“读”操作");
            }
            System.out.println(thread.getName() + "“读”操作完毕");
        } finally {
            readWriteLock.readLock().unlock();
        }
    }

    /**
     * 写
     */
    public void write(Thread thread) {
        readWriteLock.writeLock().lock();
        try {
            long start = System.currentTimeMillis();
            while (System.currentTimeMillis() - start <= 1) {
                System.out.println(thread.getName() + "正在进行“写”操作");
            }
            System.out.println(thread.getName() + "“写”操作完毕");
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }
}

提供了writeLock 和 readLock ,一个读锁一个写锁,读锁可以共享的。写锁是独立的。当读锁在被多个线程调用的时候,写锁不允许写入数据。

  • ④ 锁降级

锁降级 : 是指保持住当前的写锁(已拥有),再获取读锁,随后释放写锁的过程。
首先写锁是独占的,读锁是共享的,然后读写锁是线程间互斥的,锁降级的前提是所有线程都希望对数据变化敏感,但是因为写锁只有一个,所以会发生降级。
如果先释放写锁,再获取读锁,可能在获取之前,会有其他线程获取到写锁,阻塞读锁的获取,就无法感知数据变化了。所以需要先hold住写锁,保证数据无变化,获取读锁,然后再释放写锁。 写可以到读是降级,读不可以到写

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

// 缓存示例
public class CacheDataDemo {
    // 创建一个map用于缓存
    private Map<String, Object> map = new HashMap<>();
    private static ReadWriteLock rwl = new ReentrantReadWriteLock();

    public static void main(String[] args) {
        // 1 读取缓存里面的数据
        // cache.query()
        // 2 如果换成没数据,则取数据库里面查询  database.query()
        // 3 查询完成之后,数据塞到塞到缓存里面 cache.put(data)
    }

    public Object get(String id) {
        Object value = null;
        // 首先开启读锁,从缓存中去取
        rwl.readLock().lock();
        try {
            if (map.get(id) == null) {
                // TODO database.query();  全部查询数据库 ,缓存雪崩
                // 必须释放读锁
                rwl.readLock().unlock();
                // 如果缓存中没有释放读锁,上写锁。如果不加锁,所有请求全部去查询数据库,就崩溃了
                rwl.writeLock().lock(); // 所有线程在此处等待  1000  1  999 (在同步代码里面再次检查是否缓存)
                try {
                    // 双重检查,防止已经有线程改变了当前的值,从而出现重复处理的情况
                    if (map.get(id) == null) {
                        // TODO value = ...如果缓存没有,就去数据库里面读取
                    }
                    rwl.readLock().lock(); // 加读锁降级写锁,这样就不会有其他线程能够改这个值,保证了数据一致性
                } finally {
                    rwl.writeLock().unlock(); // 释放写锁@
                }
            }
        } finally {
            rwl.readLock().unlock();
        }
        return value;
    }
}

(三)同步锁的本质-排队

1.同步的方式

独享锁-单个队列窗口,共享锁-多个队列窗口

2.强锁的方式

插队抢(不公平锁),先来后到抢锁(公平锁)

3.没抢到锁的处理方式

快速尝试多次(CAS自旋锁)阻塞等待

  1. 唤醒阻塞线程的方式(叫号器)

全部通知,通知下一个

(四)AQS抽象队列同步器

  • ① 介绍

对资源占用,释放,线程的等待,唤醒等等接口和具体实现
AQS是AbustactQueuedSynchronizer的简称,它是一个Java提供的底层同步工具类,用一个int类型的变量表示同步状态,并提供了一系列的CAS操作来管理这个同步状态。AQS的主要作用是为Java中的并发同步组件提供统一的底层支持,例如ReentrantLock,CountdowLatch就是基于AQS实现的,用法是通过继承AQS实现其模版方法,然后将子类作为同步组件的内部类。

  • ② AQS可重写的方法
  • ③ 在实现同步组件时AQS提供的模板方法
image
  • ④ AQS提供的模板方法
  1. 独占式获取与释放同步状态。
  2. 共享式获取与释放同步状态。
  3. 查询同步队列中等待线程情况。

同步组件通过AQS提供的模板方法实现自己的同步语义。

  • ⑤ 八个方法
    1.acquire,acquireShared

定义了资源争用的逻辑,如果没拿到,则等待。

2.tryAcquire,tryAcquireShared

实际直行占用资源的操作,如何判定一个由使用者具体去实现。

3.release,releaseShared

释放资源的逻辑,释放之后,通知后续节点进行争抢。

4.tryRelease,tryReleaseShared

实际执行资源释放的操作,具体的AQS使用者来实现。

PS:AQS提供了三大功能:独占锁、共享锁、ConditionObject。子类在实现中,可以实现其一部分方法。其编程思想值得借鉴,通过超类实现基本的处理流程,将其中部分抽成未实现方法,默认抛出异常,由子类实现,这种解耦方式,最大化的减少了代码的重复,且便于子类在实现中个性化自己的处理逻辑。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容