锁是为了在多线程情况下保证程序运行正确的一种手段。
锁可以从多个维度进行分类:
1.从是否占用资源,锁可以分为悲观锁与乐观锁。
2.从锁的获取是否公平,可以分为公平锁与非公平锁
3.从当前线程能否重复获取这把锁,可以为分可重入锁与不可重入锁
4.从多个线程能否获取同一把锁可以分为排它锁跟共享锁
java的锁,主要有synchronized与lock这2种。
其中synchronized是系统提供的关键字。而lock是底层由AQS实现的类锁。
synchronized
synchronized的使用往往非常简单。看如下示例:
public class Demo {
private static Object object = new Object();
public static synchronized String getDemo0() {
return "Demo0";
}
public synchronized String getDemo1(){
return "Demo1";
}
public String getDemo2(){
synchronized (this){
return "Demo2";
}
}
public String getDemo3(){
synchronized (Demo.class){
return "Demo3";
}
}
public String getDemo4(){
synchronized (object){
return "Demo4";
}
}
}
虽然synchronized的使用非常简单,但是如果用的不好也会导致性能不佳。
synchronized主要分为2种,一是作用于方法,二是作用于代码块。
同步方法
synchronized修饰在方法上,如getDemo0是静态方法,这时候锁的就是类锁,不管你new了多少对象,锁都是同一个。
getDemo1则是实例方法,只有用一个对象上的多个线程对这个方法进行调用才会互斥,new多个对象时不会进行互斥。
同步代码块
synchronized (this)时,这个就是锁的是当前的对象,new多个对象时互不影响,同一个对象的不同带有synchronized的方法也会互相影响,即对同一个对象而言,每次只能有一个线程访问synchronized代码块,没有synchronized修饰的方法或者代码块不影响。
synchronized (Demo.class)时,锁的就是当前这个类,new多个对象时也会互相影响
synchronized (object)时,就是以object这个对象为锁。
所以synchronized使用虽然简单,但是放置的地方不一样,锁的粒度也是不同的。
synchronized小结
synchronized虽然使用起来非常方便,但是在jdk1.6以前是非常重量级的锁,只要其中一个线程占用了这个锁,其他线程全部阻塞。即使你等待的时间非常非常短,也会阻塞然后切换到其他线程,而线程的切换也是非常耗费资源的。所以在jdk1.6对synchronized做了优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等
synchronized的锁等级从小到大可以分为偏向锁,轻量级锁,重量级锁。
synchronized依赖于对象头(MarkWord)的结构。如下图所示
对象头.png
根据对象头的锁标志位来判断到底是什么锁。
我自己总结了一下:
偏向锁:查看锁的线程id是否指向当前线程,如果是就执行代码
如果不是,就cas替换现场id。成功就执行代码,失败就进化为轻量级锁
偏向锁认为其实就一个线程持有这个锁。
轻量级锁:认为是在不同的时间段内有不同的线程来获取这把锁,不存在竞争
当a线程将该对象的markowrd记录在自己的锁记录中,将markword指向自己的锁记录,线程b也做这个操作,失败,然后自旋,自旋一定次数失败,就重量级锁,全部阻塞
重量级锁:全部阻塞。
lock
lock是一个锁的接口,大概有2种锁,一种是独占锁,一种是共享锁,
独占锁的实现类如ReentrantLock ,共享锁的实现入ReentrantReadWriteLock.ReadLock , ReentrantReadWriteLock.WriteLock 。
我们一般比较常用的还是ReentrantLock。
lock的使用就比较灵活了,但是必须要自己解锁,锁的解除必须放在finally里,防止异常情况下导致无法解锁。相对于synchronized而言,lock可以设置一个锁的超时时间,时间一到自动解锁。
Lock lock=new ReentrantLock();
lock.lock();
try{
//doSomething
}finally{
lock.unlock();
}
现在我们来看一下ReentrantLock的底层实现。
首先我们来看下ReentrantLock的构造器
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
可以看出来ReentrantLock支持公平锁跟非公平锁2种锁方式,默认的是非公平锁的方式。从下图可以看出来非公平锁的继承方式
图片.png
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
*非公平锁进来就尝试获取锁,如果获取失败就走acquire逻辑
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
/**
*这段代码是AQS中的代码。tryAcquire提供了一个模板方法,
*由具体的实现来实现这个这个方法,意思就是试着来获取一下锁。以下这段代码的意思就是如果获取锁失败,并且入队成功,就给当前线程一个中断标记
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
可以看出非公平锁跟公平锁都继承的Sync这个类继承了AbstractQueuedSynchronizer这个类。AbstractQueuedSynchronizer一般称为AQS,是java多线程并发包的核心。它提供了一套模板方法,以及实现了一个CLH的FIFO的双项队列来满足java底层的多线程实现。
ReentrantLock的非公平锁实现tryAcquire的逻辑如下
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取state的值,这个state是AQS中很重要的一个参数,
//如果大于0则认为该锁已被持有,可以实现可重入锁逻辑,
//持有一次则加一,解锁则减一,如果等于0则认为没有线程持有这把锁了
int c = getState();
//如果等于0则认为没有线程获取这把锁,则cas一下,
//如果cas成功就设置排他线程为当前线程
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//如果当前线程与排他线程一致,则认为可以重复获取这把锁,就将state往上加
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;
}
如果tryAcquire失败,则认为无法获取当前锁执行以下逻辑
首先先addWaiter
private Node addWaiter(Node mode) { //mode=Node.EXCLUSIVE
//将当前线程封装成Node
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
//tail不为空的情况,说明队列中存在节点数据
if (pred != null) {
//将当前线程的Node的prev节点指向tail
node.prev = pred;
if (compareAndSetTail(pred, node)) {//通过cas讲node添加到AQS队列
pred.next = node;//cas成功,把旧的tail的next指针指向新的tail
return node;
}
}
//如果tail=null,将node添加到同步队列中,end方法就是自旋然后塞入队列的队尾
enq(node);
return node;
}
/**
* 将当前线程组装成node节点,塞入队列
*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//进行自旋
for (;;) {
final Node p = node.predecessor();
//获取的前驱结点是头节点并且cas成功,就可以认为获取了锁,不再入队
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
网友评论