java并发线程锁技术的使用

Posted 武哥聊编程

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java并发线程锁技术的使用相关的知识,希望对你有一定的参考价值。

线程锁好比传统线程模型中的synchronized技术,但是比sychronized方式更加面向对象,与生活中的锁类似,锁本身也应该是个对象。两个线程执行的代码片段如果要实现同步互斥的效果,它们必须用同一个锁对象。锁是上在代表要操作的资源的类的内部方法中,而不是线程代码中。这一篇博文主要总结一下线程锁技术中Lock锁、ReadWriteLock锁的使用。

1. Lock的简单使用

   有了synchronized的基础,Lock就比较简单了,首先看一个实例: ```java public class LockTest {
public static void main(String[] args) {

	new LockTest().init();
}

private void init() {
	final Outputer outputer = new Outputer();
	// 线程1打印:duoxiancheng
	new Thread(new Runnable() {
		@Override
		public void run() {
			while (true) {
				try {
					Thread.sleep(5);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				outputer.output("duoxiancheng");
			}

		}
	}).start();
	;

	// 线程2打印:eson15
	new Thread(new Runnable() {
		@Override
		public void run() {
			while (true) {
				try {
					Thread.sleep(5);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				outputer.output("eson15");
			}

		}
	}).start();
	;
}

// 自定义一个类,保存锁和待执行的任务
static class Outputer {
	Lock lock = new ReentrantLock(); //定义一个锁,Lock是个接口,需实例化一个具体的Lock
	//字符串打印方法,一个个字符的打印
	public void output(String name) {

		int len = name.length();
		lock.lock();
		try {
			for (int i = 0; i < len; i++) {
				System.out.print(name.charAt(i));
			}
			System.out.println("");
		} finally {
			lock.unlock(); //try起来的原因是万一一个线程进去了然后挂了或者抛异常了,那么这个锁根本没有释放
		}
	}

}

  <font size=3>这个例子和前面介绍synchronized的例子差不多,区别在于将synchronized改成了lock。从程序中可以看出,使用Lock的时候,需要先new一个Lock对象,然后在线程任务中需要同步的地方上锁,但是一定要记得放锁,所以使用try块去处理了一下,将放锁的动作放在finally块中了。
  <font size=3>这是一个线程任务的情况,如果两个线程任务也不麻烦,还是在这个类中新建一个任务方法,因为Lock是这个类的成员变量,还是可以用这个lock,而且必须用这个lock,因为要实现同步互斥,必须使用同一把锁。
<table><tr><td bgcolor=#F5FFFA><h2><strong>2. 读写锁的妙用</h2></td></tr></table>
  <font size=3>锁又分为读锁和写锁,读锁与读锁不互斥,读锁与写锁互斥,写锁与写锁互斥,这是由jvm自己控制的。这很好理解,读嘛,大家都能读,不会对数据造成修改,只要涉及到写,那就可能出问题。 我们写代码的时候只要在正确的位置上相应的锁即可。读写锁有个接口叫ReadWriteLock,我们可以创建具体的读写锁实例,通过读写锁也可以拿到读锁和写锁。下面看一下读写锁的例子:
###**<font size=4>2.1 读写锁的基本用法**
```java
public class ReadWriteLockTest {

	public static void main(String[] args) {
		final Queue3 q3 = new Queue3(); //封装共享的数据、读写锁和待执行的任务的类

		for (int i = 0; i < 3; i++) {
			new Thread() { // 开启三个线程写数据
				public void run() {
					while (true) {
						q3.put(new Random().nextInt(10000));
					}
				}
			}.start();

			new Thread() { // 开启三个线程读数据
				public void run() {
					while (true) {
						q3.get();
					}
				}
			}.start();
		}
	}
}

class Queue3 {

	private Object data = null; // 共享的数据
	private ReadWriteLock rwl = new ReentrantReadWriteLock();// 定义读写锁

	// 读取数据的任务方法
	public void get() {
		rwl.readLock().lock(); // 上读锁
		try {
			System.out.println(Thread.currentThread().getName() 
					+ ":before read: " + data); // 读之前打印数据显示
			
			Thread.sleep((long) (Math.random() * 1000)); // 睡一会儿~
			
			System.out.println(Thread.currentThread().getName() 
					+ ":after read: " + data); // 读之后打印数据显示
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			rwl.readLock().unlock();// 释放读锁
		}
	}

	// 写数据的任务方法
	public void put(Object data) {
		rwl.writeLock().lock(); // 上写锁
		try {
			System.out.println(Thread.currentThread().getName() 
					+ ":before write: " + this.data); // 读之前打印数据显示
			
			Thread.sleep((long) (Math.random() * 1000)); // 睡一会儿~
			this.data = data; //写数据
			
			System.out.println(Thread.currentThread().getName() 
					+ ":after write: " + this.data); // 读之后打印数据显示
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			rwl.writeLock().unlock();// 释放写锁		
		}
	}
}

为了说明读锁和写锁的特点(读锁与读锁不互斥,读锁与写锁互斥,写锁与写锁互斥),我先把上面两个任务方法中上锁和释放锁的四行代码注释掉,来看一下运行结果:
没上锁
  其实不管是注释掉读锁还是注释掉写锁还是全注释掉,都会出问题,写的时候会有线程去读。那么将读写锁加上后,再看一下运行结果:
上锁后
  可以看出,有了读写锁,各个线程运行有序,从结果来看,也印证了读锁与读锁不互斥,写锁与读锁、写锁都互斥的特点。
###2.2 读写锁用于缓存数据
  现在使用读写锁写一个稍微高级一点的应用demo,即模拟缓存数据。实现的功能如下:现在有5个线程都需要拿数据,一开始是没有数据的,所以最先去拿数据的那个线程发现没数据,它就得去初始化一个数据,然后其他线程拿数据的时候就可以直接拿了。代码如下:

public class ReadWriteLockTest2 {

	public static void main(String[] args) {
		
		CacheData cache = new CacheData();
		
		for(int i = 1; i <= 5; i ++) { //开启5个线程
			new Thread(new Runnable() {
				
				@Override
				public void run() {
					cache.processCache(); //都去拿数据
				}
			}).start();
		}
	}
}

class CacheData {

	private Object data = null; // 需要缓存的数据
	private boolean cacheValid; //用来标记是否有缓存数据
	private ReadWriteLock rwl = new ReentrantReadWriteLock();// 定义读写锁

	public void processCache() {
		rwl.readLock().lock(); //上读锁
		
		if(!cacheValid) { //如果没有缓存,那说明是第一次访问,需要给data赋个值
			rwl.readLock().unlock(); //先把读锁释放掉
			rwl.writeLock().lock(); //上写锁
			if(!cacheValid) {
				System.out.println(Thread.currentThread().getName() + ": no cache!");
				data = new Random().nextInt(1000); //赋值
				cacheValid = true; //标记已经有缓存了
				System.out.println(Thread.currentThread().getName() + ": already cached!");
			}
			rwl.readLock().lock(); //再把读锁上上
			rwl.writeLock().unlock(); //把刚刚上的写锁释放掉
		}
		System.out.println(Thread.currentThread().getName() + " get data: " + data);
		rwl.readLock().unlock(); //释放读锁
	}
}

从代码中可以看出,在processCache方法中对读锁和写锁的交替使用。一开始进来都是读数据的,所以一开始都是上了读锁,但当第一个线程进来发现没有缓存数据的时候,它得写数据,那么此时它得先把读锁给释放掉,换了把写锁,告诉其他线程:”哎哥们,这里面边儿根本没数据啊,我们被坑了,让我先弄个数据来吧,不好意思你们先等会儿~“,等该线程初始化好了数据后,其他线程就可以读了,于是它又把读锁装起来了,把写锁释放了,然后它出去了。这就模拟了拿缓存数据的一个demo,可以看出,在一个方法中,同一个线程可以操作两个锁的。看一下运行结果:

Thread-1: no cache!
Thread-1: already cached!
Thread-1 get data: 893
Thread-0 get data: 893
Thread-2 get data: 893

这和Hibernate中的那个load(id, Class.class)方法有点类似,先拿到的是代理对象,要使用该对象的时候,如果发现没有,就新产生一个,如果有了就直接拿来用。
###2.3 读写锁用于缓存系统
  继续进阶,如果现在要缓存多个数据,即要写一个缓存系统,那该如何做呢?一个缓存系统无非就是一个容器,可以存储很多缓存数据,很自然的想到使用一个Map,专门装缓存数据,然后供多个线程去使用。所以整个设计思路,跟上面缓存单个数据是一样的,不过就是多考虑一些东西而已,看下代码:

public class CacheDemo {

	public static void main(String[] args) {
		
		Cache cac = new Cache();
		for(int i = 0; i < 3; i ++) { //开启三个线程去缓存中拿key为cache1的数据,
			new Thread(new Runnable() {
				
				@Override
				public void run() {
					String value = (String) cac.getData("cache1"); //第一个进入的线程要先写一个数据进去(相当于第一次从数据库中取)	
					System.out.println(Thread.currentThread().getName() + ": " + value);	
				}
			}).start();
		}
		
		for(int i = 0; i < 3; i ++) { //开启三个线程去缓存中拿key为cacahe2的数据
			new Thread(new Runnable() {
				
				@Override
				public void run() {
					String value = (String) cac.getData("cache2");//第一个进入的线程要先写一个数据进去(相当于第一次从数据库中取)
					System.out.println(Thread.currentThread().getName() + ": " + value);
				}
			}).start();
		}
	}
}

class Cache {
	//存储缓存数据的Map,注意HashMap是非线程安全的,也要进行同步操作
	private Map<String, Object> cache = Collections.synchronizedMap(new HashMap<String, Object>()); 
	private ReadWriteLock rwl = new ReentrantReadWriteLock(); //定义读写锁

	public synchronized Object getData(String key) {
		rwl.readLock().lock(); //上读锁
		Object value = null; 
		try {
			value = cache.get(key); //根据key从缓存中拿数据
			if (value == null) { //如果第一次那该key对应的数据,拿不到
				rwl.readLock().unlock(); //释放读锁
				rwl.writeLock().lock(); //换成写锁
				try {
					if (value == null) { //之所以再去判断,是为了防止几个线程同时进入了上面那个if,然后一个个都来重写赋值一遍
						System.out.println(Thread.currentThread().getName() + " write cache for " + key);
						value = "aaa" + System.currentTimeMillis(); // 实际中是去数据库中取,这里只是模拟
						cache.put(key, value); //放到缓存中
						System.out.println(Thread.currentThread().getName() + " has already written cache!");
					}
				} finally {
					rwl.writeLock().unlock(); //写完了释放写锁
				}
				rwl.readLock().lock(); //换读锁
			}
		} finally {
			rwl.readLock().unlock(); //最后呢释放读锁
		}
		return value; //返回要取的数据
	}
}

整个代码的结构和上面的一样,理解了缓存单个数据后,这个代码也不难理解。这里只是个demo,实际中可以是跟数据库打交道,第一次从缓存中拿肯定是没有的,那么就要去数据库中查,然后把取到的数据放到缓存中,下次别的线程来就能直接从缓存中取了。看一下运行结果:

Thread-0 write cache for cache1
Thread-0 has already written cache!
Thread-4 write cache for cache2
Thread-0: aaa1464782404722
Thread-4 has already written cache!
Thread-4: aaa1464782404723
Thread-3: aaa1464782404723
Thread-2: aaa1464782404722
Thread-1: aaa1464782404722
Thread-5: aaa1464782404723

从结果可以看出,线程0首先去缓存中拿key为cache1的值,没拿到,往里面写了一个,然后线程4去缓存中拿key为cache2的值也没拿到,于是也写了一个,在此期间线程0把值拿了出来,后面几个线程也随后陆续的拿出来了。
  读写锁的应用还是很广泛的,而且很好用。关于线程锁的技术就总结这么多吧。
  
  相关阅读:http://blog.csdn.net/column/details/bingfa.html


欢迎大家关注我的公众号:“武哥聊编程”,一个有温度的公众号~
关注回复:资源,可以领取海量优质视频资料
程序员私房菜


—–乐于分享,共同进步!
—–更多文章请看:http://blog.csdn.net/eson_15

以上是关于java并发线程锁技术的使用的主要内容,如果未能解决你的问题,请参考以下文章

并发技术12线程锁技术的使用

Java并发系列04线程锁synchronized和Lock和volatile和Condition

Java并发编程:Lock

Java并发006使用层面:Lock锁机制全解析

java并发编程(synchronized同步和Lock显示锁)

并发库应用之四 & 线程锁Lock应用