锁系列—Synchronized和ReentrantLock对比

一、 synchronized

1.synchronized关键字介绍

synchronized块是java提供的一种原子性内置锁,java中的每个对象都可以把它当作一个同步锁来使用。

这些java内置的使用者看不到的锁被称为内部锁,也叫做监视器锁

java中内置锁包括:

无锁、偏向锁、轻量级锁、重量级锁(同步锁)

synchronized就是重量级锁。

线程的执行代码在进入synchronized代码块前会自动获取内部锁,这时候其他线程访问该同步代码块时会被阻塞挂起。拿到了内部锁的线程会在正常退出同步代码块或者抛出异常后或者在同步块内调用了wait系列方法时释放该内置锁。

另外,由于java中的线程与操作系统的原生线程是一一对应的(java线程非用户线程),所以当阻塞一个线程时,需要从用户态切换到内核态执行阻塞操作,也就是很耗时的上下文切换

2.synchronized的内存语义

synchronized可以解决共享变量内存可见性问题。

进入synchronized块的内存语义是把在synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。退出synchronized块的语义就是把在synchronized块内对共享变量的修改刷新到主内存

关于共享变量内存的可见性

根据java内存模型可知,线程在对共享变量进行操作时,会先将其从主内存中加载到工作内存(比如cpu的L1、L2和L3缓存),然后操作完成后将修改后的值同步回主内存。假如有两个线程AB同时读取了共享变量,A修改了变量的值并刷回了主内存,但是B由于自己的工作内存中有共享变量的值,所以不会去读取主内存了,此时A对共享变量的修改对于B就是不可见的。volatile就是用来解决这个问题的。

3.三种使用方式

  • 修饰实例方法:给对象实例加锁。
  • 修饰静态方法:给当前类加锁。
  • 修饰代码块:指定加锁对象(类或者指定对象)。

4.底层原理(jvm层面)

(1)修饰代码块

synchronized 同步语句块使用的是 monitorenter(对应JMM模型lock指令) 和 monitorexit (unlock)指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

**当执行 monitorenter 指令时,线程试图获取锁也就是获取 对象监视器 monitor 的持有权。**如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放,其他线程可以尝试获取锁。

image-20220409153707473

(2)修饰方法

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。如果是实例方法,JVM 会尝试获取实例对象的锁。如果是静态方法,JVM 会尝试获取当前 class 的锁。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ue7GhW9b-1652711420554)(E:/Blog/lansg/source/img/image-20220409154648568.png)]

总结:本质都是对对象监视器monitor的获取。

加锁时,对锁计数器+1;释放锁时,对锁计数器-1;锁计数器为0时,表示可以获取该锁。


补充:lock和unlock

lock 和 unlock 在Java内存模型中是必须满足下面四条规则的:

① 一个变量同一时刻只允许一条线程对其进行 lock ,但 lock 操作可以被同一个线程执行多次,多次执行 lock 后,只有执行相同次数的 unlock ,变量才能被解锁。
② 如果对一个变量执行 lock ,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 load 或 assign 操作初始化变量的值;
③ 如果一个变量没有被 lock 锁定,则不允许对其执行 unlock 操作,也不允许 unlock 一个其它线程锁定的变量;
④ 对一个变量执行 unlock 之前,必须先把此变量同步回主内存中,即执行 store 和 write 操作;

5.锁优化

首先明确锁有四种状态,依次是:无锁状态偏向锁状态轻量级锁状态重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

在jdk1.6以后,synchronized 有如下三种状态:

  • 偏向锁,是指一段同步代码一直被一个线程访问,那么这个线程会自动获取锁,降低获取锁的代价。
  • 轻量级锁,是指当锁是偏向锁时,被另一个线程所访问,偏向锁会升级为轻量级锁,这个线程会通过自旋的方式尝试获取锁,不会阻塞,提高性能。
  • 重量级锁,是指当锁是轻量级锁时,当自旋的线程自旋了一定的次数后,还没有获取到锁,就会进入阻塞状态,该锁升级为重量级锁,重量级锁会使其他线程阻塞,性能降低。

6.总结

(1)synchronized 是可重入锁;

(2)synchronized 是非公平锁;

(3)synchronized 可以同时保证原子性、可见性、有序性;

(4)synchronized 有三种状态:偏向锁、轻量级锁、重量级锁;

(5)synchronized 是悲观锁;

synchronized的不⾜之处

  • 如果临界区是只读操作,其实可以多线程⼀起执⾏,但使⽤synchronized的话,同⼀时间只能有⼀个线程执⾏。
  • synchronized⽆法知道线程有没有成功获取到锁。
  • 使⽤synchronized,如果临界区因为IO或者sleep⽅法等原因阻塞了,⽽当前 线程⼜没有释放锁,就会导致所有线程等待

为解决以上问题,可以使用lock包下的锁。

二、ReentrantLock

juc.locks包下共有三个接⼝: ConditionLockReadWriteLock 。其中,Lock 和ReadWriteLock从名字就可以看得出来,分别是锁和读写锁的意思。Lock接⼝⾥ ⾯有⼀些获取锁和释放锁的⽅法声明,⽽ReadWriteLock⾥⾯只有两个⽅法,分别 返回“读锁”和“写锁”。

Lock接⼝中有⼀个⽅法是可以获得⼀个 Condition :

Condition newCondition();

Condition:await、signal、signalAll。

Object:wait、notify、notifyAll()。

但Condition类似于Object的等待/通知机制的加强版,可以唤醒指定线程。

1.概述:

ReentrantLock是⼀个非抽象类,它是Lock接⼝的JDK默认实现,实现了锁的基本功能。

2.ReentrantLock的高级特性(相比synchronized )

(1)等待可中断:ReentrantLock提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly() 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。

(2)可实现公平锁(先等待先获得):默认是非公平锁,可以通过修改构造方法参数来决定是否公平。

(3)可实现选择性通知(锁可以绑定多个条件):通过Condition接口与newCondition()方法实现等待/通知机制。

3.源码分析

(1)ReentrantLock实现了Lock接口,里面定义了java中锁应该实现的几个方法:

//获取锁
void lock();
//获取锁可中断
void lockInterruptibly() throws InterruptedException;
// 尝试获取锁,如果没获取到锁,就返回false
boolean tryLock();
// 尝试获取锁,如果没获取到锁,就等待一段时间,这段时间内还没获取到锁就返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//释放锁
void unlock();
//条件锁
Condition newCondition();

(2)主要属性

//在构造方法中初始化,决定使用公平锁还是非公平锁,默认非公平锁
private final Sync sync;

(3)主要内部类

//实现了AQS的部分方法
abstract static class Sync extends AbstractQueuedSynchronizer{}
//非公平锁的获取
static final class NonfairSync extends Sync {}
//公平锁的获取
static final class fairSync extends Sync {}

(4)主要构造方法

//默认非公平
public ReentrantLock() {
        sync = new NonfairSync();
}
//传入参数实现公平锁
public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
}

(5)公平锁实现

//ReentrantLock.lock()
public void lock() {
    //调用sync的lock方法,sync是继承了AQS的静态内部类
    //这里是公平锁,调用FairSync的实例
    sync.lock();
}

//ReentrantLock.FairSync.lock()
final void lock() {
    //调用AQS的acquire()方法获取锁,传值为1
    acquire(1);
}

// AbstractQueuedSynchronizer.acquire()
public final void acquire(int arg) {
    	// 尝试获取锁
		// 如果失败了,就排队
        if (!tryAcquire(arg) &&
			// 注意addWaiter()这里传入的节点模式为独占模式
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
}

// ReentrantLock.FairSync.tryAcquire()
        protected final boolean tryAcquire(int acquires) {
            //当前线程
            final Thread current = Thread.currentThread();
            //查看当前状态变量的值
            int c = getState();
            //如果状态变量的值为0,说明暂时还没有人占有锁
            if (c == 0) {
                // 如果没有其它线程在排队,那么当前线程尝试更新state的值为1       
				// 如果成功了,则说明当前线程获取了锁
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {d
                     当前线程获取了锁,把自己设置到(AQS)dexclusiveOwnerThread变量中
                    setExclusiveOwnerThread(current);
                    //返回true说明成功获取锁
                    return true;
                }
            }
            //如果当前线程本身就占有着锁,现在又尝试获取锁
            // 那么,直接让它获取锁并返回true
            else if (current == getExclusiveOwnerThread()) {
                // 状态变量state的值加1
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                // 设置到state中        
				// 这里不需要CAS更新state        
				// 因为当前线程占有着锁,其它线程只会CAS,把state从0更新成1是不会成功的       
                setState(nextc);
                //获取成功
                return true;
            }
            //获取失败
            return false;
        }
    }

//AbstractQueuedSynchronizer.addWaiter()
// 调用这个方法,说明上面尝试获取锁失败了
    private Node addWaiter(Node mode) {
        // 新建一个节点
        Node node = new Node(Thread.currentThread(), mode);
        // 这里先尝试把新节点加到尾节点后面
        // 如果成功了就返回新节点,如果没成功再调用enq()方法不断尝试
        Node pred = tail;
        // 如果尾节点不为空
        if (pred != null) {
            // 设置新节点的前置节点为现在的尾节点
            node.prev = pred;
            // CAS更新尾节点为新节点
            if (compareAndSetTail(pred, node)) {
                // 如果成功了,把旧尾节点的下一个节点指向新节点,并返回新节点
                pred.next = node;
                return node;
            }
        }
        // 如果上面尝试入队新节点没成功,调用enq()处理
        enq(node);
        return node;
    }

公平锁情况下获取锁的步骤总结:

image-20220411093437211

(a)尝试获取锁,如果获取到了就直接返回了;

(b)尝试获取锁失败,再调用addWaiter()构建新节点并把新节点入队;

(c)然后调用acquireQueued()再次尝试获取锁,如果成功了,直接返回;

(d)如果再次失败,再调用shouldParkAfterFailedAcquire()将节点的等待状态置为等待唤醒(SIGNAL);

(e)调用parkAndCheckInterrupt()阻塞当前线程;

(f)如果被唤醒了,会继续在acquireQueued()的for()循环再次尝试获取锁,如果成功了就返回;

(g)如果不成功,再次阻塞,重复(c)(d)(e)直到成功获取到锁。

(6)非公平锁的实现

// ReentrantLock.lock()
public void lock() {
        sync.lock();
    }

// ReentrantLock.NonfairSync.lock()
final void lock() {
    		// 直接尝试CAS更新状态变量
            if (compareAndSetState(0, 1))
                // 如果更新成功,说明获取到锁,把当前线程设为独占线程
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

// AbstractQueuedSynchronizer.acquire()
public final void acquire(int arg) {
    	//尝试获取,失败就排队
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

// ReentrantLock.NonfairSync.tryAcquire()
protected final boolean tryAcquire(int acquires) {
    		//调用父类sync的方法(默认非公平锁)
            return nonfairTryAcquire(acquires);
        }

// ReentrantLock.Sync.nonfairTryAcquire()
final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                // 如果状态变量的值为0,再次尝试CAS更新状态变量的值        
				// 相对于公平锁模式少了!hasQueuedPredecessors()条件
                //!hasQueuedPredecessors()是为了检查前面是否有排队的线程
                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;
        }

总结:

相对于公平锁,非公平锁在一开始就多了两次直接尝试获取锁的过程。

(a)一开始就尝试CAS更新状态变量state的值,如果成功了就获取到锁了;

(b)在tryAcquire()的时候没有检查是否前面有排队的线程,直接上去获取锁才不管别人有没有排队呢;

(7)tryLock()方法

​ 以非公平模式尝试获取一次锁,成功了就返回true,没成功就返回false,不会继续尝试。

public boolean tryLock() {
    	//直接调用Sync的nonfairTryAcquire()方法,不排队直接获取
        return sync.nonfairTryAcquire(1);
    }

(8)unlock方法

//ReentrantLock.unlock()
public void unlock() {
        sync.release(1);
    }

//AbstractQueuedSynchronizer.release
public final boolean release(int arg) {
    	//调用AQS实现类的tryRelease()方法释放锁
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

//ReentrantLock.Sync.tryRelease(同tryAcquire,调用子类)
protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
    		// 如果当前线程不是占有着锁的线程,抛出异常
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
        	// 如果状态变量的值为0了,说明完全释放了锁
			// 这也就是为什么重入锁调用了多少次lock()就要调用多少次unlock()的原因
			// 如果不这样做,会导致锁不会完全释放,别的线程永远无法获取到锁
            if (c == 0) {
                free = true;
                // 清空占有线程
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

释放锁的过程大致为:

(1)将state的值减1;

(2)如果state减到了0,说明已经完全释放锁了,唤醒下一个等待着的节点;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值