Java中的锁主要有内置的synchronized以及基于AbstractQueuedSynchronizer的ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock和CyclicBarrier。
synchronized
synchronized是Java中常用的加锁方式。synchronized主要解决了原子性问题,即确保多个线程可以互斥的访问竞争资源。synchronized可以修饰普通方式、静态方法和代码块。
synchronized主要是理解锁住的是什么对象,多个线程会不会互斥就取决于他们所获取的锁对象是不是同一个对象。例如,synchronized(this)那么这就是锁住的当前对象的示例,所有该对象示例调用的方法都会互斥执行,效果等同于synchronized加在普通方法上;synchronized加在static方法上,那么就等同于加在类上,那么所有的static方法调用都会互斥执行。
synchronized在JDK1.6后,进行了多项优化,现在已经不是那么重量级了。适应性自旋锁,就是动态的调整自旋次数,由上一次自旋时间和拥有者状态来决定。
锁消除,JVM检测不到共享数据存在竞争的情况下,会对同步锁进行消除。
锁粗化,加锁时的原则就是要将锁的区域变小,就是能不加锁的地方不要加锁,但这样做的时候可能会导致多个加锁解锁区域,这样反而性能更差,锁粗化就是将多个锁合并为一个锁来操作。
轻量级锁,在没有多线程竞争的条件下,减少重量级锁使用monitor(依赖底层操作系统Mutex实现)带来的性能问题。
偏向锁,减少轻量级锁时CAS操作的次数和路径。偏向锁的释放,需要其它线程主动来竞争才能释放。
ReentrantLock
在JDK对synchronized多次优化后,使用synchronized能满足基本的锁需求。那么ReentrantLock在什么情况下使用呢?或者说ReentrantLock与synchronized有什么区别?ReentrantLock是非结构化的(代码块),不需要像synchronized那样在代码块中,而且需要显示释放的,可以在方法1中lock,在方法2中unlock。
ReentrantLock支持公平和非公平两种模式。
ReentrantLock支持超时、中断、轮询、条件(conditions)。
Semaphore
Semaphore是持有一些许可(permits)的计数信号量,acquire获取一个许可,release释放一个许可。Semaphore支持公平(fair)和非公平(nonfair)方式。在资源访问控制上,建议采用公平方式,以避免线程饥饿问题.
在其他同步控制上,建议采用非公平方式,可以最大化吞吐量。
CountDownLatch
CountDownLatch让一个或者多个线程等待另外的一个或者多个线程完成任务,支持超时和中断。例如,将CountDownLatch设置为1,然后让很多线程await,那么这些线程就几乎是并发执行的;需要准备很多任务作,在满足多个条件后(e.g.等待几个线程分别处理数据),等待线程才能继续执行。CountDownLatch的计数器不能重置。
点击查看CountDownLatch应用示例代码
ReentrantReadWriteLock
ReentrantReadWriteLock是读写锁,支持公平和非公平模式,读锁是共享锁,可以被多个线程获取,写锁是独占锁,只能被一个线程获取。ReentrantReadWriteLock适用于大量读少量写的业务场景。如果一个线程拥有写锁,那么它还可以获取读锁,反之则不行。
ReentrantReadWriteLock总共只能创建65535个读锁和65535个写锁,这是因为继承的AbstractQueuedSynchronizer中的记录锁的个数的state是32位的,而ReentrantReadWriteLock又分共享锁(读锁)和独占锁(写锁),所以state高16位用来标识读锁,低16位来标识写锁。
线程获取读锁:没有其他线程的写锁。
没有写请求,或者有写请求,但是是本身线程的写请求。
线程获取写锁:没有其他线程的读锁。
没有其他线程的写锁。
CyclicBarrier
谈到CyclicBarrier,就肯定要知道它和CountDownLatch的区别,CyclicBarrier强调的是多个线程互相等待,可以理解成就是一个栅栏,所有的线程都跑到一个栅栏的位置,然后才能继续下面的执行。CyclicBarrier在所有线程到达之后,计数器自动重置。如果调用reset手动重置计数器,那么会抛出BrokenBarrierException,即所有未达到栅栏的线程会立即被唤醒。CyclicBarrier中所有的线程要么都成功,要么都失败。