Java Note: 多线程的同步(互斥锁)的方法对比,信号量锁,读写锁的实现,生产者-消费者模式的实现

本文摘录自:http://blog.csdn.net/ns_code/article/details/17487337

Java 5中引入了新的锁机制——java.util.concurrent.locks中的显式的互斥锁:Lock接口,它提供了比synchronized更加广泛的锁定操作。Lock接口有3个实现它的类:ReentrantLock、ReetrantReadWriteLock.ReadLock和ReetrantReadWriteLock.WriteLock,即重入锁、读锁和写锁。

所以,读写锁在Java中的实现是用ReentranLock实现的。

ReentranLock与synchronized比较

不同的:

在JDK1.5中,synchronized是性能低效的。因为这是一个重量级操作,它对性能最大的影响是阻塞的是实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来了很大的压力。在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。

synchronize代表一种悲观的并发策略,这种同步又称为阻塞同步,它属于一种悲观的并发策略,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。

而ReentranLock是基于冲突检测的乐观并发策略,通俗地讲就是先进性操作,如果没有其他线程争用共享数据,那操作就成功了,如果共享数据被争用,产生了冲突,那就再进行其他的补偿措施(最常见的补偿措施就是不断地重拾,直到试成功为止)。

相同的:

基本语法上,ReentrantLock与synchronized很相似,它们都具备一样的线程重入特性。

同时,ReentrantLock有三个高级功能:

1、等待可中断:当持有锁的线程长期不释放锁时,正在等待的线程可以选择放弃等待,改为处理其他事情,它对处理执行时间非常上的同步块很有帮助。而在等待由synchronized产生的互斥锁时,会一直阻塞,是不能被中断的。

2、可实现公平锁:多个线程在等待同一个锁时,必须按照申请锁的时间顺序排队等待,而非公平锁则不保证这点,在锁释放时,任何一个等待锁的线程都有机会获得锁。synchronized中的锁时非公平锁,ReentrantLock默认情况下也是非公平锁,但可以通过构造方法ReentrantLock(ture)来要求使用公平锁。公平锁确保下一个进入临界区的线程是最先开始等待的线程。

3、锁可以绑定多个条件:ReentrantLock对象可以同时绑定多个Condition对象(名曰:条件变量或条件队列),而在synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含条件,但如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock则无需这么做,只需要多次调用newCondition()方法(方法返回一个条件锁,可以调用await()和signal和signalAll等方法)即可。而且我们还可以通过绑定Condition对象来判断当前线程通知的是哪些线程(即与Condition对象绑定在一起的其他线程)。

使用方法如下:

Java总体而言,更支持Synchronized的同步方法来消除race condition。但从Java1.6开始,Java也提供了在调用的类中添加Lock对象的类成员,然后在类方法中调用Lock对象的lock()方法来加锁的语法。这种Lock对象加锁的方法可以在某些领域中比Synchronized更有用[1]:

1.某个线程在等待一个锁的控制权的这段时间需要中断
2.需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify哪个线程
3.具有公平锁功能,每个到来的线程都将排队等候

一般,我们会使用Lock的ReentrantLock这个实现类来实现Lock的功能。可重入锁具有可重入的特点。意思就是说,一个线程可以对已经被加锁的对象,继续加锁。在这一点上,和Synchronized是一样的。之前提过,一旦一个线程进入“浴室隔间”,其它人就只能在外面等着。因为所有的对象级别的同步方法都会被锁上。这时候,如果已经进入浴室的那个线程想再调用其它的“浴室隔间”,是没有问题的。这就是可重入的问题。在我进入了一个ReentrantLock的.lock()的方法之后,我可以继续调用其它有.lock()的方法。然后,这个ReentrantLock的对象就会在锁计数器中加1。每次加一都需要在进入的方法块中的finally 中调用.unlock()来-1同时释放锁。直到计数器到0。那个进去隔间的线程出来了,其它线程才能进入锁的临界区(隔间)。

当然,ReentrantLock有Synchronized不具备的功能:

1)提供tryLock,如果获取了锁立即返回true,如果别的线程正持有锁,立即返回false,不会再继续尝试。
2)提供tryLock(long timeout,TimeUnit unit), 如果获取了锁定立即返回true,如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false;
3)如果是Synchronized,一个线程无法获得锁对象,则会继续无休止等待。而Lock.lock()也是一样的原理。但如果使用ReentrantLock的lockInterruptibly(),可以使得该线程进入中断。转而先做其它事情。使用的原理是:Java的中断机制。通过让等待的线程接收到了lock.lockInterruptibly()中断,并且有效处理了这个“异常”来放弃等待。转而做别的事情。

locks.Lock是locks.ReentrantLock的接口。通过ReentrantLock的对象方法.newCondition创建一个Condition的对象。和Object的wait和notify/ notifyAll一样,Condition.await()会抛出Interrupted异常,signal和signalAll则不会。不同condition可以让不同的线程wait。再用相同的condition去唤醒之前用同一个condition.await的线程。使用方法可参考:

注意,Lock对象和Synchronized最大的不同是Synchronized会在同步代码块块抛出异常时,依然释放锁。但是,Lock不会。所以在使用Lock对象的时候需要这么写:

public class Test{
  private final ReentrantLock test_lock = new ReentrantLock();
   ...
   public void testFunct(){
     test_lock.lock(); //上锁
     try{
       //执行相应的同步代码


     }


      finally{
         test_lock.unlock();
      }
   }
}

Java的读写锁是通过如下的方式实现的:

Java 5中提供了读写锁,它将读锁和写锁分离,使得读读操作不互斥,获取读锁和写锁的一般形式如下:

ReadWriteLock rwl = new ReentrantReadWriteLock();      
rwl.writeLock().lock()  //获取写锁  
rwl.readLock().lock()  //获取读锁  

用读锁来锁定读操作,用写锁来锁定写操作,这样写操作和写操作之间会互斥,读操作和写操作之间会互斥,但读操作和读操作就不会互斥。

Java的信号量锁是通过Semaphore类实现的:

Semaphore当前在多线程环境下被扩放使用,操作系统的信号量是个很重要的概念,在进程控制方面都有应用。Java 并发库 的Semaphore 可以很轻松完成信号量控制,Semaphore可以控制某个资源可被同时访问的个数,通过 acquire() 获取一个许可,而 release() 释放一个许可。

下面这个例子来自:http://www.cnblogs.com/whgw/archive/2011/09/29/2195555.html

使用线程池描述了Semaphore的使用:

// 线程池
ExecutorService exec = Executors.newCachedThreadPool();

// 只能5个线程同时访问
final Semaphore semp = new Semaphore(5);

// 模拟20个客户端访问
for (int index = 0; index < 20; index++) {
      final int NO = index;    
      Runnable run = new Runnable() {   
         public void run() {
                    try {
                     // 获取许可
                    semp.acquire();
                    System.out.println("Accessing: " + NO);
                    Thread.sleep((long) (Math.random() * 10000));
                    // 访问完后,释放
                    semp.release();
                    System.out.println("-----------------"+semp.availablePermits());
            } catch (InterruptedException e) {
                    e.printStackTrace();
            }            
          }   
      };
      exec.execute(run);
}
// 退出线程池
exec.shutdown();

生产者-消费者模式的最简单实现(利用Object的notify()和wait()方法):

wait() / nofity()方法是基类Object的两个方法,也就意味着所有Java类都会拥有这两个方法,这样,我们就可以为任何对象实现同步机制。
wait()方法:当缓冲区已满/空时,生产者/消费者线程停止自己的执行,放弃锁,使自己处于等等状态,让其他线程执行。
notify()方法:当生产者/消费者向缓冲区放入/取出一个产品时,向其他等待的线程发出可执行的通知,同时放弃锁,使自己处于等待状态。

这里插一句题外话,关于notify和notifyAll的区别(来自知乎:https://www.zhihu.com/question/37601861):

先说两个概念:

锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。

等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中。

notify和notifyAll的区别 

如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。当有线程调用了对象的 notifyAll()方法(唤醒所有等待池里的线程)或 notify()方法(只随机唤醒一个等待池里线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。

代码来源:http://blog.csdn.net/monkey_d_meng/article/details/6251879

public class Storage  
{  
    // 仓库最大存储量  
    private final int MAX_SIZE = 100;  
  
    // 仓库存储的载体  
    private LinkedList<Object> list = new LinkedList<Object>();  
  
    // 生产num个产品  
    public void produce(int num)  
    {  
        // 同步代码段  
        synchronized (list)  
        {  
            // 如果仓库剩余容量不足  
            while (list.size() + num > MAX_SIZE)  
            {  
                System.out.println("【要生产的产品数量】:" + num + "/t【库存量】:"  
                        + list.size() + "/t暂时不能执行生产任务!");  
                try  
                {  
                    // 由于条件不满足,生产阻塞  
                    list.wait();  
                }  
                catch (InterruptedException e)  
                {  
                    e.printStackTrace();  
                }  
            }  
  
            // 生产条件满足情况下,生产num个产品  
            for (int i = 1; i <= num; ++i)  
            {  
                list.add(new Object());  
            }  
  
            System.out.println("【已经生产产品数】:" + num + "/t【现仓储量为】:" + list.size());  
  
            list.notifyAll();  
        }  
    }  
  
    // 消费num个产品  
    public void consume(int num)  
    {  
        // 同步代码段  
        synchronized (list)  
        {  
            // 如果仓库存储量不足  
            while (list.size() < num)  
            {  
                System.out.println("【要消费的产品数量】:" + num + "/t【库存量】:"  
                        + list.size() + "/t暂时不能执行生产任务!");  
                try  
                {  
                    // 由于条件不满足,消费阻塞  
                    list.wait();  
                }  
                catch (InterruptedException e)  
                {  
                    e.printStackTrace();  
                }  
            }  
  
            // 消费条件满足情况下,消费num个产品  
            for (int i = 1; i <= num; ++i)  
            {  
                list.remove();  
            }  
  
            System.out.println("【已经消费产品数】:" + num + "/t【现仓储量为】:" + list.size());  
  
            list.notifyAll();  
        }  
    }  
}

生产者-消费者较为复杂的实现(条件锁):


Java 5之后,我们可以用Reentrantlock锁配合Condition对象上的await()和signal()或signalAll()方法来实现线程间协作。在ReentrantLock对象上newCondition()可以得到一个Condition对象,可以通过在Condition上调用await()方法来挂起一个任务(线程),通过在Condition上调用signal()来通知任务,从而唤醒一个任务,或者调用signalAll()来唤醒所有在这个Condition上被其自身挂起的任务。另外,如果使用了公平锁,signalAll()的与Condition关联的所有任务将以FIFO队列的形式获取锁,如果没有使用公平锁,则获取锁的任务是随机的,这样我们便可以更好地控制处在await状态的任务获取锁的顺序。与notifyAll()相比,signalAll()是更安全的方式。

理论上来说,条件所的await()和signal()是可以完全替代wait()和notify()的。而且由于可以实现公平锁,比wait()和notify()更优秀。

import java.util.concurrent.*;  
import java.util.concurrent.locks.*;  
  
class Info{ // 定义信息类  
    private String name = "name";//定义name属性,为了与下面set的name属性区别开  
    private String content = "content" ;// 定义content属性,为了与下面set的content属性区别开  
    private boolean flag = true ;   // 设置标志位,初始时先生产  
    private Lock lock = new ReentrantLock();    
    private Condition condition = lock.newCondition(); //产生一个Condition对象  
    public  void set(String name,String content){  
        lock.lock();  
        try{  
            while(!flag){  
                condition.await() ;  
            }  
            this.setName(name) ;    // 设置名称  
            Thread.sleep(300) ;  
            this.setContent(content) ;  // 设置内容  
            flag  = false ; // 改变标志位,表示可以取走  
            condition.signal();  
        }catch(InterruptedException e){  
            e.printStackTrace() ;  
        }finally{  
            lock.unlock();  
        }  
    }  
  
    public void get(){  
        lock.lock();  
        try{  
            while(flag){  
                condition.await() ;  
            }     
            Thread.sleep(300) ;  
            System.out.println(this.getName() +   
                " --> " + this.getContent()) ;  
            flag  = true ;  // 改变标志位,表示可以生产  
            condition.signal();  
        }catch(InterruptedException e){  
            e.printStackTrace() ;  
        }finally{  
            lock.unlock();  
        }  
    }  
}

    原文作者:java锁
    原文地址: https://blog.csdn.net/firehotest/article/details/60455811
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞