我们不仅仅可以通过synchronized来达到锁的目的,Java还 提供了很多java语言级别的锁,其接口为java.uti.concurrent.Lock.如图所示
上图是jdk1.8
首先来说说一下readLock和writeLock,他们都是reentrantreadwritelock中的public statIc 类型的内部类,很多时候是先定义一个reentrantreadwritelock对象,然后再通过writeLock()和readLock()来获取读锁和写锁,所以我们通常把这个叫做读写锁。
ReentrantLock最基本的两个操作就是lock()和unlock()方法。通过lock可以实现对临界资源的同步,Lock的实现出了基于CAS自旋之外,还有一个就是AQS机制。
CAS,Compare And Set,利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。顺便提一下,同步异步,阻塞和非阻塞的区别,例子如下,知乎大神总结如下
1 同步和异步
同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)
所谓同步,就是在发出一个*调用*时,在没有得到结果之前,该*调用*就不返回。但是一旦调用返回,就得到返回值了。
换句话说,就是由*调用者*主动等待这个*调用*的结果。
而异步则是相反,*调用*在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在*调用*发出后,*被调用者*通过状态、通知来通知调用者,或通过回调函数处理这个调用。
典型的异步编程模型比如Node.js
举个通俗的例子:
你打电话问书店老板有没有《分布式系统》这本书,如果是同步通信机制,书店老板会说,你稍等,”我查一下",然后开始查啊查,等查好了(可能是5秒,也可能是一天)告诉你结果(返回结果)。
而异步通信机制,书店老板直接告诉你我查一下啊,查好了打电话给你,然后直接挂电话了(不返回结果)。然后查好了,他会主动打电话给你。在这里老板通过“回电”这种方式来回调。
2阻塞和非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
还是上面的例子,
你打电话问书店老板有没有《分布式系统》这本书,你如果是阻塞式调用,你会一直把自己“挂起”,直到得到这本书有没有的结果,如果是非阻塞式调用,你不管老板有没有告诉你,你自己先一边去玩了, 当然你也要偶尔过几分钟check一下老板有没有返回结果。
在这里阻塞与非阻塞与是否同步异步无关。跟老板通过什么方式回答你结果无关。
关于同步异步阻塞和非阻塞,引用自http://www.zhihu.com/question/19732473,知乎用户段子手;
那么接下的重点是AQS机制
AQS全程AbstractQueneSynchronizer,这是一个提供一系列的公共的方法让子类来调用抽象类。要看懂这个类似乎很麻烦,我们就先从ReentrantLock看起吧
public ReentrantLock()
{
sync = new NonfairSync();
}
public ReentrantLock(boolean flag)
{
sync = ((Sync) (flag ? ((Sync) (new FairSync())) : ((Sync) (new NonfairSync()))));
}
上面是ReentrantLock的两个构造方法,默认sync的实现类是NonFaidSync实现类。另一个是FairSync,无论使用哪一个都能实现排它锁,只是说内部原理有点区别,这里以NonfairSync入手,来看一下它的lock方法是怎么样实现的额。
final void lock()
{
if(compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final void setExclusiveOwnerThread(Thread thread)
{
exclusiveOwnerThread = thread;
}
public final void acquire(int i)
{
if(!tryAcquire(i) && acquireQueued(addWaiter(Node.EXCLUSIVE), i))
selfInterrupt();
}
lock方法首先通过CAS尝试将状态从0修改为1,如果直接修改成功的话,那么久会把线程的owner修改为当前线程,这是一种理想情况,如果并发粒度适当的话没那么这也是种乐观情况。
如果说上一个动作未获成功的话,那么会间接调用acquire(1)来继续操作,首先会调用tryAcquire方法
protected final boolean tryAcquire(int i)
{
Thread thread = Thread.currentThread();
int j = getState();
if(j == 0)
{
if(!hasQueuedPredecessors() && compareAndSetState(0, i))
{
setExclusiveOwnerThread(thread);
return true;
}
} else
if(thread == getExclusiveOwnerThread())
{
int k = j + i;
if(k < 0)
{
throw new Error("Maximum lock count exceeded");
} else
{
setState(k);
return true;
}
}
return false;
}
在该方法当中我们首先会得到当前状态state的值,通过getState()方法,这个state字段是volatile修饰的,volatile这个关键字最重要的两个特点,是防止指令重排序和保证多线程中的共享变量是始终可见的(但是并不能保证volatile引用对象内部的属性是完全可见的)。正是因为volatile这两个关键特点,结合读写锁的currentHashMap才将hashTable给淘汰掉。
这个state值在我看来可以被认为是一个起一个类似于版本号的作用的值。
如果state值为0,首先先检查AbstractQueuedSynchronizer中的hasQueuedPredecessors()方法,来判断当前队列没有其他线程。当然一定要配出当前线程,有可能是当前线程又一次进入。
public final boolean hasQueuedPredecessors()
{
Node node = tail;
Node node1 = head;
Node node2;
return node1 != node && ((node2 = node1.next) == null || node2.thread != Thread.currentThread());
}
上面的hasQueuedPredecessors()方法是用来判断是否有除当前线程之外的线程正在占用着lock;
同时进行CAS操作,修改state值;设置lock的占有者为当前线程;并且返回true;
当state值不为0,首先查看当前占用lock的线程是不是当前,如果是当前线程的,计算新的state值并且保存返回true,如果state值异常的话,抛出一个错误;其他的情况下直接返回false;
那么接下来的话,如果再次的tryAcquire没有成功的话,会进入addWaiter()方法,如果tryAcquire成功的话,那么久愉快的ok了;
addWaiter()在下一篇里面介绍,同时我还会捎带手介绍下condition,在介绍完addWaiter()方法的最后我再去从另一个稍微全面的视角来看一下Lock的整体工作机制和原理。