前面我们讲到synchronized,既然synchronized已经能够保证线程安全,那么为什么还需要Lock呢?
我们从synchronized的缺陷讲起。
package com.rancho945.concurrent;
public class LockDemo {
public synchronized void methodA() {
System.out.println(Thread.currentThread().getName() + "获取到了锁");
// 这里模拟一些耗时的工作,5s
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "释放了锁");
}
public synchronized void methodB() {
System.out.println(Thread.currentThread().getName()+ "获取到了锁");
// 这里模拟一些耗时的工作,1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+ "释放了锁");
}
}
测试类
package com.rancho945.concurrent;
public class Test {
public static void main(String[] args) {
final LockDemo lockDemo = new LockDemo();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程"+Thread.currentThread().getName()+"启动");
lockDemo.methodA();
System.out.println("线程"+Thread.currentThread().getName()+"销毁");
}
}).start();
//延迟30ms再启动第二个线程,保证第一个线程已经获取到锁
try {
Thread.sleep(30);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程"+Thread.currentThread().getName()+"启动");
lockDemo.methodB();
System.out.println("线程"+Thread.currentThread().getName()+"销毁");
}
}).start();;
}
}
执行结果
线程Thread-0启动
Thread-0获取到了锁
线程Thread-1启动
//线程1在这里等待了大概5s
Thread-0释放了锁
线程Thread-0销毁
Thread-1获取到了锁
Thread-1释放了锁
线程Thread-1销毁
像上面的例子,如果线程1执行methodB的时候,必须等到线程0释放锁才可以继续执行。如果有个需求,methodB获取不到锁则执行其他任务,或者在等待锁的时候可以被中断,显然内置锁无法做到。
2. 一个文件,在写的时候必须单个线程执行,但是在读的时候可以多个线程并发读取,如果用synchronized的同一把锁,那么在读取的时候只能单个线程去读取。
3. 当一个锁被释放后,哪个线程获取到锁是不确定的,也就是synchronized锁是非公平的。极端情况下,那么有可能个线程永远不会得到锁,我门需要一种锁,释放的锁由等待该锁最久的线程获取。
基于synchronized锁的一些限制条件,JDK提供了一些接口及其实现类。打开Lock源码,我们看一下Lock是个什么东西:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
可以看到,Lock是一个接口,从方法名称我们可以看出来,它提供了加锁lock(),可中断锁lockInterruptibly(),尝试获取锁tryLock()以及在某个时间内尝试获取锁tryLock(long time,TimeUnit unit),释放锁unlock(),newConditon是涉及到线程协作的,在这里暂时不讲。
Lock 是一个接口,我们使用的时候需要用到它的实现类ReentrantLock(可重入锁),看看这些方法都是怎么使用的,他们的使用套路如下
Lock lock = new ReentrantLock();
public void method() {
//这里获取锁
lock.lock();
try {
// 这里执行任务
} finally {
//这里释放锁
lock.unlock();
}
}
Lock lock = new ReentrantLock();
public void method() {
//如果获取锁成功,则返回true,否则返回false。调用无参数tryLock立刻返回结果,调用有参数的则等待一定的时间,在该时间内没有获得锁返回false
if (lock.tryLock()) {
try {
//执行获取到锁的操作
} finally {
//释放锁
lock.unlock();
}
} else {
//执行没有获取到锁的操作
}
}
Lock lock = new ReentrantLock();
//这里向上抛出异常,由调用方进行处理
public void method() throws InterruptedException{
//这里获取锁,并且在等待锁的过程中可以响应中断
lock.lockInterruptibly();
try {
// 这里执行任务
} finally {
//这里释放锁
lock.unlock();
}
}
或者
Lock lock = new ReentrantLock();
public void methodC() {
//这里在函数内部捕获异常并处理
try {
//获取锁
lock.lockInterruptibly();
try {
// 这里执行任务
} finally {
//这里释放锁
lock.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
//处理中断
}
}
上面的几种锁获取流程都差不多,所有的操作必须放在try中执行,并且在finally块中释放,保证发生异常也可以释放锁。我们把前面synchronized的例子用lock进行同步:
package com.rancho945.concurrent;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockDemo {
private Lock lock = new ReentrantLock();
public void methodA() {
System.out.println(Thread.currentThread().getName() + "尝试获取锁");
lock.lock();
System.out.println(Thread.currentThread().getName() + "获取到了锁");
try {
// 这里模拟一些耗时的工作,5s
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + "释放了锁");
}
}
public void methodB() {
System.out.println(Thread.currentThread().getName() + "尝试获取锁");
//如果获取锁成功,则返回true,否则返回false。调用无参数tryLock立刻返回结果,调用有参数的则等待一定的时间,在该时间内没有获得锁返回false
if (lock.tryLock()) {
System.out.println(Thread.currentThread().getName() + "获取到了锁");
try {
// 这里模拟一些耗时的工作,1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + "释放了锁");
}
} else {
System.out.println(Thread.currentThread().getName() + "获取锁失败");
}
}
public void methodC() {
try {
System.out.println(Thread.currentThread().getName() + "尝试获取锁");
//获取锁,等待锁的过程中可以被中断
lock.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + "获取到了锁");
try {
// 这里模拟一些耗时的工作,1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
//这里释放锁
lock.unlock();
System.out.println(Thread.currentThread().getName() + "释放了锁");
}
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println(Thread.currentThread().getName() + "获取锁被中断");
}
}
}
测试类
package com.rancho945.concurrent;
public class Test {
public static void main(String[] args) {
final LockDemo lockDemo = new LockDemo();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程"+Thread.currentThread().getName()+"启动");
lockDemo.methodA();
System.out.println("线程"+Thread.currentThread().getName()+"销毁");
}
}).start();
//延迟30ms再启动第二个线程,保证第一个线程已经获取到锁
try {
Thread.sleep(30);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程"+Thread.currentThread().getName()+"启动");
lockDemo.methodB();
System.out.println("线程"+Thread.currentThread().getName()+"销毁");
}
}).start();
}
}
运行结果
线程Thread-0启动
Thread-0尝试获取锁
Thread-0获取到了锁
线程Thread-1启动
Thread-1尝试获取锁
//注意这里和前面synchronized对比,没有获取到锁,则去执行其他任务,没有一直等待
Thread-1获取锁失败
线程Thread-1销毁
Thread-0释放了锁
线程Thread-0销毁
可以看到,当获取锁失败的时候可以转去做其他的任务,如果是内置的Synchronized锁,则必须等待到获取锁为止。
测试可中断锁
package com.rancho945.concurrent;
public class Test {
public static void main(String[] args) {
final LockDemo lockDemo = new LockDemo();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程"+Thread.currentThread().getName()+"启动");
lockDemo.methodA();
System.out.println("线程"+Thread.currentThread().getName()+"销毁");
}
}).start();
//延迟30ms再启动第二个线程,保证第一个线程已经获取到锁
try {
Thread.sleep(30);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程"+Thread.currentThread().getName()+"启动");
//注意methodC是等待锁过程中是可中断的
lockDemo.methodC();
System.out.println("线程"+Thread.currentThread().getName()+"销毁");
}
});
thread.start();
//执行中断,此时锁的等待可以相应中断
thread.interrupt();
}
}
执行结果
线程Thread-0启动
Thread-0尝试获取锁
Thread-0获取到了锁
线程Thread-1启动
Thread-1尝试获取锁
//这里就是执行thread.interrupt()后的中断响应
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1220)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
at com.rancho945.concurrent.LockDemo.methodC(LockDemo.java:51)
at com.rancho945.concurrent.Test$2.run(Test.java:29)
at java.lang.Thread.run(Thread.java:745)
Thread-1获取锁被中断
线程Thread-1销毁
Thread-0释放了锁
线程Thread-0销毁
可以看到,在等待锁的过程中,等待锁的过程中可以相应线程的中断。
ReadWriteLock同样是一个接口
public interface ReadWriteLock {
//获取读锁
Lock readLock();
//获取写锁
Lock writeLock();
}
该接口的实现类为ReentrantReadWriteLock。所谓的读写锁,就是读的时候可以并发读,写的时候只能一个线程写,并且写的时候不能读。看例子:
package com.rancho945.concurrent;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockDemo {
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private int count = 0;
public int readCount() {
readWriteLock.readLock().lock();
System.out.println(Thread.currentThread().getName()+"获得读锁");
try {
System.out.println(Thread.currentThread().getName()+"正在读取count---"+count);
return count;
} finally{
//这里输出放在锁释放之前,因为如果放在释放锁之后的话,有可能释放完锁之后还没有进行打印,就执行到第二个线程的获取锁,会造成一种没有释放锁就被其他线程获取的错觉
System.out.println(Thread.currentThread().getName()+"释放读锁");
readWriteLock.readLock().unlock();
}
}
public void writeCount(int value){
readWriteLock.writeLock().lock();
System.out.println(Thread.currentThread().getName()+"获得写锁");
try {
System.out.println(Thread.currentThread().getName()+"正在设置count----"+value);
count = value;
//模拟一个1s写的延迟过程
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} finally{
//这里输出放在锁释放之前,因为如果放在释放锁之后的话,有可能释放完锁之后还没有进行打印,就执行到第二个线程的获取锁,会造成一种没有释放锁就被其他线程获取的错觉
System.out.println(Thread.currentThread().getName()+"释放写锁");
readWriteLock.writeLock().unlock();
}
}
}
测试写锁
package com.rancho945.concurrent;
public class Test {
public static void main(String[] args) {
final ReadWriteLockDemo readWriteLockDemo = new ReadWriteLockDemo();
//开启10个线程并发写
for (int i = 0; i <10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
//进行写操作
readWriteLockDemo.writeCount(10);
}
}).start();
}
}
}
执行结果
Thread-0获得写锁
Thread-0正在设置count----10
Thread-0释放写锁
Thread-2获得写锁
Thread-2正在设置count----10
Thread-2释放写锁
Thread-1获得写锁
Thread-1正在设置count----10
Thread-1释放写锁
Thread-3获得写锁
Thread-3正在设置count----10
Thread-3释放写锁
Thread-4获得写锁
Thread-4正在设置count----10
Thread-4释放写锁
Thread-5获得写锁
Thread-5正在设置count----10
Thread-5释放写锁
Thread-6获得写锁
Thread-6正在设置count----10
Thread-6释放写锁
Thread-7获得写锁
Thread-7正在设置count----10
Thread-7释放写锁
Thread-8获得写锁
Thread-8正在设置count----10
Thread-8释放写锁
Thread-9获得写锁
Thread-9正在设置count----10
Thread-9释放写锁
可以看到,写锁必须只能单个线程持有。
测试读锁
package com.rancho945.concurrent;
public class Test {
public static void main(String[] args) {
final ReadWriteLockDemo readWriteLockDemo = new ReadWriteLockDemo();
//开启10个线程并发读
for (int i = 0; i <10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
//执行读操作
readWriteLockDemo.readCount();
}
}).start();
}
}
}
执行结果
Thread-2获得读锁
Thread-4获得读锁
Thread-4正在读取count---0
Thread-0获得读锁
Thread-3获得读锁
Thread-1获得读锁
Thread-1正在读取count---0
Thread-1释放读锁
Thread-7获得读锁
Thread-7正在读取count---0
Thread-7释放读锁
Thread-8获得读锁
Thread-8正在读取count---0
Thread-8释放读锁
Thread-3正在读取count---0
Thread-3释放读锁
Thread-4释放读锁
Thread-0正在读取count---0
Thread-0释放读锁
Thread-5获得读锁
Thread-5正在读取count---0
Thread-2正在读取count---0
Thread-5释放读锁
Thread-9获得读锁
Thread-9正在读取count---0
Thread-6获得读锁
Thread-6正在读取count---0
Thread-6释放读锁
Thread-9释放读锁
Thread-2释放读锁
可以看到,读锁可以同时被多个线程获取。
读写锁测试
package com.rancho945.concurrent;
public class Test {
public static void main(String[] args) {
final ReadWriteLockDemo readWriteLockDemo = new ReadWriteLockDemo();
new Thread(new Runnable() {
@Override
public void run() {
//执行写操作
readWriteLockDemo.writeCount(10);
}
}).start();
//等待20ms再开始启动读操作,保证前面一个线程获取到了写锁
try {
Thread.sleep(20);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
//执行读操作
readWriteLockDemo.readCount();
}
}).start();
}
}
执行结果
Thread-0获得写锁
Thread-0正在设置count----10
//这里停顿了一秒钟,证明读的线程一直在等待着写锁。
Thread-0释放写锁
Thread-1获得读锁
Thread-1正在读取count---10
Thread-1释放读锁
读写锁总结
前面我们讲到,内置锁无法做到公平性,也就是无法保证哪个线程会先获得锁,有可能等待了很久的线程没有获得锁,而等待时间很短的线程获得了锁。而ReentrantLock和ReadWriteReentrantLock可以实现公平锁,也就是等待时间久的可以先获得锁,等待时间短的后获得锁,他们默认情况下是非公平锁,如果要实现其公平性,则需要在构造函数中传入true参数实现公平锁。
《深入理解Java虚拟机》 周志明 著
《Java并发编程的艺术》 方腾飞 著
《Java并发编程实战》 Brian Goetz等著 童云兰等译