等待-唤醒机制

标签(空格分隔): java


java 5新增机制--->同步锁(Lock)

从java5开始,java提供了一种更为强大的线程同步机制,就是同步所对象来实现同步,同步锁对象有Lock对象充当。
Lock提供了比synchronized方法和sysnchronized代码块更为广泛的锁定操作,Lock允许实现更为灵活的结构,可以具有很大差别的属性,并支持多个相关的Condition对象。

Lock是控制多个线程对共享资源进行访问的工具。通常锁提供了对共享资源的单独访问,没次只能有一个线程对Lock对象进行加锁,线程开始访问共享资源之前,先获得Lock对象。

某些锁可能允许对共享资源的并发访问,如:ReadWriteLock(读写锁)

Lock,ReadWritedLock是java5提供的两个根接口
同时为Lock接口提供了ReentrantLock(可重入锁实现)实现类,为ReadWriteLock提供了ReentrantReadWriteLock实现类。

Java 8新增了新型的StampedLock类,在大多数场景中可以替代传统的ReetrantReadWriteLockReetrantReadWriteLock为读写操作提供了三种锁模式,Writing,ReadingOptimistic,Reading

1、ReentrantLock锁(可重用锁)

使用很简单,获取ReentrantLock对象,注意在同步的时候一个定要保证ReentrantLock对象时同一个对象

ReentrantLock lock = new ReentrantLock();

加锁

lock.lock();

由于Lock机制是使用java实现的,synchronized是底层jvm实现,因此使用Lock必须手动释放锁,为了保证锁一定被释放,我们使用try{}finally{}来实现锁的释放

finally{
//最后一步,无论怎样都要释放锁
 lock.unlock();
}
private static final ReentrantLock lock = new ReentrantLock();
public void func(){
    //加锁
    lock.lock();
    try{
    
      要同步的代码区域
    
    } finally{
    
    //最后一步,无论怎样都要释放锁
    lock.unlock();
    }
  }

ReentrantLock锁,它是一种可重用锁。(什么是可重用锁,一个线程可以对已经加锁的Reentrantlock再次加锁,ReentrantLock对象会维持一个计数器,来追踪lock()方法的嵌套调用,线程在每次调用lock()加锁后,必须显示的调用unlock()来释放锁。所以一段被锁保护的代码,可以调用另一个被相同锁锁定的方法)。

2、ReentrantReadWriteLock(读写锁)

ReentrantLock'实现了标准的互斥操作,也就是一次只能有一个线程持有锁,也即所谓独占锁的概念。也就是说读/写读/读写/写`操作不能同时发生,在一定程度上面减低了吞吐量。需要强调的一个概念是,锁是有一定的开销的,当并发比较大的时候,锁的开销就比较客观了。所以如果可能的话就尽量少用锁,非要用锁的话就尝试看能否改造为读写锁。

ReadWriteLock一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程也就是说读写锁使用的场合是一个共享资源被大量读取操作,而只有少量的写操作(修改数据)。

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}

这里需要说明的是ReadWriteLock并不是Lock的子接口,只不过ReadWriteLock借助Lock来实现读写两个视角,在ReadWriteLock中每次读取共享数据就需要读取锁,当需要修改共享数据时就需要写入锁,看起来好像是两个锁,但其实不尽然

ReentrantReadWriteLock的几个特性:

公平性:

  • 非公平锁: 这个和独占锁的非公平性一样,所以读取操作没有公平性和非公平性,写操作时,由于写操作可能立即获取到锁,所以会推迟一个或多个读操作或者写操作,因此非公平锁的吞吐量要高于公平锁.
  • 公平锁 : 利用AQS的CLH队列,释放当前保持的锁(读锁或者写锁)时,优先为等待时间最长的那个写线程分配写入锁,当前前提是写线程的等待时间要比所有读线程的等待时间要长。同样一个线程持有写入锁或者有一个写线程已经在等待了,那么试图获取公平锁的(非重入)所有线程(包括读写线程)都将被阻塞,直到最先的写线程释放锁。如果读线程的等待时间比写线程的等待时间还有长,那么一旦上一个写线程释放锁,这一组读线程将获取锁

重入性:

  • 读写锁允许读线程和写线程按照请求锁的顺序重新获取读取锁或者写入锁。当然了只有写线程释放了锁,读线程才能获取重入锁。
  • 写线程获取写入锁后可以再次获取读取锁,但是读线程获取读取锁后却不能获取写入锁。
  • 另外读写锁最多支持65535个递归写入锁和65535个递归读取锁。

锁降级:

  • 写线程获取写入锁后可以获取读取锁,然后释放写入锁,这样就从写入锁变成了读取锁,从而实现锁降级的特性。

想知道更多特性可以参考[ReentrantReadWriteLock深入分析][1]

如何使用?

public class Databean {

    private int mDataStr = 123;

    private ReentrantReadWriteLock mLock = new ReentrantReadWriteLock();

    public void getData(){
        mLock.readLock().lock();//上读读锁其他线程不能写

        System.out.println(Thread.currentThread().getName()+"可以读取数据");
         System.out.println(Thread.currentThread().getName()+"读取到数据--->"+mDataStr);
        try {
            Thread.sleep((long) (Math.random()*1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally{
           mLock.readLock().unlock();//释放读取锁
        }
        

    }
    public void writeData(int data ){

        mLock.writeLock().lock();//上写入锁,同一个时间内只有一个线程可以写入数据,其他线程都必须等待
        System.out.println(Thread.currentThread().getName()+"可以写入数据");
        try {
            Thread.sleep((long) (Math.random()*1000));
            System.out.println(Thread.currentThread().getName()+"写入数据-------->"+data);
            mDataStr = data;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally{
          mLock.writeLock().unlock();//释放写入锁
        }
    }
}

线程通信

1、等待-唤醒机制(生产者-消费者机制)

当我们在开发工程中,需要这样一个需求:生产方不停的生产物品,消费方不停的消费物品。
由于计算机的线程调度具有随机性,这样就会出现以下问题:
1.生产线程的物品还没有生产,消费线程不停的消费,这样会导致消费线程工作无用,浪费系统资源
2.生产线程的物品还没有完全生产完毕,消费线程就开始消费了该物品,造成数据发生错误。
为了解决上面的问题,我们做如下设想:
当生产线程完全生产了一个物品之后,自己停止生产,然后告诉消费线程,开始消费,当消费线程消费完毕之后,自己停止消费,告诉生产线程,开始生产,这样就完成了一个良性的循环,既没有避免的系统资源的无效开销,同时也可以保证,数据的正确性。

为了实现这样的功能,java的Object类提供了wait()notify()notifyAll()三个方法,这三个方法比不属于Thread类,而是属于Object,这个三个方法必须由同步监视器对象来调用,这可以分成以下两种情况。

  1. 对于使用synchronized修饰的同步方法,因为该类的默认实例(this)就是同步监视器,所以可以在同步方法中直接调用者三个方法。
  2. 对于使用synchronized修饰的同步代码块,同步监视器是synchronized后括号里的对象,因此必须使用该对象调用者三个方法。

关于这三个方法有如下解释:

  1. wait()是导致当前线程等待,直到其他线程调用该同步监视器对象的notify()方法或者notifyAll()方法来唤醒该线程,wait()分别有三个重载方法,分别是传入等待的时间(等待指定的时间后自动苏醒),wait()sleep()方法的不同是:a.调用者不同,wait的调用者必须是同步监视器对象,sleep的调用者是Thread类,是一个静态的方法;b.wait方法执行后该线程会释放锁,sleep方法不会释放锁。
  1. notify()方法此同步监视器上等待的单个线程,如果所有的线程都在此同步监视器上等待,则会唤醒其中的一个线程,唤醒具有任意性。只有当前线程放弃对同步监视器上的锁定之后(使用wait()方法),才可以执行被唤醒的线程。
  1. notifyAll()唤醒此同步监视器上的所有等待的线程,只有当前线程放弃对同步监视器上的锁定之后,才可以执行被唤醒的线程

示例:

 public class Goods {

    private int mNumber;
    private boolean flag = false;
    
    
     /**
     * 商品的生产方法
     */
    public void product2() {

        synchronized (this) {

            if (isProduct) { //如果flag = true 这里是所有生产线程都将等待
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            if (!isProduct) { //二次判断,保证不会重复生产
                mNumber++;
                System.out.println(Thread.currentThread().getName() + "------------生产--" + mNumber );
            }
            isProduct = true;


            this.notifyAll();
        }
    }

    /**
     * 商品的消费方法
     */
    public void consume2() {
        synchronized (this) {
            if (!isProduct) {// 如果flag = false 所有消费线程都将等待
                try {
                    System.out.println(Thread.currentThread().getName() + "++等待--");
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            if (isProduct){//二次判断保证不会重复消费
                System.out.println(Thread.currentThread().getName() + "---消费--" + mNumber);
            }
            isProduct = false;
            this.notifyAll();
        }

    }

/**
 * 生产者
 */
class Producer implements Runnable{

    private Goods mGoods=null;

    public Producer(Goods mGoods) {
        this.mGoods = mGoods;
    }

    @Override
    public void run() {
        while(true)
            mGoods.product();
    }
}

/**
 * 消费者
 */
class Consumer implements Runnable{

    private Goods mGoods=null;

    public Consumer(Goods mGoods) {
        this.mGoods = mGoods;
    }

    @Override
    public void run() {
        while(true)
            mGoods.consume();
    }
}

/**
*测试
*/
public class ThreadTest  {

    public static void main(String[] args) {
        Goods goods = new Goods();
        Thread producer1 = new Thread(new Producer(goods),"生产线程1");
        Thread producer2 = new Thread(new Producer(goods),"生产线程2");
        Thread consumer1 = new Thread(new Consumer(goods) ,"消费线程1");
        Thread consumer2 = new Thread(new Consumer(goods) ,"消费线程2");
        
        producer1.start();
       // producer2.start();

        consumer1.start();
       // consumer2.start();
    }
}

上面的程序测试发现,在一个上产线程,一个消费线程的条件下运行一切正常,但是当开启多个生产线程,多个消费线程的时候,问题就来了,因为notifyAllnotify唤醒的线程是任意的,很可能出现生产线程又将生产线程唤醒。出现多次生产的情况,或者消费线程唤醒了消费线程,出现了多次消费的情况。

如何解决这个问题,我们在代码中加上二次判断,保证了生产和消费的唯一性。

2、使用Condition控制线程通信

如果程序不使用synchronized来保证线程同步,而是直接使用Lock对象来保证线程同步,则系统不存在隐式的同步监视器。也就不能使用wait()notify()notifyAll()方法进行线程通信了。

当使用lock对象来保证线程同步时,java提供了Condition类来保持协调,使用Condition可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,Condition也可以唤醒其他处于等待的线程。

Condition将同步监视器的方法(waitnotifynotifAll)分解成截然不同的对象,以便通过这些对象鱼Lock对象组合使用,为每一个对象提供多个等待集(wait-set)。在这种情况下,Lock取代了同步方法和同步代码块,Condition取代了同步监视器。

Condition实例被绑定到一个Lock对象上。要获取特定Lock实例的Condition实例,调用Lock对象的newCondition()方法即可。Condition类提供了如下三个方法。

  • await():类似于隐示同步监视器的wait(),导致当前线程等待,直到其他线程调用该Conditionsignal()或者signalAll()方法来唤醒该线程,该await()方法有更多的变体,如:long awaitNanos(long nanosTimeout)void awaitUninterruptibly()awaitUntil(Date deadline)等,可以完成更丰富的操作。
  • signal()方法,唤醒此Lock对象上等待的单个线程。如果所有的线程都在Lock上等待,则会唤醒其中的一个,唤醒具有任意性。只用当前线程放弃对该Lock对象的锁定后(使用await()方法),才可以执行唤醒操作。
  • signalAll():唤醒此Lock对象上等待的所有线程,只用当前线程放弃对该Lock对象的锁定后(使用await()方法),才可以执行唤醒操作。

示例:


private final Lock mLock = new ReentrantLock();
private final Condition mCon_pro = mLock.newCondition();
private final Condition mCon_con = mLock.newCondition();

 /**
     * 商品的生产方法
     */
    public void product() {

        mLock.lock();

        try {
            while (isProduct) { //如果flag = true 这里是所有生产线程都将等待
                try {
                    mCon_pro.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            if (!isProduct) {
                mNumber++;
            System.out.println(Thread.currentThread().getName() + "------------生产--" +mNumber);
            }

            isProduct = true;
            mCon_con.signalAll();
        } finally {

            mLock.unlock();
        }
    }



 /**
     * 商品的消费方法
     */
    public void consume() {
        mLock.lock();
        try {
            while (!isProduct) {// 如果flag = false 所有消费线程都将等待
                try {
                    mCon_con.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            if (isProduct) {
                System.out.println(Thread.currentThread().getName() + "---消费--" + mNumber);
            }
            isProduct = false;
            mCon_pro.signalAll();
        } finally {
            mLock.unlock();
        }

    }

3、线程的虚假唤醒

对于上面的例子,这里我要提一个小问题,也是我在测试中遇到的问题,我挑一段其中的代码:

try {
    while (!isProduct) {// 如果flag = false 所有消费线程都将等待
     try {
        mCon_con.await();
    } catch (InterruptedException e) {
           e.printStackTrace();
     }
}

我想问的是既然isProduct是判断是否有商品生产过了,那么我们完全可以使用if来代替啊,为什么要使用while呢,而且官方API的例子也是while。我当时就感觉可以,于是我就是用if来代替,结果出现问题了,处于等待的线程有的时候会突然被唤醒?这是什么情况,当时我就蒙逼了,API的例子使用的是while,原来它也发现了这个问题才这样写的。于是我仔细阅读了一下await()方法被唤醒的条件,有一个第四条spurious wakeup什么意思呢?中文解释是虚假唤醒,就是说不是被signal、和signalAll唤醒的,通俗一点就是莫名的醒了,所以我们使用while来代替if,即使他醒了,我们也要让他再次await

线程池

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,599评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,629评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,084评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,708评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,813评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,021评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,120评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,866评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,308评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,633评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,768评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,461评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,094评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,850评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,082评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,571评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,666评论 2 350

推荐阅读更多精彩内容

  • 写在前面的话: 这篇博客是我从这里“转载”的,为什么转载两个字加“”呢?因为这绝不是简单的复制粘贴,我花了五六个小...
    SmartSean阅读 4,717评论 12 45
  • 1.解决信号量丢失和假唤醒 public class MyWaitNotify3{ MonitorObject m...
    Q罗阅读 873评论 0 1
  • 下面是我自己收集整理的Java线程相关的面试题,可以用它来好好准备面试。 参考文档:-《Java核心技术 卷一》-...
    阿呆变Geek阅读 14,777评论 14 507
  • Java-Review-Note——4.多线程 标签: JavaStudy PS:本来是分开三篇的,后来想想还是整...
    coder_pig阅读 1,641评论 2 17
  • 本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。 首先讲...
    李欣阳阅读 2,444评论 1 15