Java高并发Lock接口讲解,精准通知线程间的执行顺序
题目:两个线程操作一个变量,实现两个线程对同一个资源一个进行加1操作,另外一个进行减1操作,且需要交替实现,变量的初始值为0。即两个线程对同一个资源进行加一减一交替操作。
Lock接口与Condition接口
JUC指的是上述三个api包,lock接口位于Java.util.concurrent.locks下
既然我们标题上写的是Lock,那我们自然用JUC来解决这个问题:先阅读API:
Lock
实现提供比使用synchronized
方法和语句可以获得的更广泛的锁定操作。 它们允许更灵活的结构化,可能具有完全不同的属性,并且可以支持多个相关联的对象Condition
。- 锁是用于通过多个线程控制对共享资源的访问的工具。 通常,锁提供对共享资源的独占访问:一次只能有一个线程可以获取锁,并且对共享资源的所有访问都要求首先获取锁。 但是,一些锁可能允许并发访问共享资源,如
ReadWriteLock
的读锁。 - 使用
synchronized
方法或语句提供对与每个对象相关联的隐式监视器锁的访问,但是强制所有锁获取和释放以块结构的方式发生:当获取多个锁时,它们必须以相反的顺序被释放,并且所有的锁都必须被释放在与它们相同的词汇范围内。 - 虽然
synchronized
方法和语句的范围机制使得使用监视器锁更容易编程,并且有助于避免涉及锁的许多常见编程错误,但是有时您需要以更灵活的方式处理锁。 例如,用于遍历并发访问的数据结构的一些算法需要使用“手动”或“链锁定”:您获取节点A的锁定,然后获取节点B,然后释放A并获取C,然后释放B并获得D等。 所述的实施方式中Lock
接口通过允许获得并在不同的范围释放的锁,并允许获得并以任何顺序释放多个锁使得能够使用这样的技术。
Condition
因素出Object
监视器方法(wait
,notify
和notifyAll
)成不同的对象,以得到具有多个等待集的每个对象,通过将它们与使用任意的组合的效果Lock
个实现。Lock
替换synchronized
方法和语句的使用,Condition
取代了对象监视器方法的使用。- 条件(也称为条件队列或条件变量 )为一个线程暂停执行(“等待”)提供了一种方法,直到另一个线程通知某些状态现在可能为真。 因为访问此共享状态信息发生在不同的线程中,所以它必须被保护,因此某种形式的锁与该条件相关联。 等待条件的关键属性是它原子地释放相关的锁并挂起当前线程,就像
Object.wait
。 - 一个
Condition
实例本质上绑定到一个锁。 要获得特定Condition
实例的Condition实例,请使用其newCondition()
方法。 - 例如,假设我们有一个有限的缓冲区,它支持
put
和take
方法。 如果在一个空的缓冲区尝试一个take
,则线程将阻塞直到一个项目可用; 如果put
试图在一个完整的缓冲区,那么线程将阻塞,直到空间变得可用。 我们希望在单独的等待集中等待put
线程和take
线程,以便我们可以在缓冲区中的项目或空间可用的时候使用仅通知单个线程的优化。 这可以使用两个Condition
实例来实现。
class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
lock.lock(); try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally { lock.unlock(); }
}
public Object take() throws InterruptedException {
lock.lock(); try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal();
return x;
} finally { lock.unlock(); }
}
}
即就是原先我们使用synchronized ,wait,notify,现在使用JUC的lock(ReentrantLock),condition,condition.await(), condition.signalAll();
实现Lock接口后的代码如下
class Resource {
private int number = 0;
private Lock lock = new ReentrantLock(); //可重入锁
private Condition condition = lock.newCondition();
public void up() throws InterruptedException{
lock.lock();
try {
while (number != 0) {
condition.await(); //相当于wait
}
number++;
System.out.println(Thread.currentThread().getName()+"\t"+number);
condition.signalAll(); //相当于notifyAll
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void down() throws InterruptedException{
lock.lock();
try {
while (number == 0) {
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName()+"\t"+number);
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
结果如下:
那么问题来了,为什么我们要用JUC的方式来替代呢?
因为原本的notify只能唤醒三个里面任意一个(注意这样唤醒并不精准),notifyall唤醒全部。
精准通知线程间的执行顺序
接着看看JUC的优势。这次我们的题目升级:多个线程之间的调用顺序,实现A->B->C:俗称风火轮
要求三个线程的启动顺序如下:
AA打印5次,BB打印10次,CC打印15次,…持续10轮。
代码如下,主方法:
public class ThreadOrderAccess { public static void main(String[] args) { ShareResource resource = new ShareResource(); new Thread(() -> { for (int i = 0; i < 10; i++) { resource.printf5(); } }, "A").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { resource.printf10(); } }, "B").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { resource.printf15(); } }, "C").start(); } }
资源类:注意标志位的修改和定位
class ShareResource { private int number = 1;// 1:A 2:B 3:C private Lock lock = new ReentrantLock(); private Condition condition1 = lock.newCondition(); private Condition condition2 = lock.newCondition(); private Condition condition3 = lock.newCondition(); //注意标志位的修改和定位 //A public void printf5() { lock.lock(); try { //判断 while (number != 1) { condition1.await(); } //干活 for (int i = 1; i <= 5; i++) { System.out.println(Thread.currentThread().getName() + "\t" + i); } //通知 number = 2; //此时B、C一直在wait状态中 condition2.signal(); //精确打击 唤醒B } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } //B public void printf10() { lock.lock(); try { while (number != 2) { condition2.await(); } for (int i = 1; i <= 10; i++) { System.out.println(Thread.currentThread().getName() + "\t" + i); } number = 3; condition3.signal(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } //C public void printf15() { lock.lock(); try { while (number != 3) { condition3.await(); } for (int i = 1; i <= 15; i++) { System.out.println(Thread.currentThread().getName() + "\t" + i); } number = 1; condition1.signal(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } }
注意这里之后的wait等方法都换成JUC的不然后报错java.lang.IllegalMonitorStateException
部分结果如下:
所以这Lock锁就是对老技术synchronized的优化,精准通知,精准唤醒,通过对指定的condition唤醒来达到对指定的线程的唤醒。
这就是本次的讲解,如果有需要改进的地方,欢迎在下方留言!