2 Lock

悲观锁:对数据被外界修改持保守态度,任务数据很容易被其他线程修改,所以在数据被处理以前先对数据进行加锁,并在整个数据处理过程中,使数据处于锁定状态,其它线程就无法访问。悲观锁是排它锁。

乐观锁:总认为资源和数据不会被别人所修改,所以读取不会上锁,但是乐观锁在进行写入操作的时候会判断当前数据是否被修改过。乐观锁的实现方案一般来说有两种:版本号机制 和 CAS实现 。乐观锁多适用于多读的应用类型,这样可以提高吞吐量。然而,cas机制容易出现aba问题,循环时间过长,以及只能保证一个变量的原子性等缺点

具体可参考:https://segmentfault.com/a/1190000020665558

 

公平锁与非公平锁:根据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁。公平锁表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定的,先请求先得到。而非公平锁则在运行时闯入,不一定先到先得。ReentrantLock提供了公平和非公平锁的实现。

● 公平锁:ReentrantLock pairLock = new ReentrantLock(true)。

● 非公平锁:ReentrantLock pairLock = new ReentrantLock(false)。如果构造函数不传递参数,则默认是非公平锁。

public ReentrantLock(boolean fair) {
   sync = fair ? new FairSync() : new NonfairSync();
}

ReentrantLock的加锁方法:

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

1 NonfairSync的源码

复制代码
static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
       // 可重入锁在非公平机制下执行lock方法时,先尝试用以cas方式将state的值从0修改为1,如果修改成功了,那么就将当前线程设置为独占线程的拥有者。否则的话,执行acquire(1)
if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else
         //设置失败时,会调用AbstractQueuedSynchronizder的acquire()方法 acquire(1); } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } }
复制代码
复制代码
//所属类sync
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;
        }
复制代码

FairSync的源码:

复制代码
static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        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;
        }
    }
复制代码

可以看出当reentrantLock调用lock方法时,最终都可能会调到AbstractQueuedSynchronizder的模板方法acquire方法,并根据不同的公平机制,分别调用对应的tryAcquire方法

//所属类AbstractQueuedSynchronizder
public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

而从FairSync的与NonfairSync的源码可以看出二者tryAcquire()方法的不同在于,当state为0时多了一个hasQueuedPredecessors()方法的判断逻辑,即判断当前的同步队列中是否已经有正在等待获取锁资源的线程,如果没有,则返回true。否则有则返回false。因此,公平机制这样就保证了时间上的先后顺序,确保先到着可以先得。而非公平机制根本不管之前是否已经有等待的。

ReentrantLock的解锁方法:

public void unlock() {
        sync.release(1);
    }
复制代码

//所属类AbstractQueuedSynchronizder
public
final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
复制代码
复制代码
//所属类Sync
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); } setState(c); return free; }
复制代码

ReentrantLock的unlock()方法会直接调用AbstractQueuedSynchronizer的模板方法release(),release()方法中会调用tryRelease()方法,这里调用的是Sync实现的tryRelease()方法,tryRelease()方法中每成功释放一次锁资源,就会将state减1,所以当state为0时,就判断锁资源被全部释放,即释放锁资源成功。(从源码中我们可以看出,FairSync的与NonfairSync都公用了父类Sync的tryRelease()方法)

 

可重入锁:当一个线程要获取一个被其他线程持有的独占锁时,该线程会被阻塞,那么当一个线程再次获取它自己已经获取的锁时是否会被阻塞呢?如果不被阻塞,那么我们说该锁是可重入的,也就是只要该线程获取了该锁,那么可以无限次数(严格来说是有限次数)地进入被该锁锁住的代码。

synchronized内部锁是可重入锁。可重入锁的原理是在锁内部维护一个线程标示,用来标示该锁目前被哪个线程占用,然后关联一个计数器。一开始计数器值为0,说明该锁没有被任何线程占用。当一个线程获取了该锁时,计数器的值会变成1,这时其他线程再来获取该锁时会发现锁的所有者不是自己而被阻塞挂起。但是当获取了该锁的线程再次获取锁时发现锁拥有者是自己,就会把计数器值加+1,当释放锁后计数器值-1。当计数器值为0时,锁里面的线程标示被重置为null,这时候被阻塞的线程会被唤醒来竞争获取该锁.

 

ReentrantLock 和 Synchronized 的区别:

 最核心的区别就在于 Synchronized 适合于并发竞争低的情况,因为 Synchronized 的锁升级如果最终升级为重量级锁在使用的过程中是没有办法消除的,意味着每次都要和 cpu 去请求锁资源,而 ReentrantLock 主要是提供了阻塞的能力,通过在高并发下线程的挂起,来减少竞争,提高并发能力。

Synchronized 是一个关键字,是由 JVM 层面去实现的,而 ReentrantLock 是由 java api 去实现的

Synchronized 是隐式锁,可以自动释放锁,ReentrantLock 是显式锁,需要手动释放锁

ReentrantLock 可以让等待锁的线程响应中断,而 Synchronized 却不行,使用 Synchronized 时,等待的线程会一直等待下去,不能够响应中断

ReentrantLock 可以获取锁状态,而 Synchronized 不能

独占锁与共享锁:根据锁只能被单个线程持有还是能被多个线程共同持有,锁可以分为独占锁和共享锁。

独占锁保证任何时候都只有一个线程能得到锁,ReentrantLock就是以独占方式实现的。独占锁是一种悲观锁,由于每次访问资源都先加上互斥锁,这限制了并发性,因为读操作并不会影响数据的一致性,而独占锁只允许在同一时间由一个线程读取数据,其他线程必须等待当前线程释放锁才能进行读取。

共享锁则是一种乐观锁,它放宽了加锁的条件,允许多个线程同时进行读操作。共享锁则可以同时由多个线程持有,例如ReadWriteLock读写锁,它允许一个资源可以被多线程同时进行读操作。

自旋锁:由于Java中的线程是与操作系统中的线程一一对应的,所以当一个线程在获取锁(比如独占锁)失败后,会被切换到内核状态而被挂起。当该线程获取到锁时又需要将其切换到内核状态而唤醒该线程。而从用户状态切换到内核状态的开销是比较大的,在一定程度上会影响并发性能。自旋锁则是,当前线程在获取锁时,如果发现锁已经被其他线程占有,它不马上阻塞自己,在不放弃CPU使用权的情况下,多次尝试获取(默认次数是10,可以使用-XX:PreBlockSpinsh参数设置该值),很有可能在后面几次尝试中其他线程已经释放了锁。如果尝试指定的次数后仍没有获取到锁则当前线程才会被阻塞挂起。由此看来自旋锁是使用CPU时间换取线程阻塞与调度的开销,但是很有可能这些CPU时间白白浪费了。【并不是马上阻塞自己,而是继续使用当前占有的CPU使用权来重试获取锁】

 

Synchronzied 和 Lock 的主要区别如下:
  • 存在层面:Syncronized 是Java 中的一个关键字,存在于 JVM 层面,Lock 是 Java 中的一个接口
  • 锁的释放条件:1. 获取锁的线程执行完同步代码后,自动释放;2. 线程发生异常时,JVM会让线程释放锁;Lock 必须在 finally 关键字中释放锁,不然容易造成线程死锁
  • 锁的获取: 在 Syncronized 中,假设线程 A 获得锁,B 线程等待。如果 A 发生阻塞,那么 B 会一直等待。在 Lock 中,会分情况而定,Lock 中有尝试获取锁的方法,如果尝试获取到锁,则不用一直等待
  • 锁的状态:Synchronized 无法判断锁的状态,Lock 则可以判断
  • 锁的类型:Synchronized 是可重入,不可中断,非公平锁;Lock 锁则是 可重入,可判断,可公平锁
  • 锁的性能:Synchronized 适用于少量同步的情况下,性能开销比较大。Lock 锁适用于大量同步阶段:
  • Lock 锁可以提高多个线程进行读的效率(使用 readWriteLock)
  • 在竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;
  • ReetrantLock 提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。
posted @   小兵要进步  阅读(118)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)

侧边栏
点击右上角即可分享
微信分享提示