目录
1.普通方法
2.静态方法
3.锁对象
4.锁升级过程
5.可重入的锁
6.不公平锁
非公平锁的 lock 方法:
1.普通方法
将synchronized修饰在普通同步方法,那么该锁的作用域是在当前实例对象范围内,也就是说对于 SyncDemosd=newSyncDemo();这一个实例对象sd来说,多个线程访问access方法会有锁的限制。如果access已经有线程持有了锁,那这个线程会独占锁,直到锁释放完毕之前,其他线程都会被阻塞
public SyncDemo{
Object lock =new Object();
//形式1
public synchronized void access(){
//
}
//形式2,作用域等同于形式1
public void access1(){
synchronized(lock){
//
}
}
//形式3,作用域等同于前面两种
public void access2(){
synchronized(this){
//
}
}
}
2.静态方法
修饰静态同步方法或者静态对象、类,那么这个锁的作用范围是类级别。举个简单的例子,
SyncDemo sd=SyncDemo();
SyncDemo sd2=new SyncDemo();}
两个不同的实例sd和sd2, 如果sd这个实例访问access方法并且成功持有了锁,那么sd2这个对象如果同样来访问access方法,那么它必须要等待sd这个对象的锁释放以后,sd2这个对象的线程才能访问该方法,这就是类锁;也就是说类锁就相当于全局锁的概念,作用范围是类级别。
这里抛一个小问题,大家看看能不能回答,如果不能也没关系,后面会讲解;问题是如果sd先访问access获得了锁,sd2对象的线程再访问access1方法,那么它会被阻塞吗?
public SyncDemo{
static Object lock=new Object();
//形式1
public synchronized static void access(){
//
}
//形式2等同于形式1
public void access1(){
synchronized(lock){
//
}
}
//形式3等同于前面两种
public void access2(){
synchronzied(SyncDemo.class){
//
}
}
}
3.锁对象
普通同步方法,锁是当前实例对象。比如:
public synchronized void doLongTimeTaskC() {}
4.锁升级过程
无锁——》偏向锁——》轻量级锁——》重量级锁——》GC锁
5.可重入的锁
可重入的含义:指的是同一个线程获得锁之后,再不释放锁的情况下,可以直接再次获取到该锁
@Slf4j
public class SynReentrantDemo {
public static void main(String[] args) {
Runnable sellTicket = new Runnable() {
@Override
public void run() {
synchronized (this) {
String name = Thread.currentThread().getName();
log.info("我是run,抢到锁的是{}", name);
test01();
} //正常来说走出临界区(这个括号)才会释放锁,但是再没走出之前,又进入test01,
//而test01需要和本方法一样的锁
//如果不可重入的话,就将出现死锁了-->即test01方法等着释放锁,而run方法又不会释放锁
//因此synchronized只有可以在不释放run方法的锁的情况下,又再次获得该锁才不会有问题
}
public void test01() {
synchronized (this) {
String name = Thread.currentThread().getName();
log.info("我是test01,抢到锁的是{}", name);
}
}
};
new Thread(sellTicket).start();
new Thread(sellTicket).start();
}
}
首先应该知道synchronized锁的并不是同步代码块,而是锁对象关联的一个monitor对象(在java中每一个对象都会关联一个monitor对象),而这个对象里有一个变量叫recursions — 中文是递归的意思(我想大概是因为递归的时候发生可重入的几率应该是最大的,所以才用这个当变量名的吧),其实可以将它简单理解为一个计数器。
以上面的栗子为例:
(1)当线程1抢到run方法的执行权即抢到锁时,这个recursions的值就变为了1;
(2)线程1接着运行并进入test01方法后,发现还是线程1且还是要this这把锁,就将recursions的值再+1;
(3)当线程1,执行完test01方法时,recursions的值又-1
(4)执行完run方法时recursions的值又-1,就变为了0,也就是表示线程1已经释放了this锁。
(5)之后其他线程就可以继续抢this锁了。
6.不公平锁
非公平锁的 lock 方法:
static final class NonfairSync extends Sync {
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
// AbstractQueuedSynchronizer.acquire(int arg)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 这里没有对阻塞队列进行判断
if (compareAndSetState(0, acquires)) {
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;
}
公平锁和非公平锁只有两处不同:
非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。
公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。
相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。