锁:Sychronized、Lock

时间:2022-07-24
本文章向大家介绍锁:Sychronized、Lock,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

锁是用来在多线程并发阶段保障数据同步的重要手段,防止出现脏数据,加锁代码在某个时间点只能由一个线程运行,其他线程等待。

普遍熟知的锁是synchronized关键字和Lock类。

一、synchronized关键字

这个在同步中是最常用的,分成对象锁和类锁,可以对方法和代码块进行加锁。

1、对象锁,锁住的是对象,用于
synchronized(this){}
synchronized(Object){}
2、类锁,锁住的是该类的所有实例  
synchronized(Object.class){}

synchronized锁是在字节码级别上的锁,可以用javap(java自带的反编译工具)查看

例如查看这一段代码的字节码指令

javap执行结果如下:

可以看到是由monitorenter和monitorexit指令来实现加锁的,出现两次的monitorexit是为了确保解锁完成。

二、Lock

Lock是接口,有以下几个方法

1、获取锁
lock()
2、获取锁,除非线程中断
lockInterruptibly()
3、尝试获取锁,在锁可用获取锁
tryLock()
4、解锁
unlock()
5、返回该Lock的condition
newCondition()

Lock是需要配合Condition使用的,其有await和signal方法,类似于Object的wait和notify方法。

主要看一下Lock的实现类ReentrantLock。

(1)ReentrantLock默认是非公平锁(获取锁的顺序不是按照申请顺序来)

public ReentrantLock() {
    sync = new NonfairSync();
}

也可以用参数指定是公平锁OR非公平锁

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

FairSync和NonFairSync是继承Sync的,而Sync是继承自AbstractQueuedSynchronizer(简称AQS),AQS里面维护了锁的当前状态和线程等待队列。

(2)底层加锁方法

每个资源都有一个状态字段

private volatile int state;

1、非公平锁

final void lock() {
    1、当资源状态为0的时候,更改为1 
    if (compareAndSetState(0, 1))
        2、当前线程获取锁
        setExclusiveOwnerThread(Thread.currentThread());
    else
        3、资源状态不为0的时候 
        acquire(1);
}

状态为0时,将拥有锁的线程设置为当前线程

void setExclusiveOwnerThread(Thread thread) {
    exclusiveOwnerThread = thread;
}

状态非0时,尝试获取锁

void acquire(int arg) {
    if (!tryAcquire(arg) &&
        //addWaiter是往线程等待队列中新增一个节点 
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        //中断当前线程
        selfInterrupt();
}

tryAcquire方法指向的就是nonfairTryAcquire方法:

final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        1、获取锁状态
        int c = getState();
        if (c == 0) {
            2、当资源状态为0的时候,更改为1,当前线程获取锁
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        3、当前线程为拥有锁的线程 
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            4、修改锁状态
            setState(nextc);
            return true;
        }
        return false;
    }

addWaiter方法是在AQS里面实现的,往线程等待队列尾部新增一个节点。

而acquireQueued方法作用是阻塞线程,重试获取锁,死循环,直至获取锁。

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                1、获取前一个节点
                final Node p = node.predecessor();
                2、如果是头结点并且获取了锁 
                if (p == head && tryAcquire(arg)) {
                    3、将获取到锁的节点设置为头结点  
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                4、shouldParkAfterFailedAcquire是获取
                锁失败后检查上一个节点的状态,判断是否
                要阻塞当前线程  
                if (shouldParkAfterFailedAcquire(p, node) &&
                    5、调用线程阻塞,判断是否已中断
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

parkAndCheckInterrupt方法:

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}
阻塞当前线程 
 public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    UNSAFE.park(false, 0L);
    setBlocker(t, null);
}

可以看出,最终是调用Unsafe的park方法实现加锁的。

2、公平锁

公平锁和非公平锁的区别只有一个,就是在tryAcquire方法中多了一个hasQueuedPredecessors方法。

boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    1、获取锁状态
    int c = getState();
    if (c == 0) {
        2、判断当前线程在等待队列中是否是第二个结点,
        也就是等待时间最长的节点
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    3、当前线程已经拥有锁
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

hasQueuedPredecessors方法,判断当前线程是否是第二个节点,也就是等待时间最长的节点

boolean hasQueuedPredecessors() {
        Node t = tail; 
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

(3)释放锁

1、unlock方法

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

2、release方法

boolean release(int arg) {
    1、释放锁,也就是修改锁状态  
    if (tryRelease(arg)) {
        Node h = head;
        2、释放完成后,唤醒下一个等待线程。
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
boolean tryRelease(int releases) {
    1、获取剩余状态
    int c = getState() - releases;
    2、判断当前线程是否拥有锁
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    3、只有当锁状态为0时,才是完全释放 
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

第3步是因为同一个线程多次调用lock时,锁状态都会相应的新增,所以可以从这里看出,如果多次调用Lock进行加锁,在解锁的时候也要多次调用unLock方法。

三、Synchronized和Lock的区别

(1)synchronized是关键字,Lock是类,类拥有更大的自由度。

(2)synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁。

(3)synchronized会自动释放锁,Lock需要执行unLock方法。

(4)synchronized是非公平的,Lock两者都可。

(5)Lock可以通过某个condition精确唤醒某个线程。

ok,以上就是synchronized和Lock的粗略分析。