锁的概念及synchronized使用原理解析
前言
前篇文章主要对锁的类型和synchronized如何使用,及锁对象在堆中各个变化状态做了一个分析。然后这篇文章会继续讲解lock的实现ReentrantLock 和condition如何实现及原理解析,与synchronized的对比分析。ReentrantLock相比synchronized而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景。
锁
java除了synchronized关键字可以实现锁,也可以由Doug Lea 大神写juc.locks包下面实现类实现,从reentrantlock开始充分理解和分析信息
本质
锁是一种控制用户访问共享资源的工具,多线程。通常,锁提供对数据库的独占访问共享资源:一次只有一个线程可以获取锁和对共享资源的所有访问都要求锁定先得的。但是,某些锁可能允许并发访问共享资源,如{@link ReadWriteLock}的读锁。 -------截取自lock接口的注释
加锁是加一种限制,获取锁是获取一个权限。
例如:一个场景,房子出租,房东贴出出租信息,而就是给这间房子加了把锁,有个人租下了房子,就说这个人有这个房子的使用权限,当这个房子租期到了,释放了锁。
public class MyLock implements Lock {
//锁拥有者
AtomicReference<Thread> owner = new AtomicReference<>();
//等待队列
private LinkedBlockingQueue<Thread> waiters = new LinkedBlockingQueue<>();
@Override
public boolean tryLock() {
return owner.compareAndSet(null, Thread.currentThread());
}
@Override
public void lock() {
while (!tryLock()){
waiters.offer(Thread.currentThread());
LockSupport.park();
}
}
@Override
public void unlock() {
if (owner.compareAndSet(Thread.currentThread(), null)){
Thread th = waiters.poll();
LockSupport.unpark(th);
}
}
}
Locks Api
lock层次结构
上图为整个接口实现的层次图
Lock接口
- lock方法一般都是我们用到的方法,一定要获得到锁为止, lock()最常用;
- trylock方法只获取锁一次,获取不到就不获取了
-
lockInterruptibly()方法一般更昂贵,有的impl可能没有实现lockInterruptibly(),只有真的需要效应中断时,才使用.
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
即该线程阻塞时被中断,抛中断异常后线程退出,不会执行后面语句。
在interrupt过后,lock方法是不会中断的,响应中断
根据Lock接口的源码注释,Lock接口的实现,具备和同步关键字同样的内存语义。代码在运行时并且不能指令重排,不会被缓存
{@code Lock}实现提供了更广泛的锁定 使用{@code synchronized}方法可以获得的操作和声明。它们允许更灵活的结构,可能有完全不同的属性,并且可能支持多个关联的{@link Condition}对象。
ReentrantLock
ReentrantLock是lock接口的最经典实现,是一个可重入锁、悲观锁、独享锁、自旋锁;
默认是非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
可以在初始化时设置为公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
重字面意思说reentrantlock是可重入的锁,可重入的概念在于下面的 代码执行过后
static Lock lc = new ReentrantLock();
static volatile int i = 0;
public static void add() throws InterruptedException {
lc.lock();
i++;
System.out.println("here im ..");
Thread.sleep(1000L);
add();
lc.unlock();
}
public static void main(String args[]) throws InterruptedException {
add();
}
得到结果会一直打印值。
相同的线程,不互斥,会再次获取到锁,执行锁内代码。
reentrantlock怎么实现可重入锁的。
首先实现一个简单的不重入锁
public class MyLock implements Lock {
//当前锁拥有者,若onwer为null,说明没有线程占用锁
private Thread owner = null;
//锁占用是,线程被挂起,挂起的线程的引用被放到waiters队列
private BlockingQueue<Thread> waiters = new LinkedBlockingQueue<>();
@Override
public boolean tryLock() { //尝试获取锁
//若锁未占用
if (owner == null){
owner = Thread.currentThread(); //当前线程获得锁
return true; //获得锁,返回true
}else{
return false; //获取锁失败,返回false
}
}
@Override
public void lock() {
while(!tryLock()){ //尝试拿锁,如果失败,
Thread curTh = Thread.currentThread(); //获取当前线程引用
waiters.offer(curTh); //将当前线程引用放入等待队列
LockSupport.park();
}
}
@Override
public void unlock() { //释放锁
if (owner == Thread.currentThread()){ //若确实是我占有了锁
owner = null; //将onwer置为null,释放锁
Thread th = waiters.poll(); //取出队列头部的元素,并移除该元素
LockSupport.unpark(th); //唤醒队列头部的元素
}
}
}
这里只用一个owner来判断当前那个锁被占用 ,并用一个队列来存储阻塞线程
在ReentrantLock中去判断是否重入锁,需要添加State值。来判断当前锁是否被被占用
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
这里新加了个同步状态来维护,
实现的一个简单的可重入锁
public class MyLock implements Lock {
//锁拥有者
volatile AtomicReference<Thread> owner = new AtomicReference<>();
//等待队列
private LinkedBlockingQueue<Thread> waiters = new LinkedBlockingQueue<>();
//记录重入的次数
volatile AtomicInteger state = new AtomicInteger(0);
@Override
public boolean tryLock() {
int s = state.get();
if (s != 0) {
// 判断state是否为0,state!=0,说明锁被占用
if (owner.get() == Thread.currentThread()) {
state.set(s + 1);
return true;
}
} else {
// 如state==0,说明锁未未被占用,CAS操作来抢锁,修改state的值
while (state.compareAndSet(s, s + 1)) {
owner.set(Thread.currentThread());
return true;
}
}
return false;
}
@Override
public void lock() {
if (!tryLock()){
waiters.offer(Thread.currentThread());
//自旋抢锁
for (;;){
Thread head = waiters.peek();
if (head == Thread.currentThread()){
if (!tryLock()){
LockSupport.park();
}else{
waiters.poll();
return;
}
}else{
LockSupport.park();
}
}
}
}
@Override
public void unlock() {
if (tryUnlock()){
Thread th = waiters.peek();
if (th !=null){
LockSupport.unpark(th);
}
}
}
public boolean tryUnlock(){
//判断owner是不是自己
if (owner.get() != Thread.currentThread()){
throw new IllegalMonitorStateException();
}else{
//释放锁,state-1,
int ct = state.get();
int nextc = ct -1;
state.set(nextc);
//是不是一定将onwer该问null
if (nextc == 0){
owner.set(null);
return true;
}else{
return false;
}
}
}
}
这里主要是为了实现一个简单的可重入锁,因此并没有考虑到其他情况
Condition
final ConditionObject newCondition() {
return new ConditionObject();
}
包含awit方法的实现 和sign方法的实现
- 搭配lock对象进行使用,就是condition的使用方法
public class Condition {
private static ReentrantLock lock = new ReentrantLock();
private static java.util.concurrent.locks.Condition condition = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
lock.lock();
System.out.println("子线程或得锁...\n");
try {
System.out.println("开始wait...\n");
condition.await();
System.out.println("唤醒了...\n");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
lock.unlock();
}
}
});
thread.start();
Thread.sleep(4000L);
lock.lock();
System.out.println("唤醒子线程...");
condition.signal();
Thread.sleep(10000L);
lock.unlock();
}
}
- 当signal只有unlock释放掉锁,wait的方法才会结束等待。这个和synchronized一样
- condition.awit底层是使用的park,因此这时的线程状态也是awit
- 这里在源码中去校验锁对象不同时,也会和synchronized一样抛出monitorstate异常,虽然这里不存在 monitor监听器 重量级锁, 应该是Doug Lea大神为了和虚拟机synchronized抛出异常一致
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
- condition.awit 和awit方法是一样,在等待期间,也会把锁给释放掉,主线程可以拿到锁
- 造成死锁,例如park和unpark ,我随意写的一段小代码,这个park的时候,是会造成死锁的,park的时候,没有释放掉锁。 因此park 和unpark是没办法在锁中用的;包括先notify 在 wait也是会出现死锁的情况
Thread th=new Thread(new Runnable(){
public void run(){
synchronized(lock){
Locksupport.park();
}
}
}
).start();
synchronized(lock){
Locksupport.unpark(th);
}
- condition 底层是由park和unpark实现的,因此不会出现 先unpark造成死锁;并且在wait的时候会释放锁对象,因此也不会造成死锁,总的来说,这是最好的选择
condition 在两种死锁方式里面都不会死锁。
condition代码应用
实现一个阻塞队列,只能存储 n个元素; 这里做了一个很简单的阻塞队列,主要利用的两个Condition 一个读取condition 一个写入condition 分别取控制
public class MyQueue {
Lock lock = new ReentrantLock();
Condition putCondition = lock.newCondition();
Condition takeCondition = lock.newCondition();
private volatile int size = 0;
public MyQueue() {
size = 11;
};
public MyQueue(int size) {
this.size = size;
};
private List<Object> list = new ArrayList<Object>();
public void put(Object o) throws InterruptedException {
lock.lock();
try {
if (list.size() < size) {
list.add(o);
takeCondition.signal();
} else {
while (true) {
putCondition.await();
}
}
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
Object obj;
try {
if (list.size() == 0) {
while (true) {
takeCondition.await();
}
} else {
obj = list.get(0);
list.remove(0);
putCondition.signal();
}
} finally {
lock.unlock();
}
return obj;
}
}
Synchronized和Lock接口
总结
整篇文章介绍的lock经典的实现reentrantLock与condition。而这个实现是对比着synchronized关键字实现的。有很多相似的地方,但由于是doug lea 自己实现的,因此有很大的可变性,而且还非常好用,希望大家看了有所理解和成长