Java中的锁

锁是一种线程同步机制,与同步代码块不同,锁显得更加精致。当然锁底层还是离不开synchronized关键字。

从Java5开始,在包java.util.concurrent.locks就提供了几种锁的实现。

来看一个简单的锁

先看用同步代码块是怎么实现的:

public class Counter {
    private int count = 0;
    
    public int inc () {
        synchronized (this) {
            return ++count;
        }
    }
}
复制代码

注意到synchronized (this)inc()方法内,确保了同一时间只有一个线程可以执行return ++count

如果使用锁的话,就是以下写法:

public class Counter {
    private Lock lock = new Lock();
    private int count = 0;
    
    public int inc () {
        lock.lock();
        int newCount = ++count;
        lock.unlock();
        return newCount;
    }
}
复制代码

锁这个类是怎么实现的呢?一个简单的写法是:

public class Lock {
    private boolean isLocked = false;
    
    public synchronized void lock() thrwos InterruptedException {
        while (isLocked) {
            wait();
        }
        isLocked = true;
    }
    
    public synchronized void unlock() {
        isLocked = false;
        notify();
    }
}
复制代码

当加锁时会进行循环等待isLocked变为true,这一过程也被称为自旋,因此这种锁也被称为自旋锁。

isLockedtrue时,调用lock()的线程将停在wait()调用中。万一线程在此时没有收到notify()就挂了,线程会重新检查isLocked条件以查看继续执行是否安全,而不是说被唤醒了就意味线程是安全的。如果isLockedfalse,则线程退出while (isLocked)循环,并将isLocked设置为true,其他调用lock()的线程将锁定Lock实例。

可重入锁

Java中的同步块是可重入的。这意味着,如果Java线程进入了同步的代码块,从而锁定了同步块的监视器,则该线程可以进入在同一监视对象上其他Java同步代码块。 这是一个例子:

public class Reentrant {
    public synchronized outer () {
        inner();
    }
    
    public synchronzied inner () {
        // do something
    }
}
复制代码

如果线程调用了outer(),那么也会调用inner(),因为两个方法由一个监视器同步。如果线程已经拥有监视器上的锁,则它可以访问在同一监视器上的所有同步块。这称为重入,线程可以重新进入已经为其持有锁的任何代码块。

但是,同步块可重入不代表类可重入。改写一下类,调用outer()的线程将在inner()方法的lock.lock()内部被阻塞。

public class Reentrant2{
    Lock lock = new Lock();
    
    public outer(){
        lock.lock();
        inner();
        lock.unlock();
    }
    
    public synchronized inner(){
        lock.lock();
        //do something
        lock.unlock();
    }
}
复制代码

想让Lock类可重入,代码需要小小改变。

public class Lock{

    boolean isLocked = false;
    Thread  lockedBy = null;
    int     lockedCount = 0;

    public synchronized void lock() throws InterruptedException{
        Thread callingThread = Thread.currentThread();
        while(isLocked && lockedBy != callingThread){
            wait();
        }
        isLocked = true;
        lockedCount++;
        lockedBy = callingThread;
    }

    public synchronized void unlock(){
        if(Thread.curentThread() == this.lockedBy){
            lockedCount--;
            if(lockedCount == 0){
            isLocked = false;
            notify();
            }
        }
    }

    ...
}
复制代码

这里自旋时考虑到了锁实例,如果锁被释放或者调用线程为锁住类实例的线程,就不会自旋等待。

此外,我们需要计算锁被同一线程锁定的次数。 否则,即使已多次锁定该锁,一次对unlock()的调用也将解除该锁的锁定。 我们不希望在锁定该锁的线程执行与lock()调用相同数量的unlock()调用之前将其解锁。

锁的公平性

Java的同步块无法保证进入的线程顺序。因此,如果多线程一直在争夺同一同步块的访问权限,则存在一个或多个线程永远不会访问权的风险,这称为饥饿。为了避免这种情况,锁应该是公平的。

锁释放

当锁住的代码块引发异常,就必须从finally子句内部调用unlock()。这样做可以确保锁定已解锁,以便其他线程可以锁定它。这是一个例子:

lock.lock();
try{
    //do critical section code, which may throw exception
} finally {
    lock.unlock();
}
复制代码

这个小结构可以确保在关键部分的代码中引发异常时,可以解除锁定。如果未从finally子句内部调用unlock(),并且从关键部分引发了异常,则锁将永远保持锁定状态,从而导致在该实例上调用lock()的所有线程无限期地停止。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享