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 可以实现公平和非公平锁。
什么是可重入锁?
可重入锁
就是当前线程获取到锁,并执行需要加锁的方法时,在方法内部再次的获取该锁是可以直接锁去,这种情况就是避免了自己把自己给锁死了。ReentrantLock
和 synchronized
都是可重入锁。
文字看不懂,可以直接看下面这段代码就明白了
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 的差异的知识。
参考
- Java 多线程编程核心技术
- https://www.jianshu.com/p/ba6f000e6c56
记录于2019年4月14日