实现管程除了synchronized之外,java SDK 并发包中提供的ReentrantLock通过实现Lock以及Condition接口实现管程模型。其中Lock实现互斥,Condition实现同步。
一.Lock实现互斥,原子性可见性
1.为什么已经有synchronized锁还要重复造轮子呢?
jdk1.6之后由于锁升级的优化synchronized的效率提升了,为什么还需要ReentrantLock呢,因为两点:破坏不可抢占条件;以及多个等待队列。
死锁的四个条件:
a.互斥(由于使用的就是互斥锁,所以这个无法破坏)
b.占有且等待(可以一次性申请全部资源)
c.不可抢占(synchronized无法破坏,因为线程申请不到资源就会进入等待队列,无法获得cpu执行权)
d.循环等待(可以按照资源的优先级申请)
ReentrantLock,对于占有部分资源的线程进一步申请其他资源的时候,如果申请不到,可以主动释放,而不是进入等待队列。那么不可抢占条件也就被破坏了。因此提供了三个方法。
响应中断的API:lockInterruptibly()
支持超时的API:boolean tryLock(long var1, TimeUnit var3)
支持非阻塞的API:boolean tryLock()
上诉三个方法都是获取锁资源的时候,如果获取失败不进入等待队列,或者是进入等待队列的被中断的,从而主动的释放当前线程持有的资源。
2.如何实现互斥的
通过AQS,AQS中有一个volatile state锁状态标志,0是代表无人占有锁资源,ReentrantLock中大于等于1代表占有锁资源,并且设置锁状态的操作是原子性的,通过CAS,CAS操作是由CPU指令保证其原子性的,也就是同一时刻只有一个线程可以通过CAS占有锁资源,保证多线程之间的互斥性。
3.如何实现可见性的
class Test {
private final ReentrantLock lock = new ReentrantLock();
int value;
public void addOne() {
// 获取锁
lock .lock();
try {
value+=1;
} finally {
// 保证锁能释放
lock .unlock();
}
}
}
对于上诉的addOne()如何保证可见性的,可见性的分析一定是根据Java内存模型。因为AQS中的state是volatile的,并且在lock以及unlock的时候都会对state进行读写操作,由于程序的顺序性value的改变对于unlock中state是可见的,volatile的写操作对于读操作是可见的,最后根据内存模型的传递性可知,lock和unlock之间的临界区域中对于共享变量的操作是可见的。
4.ReentrantLock底层实现的原理
ReentrantLock的lock方法以及unlock的流程图
根据EMA根据管程的模型,需要等待队列以及锁资源
ReentrantLock底层使用双端队列(CLH)作为等待队列,volatile state的值代表锁资源,state=0时代表没有线程占用锁资源,>=1代表锁资源被占用。并且对于state的修改,调用Unsafe的CAS操作方法(直接操作内存,原子操作)。这样就可以保证获取锁的线程的安全性。
双端队列的添加和移除的Node也是同样的CAS操作保证等待线程的添加和删除的安全性。同时内部采用LockSupport的park以及unpark实现等待唤醒机制。
至于条件等待队列,是通过Condition结构实现,并且提供多个条件等待队列,更加的灵活。
上诉是从管程的设计模型解读。(个人理解)
可重入锁:所谓可重入锁,顾名思义,指的是线程可以重复获取同一把锁
可重入函数,指的是多个线程可以同时调用该函数,每个线程都能得到正确结果(线程安全)
公平锁和非公平锁
ReentrantLock创建的时候可以指定是否公平,公平锁是按照等待队列中的时间获取锁资源,非公平锁:等待队列中的线程有可能是等待时间短的获得锁,也有可能是其他线程竞争到锁资源,所以不公平。
用锁的最佳实践:
1.永远只在更新对象的成员变量时加锁
2.永远只在访问可变的成员变量时加锁
3.永远不在调用其他对象的方法时加锁
二.Condition实现同步
1.实现线程之间的同步,最好的案例就是使用两个等待条件实现阻塞队列。相对于synchronized的wait()、notify()、notifyAll(),ReentrantLock的await()、signal()、signalAll()语义都是一样的,都是等待唤醒机制。
2.异步转同步
使用等待唤醒机制以及回调方法,实现异步转同步。
// 创建锁与条件变量
private final Lock lock = new ReentrantLock();
private final Condition done = lock.newCondition();
// 调用方通过该方法等待结果
Object get(int timeout){
long start = System.nanoTime();
lock.lock();
try {
while (!isDone()) {
done.await(timeout);
long cur=System.nanoTime();
if (isDone() ||
cur-start > timeout){
break;
}
}
} finally {
lock.unlock();
}
if (!isDone()) {
throw new TimeoutException();
}
return returnFromResponse();
}
// 结果是否已经返回
boolean isDone() {
return response != null;
}
// 结果返回时调用该方法
private void doReceived(Response res) {
lock.lock();
try {
response = res;
if (done != null) {
done.signalAll();
}
} finally {
lock.unlock();
}
}
异步线程执行完之后需要调用doReceived(),唤醒等待获取结果的线程。这样对于调用方异步调用转为同步调用就完成了,Dubbo中的RPC就是将异步调用转为同步等待获取结果的。
三.Semaphore限流器
JUC中根据不同的场景提供了很多的并发类,每个类适用的场景都不同。
Semaphore其中一个,可以用于限流器。
1.信号量模型
图片来源
有三部分组成:计数器,等待队列,三个原子方法。
init():初始化计数器
up():计数器加1,如果此时计数器<=0的话,唤醒等待队列一个线程并且将其从等待队列中移除。
down():计数器减1,如果此时计数器<0,当天线程阻塞进入等待队列,否则可以执行
从上诉模型中我们得出与synchronized以及lock的引用区别,管程模型可以实现的是同一时刻只有一个线程,而Semaphore可以设置同一时间多少线程同时执行临界区中的代码。
java中对应的up():release(),down():acquire()方法。
2.实现一个对象池
对象池呢,指的是一次性创建出 N 个对象,之后所有的线程重复利用这 N 个对象,当然对象在被释放前,也是不允许其他线程使用的。
class TestPool<T, R> {
final List<T> pool;
// 用信号量实现限流器
final Semaphore sem;
// 构造函数
ObjPool(int size, T t){
pool = new Vector<T>(){};
for(int i=0; i<size; i++){
pool.add(t);
}
sem = new Semaphore(size);
}
// 利用对象池的对象,调用func
R exec(Function<T,R> func) {
T t = null;
sem.acquire();
try {
t = pool.remove(0);
return func.apply(t);
} finally {
pool.add(t);
sem.release();
}
}
}
// 创建对象池
ObjPool<Long, String> pool =
new ObjPool<Long, String>(10, 2);
// 通过对象池获取t,之后执行
pool.exec(t -> {
System.out.println(t);
return t.toString();
});
Function<T,R> 类中提供四个方法,apply(T t),compose(T t),andThen(T t),以及静态方法indentify()方法。下面的是操作。
public class Test2 {
public static void main(String[] args) {
Function<Integer, Integer> f1 = i -> i*4; // lambda
System.out.println(f1.apply(3));
Function<Integer, Integer> f2 = i -> i+4; // lambda
System.out.println(f2.apply(3));
Function<String, Integer> f3 = s -> s.length(); // lambda
System.out.println(f3.apply("Adithya"));
System.out.println("组合函数:"+f2.compose(f1).apply(3));
System.out.println(f2.andThen(f1).apply(3));
System.out.println(Function.identity().apply(10));
System.out.println(Function.identity().apply("Adithya"));
}
}