Java Note: 多线程的同步(互斥锁)的方法对比,信号量锁,读写锁的实现,生产者-消费者模式的实现
2017-03-05 05:20
597 查看
本文摘录自: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对象的时候需要这么写:
Java的读写锁是通过如下的方式实现的:
Java 5中提供了读写锁,它将读锁和写锁分离,使得读读操作不互斥,获取读锁和写锁的一般形式如下:
用读锁来锁定读操作,用写锁来锁定写操作,这样写操作和写操作之间会互斥,读操作和写操作之间会互斥,但读操作和读操作就不会互斥。
Java的信号量锁是通过Semaphore类实现的:
Semaphore当前在多线程环境下被扩放使用,操作系统的信号量是个很重要的概念,在进程控制方面都有应用。Java 并发库 的Semaphore 可以很轻松完成信号量控制,Semaphore可以控制某个资源可被同时访问的个数,通过 acquire() 获取一个许可,而 release() 释放一个许可。
下面这个例子来自:http://www.cnblogs.com/whgw/archive/2011/09/29/2195555.html
使用线程池描述了Semaphore的使用:
生产者-消费者模式的最简单实现(利用Object的notify()和wait()方法):
wait() / nofity()方法是基类Object的两个方法,也就意味着所有Java类都会拥有这两个方法,这样,我们就可以为任何对象实现同步机制。
wait()方法:当缓冲区已满/空时,生产者/消费者线程停止自己的执行,放弃锁,使自己处于等等状态,让其他线程执行。
notify()方法:当生产者/消费者向缓冲区放入/取出一个产品时,向其他等待的线程发出可执行的通知,同时放弃锁,使自己处于等待状态。
代码来源:http://blog.csdn.net/monkey_d_meng/article/details/6251879
生产者-消费者较为复杂的实现(条件锁):
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()更优秀。
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(); e303 } } }; exec.execute(run); } // 退出线程池 exec.shutdown();
生产者-消费者模式的最简单实现(利用Object的notify()和wait()方法):
wait() / nofity()方法是基类Object的两个方法,也就意味着所有Java类都会拥有这两个方法,这样,我们就可以为任何对象实现同步机制。
wait()方法:当缓冲区已满/空时,生产者/消费者线程停止自己的执行,放弃锁,使自己处于等等状态,让其他线程执行。
notify()方法:当生产者/消费者向缓冲区放入/取出一个产品时,向其他等待的线程发出可执行的通知,同时放弃锁,使自己处于等待状态。
代码来源: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 Note: 多线程的同步(互斥锁)的方法对比,信号量锁,读写锁的实现,生产者-消费者模式的实现
- Java Note: 多线程的同步(互斥锁)的方法对比,信号量锁,读写锁的实现,生产者-消费者模式的实现
- Java Note: 多线程的同步(互斥锁)的方法对比,信号量锁,读写锁的实现,生产者-消费者模式的实现
- Java Note: 多线程的同步(互斥锁)的方法对比,信号量锁,读写锁的实现,生产者-消费者模式的实现
- Java Note: 多线程的同步(互斥锁)的方法对比,信号量锁,读写锁的实现,生产者-消费者模式的实现
- Java Note: 多线程的同步(互斥锁)的方法对比,信号量锁,读写锁的实现,生产者-消费者模式的实现
- java多线程实现生产者/消费者同步
- Java多线程 - 实现生产者与消费者模式
- java多线程一 基本实现方法、消费者生产者队列、死锁
- java 多线程 22 :生产者/消费者模式 进阶 利用await()/signal()实现
- Java多线程——使用wait/notify实现生产者/消费者模式
- Java多线程 多个生产者和多个消费者实现同步 jdk1.4
- 进程同步-生产者和消费者(PV操作实现——java多线程模拟)
- java多线程(2)-实现生产者/消费者模式
- Java多线程-工具篇-BlockingQueue(实现生产者和消费者模式)
- IOS多线程使用GCD与信号量实现生产者与消费者模式
- 信号量,互斥锁实现 生产者-消费者模式
- Java多线程 多个生产者和多个消费者实现同步 jdk1.5
- 【转】Java生产者消费者模式的实现的几种方法
- JAVA多线程-线程间通信(二)-生产者/消费者模式实现