大数据时代随之而来的就是并发问题。Java开发本身提供了关于锁的操作。我们知道的有Synchronized。 这个是JVM层面的锁。操作简单
Lock的由来
- 因为Synchronized简单所以不可控制,或者说不是很灵活。Synchronized是已块进行执行加锁的。这个时候我们需要通过Lock进行更加灵活的控制。
我们通过tryLock 、 unLock方法进行上锁释放锁。
线程之间的交互
- 在多线程开发中有的时候我们一个线程需要进行等待、休眠操作。这个时候其他线程没必要一直等待。Java中提供了对应的方法进行线程切换
- | await/wait | sleep | yield |
---|---|---|---|
释放锁 | 释放 | 不释放 | 不释放 |
就绪节点 | notify/notifyall方法后 | 休眠时间后 | 立刻就绪 |
提供者 | Object/Condition | Thread | Thread |
代码位置 | 代码块 | 任意 | 任意 |
- 通过上述表格我们可以看出来。在线程中我们可以通过Object.wait方法或者Condition.wait方法进行线程挂起的等待(将资源让给其他线程)。在其他线程通过Object.notify、Object.notifyall 、 Condition.signal方法进行唤醒当前挂载的线程(当前挂载的线程不止一个)。
Object.notify | Object.notifyall | Condition.signal |
---|---|---|
随机唤醒挂载线程之一 | 随机唤醒挂载线程之一 | 按顺序唤醒当前condition上的挂载线程 |
- 这里主要区别是Object和Condition两个类。Condition.signal会通知相同Condition上的线程就绪(按序通知)
Lock方法简介
- 通过查看源码我们发现Lock下方法如上。下面我们简单介绍下方法功能
lock()
- 当前线程对资源进行上锁操作。(如果已被上锁会一直阻塞住。一直到获取到锁)。为什么避免死锁的发生,建议在try,catch,finally中结合使用。保证在finally中一定会对资源的释放
lockInterruptibly()
- 顾名思义就是打断锁,在我们对资源进行加锁被占用是进行等待时,我们可以通过interrupt()方法打断在阻塞的线程。
trylock()
- trylock就是尝试去加锁,如果资源被锁则返回false,否则返回true表示加锁成功。
trylock(long,TimeUnit)
- 尝试加锁是被占用,通过TimeUnit指定等待时间段。超时后返回false
unlock()
- unlock就是去释放锁占用的锁。在finnally中释放。使用是一定要让代码走到释放锁的地方。避免死锁。
newCondition()
- 和Object的notify不同的是。newCondition会创建一个Condition将与此线程进行绑定。这里可以理解为不同的线程绑定在同一个Condition上是一队列的方式绑定的。当Condition.signal方法是,会从该队列中取出头部的线程进行唤醒就绪。
使用
- 通过查看Lock的引用关系得治,JDK中锁都是继承Lock实现的。使用最多的应该是ReentrantLock(可重入式锁) 。 什么叫可重入式锁呢就是一个线程可以多次调用lock方法,对应需要多次调用unlock进行解锁。
Lock保障高并发
package com.github.zxhtom.lock;
import lombok.Data;
import java.util.concurrent.locks.Lock;
/**
* @author 张新华
* @version V1.0
* @Package com.github.zxhtom.lock
* @date 2020年07月09日, 0009 14:30
* @Copyright © 2020 安元科技有限公司
*/
public class Counter {
private static Counter util = new Counter();
public static Counter getInstance(){
return util;
}
private int index;
public static Counter getUtil() {
return util;
}
public static void setUtil(Counter util) {
Counter.util = util;
}
public int getIndex() {
return index;
}
public void setIndex(Lock lock , int index) {
/*这里加锁解锁是为了显示可重入性,在外部为加锁解锁*/
lock.lock();
this.index = index;
lock.unlock();
}
}
package com.github.zxhtom.lock;
import java.util.Random;
import java.util.concurrent.locks.Lock;
/**
* @author 张新华
* @version V1.0
* @Package com.github.zxhtom.lock
* @date 2020年07月09日, 0009 14:19
* @Copyright © 2020 安元科技有限公司
*/
public class LockRunnable implements Runnable {
private Lock lock;
public LockRunnable(Lock lock ) {
this.lock = lock;
}
@Override
public void run() {
try {
Thread.sleep(new Random().nextInt(10));
} catch (InterruptedException e) {
e.printStackTrace();
}
/*lock、unlock之间的业务就能保证同一时刻只有一个线程访问。前提* 是同一个lock对象 , setIndex中也有lock 程序正常运行说明可重* 入
*/
this.lock.lock();
Counter instance = Counter.getInstance();
instance.setIndex(this.lock,instance.getIndex()+1);
this.lock.unlock();
}
}
package com.github.zxhtom.lock;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author 张新华
* @version V1.0
* @Package com.github.zxhtom.lock
* @date 2020年07月01日, 0001 14:24
* @Copyright © 2020 安元科技有限公司
*/
public class ReentrantLockDemo {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
List<Thread> threadList = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
int finalI = i;
Thread thread = new Thread(new LockRunnable(lock));
thread.start();
threadList.add(thread);
}
for (Thread thread : threadList) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Counter.getInstance().getIndex());
}
}
- 上述代码体现了ReentranLock的可重入性,另外也保障了高并发的问题。如果我们将
LockRunnable
中的加锁解锁去掉在运行输出的结果就会少于1000。在Counter中的加锁解锁不去也是会少的。因为那里的加锁解锁只是为了测试可重入性。因为在LockRunnable中的是get、set结合使用的。所以仅仅对set加锁没有用的。
Lock期间线程挂起
- 上面已经实现了高并发场景下加锁等待执行了。但是现在我们有一个这样的场景
场景: 1000个线程按名字的奇偶性分组,奇数一组、偶数一组。奇数执行完之后需要将锁传递给同组的线程 。
- 根据上述场景我们先考虑一下,第一个执行的线程和最后一个执行的线程。第一个线程毫无疑问是随机争取。而最后一个肯定是第一个同组内的最后一个。那么剩下的一组只能等待前一组全部执行完毕在执行了
- 在开发奇偶分组的场景需求时,我们先回顾下上面的高并发的代码。、
- 在介绍lock方法是我着重强调了unlock方法正常需要在try catch finally的finally中执行。但是为什么我是直接这样开发。这里其实是小编开发时大意了。后来想着正好能起一个反面作用。我们上面也看到了不在finally中执行也是可以的。但是在接下来Condition环境下不在finally中unlock就会导致线程hold on 了。
LockRunnable改造
- LockRunnalble构造函数里多接受了Condition类,这个类就是用来分组的.在run方法中我们首先去抢占锁,抢到锁就将线程挂起(condition挂起)condition.await()。这样线程就会处于等待状态。结合Demo类中所有线程都会处于awaitting状态。await阻塞现场后finally里的也不会被执行。因为线程被阻塞整体都不会再运转了。我们在ReentrantLockDemo类中会通过Condition进行分组唤醒。唤醒的线程执行await后面的代码。执行完进行同组线程唤醒并释放锁。这样就能保证线程是分组执行的。
package com.github.zxhtom.lock;
import java.util.Random;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* @author 张新华
* @version V1.0
* @Package com.github.zxhtom.lock
* @date 2020年07月09日, 0009 14:19
* @Copyright © 2020 安元科技有限公司
*/
public class LockRunnable implements Runnable {
private Lock lock;
private Condition condition;
private int index;
public LockRunnable(Lock lock , Condition condition,int index) {
this.lock = lock;
this.condition = condition;
this.index = index;
}
@Override
public void run() {
try {
this.lock.lock();
//if (index != 0) {
condition.await();
//}
System.out.println(Thread.currentThread().getName());
Counter instance = Counter.getInstance();
instance.setIndex(this.lock,instance.getIndex()+1);
condition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
this.lock.unlock();
}
}
}
ReentrantLockDemo改造
-
在构建线程的时候传入的Condition是按照序号进行传递的。我们提前准备了两个Condition.一个用来存放奇数好线程(oddCondition)。一个是存储偶数号线程(evenCondition)。
-
线程创建好之后,这个时候由于LockRunnable中condition.await方法早成线程阻塞了。后面我们通过不同的Condition进行同组线程唤醒。在所有线程结束后我们打印执行数也是1000.我在LockRunnable代码中输出了当前线程名字。我们通过日志发现是oddConditon(奇数条件)线程先输出的。50个奇数执行完了才开始evenCondition(偶数条件)。这是因为我们先oddCondition.signal的。这里读者可以自行执行代码看效果。小编试了试日志输出是分组输出的。
-
在奇偶添加signal的时候间隔时间一定要足够长。因为在释放锁的时候如果这个时候condition前面的lock会抢锁这样的话就不会是分组了。因为我们为了测试所以这里要足够长
package com.github.zxhtom.lock;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author 张新华
* @version V1.0
* @Package com.github.zxhtom.lock
* @date 2020年07月01日, 0001 14:24
* @Copyright © 2020 安元科技有限公司
*/
public class ReentrantLockDemo {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
/*奇数*/
Condition oddCondition = lock.newCondition();
/*偶数*/
Condition evenCondition = lock.newCondition();
List<Thread> threadList = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
int finalI = i;
Condition condition = null;
if (i % 2 == 0) {
condition = evenCondition;
} else {
condition = oddCondition;
}
Thread thread = new Thread(new LockRunnable(lock,condition,i));
thread.start();
threadList.add(thread);
}
try {
lock.lock();
oddCondition.signal();
}finally {
lock.unlock();
}
try {
/*休眠足够长,目的是不与前面队列抢锁.可以调更长时间。
* 这样测试准确*/
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
lock.lock();
evenCondition.signal();
}finally {
lock.unlock();
}
for (Thread thread : threadList) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Counter.getInstance().getIndex());
}
}
总结
-
我们通过Lock的lock、unlock就可以灵活的控制并发执行顺序。上面第二个列子如果我们不在finally中执行unlock就会带了很多意想不到的效果,读者可以自己放在一起执行看看效果(在第二个列子中试试).第一个放在一起没问题是因为业务简单没有造成问题的。
-
Condition条件队列,不同的Condition调用await相当于将当前线程绑定到该Condition上。当Condition唤醒线程内部会将Condition队列等待的节点转移到同步队列上,这里也是为什么上面提到两个Condition间隔时间需要足够长。因为Condition唤醒队列上等待的线程实际上不是真正的唤醒而是件线程添加到通过队列上,借由同步队列的活跃机制唤醒线程的,如果间隔时间不长这个时候回去和刚刚Condition添加过来的线程进行抢锁的。Condition唤醒实际上就是重新竞争一把锁。