java并发---lock和condition

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u013013553/article/details/78700168

一、java.util.concurrent.locks下面接口介绍       

  java.util.concurrent.locks包为锁和等待条件提供一个框架的接口和类,它不同于内置同步(synchronize)和监视器。该框架允许更灵活地使用lock和condition,但以更难用的语法为代价。 它下面有三个接口:Lock , ReadtrantLock , Condition 。

        Lock 接口支持那些语义不同(重入、公平等)的锁规则,可以在非阻塞式结构的上下文(包括 hand-over-hand 和锁重排算法)中使用这些规则。主要的实现是 ReentrantLock。 

        ReadWriteLock 接口以类似方式定义了一些读取者可以共享的锁, 但是把写者排除在外。此包只提供了一个实现,即 ReentrantReadWriteLock,因为它适用于大部分的标准用法上下文。但程序员可以创建自己的实现方法来覆盖非标准要求。 

        Condition 接口描述与锁有关联的条件变量。这些变量在用法上与 Object.wait 访问的隐式监视器用法类似,但提供了更强大的功能。需要特别指出的是,单个 Lock 可能与多个 Condition 对象关联。为了避免兼容性问题,Condition 方法的名称与对应的 Object 版本中的不同。

  包下接口关系图如下:


  

二、对比

   2.1 synchronized和lock对比

     java中使用锁的两个基本工具是synchronized和lock。synchronized可以用在代码快上,也可以用在方法上。synchronized是java的关键字,也是java内置的特性。那为什么从java1.5后出现了lock?

     当使用synchronized修饰一个代码块时,是通过监控当前当代码块所在类的实例对象。当一个线程获取到对应的锁,执行该代码块时,其他线程只能处于等待状态,等待当前线程释放锁,在这里释放锁的情况有两种:

       第一种:当前线程执行完毕,释放该锁

       第二种:当前线程执行异常,JVM让线程自动释放锁

     如果当前线程因为一些IO操作或者一些其他原因(比如sleep很久),那么,其他线程一直处于等待,无法获得该锁,无法进入代码块。这会大大的影响程序的效率,程序的执行。这就需要控制当前线程执行,不会让其他线程一直等待下去,Lock就能起到控制的作用。

     Lock可以实现,线程有没有获取到锁,这个synchronized是无法做到的。

     两点一定要记住:第一点 , synchronized是java内置的特性,但是Lock的功能更强大。 第二点:Lock锁的释放,是需要手动释放的,但是synchronized是自动释放的。synchronized和object对象的wait/notify/notifyall一起组合使用,效果是最好的。

   2.2 lock和condition关系

                    Condition可以替代传统的线程间通信,用await()替代wait,用signal替代notify(),用signalAll()替代notifyAll()。因为Object下面的wait/notify/notifyAll方法都是final的,所以名称上全都发生了改变。传统线程通信方式,condition都能实现。

     注意:condition()是被绑定到Lock上面的,要创建一个Lock的conditon,需要用newCondition 。现在知道了,synchronized和notidy/wait/notifyAll结合使用, lock和condition的await/signal/signalAll结合使用。

     condition的强大之处,它可以为多个线程之间创建不同的condition。

     java.util.concurrent.ArrayBlockingQueue的部分功能,实现put()和take()。假定有一个绑定的缓冲区,它支持puttake 方法。如果试图在空的缓冲区上执行 take 操作,则在某一个项变得可用之前,线程将一直阻塞;如果试图在满的缓冲区上执行put 操作,则在有空间变得可用之前,线程将一直阻塞。我们喜欢在单独的等待 set 中保存put 线程和take 线程,这样就可以在缓冲区中的项或空间变得可用时利用最佳规划,一次只通知一个线程。可以使用两个condition 实例来做到这一点。

                    代码如下:

    class BoundedBuffer {  
      final Lock lock = new ReentrantLock();          //锁对象  
      final Condition notFull  = lock.newCondition(); //写线程锁  
      final Condition notEmpty = lock.newCondition(); //读线程锁  
      
      final Object[] items = new Object[100];//缓存队列  
      int putptr;  //写索引  
      int takeptr; //读索引  
      int count;   //队列中数据数目  
      
      //写  
      public void put(Object x) throws InterruptedException {  
        lock.lock(); //锁定  
        try {  
          // 如果队列满,则阻塞<写线程>  
          while (count == items.length) {  
            notFull.await();   
          }  
          // 写入队列,并更新写索引  
          items[putptr] = x;   
          if (++putptr == items.length) putptr = 0;   
          ++count;  
      
          // 唤醒<读线程>  
          notEmpty.signal();   
        } finally {   
          lock.unlock();//解除锁定   
        }   
      }  
      
      //读   
      public Object take() throws InterruptedException {   
        lock.lock(); //锁定   
        try {  
          // 如果队列空,则阻塞<读线程>  
          while (count == 0) {  
             notEmpty.await();  
          }  
      
          //读取队列,并更新读索引  
          Object x = items[takeptr];   
          if (++takeptr == items.length) takeptr = 0;  
          --count;  
      
          // 唤醒<写线程>  
          notFull.signal();   
          return x;   
        } finally {   
          lock.unlock();//解除锁定   
        }   
      }   

三、Lock接口的实现类和ReadWriteLock实现类

   3.1、ReentrantLock

                    ReentrantLock是Lock接口的主要实现类,当然还有另外两种实现方式。它拥有与synchronized 相同的并发性和内存语义,但是添加了类似锁投票定时锁等候可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。)

      

    class Outputter1 {    
        private 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));    
                }    
            } finally {    
                lock.unlock();// 释放锁    
            }    
        }    
    }    

  区别:

  需要注意的是,用sychronized修饰的方法或者语句块在代码执行完之后锁自动释放,而是用Lock需手动释放所以为了保证锁最终被释放(发生异常情况),要把互斥区放在try内,释放锁放在finally内!!



   3.2、ReentrantReadWriteLock

                   ReentrantReadWriteLock是ReadWriteLock接口的实现类。  上例中展示的是和synchronized相同的功能,那Lock的优势在哪里?

     例如一个类对其内部共享数据data提供了get()和set()方法,如果用synchronized,则代码如下:      

class syncData {      
    private int data;// 共享数据      
    public synchronized void set(int data) {  
        System.out.println(Thread.currentThread().getName() + "准备写入数据");  
        try {  
            Thread.sleep(20);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        this.data = data;  
        System.out.println(Thread.currentThread().getName() + "写入" + this.data);  
    }     
    public synchronized  void get() {  
        System.out.println(Thread.currentThread().getName() + "准备读取数据");  
        try {  
            Thread.sleep(20);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        System.out.println(Thread.currentThread().getName() + "读取" + this.data);  
    }  
}  
     然后写个测试类来用多个线程分别读写这个共享数据:

public static void main(String[] args) {  
//        final Data data = new Data();  
          final syncData data = new syncData();  
//        final RwLockData data = new RwLockData();  
        
        //写入
        for (int i = 0; i < 3; i++) {  
            Thread t = new Thread(new Runnable() {  
                @Override
		public void run() {  
                    for (int j = 0; j < 5; j++) {  
                        data.set(new Random().nextInt(30));  
                    }  
                }  
            });
            t.setName("Thread-W" + i);
            t.start();
        }  
        //读取
        for (int i = 0; i < 3; i++) {  
        	Thread t = new Thread(new Runnable() {  
                @Override
		public void run() {  
                    for (int j = 0; j < 5; j++) {  
                        data.get();  
                    }  
                }  
            });  
        	t.setName("Thread-R" + i);
        	t.start();
        }  
    }  
     上面运行中,各个线程互不干扰!读取线程和写入线程互不干扰是正常的,但是两个读线程之间不需要互斥

     我们可以用读写锁ReadWriteLock实现:

     
    class Data {      
        private int data;// 共享数据  
        private ReadWriteLock rwl = new ReentrantReadWriteLock();     
        public void set(int data) {  
            rwl.writeLock().lock();// 取到写锁  
            try {  
                System.out.println(Thread.currentThread().getName() + "准备写入数据");  
                try {  
                    Thread.sleep(20);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
                this.data = data;  
                System.out.println(Thread.currentThread().getName() + "写入" + this.data);  
            } finally {  
                rwl.writeLock().unlock();// 释放写锁  
            }  
        }     

        public void get() {  
            rwl.readLock().lock();// 取到读锁  
            try {  
                System.out.println(Thread.currentThread().getName() + "准备读取数据");  
                try {  
                    Thread.sleep(20);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
                System.out.println(Thread.currentThread().getName() + "读取" + this.data);  
            } finally {  
                rwl.readLock().unlock();// 释放读锁  
            }  
        }  
    }  

     与互斥锁定相比,读-写锁定允许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据(reader 线程)

     从理论上讲,与互斥锁定相比,使用读-写锁定所允许的并发性增强将带来更大的性能提高。在实践中,只有在多处理器上并且只在访问模式适用于共享数据时,才能完全实现并发性增强。——例如,某个最初用数据填充并且之后不经常对其进行修改的 collection,因为经常对其进行搜索(比如搜索某种目录),所以这样的 collection 是使用读-写锁定的理想候选者。

参考博客:

lock和condition详解

锁机制:synchronized,Lock,Condition   

展开阅读全文

没有更多推荐了,返回首页