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

2021-03-19 03:27

阅读:344

标签:gen   基础   math   传统   process   eval   val   start   任务   

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

1. Lock的简单使用

有了synchronized 的基础,Lock 就比较简单了,首先看一个实例: 

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 

这个例子和前面介绍 synchronized 的例子差不多,区别在于将 synchronized 改成了 lock。从程序中可以看出,使用 Lock 的时候,需要先 new 一个 Lock 对象,然后在线程任务中需要同步的地方上锁,但是一定要记得放锁,所以使用 try 块去处理了一下,将放锁的动作放在 finally 块中了。

这是一个线程任务的情况,如果两个线程任务也不麻烦,还是在这个类中新建一个任务方法,因为 Lock 是这个类的成员变量,还是可以用这个 lock,而且必须用这个 lock,因为要实现同步互斥,必须使用同一把锁。

2. 读写锁的妙用

锁又分为读锁和写锁,读锁与读锁不互斥,读锁与写锁互斥,写锁与写锁互斥,这是由 jvm 自己控制的。这很好理解,读嘛,大家都能读,不会对数据造成修改,只要涉及到写,那就可能出问题。我们写代码的时候只要在挣钱的位置上相应的锁即可。读写锁有个接口叫 ReadWriteLock,我们可以创建具体的读写锁实例,通过读写锁也可以拿到读锁和写锁。下面看一下读写锁的例子。

2.1 读写锁的基本用法

public class ReadWriteLockTest {

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

       for (int i = 0; i 

为了说明读锁和写锁的特点(读锁和读锁不互斥,读锁与写锁互斥,写锁与写锁互斥),我先把上面两个任务方法中上锁和放锁的四行代码注释掉,来看一下运行结果。

技术图片

其实不管是注释调读锁还是注释调写锁,还是全注释掉,都会出问题,写的时候会有线程去读。那么将读写锁加上后,再看一下运行结果。

技术图片

可以看出,有了读写锁,各个线程运行有序,从结果来看,也印证了读锁和读锁不互斥,写锁与读锁、写锁都互斥的特点。

2.2 读写锁用于缓存数据

现在使用读写锁写一个模拟缓存数据的 demo,实现功能如下:现在有5个线程都需要拿数据,一开始是没有数据的,所以最先去拿数据的那个线程发现没数据,它就得去初始化一个数据,然后其他线程拿数据的时候就可以直接拿了。代码如下。

public class ReadWriteLockTest2 {

   public static void main(String[] args) {

       CacheData cache = new CacheData();

       for(int i = 1; i 

从代码中可以看出,在 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  cache = Collections.synchronizedMap(new HashMap()); 
   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 把值拿了出来,后面几个线程也随后陆续的拿出来了。读写锁的应用还是很广泛的,而且很好用,就总结这么多吧。

更多推荐阅读:

微服务架构盛行的时代,你需要了解点 Spring Boot
读一篇故事,交一个朋友~
Java干货视频资源
web前端干货视频资源
资源大放送!!!永久有效,持续更新(必收藏系列)
【整理分享】你需要的资源在这里,拿好不谢

如果觉得对您有帮助,请转发给更多人吧~
这里有技术、有段子、有生活、有资源
↓↓↓
技术图片
技术图片

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

标签:gen   基础   math   传统   process   eval   val   start   任务   

原文地址:https://blog.51cto.com/14989612/2548421


评论


亲,登录后才可以留言!