基本用法
Java多线程可以使用synchronized关键字来实现线程间同步,不过JDK 1.5新增加的ReentrantLock类也能达到同样的效果,并且在扩展功能上更加强大,如具有嗅探锁定、多路分支通知等功能。
ReentrantLock类
可以直接new一个ReentranLock。然后再后面的方法中使用该对象的方法来进行同步处理。
例如:
1 | private Lock lock = new ReentrantLock(); |
lock()方法
lock方法用于锁定当前的ReentrantLock对象。在此之后,所有的方法都是同步运行的了。与synchoronized代码块一样,其他调用该lock锁的代码都会被同步,等待该锁释放。
例如:
1 | private Lock lock = new ReentrantLock(); |
tryLock()
public boolean tryLock()方法的作用是嗅探拿锁,如果当前线程发现锁被其他线程持有了,则返回false,程序继续执行后面的代码,而不是呈阻塞等待锁的状态。
例如:
线程类:
1 | package service; |
运行类:
1 | package test; |
结果:
1 | A获得锁 |
注意这里为synchoronized不具有的方法,即尝试获取锁,如果没获得,就继续执行下去,而不是阻塞。
tryLock(long timeout, TimeUnit unit)
这里的方法与上面方法一样,但这里加了延迟时间。如果当前线程在指定的timeout内持有了锁,则返回值是true,超过时间则返回false。参数timeout代表当前线程抢锁的时间。
lockInterruptibly()
public void lockInterruptibly()方法的作用是当某个线程尝试获得锁并且阻塞在lockInterrup-tibly()方法时,该线程可以被中断。
unlock()方法
unlock方法会解除该锁,也就是说,后面的代码不再受该该锁的限制,可以异步执行了。
例如:
1 | private Lock lock = new ReentrantLock(); |
整体例子
服务类:
1 | package service; |
线程类:
1 | package extthread; |
运行类:
1 | package test; |
结果:
1 | ThreadName-Thread-01 |
从程序运行结果来看,只有当当前线程输出完毕之后将锁释放,其他线程才可以继续抢锁并输出,每个线程内输出的数据是有序的,从1到5,因为当前线程已经持有锁,具有互斥排他性,但线程之间输出的顺序是随机的,即谁抢到锁,谁输出。
condition对象
关键字synchronized与wait()、notify()/notifyAll()方法相结合可以实现wait/notify模式,ReentrantLock类也可以实现同样的功能,但需要借助于Condition对象。Condition类是JDK 5的技术,具有更好的灵活性,例如,可以实现多路通知功能,也就是在一个Lock对象中可以创建多个Condition实例,线程对象注册在指定的Condition中,从而可以有选择性地进行线程通知,在调度线程上更加灵活。
await()方法
await方法与synchonized的wait方法类似。调用该方法可以使线程进入到wait状态。
例如:
1 | public class MyService { |
这里的调用await方法机会调用对应Condition对象的await方法。使线程进入wait状态。直到该Condtition的signal/signalAll方法被调用,才会将其进行唤醒。
signal()方法
signal()与synchonized的notify方法类似。即唤醒等待队列中的其中一个线程。
例如:
1 | public class MyService { |
signalAll()方法
同样的,signalAll()方法类似于synchonized的notifyAll()方法。即唤醒所有的等待该Condition的线程。
示例:
1 | public class MyService { |
公平锁与非公平锁
公平锁:采用先到先得的策略,每次获取锁之前都会检查队列里面有没有排队等待的线程,没有才会尝试获取锁,如果有就将当前线程追加到队列中。
非公平锁:采用“有机会插队”的策略,一个线程获取锁之前要先去尝试获取锁而不是在队列中等待,如果获取锁成功,则说明线程虽然是后启动的,但先获得了锁,这就是“作弊插队”的效果。如果获取锁没有成功,那么才将自身追加到队列中进行等待。
而ReentrantLock默认是非公平锁,与synchoronized一致,也可以在构造函数中传入一个布尔参数来构造出公平锁。
即:
1 | Lock lock = new ReentrantLock(true); |
相关的方法
Lock-public int getHoldCount()
public int getHoldCount()方法的作用是查询“当前线程”保持此锁定的个数,即调用lock()方法的次数。
Lock-public final int getQueueLength()
public final int getQueueLength()方法的作用是返回正等待获取此锁的线程估计数,例如,这里有5个线程,其中1个线程长时间占有锁,那么调用getQueueLength()方法后,其返回值是4,说明有4个线程同时在等待锁的释放。
Lock-public int getWaitQueueLength(Condition condition)
public int getWaitQueueLength(Condition condition)方法的作用是返回等待与此锁相关的给定条件Condition的线程估计数。例如,这里有5个线程,每个线程都执行了同一个Condition对象的await()方法,则调用getWaitQueueLength(Condition condition)方法时,其返回的int值是5。
Lock-public final boolean hasQueuedThread(Thread thread)
public final boolean hasQueuedThread(Thread thread)方法的作用是查询指定的线程是否正在等待获取此锁,也就是判断参数中的线程是否在等待队列中。
Lock-public final boolean hasQueued-Threads()
public final boolean hasQueuedThreads()方法的作用是查询是否有线程正在等待获取此锁,也就是等待队列中是否有等待的线程。
Lock-public boolean hasWaiters(Condition condition)
public boolean hasWaiters(Condition condition)方法的作用是查询是否有线程正在等待与此锁有关的condition条件,也就是是否有线程执行了condition对象中的await()方法而呈等待状态。而public int getWaitQueueLength(Condition condition)方法的作用是返回有多少个线程执行了condition对象中的await()方法而呈等待状态。
Lock-public final boolean isFair()
public final boolean isFair()方法的作用是判断是不是公平锁。
Lock-public boolean isHeldByCurrentThread()
public boolean isHeldByCurrentThread()方法的作用是查询当前线程是否保持此锁。
Lock-public boolean isLocked()
public boolean isLocked()方法的作用是查询此锁是否由任意线程保持,并没有释放。
Lock-public boolean await(long time,TimeUnit unit)-重要
public boolean await(long time,TimeUnit unit)方法的作用和public final native void wait(long timeout)方法一样,都具有自动唤醒线程的功能。即在规定时间内没有线程对其进行唤醒,则自动进行唤醒。
Lock-public long awaitNanos(long nanosTimeout)
public long awaitNanos(long nanosTimeout)方法的作用和public final native void wait(long timeout)方法一样,都具有自动唤醒线程的功能,不过时间单位是纳秒(ns)。
Lock-public boolean awaitUntil(Date deadline)-重要
public boolean awaitUntil(Date deadline)方法的作用是在指定的Date结束等待。
Lock-public void awaitUninterruptibly()
public void awaitUninterruptibly()方法的作用是实现线程在等待的过程中,不允许被中断。
ReentrantReadWriteLock类
ReentrantLock类具有完全互斥排他的效果,同一时间只有一个线程在执行ReentrantLock.lock()方法后面的任务,这样做虽然保证了同时写实例变量的线程安全性,但效率是非常低下的,所以JDK提供了一种读写锁——ReentrantReadWriteLock类,使用它可以在进行读操作时不需要同步执行,提升运行速度,加快运行效率。
读写锁有两个锁:一个是读操作相关的锁,也称共享锁;另一个是写操作相关的锁,也称排他锁。
读锁之间不互斥,读锁和写锁互斥,写锁与写锁互斥,因此只要出现写锁,就会出现互斥同步的效果。
读操作是指读取实例变量的值,写操作是指向实例变量写入值。
ReentrantLock类的缺点
与ReentrantReadWriteLock类相比,ReentrantLock类的主要缺点是使用ReentrantLock对象时,所有的操作都同步,哪怕只对实例变量进行读取操作,这样会耗费大量的时间,降低运行效率。
ReentrantReadWriteLock类的使用
读读共享
由于读并不会改变数据内容,所以相当于可异步操作。因此这个锁可以共享。
例如:
1 | package test; |
写写互斥
由于写会改变数据内容,因此需要进行同步处理,这里可以获得一个排他锁。
例如:
1 | package service; |
结果:
1 | 获得写锁A1414899878671 |
读写互斥、写读互斥
实际上只要涉及到写操作,即修改数据,那么就必定需要排他锁。
例如:
1 |
|
运行类:
1 | package test; |
结果:
1 | 获得读锁1414899977328 |
总结
ReentrantLock与synchoronized的相同点
- ReentrantLock与synchoronized都默认是非公平锁。
- ReentrantLock与synchoronized都默认是可重入锁。
- Object类中的wait()方法相当于Condition类中的await()方法。
- Object类中的wait(long timeout)方法相当于Condition类中的await(long time,TimeUnit unit)方法。
- Object类中的notify()方法相当于Condition类中的signal()方法。
- Object类中的notifyAll()方法相当于Condition类中的signalAll()方法。
ReentrantLock与synchoronized的不同点
synchronized是关键字属于JVM层面,Lock是具体类是api层面的锁。
synchronized是重量级锁,重量级锁需要将线程从内核态和用户态来回切换。而ReentrantLock是轻量级锁。
synchronized不需要用户去手动释放锁,当synchronized代码执行完后系统会自动让线程释放对锁的占用。需要lock,unlock配合try finally完成。
Condition支持不响应中断,而Object不支持,也就是Object只要有中断就要响应。
Condition支持多个等待队列(new多个Condition对象),而Object只有一个等待队列,但两者都只要一个同步队列;即可以实现分组唤醒线程。
Condition支持截止时间设置,而Object是超时时间设置,支持截止时间设置,不用计算需要等多久。
ReentrantLock缺点
lock 必须在 finally 块中释放。否则,如果受保护的代码将抛出异常,锁就有可能永远得不到释放!这一点区别看起来可能没什么,但是实际上,它极为重要。忘记在 finally 块中释放锁,可能会在程序中留下一个定时炸弹,当有一天炸弹爆炸时,您要花费很大力气才有找到源头在哪。而使用同步,JVM 将确保锁会获得自动释放。
当 JVM 用 synchronized 管理锁定请求和释放时,JVM 在生成线程时能够包括锁定信息。这些对调试非常有价值,因为它们能标识死锁或者其他异常行为的来源。 Lock 类只是普通的类,JVM 不知道具体哪个线程拥有 Lock 对象,不方便调试。