欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java多线程编程---线程锁与读写锁

程序员文章站 2022-05-22 12:37:33
...

java.util.concurrent.locks

        为锁和等待条件提供一个框架的接口和类的相关包。

接口摘要

        1、Condition:Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set)。

        2、Lock:Lock实现提供了比使用synchronized 方法和语句可获得的更广泛的锁定操作。

        3、ReadWriteLock:ReadWriteLock维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。


类摘要

        1、AbstractOwnableSynchronizer:可以由线程以独占方式拥有的同步器。

        2、AbstractQueuedLongSynchronizer:以 long 形式维护同步状态的一个 AbstractQueuedSynchronizer 版本。

        3、AbstractQueuedSynchronizer:为实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供一个框架。

        4、LockSupport:用来创建锁和其他同步类的基本线程阻塞原语。

        5ReentrantLock:一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。

        6ReentrantReadWriteLock:支持与 ReentrantLock 类似语义的 ReadWriteLock 实现。

        7、ReentrantReadWriteLock.ReadLock:ReentrantReadWriteLock.readLock() 方法返回的锁。

        8、ReentrantReadWriteLock.WriteLock:ReentrantReadWriteLock.writeLock() 方法返回的锁。



线程锁

        Lock比传统线程模型中的synchronized更加面向对象,锁本身也是一个对象,两个线程执行的代码要实现同步互斥效果,就要使用同一个锁对象。锁要上在要操作的资源类的内部方法中,而不是线程代码中。

        public interface Lock

        所有已知实现类:ReentrantLock, ReentrantReadWriteLock.ReadLock, ReentrantReadWriteLock.WriteLock。

        随着灵活性的增加,也带来了更多的责任。不使用块结构锁就失去了使用 synchronized 方法和语句时会出现的锁自动释放功能。在大多数情况下,应该使用以下语句:

        Lock l = ...;

        l.lock();

        try {

            // access the resourceprotected by this lock

        } finally {

            l.unlock();

        }

        锁定和取消锁定出现在不同作用范围中时,必须谨慎地确保保持锁定时所执行的所有代码用 try-finally 或 try-catch 加以保护,以确保在必要时释放锁。

方法摘要

        1、void lock():获取锁。

        2、void lockInterruptibly():如果当前线程未被中断,则获取锁。

        3、Condition newCondition():返回绑定到此 Lock 实例的新 Condition 实例。

        4、boolean tryLock():仅在调用时锁为空闲状态才获取该锁。

        5、boolean tryLock(longtime, TimeUnit unit):如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁。

        6、void unlock():释放锁。

程序实例

打印字符串例子

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

/**
 * 
 * @Description: Lock使用实例
 *
 * @author: zxt
 *
 * @time: 2018年4月9日 下午3:20:30
 *
 */
public class LockTest {

	public static void main(String[] args) {
		final Output output = new Output();

		// 线程1
		new Thread(new Runnable() {

			public void run() {
				for (int i = 1; i <= 50; i++) {
					output.output(Thread.currentThread().getName() + " hello!!");
				}
			}
		}).start();
		
		// 线程2
		new Thread(new Runnable() {

			public void run() {
				for (int i = 1; i <= 50; i++) {
					output.output(Thread.currentThread().getName() + " world!!");
				}
			}
		}).start();
	}

}

class Output {
	
	Lock lock = new ReentrantLock();
	
	public void output(String name) {
		// 存在的问题,如果进去后程序出现异常,锁就一直不能释放了,别的线程就再也进不去了。
		lock.lock();
		try {
			// 将一个字符串挨个打印出
			for (int i = 0; i < name.length(); i++) {
				System.out.print(name.charAt(i));
			}
			System.out.println();
			
		} finally {
			// 解决办法:将锁里边的代码用try包围起来,在finally里边unlock,无论是否正常都要释放锁。
			lock.unlock();
		}
	}
}

上述例子中,当不加锁操作时,两个线程打印字符串的流程会被打乱:

Java多线程编程---线程锁与读写锁

加上锁之后,打印流程能在线程内保证原子性

Java多线程编程---线程锁与读写锁

注意:和synchronized不同的是,在线程执行完以后,要关闭锁unlock(),如果不关闭,其他在等待的线程就永远被锁在外面了。因为synchronized是在JVM层面实现的,因此系统可以监控锁的释放与否,而ReentrantLock使用代码实现的,系统无法自动释放锁,需要在代码中finally子句中显式释放锁lock.unlock()。

 


读写锁

        ReentrantReadWriteLock:读写锁,分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,写锁与写锁互斥,由JVM控制。

构造方法摘要

        ReentrantReadWriteLock():使用默认(非公平)的排序属性创建一个新的 ReentrantReadWriteLock。

        ReentrantReadWriteLock(boolean fair):使用给定的公平策略创建一个新的 ReentrantReadWriteLock。

方法摘要

        1、protected Thread getOwner():返回当前拥有写入锁的线程,如果没有这样的线程,则返回 null。

        2、protected Collection<Thread> getQueuedReaderThreads():返回一个 collection,它包含可能正在等待获取读取锁的线程。

        3、protected  Collection<Thread> getQueuedThreads():返回一个 collection,它包含可能正在等待获取读取或写入锁的线程。

        4、protected  Collection<Thread> getQueuedWriterThreads():返回一个 collection,它包含可能正在等待获取写入锁的线程。

        5、int getQueueLength():返回等待获取读取或写入锁的线程估计数目。

        6、int getReadHoldCount():查询当前线程在此锁上保持的重入读取锁数量。

        7、int getReadLockCount():查询为此锁保持的读取锁数量。

        8、protected  Collection<Thread> getWaitingThreads(Conditioncondition):返回一个 collection,它包含可能正在等待与写入锁相关的给定条件的那些线程。

        9、int getWaitQueueLength(Conditioncondition):返回正等待与写入锁相关的给定条件的线程估计数目。

        10、int getWriteHoldCount():查询当前线程在此锁上保持的重入写入锁数量。

        11、boolean hasQueuedThread(Thread thread):查询是否给定线程正在等待获取读取或写入锁。

        12、boolean hasQueuedThreads():查询是否所有的线程正在等待获取读取或写入锁。

        13、boolean   hasWaiters(Conditioncondition):查询是否有些线程正在等待与写入锁有关的给定条件。

        14、boolean isFair():如果此锁将公平性设置为 ture,则返回 true。

        15、boolean   isWriteLocked():查询是否某个线程保持了写入锁。

        16、boolean isWriteLockedByCurrentThread():查询当前线程是否保持了写入锁。

        17ReentrantReadWriteLock.ReadLockreadLock():返回用于读取操作的锁。

        18、String toString():返回标识此锁及其锁状态的字符串。

        19ReentrantReadWriteLock.WriteLock  writeLock():返回用于写入操作的锁。

程序实例

        创建三个线程读数据,三个线程写数据示例:可以同时读,读的时候不能写,不能同时写,写的时候不能读。读的时候上读锁,读完解锁;写的时候上写锁,写完解锁。注意在finally代码块中解锁。

import java.util.Random;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockTest {

	public static void main(String[] args) {
		final Mydata mydata = new Mydata();
		
		// 构造6个线程,3个读,3个写
		for(int i = 0; i < 3; i++) {
			// 读线程
			new Thread(new Runnable() {
				
				@Override
				public void run() {
					while(true) {
						mydata.get();
					}
				}
			}).start();
			
			// 写线程
			new Thread(new Runnable() {
				
				@Override
				public void run() {
					while(true) {
						mydata.put(new Random().nextInt(10000));
					}
				}
			}).start();
		}
	}
}

/**
 * 
 * @Description: 共线数据类,里面有获取数据的读方法和存放数据的写方法
 *
 * @author: zxt
 *
 * @time: 2018年4月9日 下午4:29:26
 *
 */
class Mydata {
	// 共享数据,只能有一个线程能写该数据,但可以有多个线程同时读该数据。
	private Object data = null;
	
	// 读写锁,(这里不能使用Lock,因为Lock会阻止所有的线程,而读数据的时候可以多个线程同时读)
	ReadWriteLock rwLock = new ReentrantReadWriteLock();

	/**
	 * 
	 * @Description:获取数据
	 *
	 */
	public void get() {
		// 读数据,上读锁
		rwLock.readLock().lock();
		try {
			System.out.println(Thread.currentThread().getName() + " is ready to read data!");
			Thread.sleep((long) Math.random() * 1000);
			System.out.println(Thread.currentThread().getName() + " have read data :" + data);

		} catch (InterruptedException e) {
			e.printStackTrace();
			
		} finally {
			rwLock.readLock().unlock();
		}
	}

	/**
	 * 
	 * @Description:存放数据
	 *
	 */
	public void put(Object data) {
		// 写数据上写锁
		rwLock.writeLock().lock();
		try {
			System.out.println(Thread.currentThread().getName() + " is ready to write data!");
			Thread.sleep((long) Math.random() * 1000);
			this.data = data;
			System.out.println(Thread.currentThread().getName() + " have write data :" + data);

		} catch (InterruptedException e) {
			e.printStackTrace();
			
		} finally {
			rwLock.writeLock().unlock();
		}
	}
}

当不加读写锁时,结果如下:不管是读还是写的过程,都有可能被其他线程抢占cpu执行,从而导致结果不正确。

Java多线程编程---线程锁与读写锁

加上读写锁之后的结果:

Java多线程编程---线程锁与读写锁


        JDK帮助文档中的示例用法。下面的代码展示了如何利用重入来执行升级缓存后的锁降级(为简单起见,省略了异常处理):

class CachedData {
    Object data;
    volatile boolean cacheValid;	数据有没有(存在与否)的标记
    ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    void process CachedData() { 处理数据
        rwl.readLock().lock();先上读锁
            if (!cacheValid) { 如果数据不存在
                // Must release read lock before acquiring write lock
                rwl.readLock().unlock();准备写数据,需先解除读锁
                rwl.writeLock().lock();上写锁
                // Recheck state because another thread might have acquired
                // write lock and changed state before we did.
                if (!cacheValid) {再次检查数据是否存在,防止其他线程已经存入数据
                    data = ...
                    cacheValid = true;写好数据,改变标记
                }
                // Downgrade by acquiring read lock before releasing write lock
                准备释放写锁,数据存在了,释放后就要使用数据,恢复产生数据前的读锁状态
                rwl.readLock().lock();
                rwl.writeLock().unlock(); // Unlock write, still hold read
            }

     	use(data);存在直接使用数据
	rwl.readLock().unlock();解除读锁
    }
}

面试题:设计一个缓存系统

        缓存系统:你要取数据,需调用我的public Object getData(String key)方法,我要检查我内部有没有这个数据,如果有就直接返回,如果没有,就从数据库中查找这个数,查到后将这个数据存入我内部的存储器中,下次再有人来要这个数据,我就直接返回这个数不用再到数据库中找了。你要取数据不要找数据库,来找我。

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 
 * @Description: 实现一个类似于缓存系统的功能:多个线程读某个数据,若发现数据为空,则由一个线程写数据
 *
 * @author: zxt
 *
 * @time: 2018年4月9日 下午10:36:50
 *
 */
public class CacheDemo {

	// 定义一个读写锁
	private static ReadWriteLock rwLock = new ReentrantReadWriteLock();

	// 存储数据的 map
	private static Map<String, Object> map = new HashMap<String, Object>();

	public static void main(String[] args) {
		for(int i = 0; i < 3; i++) {
			final String key = i + "";
			new Thread(new Runnable() {
				
				@Override
				public void run() {
					while(true) {
						System.out.println(Thread.currentThread().getName() + " read data: " + getData(key));
					}
				}
			}).start();
		}
	}

	/**
	 * 
	 * @Description:通过key来取数据,若没有,则写,用读写锁来同步
	 * 
	 * @param key
	 * @return
	 */
	public static Object getData(String key) {
		// 首先尝试读取数据,加读锁
		rwLock.readLock().lock();
		Object value = null;
		try {
			value = map.get(key);
			if (value == null) {
				// 没有数据可供读取,则尝试写入数据
				// 先释放读锁,在加写锁
				rwLock.readLock().unlock();
				rwLock.writeLock().lock();

				try {
					// 假设此时有多个线程同时去获取写锁,我们知道只有第一个线程能够获取,那么其他的线程只能等待。
					// 如果第一个线程按流程执行完后,刚才等待的线程可以得到写锁了, 然后接着就可以修改数据了(赋值)。所以加上再次判断!
					if (value == null) {
						// 此处在实际应用中,可能就是从数据库中查找数据,放入缓存中
						value = Thread.currentThread().getName() + new Date();
					}

				} finally {
					// 写数据结束,释放写锁
					rwLock.writeLock().unlock();
				}

				// 此时需要再次加上读锁,读取数据
				rwLock.readLock().lock();
			}

		} finally {
			rwLock.readLock().unlock();
		}

		return value;
	}
}
Java多线程编程---线程锁与读写锁

可以发现,线程不会读取到null值,同时,首次读取之后,该值也不会改变。