1.重入锁
重入锁可以替代synchronized关键字。在JDK5.0中重入锁性能好于synchronized但是从JDK6.0开始,JDK对synchronized做了大量优化是得两者性能差距不多。
重入锁使用java.util.concurrent.locks.ReentrantLock类实现:
public class ReetrantLock implements Runnable{
public static ReentrantLock lock=new ReentrantLock();
public static int i=0;
@Override
public void run() {
for(int j=0;j<100000;j++) {
lock.lock();
i++;
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ReetrantLock reetrantLock=new ReetrantLock();
Thread t1=new Thread(reetrantLock);
Thread t2=new Thread(reetrantLock);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
输出结果是200000表明了它的确起到了锁的作用。其中lock.lock();lock.unlock();这两条代码使用了重入锁来保护临界区资源i的安全性。与synchronized相比重入锁有显示的操作过程。开发人员需要手动的指定何时加锁和释放锁。但是需要注意的是在退出临界区必须要记得手动释放锁否则其他线程就 没有机会再进行访问临界区。
至于为什么叫重入锁是因为它可以反复进入,如:
需要注意的是如果你多次加锁那么释放的次数也应该相同,如果释放次数少了相当于这个线程仍然持有这个锁,会使其他线程无法进入临界区。如果多了会得到java.lang.IllegalMonitorStateException这个异常。
中断响应:
在synchronized中一个线程等待锁只有两种情况一种是持有这个锁继续执行,一种是保持等待。而重入锁则会有另一种可能,那就是线程可能中断。也就时说在线程等待锁的过程中,程序可以根据需求取消对锁的请求。这种情况对于处理死锁有一定帮助。
下面演示下死锁代码:
package thread;
import java.util.concurrent.locks.ReentrantLock;
public class IntLock implements Runnable{
public static ReentrantLock lock1=new ReentrantLock();
public static ReentrantLock lock2=new ReentrantLock();
int lock;
public IntLock( int lock) {
this.lock=lock;
}
@Override
public void run() {
try {
if(lock==1) {
System.out.println("进入锁1-1");
lock1.lockInterruptibly();
System.out.println("获取锁然后休眠-1");
Thread.sleep(500);
System.out.println("进入锁2-1");
lock2.lockInterruptibly();
System.out.println("获取锁2-1");
}else {
System.out.println("进入锁2-2");
lock2.lockInterruptibly();
System.out.println("获取锁然后休眠-2");
Thread.sleep(500);
System.out.println("进入锁1-2");
lock1.lockInterruptibly();
System.out.println("获取锁1-2");
}
}catch (Exception e) {
e.printStackTrace();
}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 {
IntLock r1=new IntLock(1);
IntLock r2=new IntLock(2);
Thread t1=new Thread(r1);
Thread t2=new Thread(r2);
t1.start();
t2.start();
Thread.sleep(1000);
// t1.interrupt();
}
}
线程t1,t2启动后,t1先占用lock1,再占用lock2,而t2先占用lock2再请求lock1。所以很容形成t1与t2之间的互相等待。这时候如果去掉注释:
可以看到t1会被中断,所以lock1的锁会被释放而t2则会拿到lock1的锁然后继续执行结束。最后真正完成运行的是t2而t1则放弃任务直接退出释放资源。
锁申请等待限时:
除了等待外部通知中断死锁,要避免死锁还有另一种方法:限时等待。通常一个线程长时间拿不到锁可能是死锁也可能是饥饿。但是如果给定一个等待时间让线程自动放弃该次请求是有意义的,我们可以使用tryLock()方法来进行一次限时等待。
package thread.lock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class TimeLock implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
try {
if (lock.tryLock(5, TimeUnit.SECONDS)) {
System.out.println("进入了if");
Thread.sleep(6000);
System.out.println("出了了if");
}else {
System.out.println("get");
}
} catch (Exception e) {
// TODO: handle exception
} finally {
if(lock.isHeldByCurrentThread()) {
System.out.println("关闭锁");
lock.unlock();
}
}
}
public static void main(String[] args) {
TimeLock reetrantLock = new TimeLock();
Thread t1 = new Thread(reetrantLock);
Thread t2 = new Thread(reetrantLock);
t1.start();
t2.start();
}
}
tryLock()接受两个参数第一个参数表示时长,第二参数表示计时单位。
这里是5秒,表示这个线程在这个锁请求中最多等待5秒,如果超过5秒没有得到锁会返回false,否则返回true。在这个例子中由于锁的线程会持有锁达到6秒,所以另一个线程无法在5秒的等待时间内获得锁,所以锁请求会失败。如果等待改成4秒
ReentrantLock.tryLock()方法也可以不带参数直接运行。这种情况下当前线程会尝试 获得锁,如果锁未被其他线程占用则申请锁成功并返回true,如果锁被其他线程占用则当前线程不会等待,而是立即返回false。这种模式不会引起线程等待也不会产生死锁。
公平锁:
大多数情况下,锁的申请是非公平的。也就是说如果线程1先请求锁A,然后线程2也请求锁A,那么当锁A可用时,系统会从这个锁的等待队列中随机挑选一个,因此不能保证其公平性。如果使用synchronized就是非公平的锁,重入锁允许我们对其进行设置,它会议先来后到的顺序进行,所以不会产生饥饿。
public ReentrantLock(boolean fair);
当参数为true则是公平的。因为实现公平锁需要系统维护一个有序队列,所以实现成本比较高性能相对低下。它默认情况下锁是非公平的。
首先看一下非公平的情况:
package thread.lock;
import java.util.concurrent.locks.ReentrantLock;
public class FairLock implements Runnable {
// public static ReentrantLock lock = new ReentrantLock(true);
public static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true) {
lock.lock();
System.out.println(Thread.currentThread().getName()+"获得了锁");
lock.unlock();
}
}
public static void main(String[] args) {
FairLock reetrantLock = new FairLock();
FairLock reetrantLock2 = new FairLock();
Thread t1 = new Thread(reetrantLock,"线程1");
Thread t2 = new Thread(reetrantLock2,"线程2");
t1.start();
t2.start();
}
}
然后看下构造器为true的情况(公平):
OK~
对于ReentrantLock的几个重要方法整理:
lock():获得锁,如果锁被占用则等待。
lockInterruptibly():获得锁,但是优先响应中断。
tryLock():尝试获得锁,如果成功返回true,失败返回false。该方法不等待,立即返回。
tryLock(Long time,TimeUnit unit):在给定的时间内尝试获取锁。
unlock():释放锁。
在重入锁的实现中主要包含三个要素:
1.原子状态。原子状态用CAS操作来存储当前锁的状态,判断锁是否已经被其他线程持有。
2.等待队列。所有没有请求到锁的线程会进入等待队列进行等待。等有线程释放锁后,系统从等待队列中唤醒一个线程继续工作。
3.是阻塞原语park()和unpark(),用来挂起和恢复线程。没有得到锁的线程会被挂起。
2.重入锁之Condition条件
Condition对象和wait()和notify()方法作用大致相同,但是wait()和notify()方法是和synchronized关键字配合使用,而Condition是与重入锁相关联的。通过Lock接口的Condition new Condition()方法可以生成一个与当前重入锁绑定的Condition实例,利用Condition对象就可以让线程在合适的时间等待或者再某一个特定的时间得到通知继续执行。
void await() throws InterruptedException;
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
void signal();
void signalAll();
其含义如下:
void await()方法会是当前线程等待,同时释放当前锁,当其他线程中使用signal()或者signalAll()方法时,线程会重新获得锁并继续执行。或者当线程被中断时,也能跳出等待。与Object.wait()方法相似。
void awaitUninterruptibly()方法与await()方法基本相同,但是它不会在等待的过程中响应中断。
void signal()方法用于唤醒一个在等待中的线程。相对的signalAll()方法会唤醒所有在等待中的线程。与Object.notify()和notifyAll()相类似。
package thread.lock;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ReenterLockCondition implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
public static Condition condition = lock.newCondition();
@Override
public void run() {
try {
lock.lock();
System.out.println("进入" + Thread.currentThread().getName() + "要等待");
condition.await();
System.out.println("出来" + Thread.currentThread().getName());
System.out.println(Thread.currentThread().getThreadGroup().activeCount());
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ReenterLockCondition reetrantLock = new ReenterLockCondition();
Thread t1 = new Thread(reetrantLock, "线程1");
Thread t2 = new Thread(reetrantLock, "线程2");
t1.start();
t2.start();
System.out.println("启动完毕");
Thread.sleep(2000);
lock.lock();
System.out.println("要唤醒所有线程");
condition.signalAll();
lock.unlock();
t1.join();
t2.join();
}
}
在启动后会执行到await()方法然后此时t1,t2都陷入了等待然后2秒后signalAll()方法会唤醒所有线程然后线程1得到重入锁继续执行打印3表示当前线程数量还剩三个,然后t2执行,最后打印2包括主线程。和Object.wait()和notify()方法一样,当线程使用Condition.await()时,要求线程持有相关的重入锁,在lock.unlock()调用后,这个线程会释放这把锁。需要注意的是如果没有释放重入锁,那么虽然唤醒了线程t1,其他线程无法获得锁,也就无法继续执行。
参考《实战Java高并发程序设计》