Java之锁总结

线程互斥

由于线程调度时间无法确定,获取/放弃CPU没有固定时间,再运行过程中可能时间片到后需放弃CPU,如果多个线程间共享
数据,那么对共享数据的修改需要是事物性的操作,此时操作共享数据的代码称为临界区,一个线程进入临界区后,只有执行完临界区里所有代码后,其他线程才能进入临界区,也即对临界区进行加锁操作,进入临界区需获取锁,出临界区时释放锁,只有获取锁成功后才能进入临界区。

Java中提供了三种实现锁语义的表达方式,synchronized关键字、ReentrantLock、ReadWriteLock,介绍三种锁之前先简单介绍几个锁类别概念。

悲观锁

悲观地认为多个线程会同时进入临界区的概率极大,因此进入临界区前必须加锁。

乐观锁

乐观地认为多个线程会同时进入临界区概率比较小,进入临界区时并不加锁,因此在进入临界区后必要时需要检测共享数据的状态,当发现状态不一致时,进行重试或Fail-Fast。乐观锁在大型并发场景下失败的次数可能比较高,要进行不断重试,性能可能还不如悲观锁。

自旋锁SPIN

在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间取挂起线程恢复现场并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一下“,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。

自旋等待不能代替阻塞。自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,因此,如果锁被占用的时间很短,自旋当代的效果就会非常好,反之,如果锁被占用的时间很长,那么自旋的线程只会拜拜浪费处理器资源。因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数没有成功获得锁,就应当使用传统的方式去挂起线程了。

可重入锁

可重入锁,也叫做递归锁,指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。ReentrantLock 和synchronized 都是可重入锁。可重入锁最大的作用是避免死锁。

更多锁类别介绍:

http://www.importnew.com/19472.html

synchronized

Java中锁是基于对象实现的,即Monitor Lock,如果持有锁则会在对象头MarkWord中进行标记,因此synchronized使用的时候必然与某个Object关联. synchronized修饰静态方法是关联当前类class实例,非静态则是this对象,synchronized代码块也是this对象,当然也可以直接synchronized(Object)明确指定某个对象。通过synchronized获取的锁会在退出临界区后自动释放。

Lock

相比synchronized,Lock提供了更灵活的语义表达,比如:设置获取锁的等待时间、声明获取锁等待时可被中断,控制锁的范围及释放顺序、判断当前线程是否持有锁等。
java.util.concurrent.locks.Lock接口如下:

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;

    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    void unlock();

    Condition newCondition();
}

synchronized关键字可自动释放锁,Lock是普通对象,需手动释放,使用lock()方法是需将lock()的代码用try-catch/finaly保护起来:

 Lock l = ...;
 l.lock();
 try {
     // access the resource protected by this lock
 } finally {
     l.unlock();
 }

tryLock类似:
Lock lock = …;
if (lock.tryLock()) {
try {
// manipulate protected state
} finally {
lock.unlock();
}
} else {
// perform alternative actions
}

Lock一般声明为成员变量,才能在多个线程间共享控制临界区。

ReentrantLock

ReentrantLock,可重入锁,是唯一实现了Lock接口的类,并提供了更多的方法。

ReentrantLock是乐观锁,默认为非公平锁,内部依靠AbstractQueuedSynchronizer(AQS)实现,AbstractQueuedSynchronizer使用CAS加自旋。多线程并发的执行通过某种共享状态来同步,只有当状态满足X条件,才能触发线程执行,这个语义可以称之为同步器,所有的锁机制都可以基于同步器定制来实现的.

CAS存在ABA问题,AtomicStampedReference这个类通过带版本号的变量解决ABA问题。

关于CAS(Compare and Set)以及CAS本身可能存在的问题及解决方式可参考:

http://zhuanlan.51cto.com/art/201706/542760.htm

ReadWriteLock

读写锁,多个线程可同时读,只能有一个线程写,读写不能同时进行

public interface ReadWriteLock {

    //获取读锁
    Lock readLock();

    //获取写锁
    Lock writeLock();
}

ReentrantReadWriteLock

实现了ReadWriteLock接口,内部一把读锁,一把写锁,同样基于AbstractQueuedSynchronizer实现。

无锁

一些数据天生就是线程安全的,对齐操作不需加锁。比如:

  • 无状态对象,在堆上无数据,其状态都通过参数传入,参考Servlet

  • volatile, CopyOnWriteArrayList内部数据使用了volatile修饰。

  • ThreadLocal

线程同步

Lock用于线程间互斥,有时我们需要线程间相互协调共同完成一些任务,因此其他机制实现线程同步。

wait/notify/notifyAll

wait/notify/notifyAll是Object方法,实现同步都是基于对象头的MarkWord,因此只有被标记为锁的对象才能调用者三个方法,也就是说wait/notify/notifyAll需在synchronized同步的代码块中被锁对象所调用。

public final void wait() throws InterruptedException {
    wait(0);
}

public final native void wait(long timeout) throws InterruptedException;

public final native void notify();

public final native void notifyAll();

三种方法都是native,互相协调同步是需基于同一个对象锁。

  • wait会释放当前锁,并阻塞当前线程;
  • notify也会释放当前锁,但是会将锁对象上因wait挂起的线程随机选择一个放入可调度执行队列中,当wait线程重新调度执行时重新获取到锁后会执行wait的后续代码,并且notify也不会阻塞当前线程;
  • notifyAll同notify类似,只是会唤醒所有使用wait挂起的线程, 由于需要重新获取锁才能执行,被唤醒的线程只能获取锁后依次执行

Condition

替代Object的wait/notify/notfyAll,同wait/notify/notfyAll与一个Object绑定一样,一个Condition必须与一个Lock绑定,但是一个Lock可以关联多个Condition, Lock接口中提供了newCondition()获取一个Condition。

public interface Condition {
    void await() throws InterruptedException;   
    void awaitUninterruptibly();
    long awaitNanos(long nanosTimeout) throws InterruptedException; 
    boolean awaitUntil(Date deadline) throws InterruptedException;  
    void signal();
    void signalAll();
}

通过Condition接口方法可以看出其相比wait/notify/notfyAll提供的更灵活语义,注意Condition是类,会继承Object方便,同样拥有wait/notify/notfyAll,因此阻塞时要调用await方法。

Condition的使用示例以及内部实现原理可参考:

http://www.importnew.com/9281.html

CountDownLatch

//参数count为计数值
public CountDownLatch(int count);  
//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public void await() throws InterruptedException { };  
//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException;  
//将count值减1
public void countDown(); 

初始化一个count值,基于AQS实现,await()阻塞当前线程,countDown()会递减内部计数器,当计数器为0时会唤醒所用调用await()阻塞的线程。因此其适用于一个或多个线程等待其他多个线程完成任务后才能执行的场景,可参考:

http://www.importnew.com/15731.html

CyclicBarrier

回环栅栏,控制N个线程到达相同的状态后才开始继续执行。

public CyclicBarrier(int parties);
public int await() throws InterruptedException, BrokenBarrierException;
public int await(long timeout, TimeUnit unit) throws InterruptedException,BrokenBarrierException,TimeoutException; 

Semaphore

信号量,同样基于AbstractQueuedSynchronizer实现,控制对共享资源最多N个线程同时操作。

//参数permits表示许可数目,即同时可以允许多少线程进行访问
public Semaphore(int permits);
public void acquire() throws InterruptedException;    //获取一个许可
public void acquire(int permits) throws InterruptedException;    //获取permits个许可
public void release(); //释放一个许可
public void release(int permits);    //释放permits个许可

Exchanger

双向SynchronousQueue,用于两个工作线程之间交换数据,当一个线程到达exchange调用点时,如果它的伙伴线程此前已经调用了此方法,那么它的伙伴会被调度唤醒并与之进行对象交换,然后各自返回。如果它的伙伴还没到达交换点,那么当前线程将会被挂起,直至伙伴线程到达完成交换正常返回;或者当前线程被中断—抛出中断异常;又或者是等候超时——抛出超时异常。

public V exchange(V x) throws InterruptedException;
public V exchange(V x, long timeout, TimeUnit unit) throws InterruptedException, TimeoutException

示例

CountDownLatch,CyclicBarrier,Semaphore使用示例:

http://www.importnew.com/21889.html

分布式锁

多进程协调同步,一般借助分布式框架,ZooKeeper使用比较多。

猜你喜欢

转载自blog.csdn.net/jinjiating/article/details/78591492
今日推荐