Java 并发编程—线程间的共享和协作(二)

线程间的共享和协作

线程间的共享和协作

Lock 显示锁

在 Java 中,一般情况下都是使用 synchronized 关键字来加锁的, synchronized 这种机制一旦开始获取锁,是不能中断的,也没有提供尝试获取锁的功能。

JDK1.5 提供了 Lock 接口,开发人员显示地去操作锁的获取释放,因此被称为显式锁。并且提供了synchronized不提供的机制。

Lock API

  • 阻塞式获取锁
void lock();

这种方式与 synchronized 获取锁差不多,如果需要获取的锁被其他线程持有,那么将挂起阻塞等待其他线程释放锁。

  • 尝试非阻塞式地获取锁

非阻塞去尝试获取锁,如果获取到锁会返回 true,如果没有获取到锁,则返回 false。带有时间参数的方法表示开始获取锁时,如果此时该锁没有被其他线程持有,那么则返回 true,否则将进行超时等待,中途如果线程发生了中断信号,则 tryLock 方法抛出中断异常。

boolean tryLock();

//如果线程发出中断信息,则该方法会抛出 InterruptedException 异常。
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
  • 能被中断获取锁

请求锁,如果当前锁被其他线程持有,那么将阻塞等待,除非外界调用 thread.interrupt() 发出中断信号。

void lockInterruptibly() throws InterruptedException;
  • 释放锁

释放锁,必须是在 finally 块中执行,这样能保证锁能正常的被释放,避免造成死锁。

void unlock();

下面是 Lock 锁的标准写法:

finally 能确保锁能正常被释放,避免出现死锁的情况。


//阻塞式获取锁
lock.lock();

try{
    //对共享资源的操作
}finally{
    lock.unlock();
}

//获取一个可中断的锁
try{
    lock.lockInterruptibly();
}catch(InterruptedException e){
    //发生了异常,不应该往下执行,因为没有获取到锁就调用unlock 会抛出异常
    return;
}
try{
    //对共享资源的操作
}finally{
    lock.unlock();
}

ReentrantLock

ReetrantLock 从字面意思来看,它表示可重入锁,它实现了 Lock 接口,ReetrantLock 可以实现公平和非公平锁。

什么是可重入锁?

可重入锁就是当前线程获取到锁,并执行需要加锁的方法时,在方法内部再次的获取该锁是可以直接锁去,这种情况就是避免了自己把自己给锁死了。ReentrantLocksynchronized 都是可重入锁。

文字看不懂,可以直接看下面这段代码就明白了

synchronized(this){
    //do sth
    
    synchronized(this){//再次获取同一把锁,可以直接获取
        //do sth
    }
}

ReentrantLock实现公平锁和非公平锁

公平锁表示线程的执行顺序按照请求锁的顺序来执行,也就是先请求锁的线程优先获取锁。非公平锁则是由 CPU 负责线程去调度获取锁,不一定是按照先请求锁的线程优先获取锁。

  • 获取一个公平锁

非公平锁只要在构造参数参入 false 即可,ReentrantLock的 的缺省实现就是非公平锁。

Lock lock = new ReentrantLock(true);

那么这里就有一个疑问了,公平锁和非公平锁的效率哪个比较高呢?

当然是非公平锁, ReentrantLock 和 synchronized 内部的缺省实现都是非公平锁。因为线程B在请求锁时发现当前锁被其他线程A持有,那么线程B该发生上下文切换,将处于挂起,这时如果还有另外一个线程C过来请求锁,发现锁还是被其他线程A持有,那么线程C该发生上下文切换,处于挂起状态,此时如果线程 A 释放锁了,那么此时锁只能被线程B 获取,线程 C 只能等待到线程 B 执行完毕。恢复一个被挂起的线程与该线程真正开始执行之间存在严重的延迟

读写锁ReentrantReadWritLock

在上面讲到的 synchronized 和 ReentrantLock 都是独占锁,也就是也就说同一时刻只能有一个线程去访问。而这里提到的读写锁是在同一时刻,允许多个读线程去访问,但是在写线程访问时,所有其他读写线程都会被阻塞。ReentrantReadWritLock 内部维护了两个锁,分别为读锁(共享锁)和写锁(排他锁),它们内部都是实现Lock 接口。

  • 读读共享
  • 读写互斥
  • 写写互斥
  • 写读互斥

public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {
    
    private final ReentrantReadWriteLock.ReadLock readerLock;

    private final ReentrantReadWriteLock.WriteLock writerLock;
    
    public static class ReadLock implements Lock, java.io.Serializable {}

    public static class WriteLock implements Lock, java.io.Serializable {}

}

在一般情况下,读写锁的性能要比独占锁好,因为大多数场景下读的操作是大于写的,因此使用读写锁的性能是要比独占锁好的。

下面来演示一下读写互斥:

开启读线程内部睡眠10s,主线程sleep1s后开始开始写线程,执行结果如下:
读线程获取到锁:1555181819480
写线程获取到锁:1555181829482
写线程是在读线程执行完之后才能开始写的

public class ReadAndWriteLockDemo {


    static class Service {
        ReadWriteLock lock = new ReentrantReadWriteLock();

        public void read() {
            lock.writeLock().lock();

            try {
                System.out.println(Thread.currentThread().getName() + "获取到锁:" + System.currentTimeMillis());

                try {
                    Thread.sleep(10000);//睡眠10s
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } finally {
                lock.writeLock().unlock();
            }
        }

        public void write() {
            lock.writeLock().lock();

            try {
                System.out.println(Thread.currentThread().getName() + "获取到锁:" + System.currentTimeMillis());

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } finally {
                lock.writeLock().unlock();
            }
        }
    }


    static class ThreadA extends Thread {
        private final Service service;

        public ThreadA(String name, Service service) {
            super(name);
            this.service = service;

        }

        @Override
        public void run() {
            super.run();
            service.read();
        }
    }


    static class ThreadB extends Thread {
        private final Service service;

        public ThreadB(String name, Service service) {
            super(name);
            this.service = service;

        }

        @Override
        public void run() {
            super.run();
            service.write();
        }
    }


    public static void main(String[] args) {
        Service service = new Service();

        ThreadA readTthread = new ThreadA("读线程", service);
        ThreadB writeThread = new ThreadB("写线程", service);

        //先执行读线程,内部会 sleep 10s
        readTthread.start();


        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }

        //执行写线程
        writeThread.start();

    }
}

Condition接口

位于 java.util.concurrent juc 包下

在 synchronized 内置锁中,一般是使用对象的 wait() 和 notify() 实现等待通知机制,而在 Lock 显示锁中是使用 Condition 的 await 和 signal 实现等待通知机制。

Condition 对象的获取

private Lock lock = new ReentrantLock();
Condition xiaomi9Condition = lock.newCondition();

对比 Object 的 wait 和 notify

signal()/notify()
signalAll/notifyAll()
await()/wait()
await(long time, TimeUnit unit)/wait(long millis),wait(long var1, int var3)

使用 Lock 配合 Condition 实现等待通知机制

使用 Lock 配合 Condition 实现等待通知机制

在之前的使用 synchronized 配合 wait() 和 notify()实现老王买小米9的栗子Java 并发编程-线程间的共享和协作1,现在使用 Lock 配合 Condition 来改造这个栗子。

package com.example.waitAndnotify;

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

/*
Lock 显示锁实现的 等待/通知 机制
 */
public class Shop implements Runnable {

    private Lock lock = new ReentrantLock();

    //小米9条件
    private Condition xiaomi9Condition = lock.newCondition();

    private int xiaomi9Discount = 10;

    /*
    通知方法
     */
    public void depreciateXiaomi9(int discount) {
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + "收到总部通知,现在进行小米9打" + discount + "折活动,通知米粉们来买吧");
            xiaomi9Discount = discount;
            //通知所有客户:小米9打折了哦,赶紧去看看价格吧。
            xiaomi9Condition.signalAll();
            //通知一个客户
            //xiaomi9Condition.signal();
        } finally {
            lock.unlock();
        }


    }

    /*
    等待
     */
    public void getXiaomi9Price() {
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + "正在查询小米9价格");

            while (xiaomi9Discount > 8) {
                try {
                    System.out.println(Thread.currentThread().getName() + "发现小米9价格折扣为" + xiaomi9Discount + "太少,我要开始等待降价,老板,降价了,就通知我哦,开始等待...");
                    xiaomi9Condition.await();
                    System.out.println(Thread.currentThread().getName() + "收到通知:小米9搞活动,打折了哦,目前折扣为:" + xiaomi9Discount);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + "剁手买顶配小米9:" + xiaomi9Discount + "折购入");
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {

        Shop shop = new Shop();

        //老王想要买手机
        Thread getXiaomiPriceThread = new Thread(shop);
        //老张也要买手机
        Thread getXiaomiPriceThread2 = new Thread(shop);
        getXiaomiPriceThread.start();
        getXiaomiPriceThread2.start();
        Thread.sleep(1000);

        //降价了
        shop.depreciateXiaomi9(9);

        Thread.sleep(1000);
        //又降价了
        shop.depreciateXiaomi9(8);
    }


    @Override
    public void run() {
        getXiaomi9Price();
    }
}

输出结果

  • xiaomi9Condition.signal();
买手机栗子
  • xiaomi9Condition.signalAll();
买手机栗子

总结

本文是线程间的共享和协作的第二篇博客,本文主要对比学习了 Lock 和 synchronized 的差异的知识。

参考

记录于2019年4月14日

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

推荐阅读更多精彩内容