一、简介
JDK 5.0为开发人员开发高性能的并发应用程序提供了一些很有效的新选择。例如,java.util.concurrent.lock
中的类 ReentrantLock
被作为 Java 语言中synchronized
功能的替代,它具有相同的内存语义、相同的锁定,但在大量争用条件下却有更好的性能,此外,它还有 synchronized
没有提供的其他特性。那么在这边可以比较一下synchronized关键字和ReentrantLock的一些区别和共同点:
共同点:
synchronized关键字和ReentrantLock都是一种重入锁(重入锁相关概念后序在synchronized关键字中讲解)
synchronized关键和ReentrantLock都能保证并发的安全性
区别:
synchronized关键字是语言内部级别的,是原生语法层面的互斥,需要JVM的实现。ReentrantLock是API层面的互斥。
ReentrantLock的实现需要调用lock.lock()方法和在finally代码块中调用lock.unlock()来释放锁。
在JDK1.6以后,对synchronized关键字进行了很多对优化,包括偏向锁,轻量级锁,重量级锁等,对于synchronized关键字对优化使得其性能上在某些场景要比ReentrantLock来的更为的有效率。但是具体使用哪些还是必须依赖场景。
二、简单使用示例
/**
* 5个线程,每个循环次数 100000 1000000 10000000 100000000
* synchronized(ms) 29 265 3293 47789
* ReentrantLock(ms) 79 165 1473 14923
* volatile(ms) count++非原子操作不能同步
*
* 10000000次 线程数 2 5 8 15
* synchronized(ms) 661 3293 7084 11380
* ReentrantLock(ms) 767 1473 2342 3672
*/
public class SynchronizedTest {
public static void main(String[] args) {
//线程数
int threadNum = 5;
Syn syn = new Syn();
Thread[] threads = new Thread[threadNum];
//记录运行时间
long l = System.currentTimeMillis();
for (int i = 0; i < threadNum; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000000; j++) {
syn.increaseLock();
}
}
});
threads[i].start();
}
//等待所有线程结束
try {
for (int i = 0; i < threadNum; i++)
threads[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(syn + " : " + (System.currentTimeMillis() - l) + "ms");
}
}
class Syn {
private int count = 0;
private Lock lock = new ReentrantLock();
//利用synchronized
public void increase() {
synchronized (this) {
count++;
}
}
//利用ReentrantLock类同步
public void increaseLock() {
lock.lock();
try{
count++;
}finally{
lock.unlock();
}
}
public void increaseVolatile() {
count = count + 1;
}
@Override
public String toString() {
return String.valueOf(count);
}
}
从上面的例子可以看出当并发量很小的适合,使用synchronized关键字的效率比Lock的效率高一点,而当并发量很高的时候,Lock的性能就会比synchronized关键字高,具体的原因的可以等我的synchronized关键字理解的文章出来,我会在那里进行讲解。
例子中看出对于Lock的使用通过lock.lock()方法获取锁,在finally中通过lock.unlock()保证释放锁。
三、实现原理
ReentrantLock实现了Lock接口,首先可以看一下Lock接口定义了哪些方法ReentrantLock又是如何实现的
lock():获取同步状态
lockInterruptibly():响应中断获取同步状态
tryLock():非阻塞的获取锁,调用这个方法会立刻返回,如果获取锁就返回true否则就是false
tryLock(long time,TimeUnit unit):超时获取同步状态
unlock():释放同步状态
newCondition():创建等待/通知对象
在研究源码之前,对于ReentrantLock需要了解了解一个概念公平锁和非公平锁:
公平锁:之前的AQS文章说过,当同步队列中首节点释放同步状态后,因为FIFO先进先出队列首先获取同步状态的为其后继节点如果不存在后继节点就获取等待时间最长的正常状态的线程。而这种唤醒的过程就是公平锁,当释放同步状态以后获取同步状态的要么是后继节点要么是等待时间最长的节点。但是由于公平锁的这个特性导致在并发很高的情况下其效率比非公平锁要低。
非公平锁:相对于公平锁而言,非公平锁在释放同步状态以后所有的线程都会进入竞争同步状态,而获取同步状态的线程是随机的不确定的。有可能是等待时间最长的也有可能是等待时间最短的。
了解了这两个概念以后就可以看看Rentrant底层到底怎么实现的。
构造方法:默认创建的为非公平锁
1 public ReentrantLock(boolean fair) {
2 sync = fair ? new FairSync() : new NonfairSync();//通过传入boolean类型的值确定到底是公平锁还是非公平锁
3 }
lock():获取同步状态
1 public void lock() {
2 sync.lock();//调用了内部AQS实现类中的方法【有两种实现方式】
3 }
公平锁获取同步状态
1 final void lock() {
2 acquire(1);//调用AQS的中方法,最终需要重点关注的方法为tryAcquire(int arg)
3 }
tryAcquire(int args):公平锁判断是否获取到锁
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;
}
}
hasQueuedProcesssors():判断同步队列中是否存在比当前线程等待时间更长的线程
1 public final boolean hasQueuedPredecessors() {
5 Node t = tail; //同步队列尾节点
6 Node h = head; //同步队列头节点
7 Node s;
//头节点和尾节点比较(如果头节点和尾节点重复代表同步对列中只有一个头节点释放以后不需要进行比较)
8 return h != t &&((s = h.next) == null || s.thread != Thread.currentThread());
10 }
公平锁的原理就是:在通过CAS设置同步状态之前会先去同步队列中查询是否存在线程比当前的线程的等待时间长的,如果存在就不去改变该线程的状态如果不存在就进行改变获取同步状态。
非公平的获取锁
1 final void lock() {
2 if (compareAndSetState(0, 1))//通过CAS改变状态
3 setExclusiveOwnerThread(Thread.currentThread());//成功就代表获取锁
4 else
5 acquire(1);//走AQS中的方法
6 }
nonfairTryAcquire(int acquires):非公平的获取同步状态
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();//获取当前线程
int c = getState();//获取同步状态值
if (c == 0) {
if (compareAndSetState(0, acquires)) {//CAS操作改变状态
setExclusiveOwnerThread(current);//成功就获取锁
return true;
}
}
//重入锁原理如果是当前线程就状态值自增
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);//设置状态值
return true;
}
return false;
}
非公平获取锁的实现比公平获取锁的实现要简单很多,它不需要去同步队列中去与其它的节点进行比较,随便谁获取到同步状态
总结:
对于ReentrantLock而言,它只实现了如何代表线程已经获取了同步状态,他不关心获取了同步状态的以后操作而这些操作都是由AQS本身去实现的,这也证明了AQS在并发包中的重要性。再来看看ReentrantLock实现的获取锁,对于非公平锁而言谁获取同步状态都无所谓对于每个线程而言获取同步状态的几率都是一样的而对于公平锁而言其就遵循了FIFO先进先出队列的原则。
unlock():释放同步状态
1 public void unlock() {
2 sync.release(1);//调用AQS中的release方法
3 }
tryRelease(int releases):释放锁的具体方法
protected final boolean tryRelease(int releases) {
int c = getState() - releases;//通过同步状态值自减
//判断当前释放同步状态的线程是否为获取同步状态的线程
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;//是否完全释放(因为是重入锁必须等待所有的锁释放才算释放)
if (c == 0) {//代表完全释放
free = true;
setExclusiveOwnerThread(null);//获取锁状态的线程为null
}
setState(c);//设置状态
return free;
}
释放锁的过程很简单,每当释放的时候都会进行一次状态自减直到为0的时候代表着这个线程已经完全释放了这个同步状态接下来才会唤醒同步队列中的节点否则代表这个同步状态未释放完全
ReentrantLock的获取锁与释放锁的操作已经了解了一些,基于这些基本方法上进行拓展的非阻塞获取锁和超时等待响应中断这些在源码上没多大区别。可以自行再去进行了解。
==================================================================================
不管岁月里经历多少辛酸和艰难,告诉自己风雨本身就是一种内涵,努力的面对,不过就是一场命运的漂流,既然在路上,那么目的地必然也就是前方。
==================================================================================