【Java多线程与并发库】10.java5的线程锁(读写锁)技术

Lock&Condition实现线程同步通信

(1)Lock概念
Lock比传统线程模型中的synchronized方式更加面向对象,与生活中的锁类似,
锁本身也是一个对象。两个线程执行的代码片段要实现同步互斥的效果,它们
必须用同一个Lock对象。锁是加在代表要操作的资源的类的内部方法中,而不是
线程代码中!

例子:
package cn.edu.hpu.test;

public class LockTest {
	public static void main(String[] args) {
		new TraditionalThreadSynchronized().init();
	}
	
	public void init(){


		final Outputer outputer=new Outputer();
		
		new Thread(new Runnable(){
			public void run() {
				while(true){
					try {
						Thread.sleep(10);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					outputer.output("ABCDEFGHIJKLNOPQRST");
				}
			}
		}	
		).start();
		
		new Thread(new Runnable(){
			public void run() {
				while(true){
					try {
						Thread.sleep(10);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					outputer.output("abcdefghijklmnopqrst");
				}
			}
		}	
		).start();
	}
	
	class Outputer{
		public void output(String name){
			int len=name.length();
			for (int i = 0; i < len; i++) {
				System.out.print(name.charAt(i));
			}
			System.out.println();
		}
	}
}
这段代码是没有加锁的,所以会发生在打印某个线程的数据的时候,内存会突然让给其它线程
去打印数据,导致数据只打印一半:



加锁:
package cn.edu.hpu.test;

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

public class LockTest {
	public static void main(String[] args) {
		new TraditionalThreadSynchronized().init();
	}
	
	public void init(){


		final Outputer outputer=new Outputer();
		
		new Thread(new Runnable(){
			public void run() {
				while(true){
					try {
						Thread.sleep(10);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					outputer.output("ABCDEFGHIJKLNOPQRST");
				}
			}
		}	
		).start();
		
		new Thread(new Runnable(){
			public void run() {
				while(true){
					try {
						Thread.sleep(10);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					outputer.output("abcdefghijklmnopqrst");
				}
			}
		}	
		).start();
	}
	
	class Outputer{
		Lock lock = new ReentrantLock();//创建一个锁
		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();//开锁
			}
		}
	}
}

加锁之后所有的线程都不会被打断:


(2)读写锁
读写锁:分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,写锁与写锁互斥,
这是由JVM自己控制的,你只要加好相应的锁即可。如果代码只读数据,可以很
多人同时读,但是不能同时写,那就加读锁;如果代码修改数据,只能有一个人
在写,且不能同时读取,那就加写锁。总之,读的时候加读锁,写的时候加写锁。

读写锁例子:
产生三个线程,用来读数据,然后产生另外三个线程,用来写数据。如果不加线程锁,
我们就会看到读和写的线程交替运行,也即是“读中有写,写中有读”。
package cn.edu.hpu.test;

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


public class ReadWriteLockTest {
	public static void main(String[] args) {
		final Queue q = new Queue();
		for (int i = 0; i < 3; i++) {
			new Thread(){
				public void run(){
					while(true){
						q.get();
					}
				}
			}.start();
			
			new Thread(){
				public void run(){
					while(true){
						q.put(new Random().nextInt(10000));
					}
				}
			}.start();
		}
	}
}


class Queue{
	private Object data = null;//共享数据,只有一个线程能写该数据,但可以有多个线程同时读该数据
	public void get(){
		System.out.println(Thread.currentThread().getName().toString()+"准备读取数据");
		try {
			Thread.sleep((long)Math.random()*1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread().getName().toString()+"读取数据:"+data);
	}
	
	public void put(Object data){
		System.out.println(Thread.currentThread().getName().toString()+"准备改写数据");
		try {
			Thread.sleep((long)Math.random()*1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		this.data=data;
		System.out.println(Thread.currentThread().getName().toString()+"改写数据为:"+data);
	}
}

效果:

可以看到,在线程5准备改写数据的时候,线程1去改写了数据,然后等线程1准备改写数据
的时候,线程5去改写了数据,此时对于变量data是线程不安全的。


如果我们上了读写锁,读的时候没有写,写的时候没有读和其它写,
即是“读中无写,写中无读写”。
package cn.edu.hpu.test;

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

public class ReadWriteLockTest {
	public static void main(String[] args) {
		final Queue q = new Queue();
		for (int i = 0; i < 3; i++) {
			new Thread(){
				public void run(){
					while(true){
						q.get();
					}
				}
			}.start();
			
			new Thread(){
				public void run(){
					while(true){
						q.put(new Random().nextInt(10000));
					}
				}
			}.start();
		}
	}
}


class Queue{
	private Object data = null;//共享数据,只有一个线程能写该数据,但可以有多个线程同时读该数据
	private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
	public void get(){
		rwl.readLock().lock();
		try {
			System.out.println(Thread.currentThread().getName().toString()+"准备读取数据");
			Thread.sleep((long)Math.random()*1000);
			System.out.println(Thread.currentThread().getName().toString()+"读取数据:"+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().toString()+"准备改写数据");
			Thread.sleep((long)Math.random()*1000);
			this.data=data;
			System.out.println(Thread.currentThread().getName().toString()+"改写数据为:"+data);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}finally{
			rwl.writeLock().unlock();
		}
	}
}
结果:

可以看到,当一个线程去写的时候,永远都不会被读取动作或者改写动作打断,而当一个线程
去读取的时候,永远都不会被改写动作打断,但是可以被另外的读取动作打断,这是合理的。


这里,大家看一下JavaAPI给我们的一个读写锁的例子:
public class CacheData {
    Object data;
    volatile boolean cacheValid;
    ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    
    void processCacheData(){
    	rwl.readLock().lock();
    	if(!cacheValid){//是否有缓存可用
    		//在加写锁之前必须释放读锁
    		rwl.readLock().unlock();
    		rwl.writeLock().lock();
    		//再次检查一下校验状态,以防止有其他线程修改
    		if(!cacheValid){
    			data=... //去真实的库中取值
    			cacheValid=true;
    		}
    		//在释放写锁之前要加读锁
    		rwl.readLock().lock();
    		rwl.writeLock().unlock();
    	}
    	
    	use(data);//直接取缓存来使用
    	rwl.readLock().unlock();
    }
}

看起来像是一个多个线程操作缓存数据的代码。在hibernate的二级缓存中出现过类似思想,
例如:
User user = session.get(id,User.class);
User user = session.load(id,User.class);
上下的区别是:
上面的是直接从数据库中把相应id的user数据取出来封装到User对象中去,
如果没有这个数据,返回的值是null;
下面是,不管数据库中有没有这个记录,都会得到一个User对象代理,实际
就是User$Proxy,该代理大概长这个样子(伪代码):
User$Proxy extends User{
    private Integer id = id;


    User realUser = null;
    getName(){
       if(realUser == null){
           realUser = session.get(id);
           if(realUser == null) throw exception;
       }    
       return realUser;
    }
}

即是,当真正的User不存在的时候,代理User从数据中去取数据,如果下次请求,
代理发现之前去过,就把缓存数据提供出去。

(3)缓存系统模拟
接下来我们通过使用读写锁,自己设计一个缓存系统。
package cn.edu.hpu.test;

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

public class CacheDemo {
	
	private Map<String,Object> cache=new HashMap<String,Object>();//缓存池
	private ReadWriteLock rwl=new ReentrantReadWriteLock();
	
	public static void main(String[] args) {
		CacheDemo cacheDemo=new CacheDemo();
		System.out.println("第一次取数据结果:"+cacheDemo.getData("1"));
		System.out.println("第二次取数据结果:"+cacheDemo.getData("1"));
	}
	
	public Object getData(String key){
		rwl.readLock().lock();
		Object value = null;
		try {
			value = cache.get(key);//先去缓存中去取
			if (value == null) {
				rwl.readLock().unlock();
				rwl.writeLock().lock();
				
				try {
					if(value == null){
						value = MySqlDB.getData(key);//实际去数据库取数据
						cache.put(key, value);
					}
				} catch (Exception e) {
					e.printStackTrace();
				}finally{
					rwl.writeLock().unlock();
				}
				rwl.readLock().lock();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}finally{
			rwl.readLock().unlock();
		}
		return value;
	}
}


class MySqlDB{
	
	public static Object getData(String key) throws InterruptedException{


		Object data=null;
		
		System.out.println("获取数据库连接...");
		Thread.sleep(2000);
		System.out.println("开启事务...");
		Thread.sleep(2000);
		System.out.println("编译sql语句...");
		Thread.sleep(2000);
		System.out.println("查找数据...");
		Thread.sleep(5000);
		System.out.println("返回数据...");
		Thread.sleep(2000);
		if(key.equals("1")){
			data=new String("张三"); 
		}
		System.out.println("关闭事务...");
		Thread.sleep(2000);
		System.out.println("关闭数据库连接...");
		Thread.sleep(2000);
		
		return data;
	}
}
结果:


这样就是实现了,当第一次取数据的时候去数据库取,后面取数据从缓存中取,

而且其中的读写都是线程绝对安全的。

转载请注明出处:http://blog.csdn.net/acmman/article/details/52902128

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

光仔December

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值