偏向锁的实现
CAS
CAS(compare and swap) 对比和交换,对比两个值,如果相同则交换,如果不同则返回false,是原子性的操作,操作系统提供用户态的方法,性能比较高。
公平锁和非公平锁的实现
公平锁和非公平锁,其中里面的值如果为false,则非公平锁,如果是true,则为公平锁。公平锁和非公平锁在Java源码的实现的并没有实现通过系统调用mutex(用户态切换到内核态调用),所以性能是比较高的,而在使用synchronized时,如果有多个线程竞争的话,会转化为主重量级锁。而此时则会产生系统调用,使性能下降。
private static Lock lock=new ReentrantLock(false);
而这两种锁的实现主要区别在他们加锁的方式上:
/**
* 公平锁的实现
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* 公平锁的实现
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//判断是否已经加锁,状态为0 则未被加锁,可以加锁,执行CAS,然后线程ID放入队列
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;
}
}
非公平锁的实现
/**
* 非公平锁的实现
*/
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);
}
}
/**
*当加锁失败,会进入队列查看自己是否为head,如果为head则自旋一次
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
总结:公平锁和非公平锁的主要区别在于一开始是否直接尝试加锁,而这点也是公平锁和非公平锁的区别,设计初衷是为了提高性能。
synchronized
偏向锁的膨胀过程
1,对象的加锁方式
JVM是通过修改对象的对象头里面的标识来进行加锁,在class文件中会有这样一个对象头记录文件,其中无锁的分为两种,无锁可偏向(101)和无锁不可偏向(001),同时有锁已偏向也是101,但会记录持有锁的线程ID。所以无锁肯定是001。而轻量级锁一般来说是00。
所以我们可以研究不同的场景下加锁方式,对象头会发生怎样的变化来搞清楚synchronized的使用方式。
重入锁和偏向锁
重入锁指的是一个线程对一个对象进行加锁,再加锁的过程中再对这个对象进行加锁。而偏向锁是对这个对象进行加锁,加锁后释放然后再次进行加锁。
A a = new A();
//偏向锁的加锁过程
synchronized (a) {
}
synchronized (a){
}
//重入锁的加锁过程
synchronized (a){
synchronized (a){
}
}
重入锁的加锁过程,简单来说就是修改对象头+记录线程ID。当一个线程尝试对对象进行加锁时,在线程操作数栈中创建一个叫lockrecord的数据结构,包括displaced word 用于记录锁对象的对象头。对对象进行首次加锁时首先lr中产生101,然后把自己的线程ID放到对象1的对象头。此时对象头标记就是 t1ID+eporch+age+101,表示线程1 的偏向锁。当锁被释放后,再次加锁,只需要对比下是否是t1的线程ID然后如果相等直接再次加锁,不用进行CAS,此时性能是最高的。
轻量锁
当一个线程加锁后,是释放,第二个线程进行加锁时,就会升级成为轻量级锁。加锁的过程,首先displaced word 会产生一个有锁不可偏向001,然后对象1的对象头markwork会创建一个指针指向了lr的地址,而Obj ref 也会产生一个指针指向对象1。
当线程2糟蹋完对象1后,就会把自身的001放到对象1的Markwork中去,表示对象1不可偏向。所以轻量锁和偏向锁还是有区别的。
轻量锁加锁(本来已经轻量)
当线程t3要对线程进行加锁时首先会在CPU的内存中产生一个无锁的Markwork001,此时要对CPU内存中的001和线程3内存中的Markwork进行对比。如果相等,则进行跟上面一样的操作,Obj ref 把指针指向线程1,Markwork指向同步块,并且把001变成00。
而如果发生CAS失败,说明在t3产生001的过程中有另外的线程t4已经将对象加锁,变成000,产生资源竞争,所以会发生膨胀。
批量重偏向和批量撤销
在线程1中,反复对一个对象进行加锁,但是没有竞争,当撤销超过20次以后,jvm 会把这些对象加锁时重新偏向t2。而如果超过40次,jvm觉得设计有问题,所有的对象都撤销偏向锁。
wait notify
wait() 是对象的方法,在对象头实现的。在对象头的前64位会指向一个指针,指向ObjectMonitor。在ObjectMonitor 这个数据结构里面存储了一个WaitSet,EntryList,ownerThread,其中ownerThread记录的是当前线程的持有者,EnteyList 存储的是 阻塞的线程状态是blocked,WaitSet 是底层是waiting状态的队列。而当EntryList的队列阻塞完成后会进入到阻塞队列中去,而不是直接执行。
wati 和sleep的区别
相同:都是使线程进入休眠状态
不同: wait 在阻塞阶段会释放锁,sleep不会。wait是作用在对象上的,而sleep是作用在线程上的。wait 可以通过notify唤醒,而sleep只能通过打断唤醒。wait 必须通过synchronized关键字一起使用,而一个对象没有获取锁直接wait会异常,而sleep不会。