线程同步分析

参考 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的资格

你可能感兴趣的:(线程同步分析)