您的位置:首页 > 编程语言 > Java开发

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对象的时候需要这么写:

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();
}
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐