既然有了 sychronized
为啥并发包下面还要提供 Lock
接口呢?
也许你会说Lock的效率比sychronized高,在JDK1.5之前确实如此,但在1.6之后两者的性能差不多了。
我们可以通过源码查看 Lock
接口定义的方法,就可以知道它比 synchronized
多了哪些功能。
可以看到Lock相比sychronized,提供了更加细粒度的线程锁控制。
Lock接口中的各个方法
- lock()方法,用来获取锁,如果锁被其他线程获取则会进入等待,需要和unlock()方法配合来释放锁,发生异常时不会主动释放锁,所以unlock()必需要放在finally块中
- lockInterruptibly(),通过这个方法获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就是说,当两个线程同时通过
lock.tryInterruptibly()
想获取同一个锁时,假若A线程获取到了锁,而B线程只有在等待, 那么对B调用threadB.interrupt()
方法能够中断B线程的等待。 - tryLock(),用来尝试获取锁,如果获取成功则返回true,如果获取失败则返回false,也就是说这个方法无论如何都会立即返回。在拿不到锁时不会一直等待。是一个非阻塞方法。
- tryLock(long time, TimeUnit unit),和
tryLock()
类似,只不过这个方法在拿不到锁时会等待一定时间,如果超过了时间期限还拿不到锁,就返回false,如果一开始就拿到了锁或者在等待期间内拿到了锁,就返回true。 - unlock(),解锁,前面说了这个方法一定要放在finally块中。
- newCondition(),定义条件
其余的方法应该都很好理解,下面主要演示一下 lockInterruptibly()
和 newCondition()
lock()和lockInterruptbily()的区别
为了能够有一个对比,我们先演示 lock()
方法,让大家能够和 lockInterruptibly()
对比,从而更好的理解两者的区别。
public static void main(String[] args) throws InterruptedException {
Lock lock = new ReentrantLock();
//先让main线程获取锁,并且故意不释放锁,使后面newThread一直阻塞等待锁的释放
lock.lock();
Thread newThread = new Thread(() -> {
try {
//新线程也去获取lock锁,导致新线程阻塞
lock.lock();
} finally {
System.out.println("进入到finally块中啦");
}
});
newThread.start();
TimeUnit.SECONDS.sleep(1);
newThread.interrupt();
TimeUnit.SECONDS.sleep(1); //这里让主线程睡眠1秒钟是为了避免主线程结束后程序直接退出而导致控制台来不及打印
}
上面代码的输出结果:
我们发现 finally
块中的打印内容没有被输出,这说明线程阻塞在获取lock锁的代码处,我们可以得出结论:当线程调用 lock()
方法进入阻塞状态时,此时的线程无法被 interrupt()
方法中断。
然后我们将上面代码中 lock()
改为 lockInterrupibly()
,注意这个方法需要捕获异常
public static void main(String[] args) throws InterruptedException {
Lock lock = new ReentrantLock();
//先让main线程获取锁,并且故意不释放锁,使后面newThread一直阻塞等待锁的释放
lock.lock();
Thread newThread = new Thread(() -> {
try {
//lock.lock();
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("进入到finally块中啦");
}
});
newThread.start();
TimeUnit.SECONDS.sleep(1);
newThread.interrupt();
TimeUnit.SECONDS.sleep(1); //这里让主线程睡眠1秒钟是为了避免主线程直接结束后程序直接退出而导致控制台来不及打印
}
再执行一次得到了执行结果如下:
我们可以看到线程被中断了并且抛出了一次,最终走入了finally块中。因此我们可以得出结论:当线程调用 lockInterruptibly()
进入阻塞状态时,若此时调用这个线程的 interrupt()
方法可以使这个线程被中断。
Condition的使用
sychronized与 wait()
和 notify()
/ notifyAll()
方法结合可以实现等待/通知模型。借助 Conditoin
也同样可以实现,并且 Condition
具有更好的灵活性,具体体现在:
- 同一个Lock可以创建多个Condition实例,实现多路通知
- notify()方法进行通知时,被通知的线程是Java虚拟机随机选择的,但是
ReentrantLock
结合Condition
可以实现有选择性的通知目标线程
public class WaitNotify {
static Lock lock = new ReentrantLock();
static Condition conditionA = lock.newCondition();
static Condition conditionB = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(new WaitA(), "WaitThreadA");
Thread threadB = new Thread(new WaitB(), "WaitThreadB");
threadB.start();
threadA.start();
TimeUnit.SECONDS.sleep(2);
lock.lock();
try {
conditionA.signal(); //在调用之前signal(),当前线程必需先调用lock(),否则会抛出异常
System.out.println("调用了conditionA.signal()");
} finally {
lock.unlock();
}
}
static class WaitA implements Runnable {
@Override
public void run() {
lock.lock();
try {
//前面和末尾几个字符串是用来改变打印的颜色的
System.out.println("\033[31;4m" + Thread.currentThread() + " begin await @ " + formatDate(new Date()) + "\033[0m");
conditionA.await();
System.out.println(Thread.currentThread() + " begin await @ " + formatDate(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
static class WaitB implements Runnable {
@Override
public void run() {
lock.lock();
try {
//前面和末尾几个字符串是用来改变打印的颜色的
System.out.println("\033[31;4m" + Thread.currentThread() + " begin await @ " + formatDate(new Date()) + "\033[0m");
conditionB.await();
System.out.println(Thread.currentThread() + " begin await @ " + formatDate(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
static String formatDate(Date date) {
return new SimpleDateFormat("HH:mm:ss").format(date);
}
}
输出结果:
WaitThreadB线程因为没有被通知,所以一直阻塞。
总结
最后总结一波sychronized和Lock的异同
Lock
支持非阻塞的方式获取锁,能够响应中断,sychronized
不行Lock
必需手动获取和释放锁,sychronized
不需要Lock
可以创建公平锁和非公平锁,而sychronized
只能是非公平锁synchronized
在发生异常时会自动释放线程占有的锁,而Lock
在发生异常时不会主动释放锁,很有可能造成死锁,所以需要在finally块中释放锁(unlock)- sychronized和Lock都是可重入锁