在Java多线程编程中,除了可以使用synchronized关键字实现线程同步外,从JDK1.5开始,新增了ReentrantLock、ReentrantReadWriteLock等类,同样能实现同步效果,而且在使用上更加方便。
ReentrantLock是可重入互斥锁,调用它的lock()方法获取锁,unlock()方法释放锁。
当一个线程调用ReentrantLock的lock()方法时,如果这个锁没有被另一个线程保持,则获取该锁并立即返回,将锁的保持计数设置为1;如果当前线程已经保持了该锁,则把保持计数加1,并且该方法立即返回;如果这个锁被另一个线程保持,则在获得锁之前,该线程将一直处于休眠状态,此时锁的保持计数被设置为1。
当一个线程调用ReentrantLock的unlock()方法时,如果当前线程是此锁的拥有者,则将保持计数减1,如果保持计数变成了0,则释放该锁。如果当前线程不是该锁的持有者,则抛出IlletalMonitorStateException。
ReentrantLock实现了Lock接口,与Lock类相关的一个类是Condition,借助Condition可以实现线程间的等待/通知。Condition也是Java5中出现的技术,它具有比object.wait()和object.notify()更好的灵活性,比如实现多路通知功能,即一个Lock可以创建多个Condition(即对象监视器)实例,线程对象可以注册在指定的Condition中,从而有选择性地进行线程通知,在调度线程上更加灵活。
使用Condition实现等待/通知的样例代码如下:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class MyService {
private Lock lock = new ReentrantLock();
public Condition condition = lock.newCondition();
public void await() {
try {
lock.lock();
System.out.println("await时间为" + System.currentTimeMillis());
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void signal() {
try {
lock.lock();
System.out.println("signal时间为" + System.currentTimeMillis());
condition.signal();
} finally {
lock.unlock();
}
}
}
class ThreadA extends Thread {
private MyService service;
ThreadA(MyService service) {
super();
this.service = service;
}
@Override
public void run() {
service.await();
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
MyService service = new MyService();
ThreadA a = new ThreadA(service);
a.start();
Thread.sleep(3000);
service.signal();
}
}
await时间为1538418609554
signal时间为1538418612513
使用多个Condition实现通知部分线程:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class MyService {
private Lock lock = new ReentrantLock();
private Condition conditionA = lock.newCondition();
private Condition conditionB = lock.newCondition();
void awaitA() {
try {
lock.lock();
System.out.println("begin awaitA时间为" + System.currentTimeMillis()
+ " ThreadName=" + Thread.currentThread().getName());
conditionA.await();
System.out.println("end awaitA时间为" + System.currentTimeMillis()
+ " ThreadName=" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
void awaitB() {
try {
lock.lock();
System.out.println("begin awaitB时间为" + System.currentTimeMillis()
+ " ThreadName=" + Thread.currentThread().getName());
conditionB.await();
System.out.println("end awaitB时间为" + System.currentTimeMillis()
+ " ThreadName=" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
void signalAll_A() {
try {
lock.lock();
System.out.println("signalAll_A时间为" + System.currentTimeMillis()
+ " ThreadName=" + Thread.currentThread().getName());
conditionA.signalAll();
} finally {
lock.unlock();
}
}
public void signalAll_B() {
try {
lock.lock();
System.out.println("signalAll_B时间为" + System.currentTimeMillis()
+ " ThreadName=" + Thread.currentThread().getName());
conditionB.signalAll();
} finally {
lock.unlock();
}
}
}
class ThreadA extends Thread {
private MyService service;
ThreadA(MyService service) {
super();
this.service = service;
}
@Override
public void run() {
service.awaitA();
}
}
class ThreadB extends Thread {
private MyService service;
ThreadB(MyService service) {
super();
this.service = service;
}
@Override
public void run() {
service.awaitB();
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
MyService service = new MyService();
ThreadA a = new ThreadA(service);
a.setName("A");
a.start();
ThreadB b = new ThreadB(service);
b.setName("B");
b.start();
Thread.sleep(3000);
service.signalAll_A();
}
}
begin awaitA时间为1538418670871 ThreadName=A
begin awaitB时间为1538418670871 ThreadName=B
signalAll_A时间为1538418673871 ThreadName=main
end awaitA时间为1538418673871 ThreadName=A
通常一个线程是在某个条件c下需要被阻塞,这时我们让该线程在条件c对应的条件对象condition下等待。另一个线程使条件c被解除,同时它调用条件对象condition的signalAll()方法,唤醒所有等待线程,告诉它们可以继续执行了。
这时所有在此条件对象上等待的线程从等待集中移出,它们再次成为可运行的,调度器将再次激活它们。一旦锁成为可用的,它们中的某个将从await调用返回,获得该锁并从被阻塞的地方继续执行。由于其执行,条件c可能又被满足,导致其他的原来被阻塞的线程在执行的时候仍然需要被阻塞,这便是“虚假唤醒”。因此,条件c需要循环进行判断。
condition.await()的典型调用方式如下:
while(!okToProceed){
condition.await();
}
关于此可以看下我的https://blog.csdn.net/nlznlz/article/details/89042809这篇文章中对阻塞队列ArrayBlockingQueue的源码分析,其中便体现了这个道理。
锁Lock分为公平锁和非公平锁,公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即FIFO的顺序。非公平锁则是一种获取锁的抢占机制,是随机获得锁的,先来的不一定先得到锁,这个方式可能造成某些线程一直得不到锁。
下面的图形象化展示了公平锁和非公平锁的区别:
公平锁与非公平锁的一个重要区别就在于上图中的2、6、10那个步骤,对应源码如下:
//非公平锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
//公平锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
分析以上代码,我们可以看到公平锁就是在获取锁之前会先判断等待队列是否为空或者自己是否位于队列头部,该条件通过才能继续获取锁。
再结合兔子喝水的图分析,非公平锁获取所得顺序基本决定在9、10、11这三个事件发生的先后顺序:
其实对于非公平锁,只要线程进入了等待队列,队列里面依然是FIFO的原则,跟公平锁的顺序是一样的。因为公平锁与非公平锁的release()部分代码是共用AQS的代码:
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
由于非公平锁减少了线程挂起的几率,后来的线程有一定几率逃离被挂起,因此非公平锁的效率要高于公平锁。
ReentrantLock的其他方法如下:
ReentrantLock是完全互斥排他的锁,这样虽然保证了线程安全,但效率却是非常低下的,对此,JDK提供了一种读写锁ReentrantReadWriteLock,使用它可以加快运行效率。
读写锁表示有两个锁,一个是读锁,也叫共享锁;一个是写锁,也叫排他锁。多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥。
这段代码展示了读锁的共享性质:
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Main {
public static void main(String[] args) {
Service s = new Service();
Thread ta = new MyThread("threadA", s);
ta.start();
Thread tb = new MyThread("threadB", s);
tb.start();
}
public static class MyThread extends Thread {
Service s;
public MyThread(String name, Service s) {
super(name);
this.s = s;
}
public void run() {
s.service();
}
}
public static class Service {
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void service() {
lock.readLock().lock();
System.out.println(Thread.currentThread().getName() + "获得了读锁,当前时间: " + System.currentTimeMillis());
lock.readLock().unlock();
}
}
}
threadA获得了读锁,当前时间: 1538414809755
threadB获得了读锁,当前时间: 1538414809756
可以看到,两个线程都能顺利走到lock()方法后面的代码中,即读锁是共享的,不同的线程可以同时持有。
这段代码展示了写锁的排他性质:
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Main {
public static void main(String[] args) {
Service s = new Service();
Thread ta = new MyThread("threadA", s);
ta.start();
Thread tb = new MyThread("threadB", s);
tb.start();
}
public static class MyThread extends Thread {
Service s;
public MyThread(String name, Service s) {
super(name);
this.s = s;
}
public void run() {
s.service();
}
}
public static class Service {
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void service() {
lock.writeLock().lock();
System.out.println(Thread.currentThread().getName() + "获得了写锁,当前时间: " + System.currentTimeMillis());
try {
Thread.sleep(10000);
} catch (Exception e) {
e.printStackTrace();
}
lock.writeLock().unlock();
}
}
}
threadA获得了写锁,当前时间: 1538415074339
threadB获得了写锁,当前时间: 1538415084340
可以看到,在大约10秒钟后另一个线程才进入到lock()方法后的代码,即写锁是排他的。
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Main {
public static void main(String[] args) {
Service s = new Service();
Thread ta = new MyThread("threadA", s);
ta.start();
Thread tb = new MyThread("threadB", s);
tb.start();
}
public static class MyThread extends Thread {
Service s;
public MyThread(String name, Service s) {
super(name);
this.s = s;
}
public void run() {
if ("threadA".equals(getName())) {
s.read();
} else if ("threadB".equals(getName())) {
s.write();
}
}
}
public static class Service {
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void read() {
lock.readLock().lock();
System.out.println(Thread.currentThread().getName() + "获得了读锁,当前时间: " + System.currentTimeMillis());
try {
Thread.sleep(10000);
} catch (Exception e) {
e.printStackTrace();
}
lock.readLock().unlock();
}
public void write() {
lock.writeLock().lock();
System.out.println(Thread.currentThread().getName() + "获得了写锁,当前时间: " + System.currentTimeMillis());
try {
Thread.sleep(10000);
} catch (Exception e) {
e.printStackTrace();
}
lock.writeLock().unlock();
}
}
}
threadA获得了读锁,当前时间: 1538415518663
threadB获得了写锁,当前时间: 1538415528663