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日

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值