上一章看这里:JAVA并发编程-3-原子操作CAS和原子类
一、Lock接口及其核心方法
JAVA从1.5开始新增了Lock接口,这里不得不提一位java并发大师Doug Lea,大家可以发现JUC包下很多类都是来自这位大神的编码,是当之无愧的JAVA并发大师,他现任纽约州立大学教授。
Lock接口中的方法如下:
public interface Lock {
void lock();
/**
* 尝试加锁
**/
boolean tryLock();
/**
* 可中断锁
**/
void lockInterruptibly() throws InterruptedException;
/**
* 超时尝试加锁
**/
boolean tryLock(long time, TimeUnit unit) throws InterruptedException
void unlock();
Condition newCondition();
}
从上面我们可以看到Lock相比synchronized关键字提供了更加丰富的方法来支持加锁,主要包括:
- 可以被中断 lockInterruptibly()
- 可以尝试获取 tryLock()
- 可以超时获取 tryLock(long time, TimeUnit unit)
- 读多写少时可用支持读写锁
上面几条是synchronized所不能完成的,这也是Lock存在的意义,而synchronized的使用是比较简洁的。所以除非我们有以上几点的需求,否则应该优先选用synchronized关键字来加锁。
synchronized关键字的性能也是随着java语言的版本迭代不断被优化,所以普通的加锁场景synchronized的性能或许更好。
二、可重入锁ReentrantLock
java为我们提供了Lock接口的一个重要实现ReentrantLock可重入锁。
所谓可重入锁,就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。换言之就是可以获取锁的多次。
比如,递归获取同一把锁
public synchronized void change() {
i = 1;
change();
}
或者调用其它方法
public synchronized void change() {
i = 1;
changeSingle();
}
public synchronized void changeSingle() {
i = 1;
}
synchronized和ReentrantLock都是典型的可重入锁。可重入锁每进入一次锁会有计数器将加锁次数加1,解锁后将锁的次数减1。
对于Lock的使用,请一定要在finally中调用unlock方法,以确保一定会解锁。
if (lock.tryLock()) {
try {
// manipulate protected state
} finally {
lock.unlock();
}
简单的demo:
public class LockDemo {
private Lock lock = new ReentrantLock();
private int count;
public void increament() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
三、锁的公平和非公平
所谓锁的公平和非公平,面试的时候经常会被问到
如果在时间上,先对锁进行获取的请求,一定先被满足,这个锁就是公平的,不满足,就是非公平的。
就是一定会保证先来拿锁的等待线程先获得锁,而非公平锁不会保证这一点。
ReentrantLock的构造方法中提供了是否是公平锁参数:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
值得一提的是,通常认为非公平锁的效率是要比公平锁高的。假设a线程获取到了锁,b线程在等待,a线程释放锁的时候恰好c线程过来拿锁,那么在非公平锁的情况下就极有可能让c线程拿到锁,让b线程继续等待,因为这样总体的开销最小,效率最高。
四、读写锁ReadWriteLock
锁有排他锁和共享锁之分。
- 排他锁:无论如何只能有一个线程拿到锁,ReentrantLock,synchronized
- 共享锁:允许多个相同业务的线程拿到同一把锁,读写锁就是同一时刻允许多个读线程同时访问
读写锁就是同一时刻允许多个读线程同时访问,但是写线程访问时,所有的读和写都被阻塞,最适合读多写少的情况。
读写锁实际上是通过提供读锁和写锁两把锁来实现的。JAVA中提供了ReadWriteLock读写锁接口,并且提供了一个实现类ReentrantReadWriteLock,下图中可看到定义了两把锁ReadLock和WriteLock,两把锁分别实现了Lock接口
通过一个简单的雷子来看下读写锁和synchronized的性能比较:
定义一个商品信息类:
public class GoodsInfo {
private final String name;
private double totalMoney;//总销售额
private int storeNumber;//库存数
public GoodsInfo(String name, int totalMoney, int storeNumber) {
this.name = name;
this.totalMoney = totalMoney;
this.storeNumber = storeNumber;
}
public double getTotalMoney() {
return totalMoney;
}
public int getStoreNumber() {
return storeNumber;
}
public void changeNumber(int sellNumber) {
this.totalMoney += sellNumber * 25;
this.storeNumber -= sellNumber;
}
}
一个商品的服务的接口:
public interface GoodsService {
GoodsInfo getNum();//获得商品的信息
void setNum(int number);//设置商品的数量
}
synchronized的实现:
public class UseSyn implements GoodsService {
private GoodsInfo goodsInfo;
public UseSyn(GoodsInfo goodsInfo) {
this.goodsInfo = goodsInfo;
}
@Override
public synchronized GoodsInfo getNum() {
SleepTools.ms(5);
return this.goodsInfo;
}
@Override
public synchronized void setNum(int number) {
SleepTools.ms(5);
goodsInfo.changeNumber(number);
}
}
读写锁实现:
public class UseRwLock implements GoodsService {
private GoodsInfo goodsInfo;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock getLock = lock.readLock();//读锁
private final Lock setLock = lock.writeLock();//写锁
public UseRwLock(GoodsInfo goodsInfo) {
this.goodsInfo = goodsInfo;
}
@Override
public GoodsInfo getNum() {
getLock.lock();
try {
SleepTools.ms(5);
return this.goodsInfo;
}finally {
getLock.unlock();
}
}
@Override
public void setNum(int number) {
setLock.lock();
try {
SleepTools.ms(5);
goodsInfo.changeNumber(number);
}finally {
setLock.unlock();
}
}
}
测试类:
public class BusiApp {
static final int readWriteRatio = 10;//读写线程的比例
static final int minthreadCount = 3;//最少线程数
//读操作
private static class GetThread implements Runnable {
private GoodsService goodsService;
public GetThread(GoodsService goodsService) {
this.goodsService = goodsService;
}
@Override
public void run() {
long start = System.currentTimeMillis();
for (int i = 0; i < 100; i++) {//操作100次
goodsService.getNum();
}
System.out.println(Thread.currentThread().getName() + "读取商品数据耗时:" + (System.currentTimeMillis() - start) + "ms");
}
}
//写操做
private static class SetThread implements Runnable {
private GoodsService goodsService;
public SetThread(GoodsService goodsService) {
this.goodsService = goodsService;
}
@Override
public void run() {
long start = System.currentTimeMillis();
Random r = new Random();
for (int i = 0; i < 10; i++) {//操作10次
SleepTools.ms(50);
goodsService.setNum(r.nextInt(10));
}
System.out.println(Thread.currentThread().getName() + "写商品数据耗时:" + (System.currentTimeMillis() - start) + "ms---------");
}
}
public static void main(String[] args) throws InterruptedException {
GoodsInfo goodsInfo = new GoodsInfo("Cup", 100000, 10000);
GoodsService goodsService = new UseRwLock(goodsInfo);
// GoodsService goodsService = new UseSyn(goodsInfo);
for (int i = 0; i < minthreadCount; i++) {
Thread setT = new Thread(new SetThread(goodsService));
for (int j = 0; j < readWriteRatio; j++) {
Thread getT = new Thread(new GetThread(goodsService));
getT.start();
}
SleepTools.ms(100);
setT.start();
}
}
}
synchronized的执行结果:
读写锁执行结果:
由此可见,在读多写少的情况下,读写锁可以大大提高性能
五、等待通知机制Condition
与线程的wait()和notify()和synchronized相配合实现的等待和通知机制相对应,显示锁Lock中也实现了等待通知机制,就是通过Condition接口来实现。
await() 是为进入等待,对应Object中的wait
signal(),signalAll()对应notify()和notifyAll
而它们的使用同样遵守wait()和notify()的标准范式
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void dowait() {
lock.lock();
try {
//doSomething
condition.await();
} catch (Exception e) {
} finally {
lock.unlock();
}
}
public void donotify() {
lock.lock();
try {
//doSomething
condition.signal();
} catch (Exception e) {
} finally {
lock.unlock();
}
}
大家可以试着将第一章中的wait和notify的事例用Condition再实现一遍。
下一章看这里:JAVA并发编程-5-AQS的实现原理