美文网首页
线程同步分析

线程同步分析

作者: Kevin_Lv | 来源:发表于2019-10-14 20:00 被阅读0次

    参考 https://github.com/fanshanhong/note/blob/master/Android/%E5%B9%B6%E5%8F%91%E5%92%8C%E5%A4%9A%E7%BA%BF%E7%A8%8B/%E7%BA%BF%E7%A8%8B%E7%B3%BB%E5%88%97/%E7%BA%BF%E7%A8%8B%E5%90%8C%E6%AD%A5.md

    线程同步是什么?

    多个线程访问同一份资源(共享资源)的时候,线程之间相同协调的过程成为线程同步。
    在一般情况下,创建一个线程是不能提高程序的执行效率的,所以要创建多个线程。但是多个线程同时运行的时候可能调用线程函数,在多个线程同时对同一个内存地址进行写入,由于CPU时间调度上的问题,写入数据会被多次的覆盖,所以就要使线程同步。

    synchronized关键字

    synchronized可以保证方法或者代码快在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。
    每个Java对象都有用作一个实现同步的锁,这些锁成为java内置锁,可以理解为,在object对象中有个lock对象。
    当你使用synchronized(this)相当于synchronized(this.lock)
    当你使用synchronized(obj)相当于synchronized(obj.lock),因此我们说是持有某个对象的锁。
    sleep()方法,睡眠过程中,并不释放当前持有的锁。

    三种应用方式:
    1、普通同步方法(实例方法),锁是当前实例对象,进入同步方法前要获得当前实例的锁
    2、静态同步方法,锁是当前类的class对象,进入同步代码前要获得当前类对象的锁
    3、同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁

    保护共享资源

    要保护好需要同步的对象,需要对访问共享资源的所有方法或代码块都要考虑是否需要加入锁。因为别的线程可以自由访问非同步(即:未加锁)的方法,这样可能会对同步的方法产生影响。

    生产者和消费者

    下面使用最经典的生产者消费者的例子,讲解线程同步。
    生产者->做馒头
    消费者->吃馒头

    /**
     * 馒头封装类
     */
    class ManTou {
        // 给馒头一个id
        int id;
        public ManTou(int id) {
            this.id = id;
        }
    
        @Override
        public String toString() {
            return "ManTou{" +
                    "id=" + id +
                    '}';
        }
    }
    

    使用一个数组(篮子)来存放馒头,然后维护一个栈顶指针


    image.png
    /**
     * 篮子
     */
    class Basket {
        // 栈顶指针, 该装第几个了
        int index = 0;
        // 容量
        ManTou[] arrayManTou = new ManTou[6];
    }
    

    提供一个向篮子里扔馒头(push)和从篮子里取馒头(pop)的方法。

    public void push(ManTou manTou) {
            arrayManTou[index] = woTou;
            index++;
        }
    public synchronized ManTou pop() {
            index--;
            ManTou mt = arrayManTou[index];
            arrayManTou[index] = null;
            return mt;
        }
    

    但是我们这样会不会有问题呢?
    对于每一个做馒头的人和吃馒头吃的人,都相当于是一个线程。
    每个做馒头的人都会调用push方法,向篮子里扔馒头。每个吃馒头的人,都会调用pop方法,从篮子里取出馒头。
    当做馒头的人小A(A线程)调用push方法向筐里扔馒头的过程中,在 arrayManTou[index] = woTou;这一句之后,很不幸,此时CPU时间片分给了做馒头的小B(B线程执行)。 那问题就来了:index 还没来得及++。小B做好了馒头也往篮子里面扔,就把刚才小A丢进去的馒头给覆盖了。这个问题的关键就在于这两条语句之间不能被打断,因此要在push方法上加synchronized。
    同样的,pop方法也有这样的问题,因此也要在pop方法上添加synchronized关键字。

    那接下来还有其他的问题:
    对于做馒头的人而言,篮子满了怎么办?因为我们篮子里面数组的容量只有6。
    既然篮子只有这么大,那就等会在做馒头吧,等篮子里的馒头被吃掉了,再往篮子里面扔馒头。

    public synchronized void push(ManTou manTou) {
            while (index == arrayManTou.length) { // 满了
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            arrayManTou[index] = manTou;
            index++;
            notify();// 唤醒一个正在wait在当前对象上的线程   notify无法唤醒自己
            // notifyAll();
        }
    

    注意:这个wait是Object的的wait方法
    this.wait()是啥意思? 是指当前执行这个代码块的线程wait, 也就是已经持有了synchronized(this)语句中的this对象的锁的线程等待。等待在哪里?等待在this对象上。等待其他线程调用这个(this)对象的notify方法的时候唤醒自己。
    一个线程进入push方法的时候, 已经拿到了锁了。在它执行的过程中,遇到一个事件,必须阻塞。也就是说做馒头的人,在往篮子里扔的时候,先检查了一下篮子满了,他就只能等着了,不能再往里扔了,再扔就冒出来了。要等到有人吃了,才能再继续往里扔。
    调用wait()或者notify()之前,必须使用synchronized关键字持有被wait/notify的对象的锁。只有持有了锁,才有资格wait。如果你压根拿不到锁,就根本无法wait。
    也就是你 synchronized(XX) 和 XX.wait 必须是同一个对象, 否则抛出 java.lang.IllegalMonitorStateException
    那何时醒来?等篮子里的馒头被别人吃了,让吃馒头的人把他叫醒就行啦, 即:等待别的线程调用同一个Basket对象的notify/nofityAll 方法的时候, 就会醒来啦。
    对于吃馒头的人而言,篮子空了咋整?很简单,等着呗。等人家做好了咱再吃。

    public synchronized ManTou pop() {
            while (index == 0) { // 空了
                try {
                    this.wait();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            index--;
            ManTou mt = arrayManTou[index];
            notify();
            return mt;
        }
    

    细心的小伙伴可能已经发现:我们在push方法和pop方法的最后都调用了notify方法。
    notify() 唤醒一个正在wait在当前对象上的线程 notify无法唤醒自己
    notifyAll() 唤醒所有正在wait在当前对象上的线程
    显然,刚才已经有线程wait在了Basket对象上。
    在push方法中:如果篮子里没有满的话,我们还是向往常一样往篮子里扔馒头,但是扔完了,记得叫醒等着吃馒头的人。因为可能有人在等着吃。
    在pop方法中:如果篮子不是空的,取出了一个就赶紧通知做馒头的人, 说:“现在篮子已经不是满的了,有空间了,你们可以做起来啦。”
    这个篮子,我们终于是封装好了,现在我们把做馒头的人和吃馒头的人也封装起来。

    /**
     * 做馒头的人
     */
    class Producer implements Runnable {
        Basket basket;
        public Producer(Basket basket) {
            this.basket = basket;
        }
    
        @Override
        public void run() {
            for (int i=0; i<20; i++) {
                ManTou manTou = new ManTou(i);
                basket.push(manTou);
                System.out.println("生产了:" + manTou);
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

    生产者(做馒头的人)需要知道往哪个篮子里扔馒头,所以要持有篮子的引用。
    当创建生产者的时候,就告诉他要往哪个篮子里扔。因此我们提供一个构造方法,为Basket赋值
    生产的过程,也就是我们的 run()方法啦。在run()方法中,不断做馒头,不断往篮子里扔。

    /**
     * 消费者(吃馒头的人)
     */
    class Consumer implements Runnable {
        Basket basket;
        public Consumer(Basket basket) {
            this.basket = basket;
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 20; i++) {
                ManTou manTou = basket.pop();
                System.out.println("消费了:" + manTou);
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

    消费者( 吃馒头的人),需要知道从哪个篮子里拿馒头吃。所以也要持有筐的引用。我们在构造方法中,为Basket赋值。
    消费的过程, 也就是run()方法。不断从篮子里取出馒头。
    有个细节,我们要注意。在push方法判断篮子满的时候, 以及在pop方法判断篮子空的时候,我们都用了while,为什么用while 而不用if呢?
    考虑下面这样一种情况:在push方法中,如果在wait的时候被打断,将进入catch 代码块去处理异常,异常处理之后,就跳出了if, 继续下面的执行。如果此时篮子还是满的呢? 就有问题了。
    所以要用while。即便是发生了Exception, 仍要要回头先检查是否已经满了, 如果满了, 还要继续wait。如果不满了,才能继续向下执行。

    总结:

    存放馒头的篮子,就是所谓的共享资源,对于共享资源的保护,就是需要对访问共享资源的所有方法和代码块都要考虑加入锁,也就是Baseket类中的push和pop方法。

    wait()和sleep()的区别

    1、wait是Object的方法,sleep是Thread的方法。
    2、wait的时候不再持有那个锁,等notify醒来才重新获取锁,sleep是睡着,还是拥有锁,并不释放
    3、调用wait的时候必须锁定对象,如果没有进入synchronized代码块,则没有wait的资格

    相关文章

      网友评论

          本文标题:线程同步分析

          本文链接:https://www.haomeiwen.com/subject/ertdmctx.html