线程的同步控制,最基础的就是 synchronized关键字。但是在使用的过程中有一些限制,并没有那么的灵活。
可重入
所以下面介绍一下ReentrantLock的使用和相关特性,一个简单的demo如下:
public class ReenterLock 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 {
ReenterLock r1 = new ReenterLock();
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r1);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
上面的demo创建了两个线程 t1、t2,并且同时使用了lock作为锁,完成了i++的操作。这里面需要注意的是,lock.lock()调用了几次,响应的lock.unlock()就应该调用几次。并且无论执行体如何,最终都要调用lock.unlock()。
响应中断
在两个线程同时抢占同一个锁资源的时候,很有可能产生死锁的现象。ReentrantLock可以支持中断响应,从而规避死锁的产生。
public class IntLock implements Runnable {
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
int lock;
/**
* 控制加锁顺序,方便构造死锁
*
* @param lock
*/
public IntLock(int lock) {
this.lock = lock;
}
@Override
public void run() {
try {
if (lock == 1) {
lock1.lockInterruptibly();
try {
Thread.sleep(500);
} catch (Exception e) {
}
lock2.lockInterruptibly();
} else {
lock2.lockInterruptibly();
try {
Thread.sleep(500);
} catch (Exception e) {
}
lock1.lockInterruptibly();
}
} 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);
// 中断t2线程
t2.interrupt();
}
}
从上面代码可以看出来,此时使用的是 lock.lockInterruptibly(),而不是 lock.lock()。当使用 lock.lockInterruptibly() 加锁等待的时候,可以响应中断,从而使线程对出死锁状态。
上面代码一共起了两个线程,故意造成了死锁的状态。此时在主线程中调用了中断方法,此时t2线程对出等待,释放锁资源,从而可以让t1继续执行下去。
超时限制
当多线程对同一监视器对象加锁时,如果超过一定时间还没获得锁,则立即返回false。这里使用到的是tryLock()。
public class TimeLock 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("I got lock failed.");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
public static void main(String[] args) {
TimeLock time1 = new TimeLock();
Thread t1 = new Thread(time1);
Thread t2 = new Thread(time1);
t1.start();
t2.start();
}
}
上述代码启动了2个线程,在5S内必定一个线程获取锁对象失败。
另外tryLock()不带参的方法,表示对当前对象加锁,如果获取不到则立即返回false。通过这个特性,可以在程序中用无限循环,使其不断的去尝试加锁。
public class TryLock implements Runnable {
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
int lock;
public TryLock(int lock) {
this.lock = lock;
}
@Override
public void run() {
if (lock == 1) {
while (true) {
if (lock1.tryLock()) {
try {
try {
Thread.sleep(500);
} catch (Exception e) {
}
if (lock2.tryLock()) {
try {
System.out.println(Thread.currentThread().getId() + ": my job done");
return;
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
}
} else {
while (true) {
if (lock2.tryLock()) {
try {
try {
Thread.sleep(500);
} catch (Exception e) {
}
if (lock1.tryLock()) {
try {
System.out.println(Thread.currentThread().getId() + ": my job done");
return;
} finally {
lock1.unlock();
}
}
} finally {
lock2.unlock();
}
}
}
}
}
public static void main(String[] args) {
TryLock r1 = new TryLock(1);
TryLock r2 = new TryLock(2);
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
}
}
公平锁性
在大部分的应用场景下,锁的申请获取都是不公平的。但是我们通过ReentrantLock可以构造出公平的锁对象。
public ReentrantLock(boolean fair)
使用上述方法可以构造出公平锁出来,我们通常默认的fair都是false。公平锁本身会维护一个有序队列,开销相对较高。
公平锁实现
ReentrantLock 的公平锁和非公平锁都委托了 AbstractQueuedSynchronizer#acquire 去请求获取。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
- 公平锁 FairSync
公平锁的实现在于,当有线程获取锁资源时, 都会检查有没有等待队列。如果有,则按先进先出的原则进行。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() && // 优先 先进先出原则
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
- 非公平锁 NonfairSync
加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待。
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
非公平锁性能比公平锁高5~10倍,因为公平锁需要在多核的情况下维护一个队列。
下面看一个公平锁的测试Demo
public class FairLock implements Runnable {
public static ReentrantLock fairLock = new ReentrantLock(true);
@Override
public void run() {
while (true) {
try {
fairLock.lock();
System.out.println(Thread.currentThread().getName() + " 获得锁");
} finally {
fairLock.unlock();
}
}
}
public static void main(String[] args) {
FairLock r1 = new FairLock();
Thread t1 = new Thread(r1, "thread t1");
Thread t2 = new Thread(r1, "Thread t2");
t1.start();
t2.start();
}
}
通过运行上述程序可以看出,锁的获取都是有序的,公平的。
综上所述
一般来说,根据JVM的调度,系统更倾向于把锁给当前已经获得锁的线程。从而减少线程频繁切换带来的系统消耗。
对于ReentrantLock主要有以下几个方法:
- lock():获取锁,如果已被占用,则等待;
- lockInterruptibly():获得锁,但是响应中断;
- tryLock():获得锁,如可以立即获得,则返回true,如不可以,则立即返回false;
- tryLock(long time, TimeUnit unit):超时未获得锁,则返回;
- unlock():释放锁;
最后给自己的群友宣传一下
欢迎Java工程师朋友们加入Java高级互联网架构:809389099
群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,
MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)
合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!