Java多线程与并发应用-(9)-锁lock+条件阻塞conditon实现线程同步通信

时间:2021-12-14 18:18:27

一. lock可以代替synchronized关键字实现互斥功能。使用方法如下:

Lock l = ...;
     l.lock();
     try {
         // access the resource protected by this lock
     } finally {
         l.unlock();
     }

需要注意的是。

1.需要互斥的一个或多个方法要使用同一个互斥锁。

2.在被锁包含的代码块中,要使用finally块将锁释放。

二. Condition的await方法(注意不是wait方法)可以替换传统通信中的wait方法,对应的signal方法替换notify。

在传统通信的条件判断时,我们会用while而不是if做条件判断,是为了虚假唤醒。那么什么是虚假唤醒呢?

虚假唤醒即:如:我们要求AB方法按顺序执行,有5个线程执行A,5个线程执行B,如果某时刻全部A等待,当其中A1被唤醒,并执行完代码后,会调用notify方法,其本意是唤醒B模块执行线程,但是由于AB公用一个锁,所以可能将A唤醒,即唤醒了不该执行的代码,这就是虚假唤醒。

所以我们使用while条件,即使被唤醒了,我们还会做一次条件判读,这样被虚假唤醒的代码将再一次等待。

这就要求程序员来控制避免虚假唤醒带来的错误。而Lock和Condition的帮我们解决了这个问题。一个锁内部可以有多个Condition.那么同一个锁内可以多个condition实现模块

之间的切换,如上面的例子中A1再执行完之后,通过B对应的Condtion.signal只可以唤醒B对应的线程。我们可以看看Condition的API中的例子,阻塞队列的简单实现:

首先我们看看传统的通信技术实现简单的阻塞队列,有何弊端?

package com.lipeng;

import java.util.Random;


public class BoundedBuffer1 <T>{
	private Object[] objs=new Object[100];
	private int length;
	private int putIndex=0;//存指针
	private int getIndex=0;//取指针
	/**
	 * 存放元素,从头到尾,再反复从头到尾
	 * @param t
	 */
	public synchronized void put(T t)
	{
		//如果已经放满了,就等待。
		while(length==objs.length)//如果N个线程在这儿等待, 其中一个线程被唤醒后,执行下面的代码,35行this.notify本意是唤醒取线程取数据,但其实可能唤醒存线程。
		{
			try {
				this.wait();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		objs[putIndex]=t; //在队尾插入元素
		length++;  //长度加1,
		putIndex++;
		if(putIndex==objs.length)
		{
			//注意不是放满了才从起始处放,而是存放指针到队尾了再从头开始。
			putIndex=0;
		}
		this.notify();
		
	}
	/**
	 * 取元素,从头到尾取,在如此反复。
	 * @return
	 */
	public synchronized T get()
	{
		while(length==0)
		{
			try {
				this.wait();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		T t=(T) objs[getIndex];
		length--;
		getIndex++;
		if(getIndex==objs.length)
		{
			getIndex=0;
		}
		this.notify();
		return t;
	}
	
	public static void main(String[] args) {
		final BoundedBuffer1<Integer> bb=new BoundedBuffer1<Integer>();
		Runnable getRun=new Runnable() {
			@Override
			public void run() {
				for(int i=0;i<10;i++)
				{
					synchronized (bb) { //这里加synchronized只是为了让读取数据和打印数据保持完整性,做演示用用
						Integer data=bb.get();
						System.out.println(Thread.currentThread().getName()+"  读取元素------   "+data);
					}
					
				}
			}
		};
		Runnable putRun=new Runnable() {
			
			@Override
			public void run() {
				for(int i=0;i<10;i++)
				{
					synchronized (bb) {//这里加synchronized只是为了让存放数据和打印数据保持完整性,做演示用用
						Integer data=new Random().nextInt(100);
						bb.put(data);
						System.out.println(Thread.currentThread().getName()+"  放入---------------------------   "+data);
					}
				}
			}
		};
		System.out.println("***********************");
		for(int i=0;i<10;i++)
		{
			new Thread(getRun).start();
			new Thread(putRun).start();
		}
	}
}






















我们再来看Condition是如何帮我们实现的?

package com.lipeng;

import java.util.Random;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;


public class BoundedBuffer2 <T>{
	private Object[] objs=new Object[100];
	private int length;
	private int putIndex=0;//存指针
	private int getIndex=0;//取指针
	
	private Lock lock=new ReentrantLock();
	private Condition putCon=lock.newCondition();//存放条件
	private Condition getCon=lock.newCondition();// 取条件
	
	/**
	 * 存放元素,从头到尾,再反复从头到尾
	 * @param t
	 */
	public void put(T t)
	{
		try {
			lock.lock();
			//如果已经放满了,就等待。
			while(length==objs.length)//如果N个线程在这儿等待, 其中一个线程被唤醒后,执行下面的代码,35行this.notify本意是唤醒取线程取数据,但其实可能唤醒存线程。
			{
				try {
					putCon.await();
				} catch (Exception e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			objs[putIndex]=t; //在队尾插入元素
			length++;  //长度加1,
			putIndex++;
			if(putIndex==objs.length)
			{
				//注意不是放满了才从起始处放,而是存放指针到队尾了再从头开始。
				putIndex=0;
			}
			getCon.signal();
			System.out.println(Thread.currentThread().getName()+"  放入---------------------------   "+t);
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}finally{
			lock.unlock();
		}
		
	}
	/**
	 * 取元素,从头到尾取,在如此反复。
	 * @return
	 */
	public T get()
	{
		try {
			lock.lock();
			while(length==0)
			{
				try {
					getCon.await();
				} catch (Exception e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			T t=(T) objs[getIndex];
			length--;
			getIndex++;
			if(getIndex==objs.length)
			{
				getIndex=0;
			}
			putCon.signal();
			System.out.println(Thread.currentThread().getName()+"  读取元素------   "+t);
			return t;
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return null;
		}finally{
			lock.unlock();
		}
	}
	
	public static void main(String[] args) {
		final BoundedBuffer2<Integer> bb=new BoundedBuffer2<Integer>();
		Runnable getRun=new Runnable() {
			@Override
			public void run() {
				for(int i=0;i<10;i++)
				{
					bb.get();
					
				}
			}
		};
		Runnable putRun=new Runnable() {
			
			@Override
			public void run() {
				for(int i=0;i<10;i++)
				{
					Integer data=new Random().nextInt(100);
					bb.put(data);
				}
			}
		};
		System.out.println("***********************");
		for(int i=0;i<10;i++)
		{
			new Thread(getRun).start();
			new Thread(putRun).start();
		}
	}
}






















注意事项:

Lock(包括读写锁)+Condition看起来更加面向对象,也似乎提高了性能*(因为我没验证过,哈哈)也更加严谨,但我认为如果传统的通信方法够用,没必要使用它。