之前在使用synchronized解决线程安全问题时,经常提到用lock也可以实现synchronized的功能,现在我们就来看看lock的使用。
本篇文章少部分内容引用了Java并发编程:Lock,文章的作者15年毕业,这是他14年写的文章,再想想自己14年的水平,汗颜的同时,也为当时没有人提携指引深感遗憾,后面有空了,打算写写这几年的感悟和认识,对自己做一个总结,与君共勉吧。感慨结束,进入正题。
1.ReentrantLock
1.1ReentrantLock实现同步
Lock是一个接口,它有一个重要的实现类ReentrantLock,通过lock方法来进行加锁,reentrantLock.lock()实际上就相当于synchronized(reentrantLock),“锁”即为reentrantLock对象,也可以实现多线程的同步。我们来看对于之前的购票例子如何使用reentrantLock实现线程安全。
public class Ticket {
public int total = 5;
public ReentrantLock reentrantLock = new ReentrantLock();
public void buy() throws InterruptedException{
try{
reentrantLock.lock();
if(total > 0){
//模拟买票过程
Thread.sleep(100);
--total;
}
}finally{
reentrantLock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
Ticket ticket = new Ticket();
TicketThread ticketThread = new TicketThread(ticket);
//10个人抢5张票
for(int i=0;i<10;i++){
new Thread(ticketThread).start();
}
//确保所有线程都执行完了
Thread.sleep(3000);
System.out.println("剩余:"+ticket.total);
}
}
public class TicketThread implements Runnable{
Ticket ticket = null;
public TicketThread(Ticket ticket) {
this.ticket = ticket;
}
@Override
public void run() {
try {
ticket.buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
可以看到,在使用ReentrantLock的时候,需要自己手动调用lock()/unlock()方法,来实现加锁/释放锁,一般会把unlock放到finally中执行,避免因程序异常,导致锁一直无法释放。
1.2ReentrantLock之Condition实现等待/通知
之前我们有提到线程间通信的一种方式wait/notify,ReentrantLock可以通过Condition来实现类似的功能,相比之前的wait/notify,这种方式更加灵活,来看实际的例子。
MyMethod类定义了一个ReentrantLock对象,从中获取两个condition,method将会执行5次输出,在执行到第3次的时候,调用await(),阻塞当前线程,等待唤醒后,才能继续执行后2次输出。在main方法中,我们指定唤醒线程1。最终线程1可以进行5次输出,线程2执行三次输出后,由于未被唤醒,一直阻塞在那里。
public class MyMethod {
//可重入锁 与synchronized(reentrantLock1)效果一致
ReentrantLock reentrantLock1 = new ReentrantLock();
//Condition实现wait/notify机制
Condition condition1 = reentrantLock1.newCondition();
Condition condition2 = reentrantLock1.newCondition();
public void method1() throws InterruptedException{
try{
reentrantLock1.lock();
for(int i = 0;i < 5;i++){
if(i == 3){
condition1.await();
}
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"调用method1()");
}
}finally{
reentrantLock1.unlock();
}
}
public void method2() throws InterruptedException{
try{
//注意这里得使用reentrantLock1,即和method1用的同一个锁,才能实现同步
reentrantLock1.lock();
for(int i = 0;i < 5;i++){
if(i == 3){
condition2.await();
}
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"调用method2()");
}
}finally{
reentrantLock1.unlock();
}
}
public void signal1(){
try{
reentrantLock1.lock();
condition1.signalAll();
}finally{
reentrantLock1.unlock();
}
}
}
public class MyThread1 extends Thread{
MyMethod myMethod = null;
public MyThread1(MyMethod myMethod) {
this.myMethod = myMethod;
}
@Override
public void run() {
try {
myMethod.method1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class MyThread2 extends Thread{
MyMethod myMethod = null;
public MyThread2(MyMethod myMethod) {
this.myMethod = myMethod;
}
@Override
public void run() {
try {
myMethod.method2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
MyMethod myMethod = new MyMethod();
MyThread1 myThread1 = new MyThread1(myMethod);
myThread1.setName("myThread1");
MyThread2 myThread2 = new MyThread2(myMethod);
myThread2.setName("myThread2");
myThread1.start();
myThread2.start();
Thread.sleep(5000);
//这里只通知线程1
myMethod.signal1();
}
}
最终执行结果:
myThread1调用method1()
myThread1调用method1()
myThread1调用method1()
myThread2调用method2()
myThread2调用method2()
myThread2调用method2()
myThread1调用method1()
myThread1调用method1()
通过这个例子,我们可以看到Lock的Condition可以实现唤醒指定线程。而如果使用notify,唤醒哪个线程是随机的,完全取决于JVM。如果有兴趣,可以把上面的例子改成wait/notify实现,你会对这两者的区别有更清楚的认识。
1.3ReentrantLock其他特性
- 指定公平锁还是非公平锁
ReentrantLock的构造方法可以传入一个boolean类型的参数,true即为公平锁,false即为非公平锁,默认为false。
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
公平锁表示线程获得锁的顺序是按照加锁的顺序来分配的,就像食堂打饭要排队,先来先得。非公平锁则是一种抢占机制,可能造成某些线程一直拿不到锁,就像早高峰一起挤公交,有人可能过了好几趟车,也没挤上去。
- 获得锁的状态
- getHoldCount(),获得当前线程保持此锁的个数,即调用lock()的次数。未调用lock()时,为0,调用lock(),为1,调用unlock(),为0。
- getQueueLength,获得等待该锁的线程个数。假设有5个线程都start(),线程1执行了lock(),一直未执行unlock(),此时其他四个线程都在等着线程1释放该锁,getQueueLength将会返回4。
- hasQueuedThread(Thread thread),查询指定线程是否在等待该锁
- hasQueuedThreads(),查询是否有线程在等待该锁
- isFair(),判断该锁是否为公平锁
- isHeldByCurrentThread(),查询当前线程是否持有该锁
- isLocked(),查询是否有线程持有该锁
- tryLock(),尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回,在拿不到锁时不会一直在那等待
- tryLock(long time, TimeUnit unit),与tryLock()类似,区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false;如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true
- 响应中断
假设有这样一个场景,线程1获得锁,但在执行较为耗时的操作,此时线程2只能干等着,但我现在不想让线程2等了,我要中断线程2,如果使用synchronized关键字,线程2是无法中断的;如果使用reentrantLock.lockInterruptibly()则可以中断线程2。
lockInterruptibly的作用为,如果当前线程未被中断,则获取锁,如果被中断,则抛出异常。
来看代码示例:
synchronized无法响应中断
public class TestSynchronized {
public static void test() throws InterruptedException{
synchronized(TestSynchronized.class){
System.out.println(Thread.currentThread());
Thread.sleep(10000);
}
}
public static void main(String[] args) throws InterruptedException {
//线程一 持有锁,并保持10s
new Thread(new Runnable() {
public void run() {
try {
TestSynchronized.test();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
//由于锁被线程一持有,线程二进入阻塞状态
Thread thread = new Thread(new Runnable() {
public void run() {
try {
TestSynchronized.test();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
//1s后,尝试中断线程二,发现线程二无法响应中断,必须等够10s
Thread.sleep(1000);
thread.interrupt();
}
}
ReentrantLock能够响应中断
public class TestReentrantLock {
public static ReentrantLock reentrantLock = new ReentrantLock();
public static void test() throws InterruptedException{
reentrantLock.lockInterruptibly();
System.out.println(Thread.currentThread());
Thread.sleep(10000);
reentrantLock.unlock();
}
public static void main(String[] args) throws InterruptedException {
//线程一 得到锁,并占用10s
new Thread(new Runnable() {
public void run() {
try {
TestReentrantLock.test();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
//线程二 因为锁被线程一持有,进入阻塞状态
Thread thread = new Thread(new Runnable() {
public void run() {
try {
TestReentrantLock.test();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
//1s后,尝试中断线程2,发现能够正常中断
Thread.sleep(1000);
thread.interrupt();
}
}
2.ReentrantReadWriteLock
假设有这样一个场景,线程1和线程2都需要读写某一文件,为了保证线程安全,我们可以使用synchronized关键字或ReentrantLock,通过加锁来保证线程安全,即读读互斥,读写互斥,写读互斥,写写互斥。但实际上对于1 2两个线程同时读取的操作,是不会出现线程安全问题的,加锁反而影响效率。我们可以使用ReentrantReadWriteLock,来实现读读共享,读写互斥,写读互斥,写写互斥的效果。
代码示例:
public class ReadWriteService {
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void read() throws InterruptedException{
try{
readWriteLock.readLock().lock();
System.out.println(Thread.currentThread().getName()+"拿到读锁"+System.currentTimeMillis());
Thread.sleep(1000);
}finally{
readWriteLock.readLock().unlock();
}
}
public void write() throws InterruptedException{
try{
readWriteLock.writeLock().lock();
System.out.println(Thread.currentThread().getName()+"拿到写锁"+System.currentTimeMillis());
Thread.sleep(1000);
}finally{
readWriteLock.writeLock().unlock();
}
}
}
public class ReadThread extends Thread{
ReadWriteService service = null;
public ReadThread(ReadWriteService service) {
this.service = service;
}
@Override
public void run() {
try {
service.read();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class WriteThread extends Thread{
ReadWriteService service = null;
public WriteThread(ReadWriteService service) {
this.service = service;
}
@Override
public void run() {
try {
service.write();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
ReadWriteService service = new ReadWriteService();
ReadThread readThread1 = new ReadThread(service);
readThread1.setName("readThread1");
ReadThread readThread2 = new ReadThread(service);
readThread2.setName("readThread2");
WriteThread writeThread1 = new WriteThread(service);
writeThread1.setName("writeThread1");
WriteThread writeThread2 = new WriteThread(service);
writeThread2.setName("writeThread2");
//读读共享
/*readThread1.start();
Thread.sleep(100);
readThread2.start();*/
//读写互斥
/*readThread1.start();
Thread.sleep(10);
writeThread1.start();*/
//写读互斥
/*writeThread1.start();
Thread.sleep(10);
readThread1.start();*/
//写写互斥
writeThread1.start();
Thread.sleep(10);
writeThread2.start();
}
}
3.synchronized与Lock的对比
通过上面的介绍,我们可以总结一下这两种锁的区别:
1)Lock是一个接口,通过Java代码来实现锁,而synchronized是Java中的关键字,是内置的实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行;
4)Lock提供了对锁的更多操作,比如可以指定公平锁还是非公平锁,获得锁的状态,是否成功获得了锁等;
5)ReentrantLock的Condition可以实现更灵活的wait/notify机制
6)ReentrantReadWriteLock可以提高多个线程进行读操作的效率
两者如何选用:
一般场景使用synchronized就够了,因为它更简单易用,而且经过不断优化,它的性能也已经得到了很大改善。建议复杂场景或者有更高级的要求的时候,再考虑使用lock,而且在使用时,一定要好好研究清楚了再用,否则到时候出现问题是很难排查的。
网友评论