1. Lock接口
1.1 Lock和synchronized的区别
锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(但是有些锁可以允许多个线程并发的访问共享资源,比如读写锁)。
在Lock接口出现之前,Java程序是靠synchronized关键字实现锁功能的,而Java 5之后,并发包中新增了Lock接口(以及相关实现类)用来实现锁功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁。虽然它缺少了(通过synchronized块或者方法所提供的)隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。
使用synchronized关键字将会隐式地获取锁,所以它将锁的获取和释放固化了,也就是先获取再释放。当然,这种方式简化了同步的管理,可是扩展性没有显示的锁获取和释放来的好。例如,针对一个场景,手把手进行锁获取和释放,先获得锁A,然后再获取锁B,当锁B获得后,释放锁A同时获取锁C,当锁C获得后,再释放B同时获取锁D,以此类推。这种场景下,synchronized关键字就不那么容易实现了,而使用Lock却容易许多。
Lock接口提供synchronized关键字所不具备的主要特性有:
1.2 Lock的使用方法
Lock的使用很简单
Lock lock = new ReentrantLock();
lock.lock();
try {
} finally {
lock.unlock();
}
在finally块中释放锁,目的是保证在获取到锁之后,最终能够被释放。不要将获取锁的过程写在try块中,因为如果在获取锁(自定义锁的实现)时发生了异常,异常抛出的同时,也会导致锁无故释放
2. ReentrantLock重入锁
ReentrantLock感觉上是synchronized的增强版,synchronized的特点是使用简单,一切交给JVM去处理,但是功能上是比较薄弱的。在JDK1.5之前,ReentrantLock的性能要好于synchronized,由于对JVM进行了优化,现在的JDK版本中,两者性能是不相上下的。如果是简单的实现,不要刻意去使用ReentrantLock。
相比于synchronized,ReentrantLock在功能上更加丰富,它具有可重入、可中断、可限时、公平锁等特点。
首先我们通过一个例子来说明ReentrantLock最初步的用法:
package test;
import java.util.concurrent.locks.ReentrantLock;
public class Test implements Runnable
{
public static ReentrantLock lock = new ReentrantLock();
public static int i = 0;
@Override
public void run()
{
for (int j = 0; j < 10000000; j++)
{
lock.lock();
try
{
i++;
}
finally
{
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException
{
Test test = new Test();
Thread t1 = new Thread(test);
Thread t2 = new Thread(test);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
有两个线程都对i进行++操作,为了保证线程安全,使用了ReentrantLock,从用法上可以看出,与 synchronized相比,ReentrantLock就稍微复杂一点。因为必须在finally中进行解锁操作,如果不在 finally解锁,有可能代码出现异常锁没被释放,而synchronized是由JVM来释放锁。
那么ReentrantLock到底有哪些优秀的特点呢?
2.1 可重入
单线程可以重复进入,但要重复退出
lock.lock();
lock.lock();
try
{
i++;
}
finally
{
lock.unlock();
lock.unlock();
}
由于ReentrantLock是重入锁,所以可以反复得到相同的一把锁,它有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放(重入锁)。这模仿了 synchronized 的语义;如果线程进入由线程已经拥有的监控器保护的 synchronized 块,就允许线程继续进行,当线程退出第二个(或者后续) synchronized 块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个synchronized 块时,才释放锁。
public class Child extends Father implements Runnable{
final static Child child = new Child();//为了保证锁唯一
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
new Thread(child).start();
}
}
public synchronized void doSomething() {
System.out.println("1child.doSomething()");
doAnotherThing(); // 调用自己类中其他的synchronized方法
}
private synchronized void doAnotherThing() {
super.doSomething(); // 调用父类的synchronized方法
System.out.println("3child.doAnotherThing()");
}
@Override
public void run() {
child.doSomething();
}
}
class Father {
public synchronized void doSomething() {
System.out.println("2father.doSomething()");
}
}
我们可以看到一个线程进入不同的 synchronized方法,是不会释放之前得到的锁的。所以输出还是顺序输出。所以synchronized也是重入锁
输出:
1child.doSomething()
2father.doSomething()
3child.doAnotherThing()
1child.doSomething()
2father.doSomething()
3child.doAnotherThing()
1child.doSomething()
2father.doSomething()
3child.doAnotherThing()
...
2.2 可中断
与synchronized不同的是,ReentrantLock对中断是有响应的。普通的lock.lock()是不能响应中断的,lock.lockInterruptibly()能够响应中断。
我们模拟出一个死锁现场,然后用中断来处理死锁
package test;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.util.concurrent.locks.ReentrantLock;
public class Test implements Runnable
{
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
int lock;
public Test(int lock)
{
this.lock = lock;
}
@Override
public void run()
{
try
{
if (lock == 1)
{
lock1.lockInterruptibly();
try
{
Thread.sleep(500);
}
catch (Exception e)
{
// TODO: handle exception
}
lock2.lockInterruptibly();
}
else
{
lock2.lockInterruptibly();
try
{
Thread.sleep(500);
}
catch (Exception e)
{
// TODO: handle exception
}
lock1.lockInterruptibly();
}
}
catch (Exception e)
{
// TODO: handle exception
}
finally
{
if (lock1.isHeldByCurrentThread())
{
lock1.unlock();
}
if (lock2.isHeldByCurrentThread())
{
lock2.unlock();
}
System.out.println(Thread.currentThread().getId() + ":线程退出");
}
}
public static void main(String[] args) throws InterruptedException
{
Test t1 = new Test(1);
Test t2 = new Test(2);
Thread thread1 = new Thread(t1);
Thread thread2 = new Thread(t2);
thread1.start();
thread2.start();
Thread.sleep(1000);
//DeadlockChecker.check();
}
static class DeadlockChecker
{
private final static ThreadMXBean mbean = ManagementFactory
.getThreadMXBean();
final static Runnable deadlockChecker = new Runnable()
{
@Override
public void run()
{
// TODO Auto-generated method stub
while (true)
{
long[] deadlockedThreadIds = mbean.findDeadlockedThreads();
if (deadlockedThreadIds != null)
{
ThreadInfo[] threadInfos = mbean.getThreadInfo(deadlockedThreadIds);
for (Thread t : Thread.getAllStackTraces().keySet())
{
for (int i = 0; i < threadInfos.length; i++)
{
if(t.getId() == threadInfos[i].getThreadId())
{
t.interrupt();
}
}
}
}
try
{
Thread.sleep(5000);
}
catch (Exception e)
{
// TODO: handle exception
}
}
}
};
public static void check()
{
Thread t = new Thread(deadlockChecker);
t.setDaemon(true);
t.start();
}
}
}
上述代码有可能会发生死锁,线程1得到lock1,线程2得到lock2,然后彼此又想获得对方的锁。
我们用jstack查看运行上述代码后的情况
的确发现了一个死锁。
DeadlockChecker.check();方法用来检测死锁,然后把死锁的线程中断。中断后,线程正常退出。
2.3 可限时
超时不能获得锁,就返回false,不会永久等待构成死锁
使用**lock.tryLock(long timeout, TimeUnit unit)**来实现可限时锁,参数为时间和单位。
举个例子来说明下可限时:
package test;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class Test implements Runnable
{
public static ReentrantLock lock = new ReentrantLock();
@Override
public void run()
{
try
{
if (lock.tryLock(5, TimeUnit.SECONDS))
{
Thread.sleep(6000);
}
else
{
System.out.println("get lock failed");
}
}
catch (Exception e)
{
}
finally
{
if (lock.isHeldByCurrentThread())
{
lock.unlock();
}
}
}
public static void main(String[] args)
{
Test t = new Test();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
t2.start();
}
}
使用两个线程来争夺一把锁,当某个线程获得锁后,sleep6秒,每个线程都只尝试5秒去获得锁。所以必定有一个线程无法获得锁。无法获得后就直接退出了。
输出:
get lock failed
2.4 公平锁
公平锁的意思就是,这个锁能保证线程是先来的先得到锁。虽然公平锁不会产生饥饿现象,但是公平锁的性能会比非公平锁差很多。公平性与否是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。
使用方式:
public ReentrantLock(boolean fair)
public static ReentrantLock fairLock = new ReentrantLock(true);
一般意义上的锁是不公平的,不一定先来的线程能先得到锁,后来的线程就后得到锁。
不公平的锁可能会产生饥饿现象。