java多线程同步(wait、notify)生产者消费者简单示例

一、为何写

最为一个Android开发者,如果做得不够深入可能为不会去处理多线程同步的问题,稍微简单点可能使用一个线程池就可以搞定了,有关线程池的介绍可以参考我的另一篇文章:ExecutorService+LruCache+DiskLruCache用一个类打造简单的图片加载库
只是前段时间研究Android音视频硬解码,看到开源项目中用到了线程同步,就是在视频的YUV数据的暂存,和解码为视频并展示,用到了两个线程去做,一个线程收集视频源数据,一个线程负责解码并播放视频,一个视频数据池是两个线程共享的,数据池满了或者空了的时候两个线程是要做出相应处理的,这就涉及到线程同步了。

这里写图片描述

学习、工作和生活的心态就要像向日葵,就算是太阳不在也要迎着月亮!

二、名字讲解

什么是线程同步?

当使用多个线程来访问同一个数据时,非常容易出现线程安全问题(比如多个线程都在操作同一数据导致数据不一致),所以我们用同步机制来解决这些问题。

实现同步机制有两个方法:

1、同步代码块:

synchronized(同一个数据){} 同一个数据:就是N条线程同时访问一个数据。

2、同步方法:

public synchronized 数据返回类型方法名(){}

通过使用同步方法,可非常方便的将某类变成线程安全的类,具有如下特征:
1,该类的对象可以被多个线程安全的访问。
2,每个线程调用该对象的任意方法之后,都将得到正确的结果。
3,每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态。
注:synchronized关键字可以修饰方法,也可以修饰代码块,但不能修饰构造器,属性等

※不要对线程安全类的所有方法都进行同步,只对那些会改变共享资源方法的进行同步。
线程通讯:
当使用synchronized 来修饰某个共享资源时(分同步代码块和同步方法两种情况),当某个线程获得共享资源的锁后就可以执行相应的代码段,直到该线程运行完该代码段后才释放对该共享资源的锁,让其他线程有机会执行对该共享资源的修改。当某个线程占有某个共享资源的锁时,如果另外一个线程也想获得这把锁运行就需要使用wait() 和notify()/notifyAll()方法来进行线程通讯了。
Java.lang.object 里的三个方法wait() notify() notifyAll()

wait()
导致当前线程等待,直到其他线程调用同步监视器的notify方法或notifyAll方法来唤醒该线程。

wait(mills)
都是等待指定时间后自动苏醒,调用wait方法的当前线程会释放该同步监视器的锁定,可以不用notify或notifyAll方法把它唤醒。

notify()
唤醒在同步监视器上等待的单个线程,如果所有线程都在同步监视器上等待,则会选择唤醒其中一个线程,选择是任意性的,只有当前线程放弃对该同步监视器的锁定后,也就是使用wait方法后,才可以执行被唤醒的线程。

notifyAll()
唤醒在同步监视器上等待的所有的线程。只用当前线程放弃对该同步监视器的锁定后,也就是使用wait方法后,才可以执行被唤醒的线程。

注意,notify方法一定要在synchronized同步里面调用,还有做异常捕捉。


原子操作:根据Java规范,对于基本类型的赋值或者返回值操作,是原子操作。但这里的基本数据类型不包括long和double, 因为JVM看到的基本存储单位是32位,而long 和double都要用64位来表示。所以无法在一个时钟周期内完成。

自增操作(++)不是原子操作,因为它涉及到一次读和一次写。

原子操作:由一组相关的操作完成,这些操作可能会操纵与其它的线程共享的资源,为了保证得到正确的运算结果,一个线程在执行原子操作其间,应该采取其他的措施使得其他的线程不能操纵共享资源。

同步代码块:为了保证每个线程能够正常执行原子操作,Java引入了同步机制,具体的做法是在代表原子操作的程序代码前加上synchronized标记,这样的代码被称为同步代码块。

同步锁:每个JAVA对象都有且只有一个同步锁,在任何时刻,最多只允许一个线程拥有这把锁。

当一个线程试图访问带有synchronized(this)标记的代码块时,必须获得 this关键字引用的对象的锁,在以下的两种情况下,本线程有着不同的命运。
1、 假如这个锁已经被其它的线程占用,JVM就会把这个线程放到本对象的锁池中。本线程进入阻塞状态。锁池中可能有很多的线程,等到其他的线程释放了锁,JVM就会从锁池中随机取出一个线程,使这个线程拥有锁,并且转到就绪状态。
2、 假如这个锁没有被其他线程占用,本线程会获得这把锁,开始执行同步代码块。 (一般情况下在执行同步代码块时不会释放同步锁,但也有特殊情况会释放对象锁 如在执行同步代码块时,遇到异常而导致线程终止,锁会被释放;在执行代码块时,执行了锁所属对象的wait()方法,这个线程会释放对象锁,进入对象的等待池中)

线程同步的特征:
1、 如果一个同步代码块和非同步代码块同时操作共享资源,仍然会造成对共享资源的竞争。因为当一个线程执行一个对象的同步代码块时,其他的线程仍然可以执行对象的非同步代码块。(所谓的线程之间保持同步,是指不同的线程在执行同一个对象的同步代码块时,因为要获得对象的同步锁而互相牵制)
2、 每个对象都有唯一的同步锁
3、 在静态方法前面可以使用synchronized修饰符。
4、 当一个线程开始执行同步代码块时,并不意味着必须以不间断的方式运行,进入同步代码块的线程可以执行Thread.sleep()或执行Thread.yield()方法,此时它并不释放对象锁,只是把运行的机会让给其他的线程。
5、 Synchronized声明不会被继承,如果一个用synchronized修饰的方法被子类覆盖,那么子类中这个方法不在保持同步,除非用synchronized修饰。

释放对象的锁:
1、 执行完同步代码块就会释放对象的锁
2、 在执行同步代码块的过程中,遇到异常而导致线程终止,锁也会被释放
3、 在执行同步代码块的过程中,执行了锁所属对象的wait()方法,这个线程会释放对象锁,进入对象的等待池。

死锁:
线程1独占(锁定)资源A,等待获得资源B后,才能继续执行
同时
线程2独占(锁定)资源B,等待获得资源A后,才能继续执行
这样就会发生死锁,程序无法正常执行

如何避免死锁
一个通用的经验法则是:当几个线程都要访问共享资源A、B、C 时,保证每个线程都按照同样的顺序去访问他们。


注意:
1、线程同步就是线程排队。同步就是排队。线程同步的目的就是避免线程“同步”执行。
2、只有共享资源的读写访问才需要同步。如果不是共享资源,那么就根本没有同步的必要。
3、只有“变量”才需要同步访问。如果共享的资源是固定不变的,那么就相当于“常量”,线程同时读取常量也不需要同步。至少一个线程修改共享资源,这样的情况下,线程之间就需要同步。
4、多个线程访问共享资源的代码有可能是同一份代码,也有可能是不同的代码;无论是否执行同一份代码,只要这些线程的代码访问同一份可变的共享资源,这些线程之间就需要同步。

5、我们要尽量避免这种直接把synchronized加在函数定义上的偷懒做法。因为我们要控制同步粒度。同步的代码段越小越好。synchronized控制的范围越小越好。

同步锁:
我们可以给共享资源加一把锁,这把锁只有一把钥匙。哪个线程获取了这把钥匙,才有权利访问该共享资源。
同步锁不是加在共享资源上,而是加在访问共享资源的代码段上。
访问同一份共享资源的不同代码段,应该加上同一个同步锁;如果加的是不同的同步锁,那么根本就起不到同步的作用,没有任何意义。
这就是说,同步锁本身也一定是多个线程之间的共享对象。

三、生产者消费者代码示例

产品仓库

package com.danxx.javalib2;

import java.util.LinkedList;
import java.util.Queue;

/**
 * 数据存储仓库和操作
 * 一个缓冲区,缓冲区有最大限制,当缓冲区满
 * 的时候,生产者是不能将产品放入到缓冲区里面的,
 * 当然,当缓冲区是空的时候,消费者也不能从中拿出来产品,
 * 这就涉及到了在多线程中的条件判断
 * Created by dawish on 2017/7/13.
 */
public class Storage {
    
    private static volatile int goodNumber = 1;
    
    private final static int MAX_SIZE = 20;
    /**
     *  Queue操作解析:
     *  add       增加一个元索                 如果队列已满, 则抛出一个IIIegaISlabEepeplian异常
     *  remove    移除并返回队列头部的元素     如果队列为空, 则抛出一个NoSuchElementException异常
     *  element   返回队列头部的元素           如果队列为空, 则抛出一个NoSuchElementException异常
     *  offer     添加一个元素并返回true       如果队列已满, 则返回false
     *  poll      移除并返问队列头部的元素     如果队列为空, 则返回null
     *  peek      返回队列头部的元素           如果队列为空, 则返回null
     *  put       添加一个元素                 如果队列满,   则阻塞
     *  take      移除并返回队列头部的元素     如果队列为空, 则阻塞
     *
     */
    Queue<String> storage;
    public Storage() {
        storage = new LinkedList<String>();
    }

    /**
     *
     * @param dataValue
     */
    public synchronized void put(String dataValue, String threadName){
        if(storage.size() >= MAX_SIZE){
            try {
                goodNumber = 1;
                super.wait();  //当生产满了后让生产线程等待
                return;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        storage.add(dataValue + goodNumber++);
        System.out.println(threadName + dataValue + goodNumber);
        super.notify();  //每次添加一个数据就唤醒一个消费等待的线程来消费
    }

    /**
     *
     * @return
     * @throws InterruptedException
     */
    public synchronized String get(String threadName) {
        if(storage.size() == 0){
            try {
                super.wait();  //当产品仓库为空的时候让消费线程等待
                System.out.println(threadName + "wait");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return null;
        }
        super.notify();  //当数据不为空的时候就唤醒一个生产线程来生产
        String value = storage.remove();
        return value;
    }

}

生产者

package com.danxx.javalib2;

import java.util.UUID;

/**
 * 生产者
 * Created by dawish on 2017/7/13.
 */
public class Producer extends Thread{

    private Storage storage;//生产者仓库
    private String name="";
    
    public Producer(Storage storage, String name) {
        this.storage = storage;
        this.name = name;
    }
    public void run(){
        //生产者每隔1s生产1~100消息
        long oldTime = System.currentTimeMillis();
        while(true){
            synchronized(storage){
                if (System.currentTimeMillis() - oldTime >= 1000) {
                    oldTime = System.currentTimeMillis();
                    String msg = UUID.randomUUID().toString();
                    storage.put("-ID:" ,name);
                }
            }
        }
    }
}

消费者

package com.danxx.javalib2;

/**
 * 消费者
 * Created by dawish on 2017/7/13.
 */

public class Consumer extends Thread{

    private Storage storage;//仓库
    
    private String name="";
    
    public Consumer(Storage storage, String name) {
        this.storage = storage;
        this.name = name;
    }
    public void run(){
        while(true){
            synchronized(storage){
                //消费者去仓库拿消息的时候,如果发现仓库数据为空,则等待
                String data = storage.get(name);
                if(data != null){
                    
                    System.out.println(name +"-------------"+ data);
                    
                }
            }
        }
    }
}

main方法

package com.danxx.javalib2;

/**
 *  Java中的多线程会涉及到线程间通信,常见的线程通信方式,例如共享变量、管道流等,
 *  这里我们要实现生产者消费者模式,也需要涉及到线程通信,不过这里我们用到了java中的
 *  wait()、notify()方法:
 *  wait():进入临界区的线程在运行到一部分后,发现进行后面的任务所需的资源还没有准备充分,
 *  所以调用wait()方法,让线程阻塞,等待资源,同时释放临界区的锁,此时线程的状态也从RUNNABLE状态变为WAITING状态;
 *  notify():准备资源的线程在准备好资源后,调用notify()方法通知需要使用资源的线程,
 *  同时释放临界区的锁,将临界区的锁交给使用资源的线程。
 *  wait()、notify()这两个方法,都必须要在临界区中调用,即是在synchronized同步块中调用,
 *  不然会抛出IllegalMonitorStateException的异常。
 *  Created by dawish on 2017/7/14.
 */

public class MainApp {
    public static void main(String[] args) {
        Storage storage = new Storage();
        
        Producer producer1 = new Producer(storage, "Producer-1");
        Producer producer2 = new Producer(storage, "Producer-2");
        Producer producer3 = new Producer(storage, "Producer-3");
        Producer producer4 = new Producer(storage, "Producer-4");
        
        Consumer consumer1 = new Consumer(storage, "Consumer-1");
        Consumer consumer2 = new Consumer(storage, "Consumer-2");
        
        producer1.start();
        producer2.start();
        producer3.start();
        producer4.start();
        
        consumer1.start();
        consumer2.start();
    }
}

运行结果(4个生产者2个消费者)

这里写图片描述

四、Github地址

https://github.com/Dawish/CustomViews/tree/master/JavaLib

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

推荐阅读更多精彩内容