线程通讯详解

上文我们说到了关于子线程中能否更新UI的问题,本篇我们来说一说关于线程又一个热考的知识点,这个问题面试中可能算是一个频繁被询问的知识点,那么今天就让我们看看,这到底是什么一个东西。

线程通讯!

乍一被问这个问题的时候还是有点蒙的,什么通讯?线程怎么和通讯扯上关系了,其实我觉得这个过程叫做线程同值更合适,其实就是在多线程的情况下,我们如何实现数据的一致性。

可能平时大家不会不会有这个概念,那我举个例子,双十一刚过去,大家应该买了不少东西吧,我就举买东西的例子。

假设现在商家生意火爆,只剩下一个商品了,但是此时此刻,有俩个人都想要,并且同时下了单,这种就算是多线程的实际情况。

然后俩个人同时问商家。

顾客1,2:老板,还有货么?

老板:还有一个,那就给你吧。

顾客1:好的。

顾客2:好的。

那么此时问题出现了,老板只有了俩个订单,但是却只有一个货。

上面的例子就暴露了在多线程情况下数据不能一致带来的后果,俩个顾客都付了钱,但是最终只有一个顾客能拿到货,那怎么办,让俩个顾客打架去?

其实这里暴露的问题就是数据的不一致性,如果其中一个顾客问老板要货时,老板给了他之后再去接待第二个顾客,就会告诉第二个顾客已经没有货了,那就不会存在这种问题了。

那么为了解决这个问题,就有了多线程的同步概念。

这里我提出几个我们为了达到同步需要的几个系统工具
```
volatile修饰符**

synchronize关键字**

Lock类**
```
这里暂时就提出常用的集中方式,下面一一介绍

   volatile修饰符**

volatile我们可以把它看做String int float long类似的修饰符,具体使用方法为:
```
private volatile int i = 0;
private volatile float j = 0;
private volatile String k = "";
```
使用比较简单,就当做普通修饰符放在变量的前面即可,那么作用是什么呢?

加了volatile修饰的变量其实就是为了这个变量操作的原子性,何谓原子性,就是指取出,修改,存储为一个整体的过程,即三者要不都执行了,要不都不执行。那我们看看最终实现的效果。
```
private volatile int i =0;
    @Override
        protected void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.activity_main);
               for (int j =0; j <10; j++) {
                       new Thread() {
                       @Override
                       public void run() {
                               super.run();
                                      i++;
                                      Log.d("volatile",i +"");
                                }
                       }.start();
                 }

                 for (int j =0; j <10; j++) {
                        new Thread() {
                        @Override
                         public void run() {
                               super.run();
                                       i++;
                                       Log.d("volatile",i +"");
                               }
                       }.start();
            }
}
```
然后我们再看一下效果:

![image](//upload-images.jianshu.io/upload_images/3481369-2d304ad3fa29f723?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

结果很明显,在多线程的情况下,我们的i值一直能保持一致,不会出现异常情况,但是有的同学也测试了,说,不对啊,我的咋就有奇怪的现象,是不是和下面一样呢?

![image](//upload-images.jianshu.io/upload_images/3481369-3adbe33d34dc5593?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

我们发现有时候居然会出现打印几个一样数值的情况,怎么会这样,那这里我们就要起稍微探索一下volatile的实现原理。

我们知道目前的手机的运行速度很快得益于手机的多核系统,多个cpu同时工作,那效率真是杠杠的,每个cpu都自带一个缓存区,为什么会带这个呢,因为cpu的运行速度是很快的,但是从内存中读取数据的速度就相对来说比较慢了,那如果我cpu没进行一次计算都要从内存中进行读取,存储,那不就大大降低了cup的速度,所以提出了缓存区的概念,这个缓存区就是为了解决这个问题,假设现在系统中有个变量i,那么当系统运行时,每个cup从内存中读取这个i到自己的缓存区,然后每次cpu的操作就不用直接和内存交互,而是直接和这个缓存区进行交互即可,大大提高了运行效率。

那现在就分析一下,当没有被volatile修饰的变量在程序运行时会发生的状况。

1.CPU1和CPU2先将i变量读到Cache中,比如内存中i=1,那么现在两个CPU中Cache中的i都等于1

2.CPU1和CPU2分别开启一个线程来对i进行自增操作,每个线程都有一块自己的独立内存空间来存放i的副本。

3.线程1对副本count进行自增操作后,i=2 ; 线程2对副本i进行自增操作后,i=2

4.线程1和线程2将操作后的i写回到Cache缓存的i中

5.CPU1和CPU2将Cache中的i写回内存中的i。

那么问题就来了,线程1和线程2操作的count都是一个i副本,这两个副本的值是一样的,所以进行了两次自增后,写回内存的i=2。而正确的结果应该为i=3。这就是多线程并发所带来的问题

那再现在就分析一下当被volatile修饰的时候会发生的状况。

如果一个变量加了volatile关键字,那就等于告诉系统这个变量是对所有线程共享的、可见的,每当cpu需要使用i的使用,会从内存中读取i到自己的缓存区,每当更改缓存区i的副本的时候,也会通知内存i及时变化,当再次取用的时候,就会取到内存中最新设置进去的值,以此保证数据的一致性。

但是这种数据的一致性的前提是保证是只有一个cpu,如果是俩个及以上的cup进行操作的时候,volatile仍然是无法保证数据的一致性的,具体原因是:

cpu1从内存取i = 1的值到自己的缓存区的时候,当cpu1更改了自己缓存区i数值的时候,假设i++;会把这个变化值通知到内存也进行变化,当cup2(另外的cup),也取值的自己缓存区的时候,虽然取到的值i会等于2,但是如果此时正在cpu2正在取的过程,cpu1又进行了i++操作,虽然也通知了内存i变化了,此时内存的i等于3了,但是由于cpu2已经完成了取值,此时cpu2缓存区中的i却仍然等于2,这就无法保证数据的一致性了。

那有同学就要问了,那搞了半天volatile没啥作用啊,那要这玩意干啥用?

也不能这么说,以前我们的时候都是单核的,volatile在单核多线程的情况下是可以保证数据的一致性的,但是现在手机的内核越来越多,多核多线程的情况下,volatile自然无法发挥它的作用了,但是我们需要理解这个过程是怎么样的。(以上部分意见参考:[android volatile的使用](https://blog.csdn.net/bzlj2912009596/article/details/79201643))
```
synchronize关键字**
```
这个关键字同学们应该是比较常见的,那就说说它的用法,它可以用于修饰类,代码块,方法,下面给出实例:
```
public class Utils {
         private int i =0;
         private static int j =0;

         public void Change() {
         synchronized (this){
                  Log.d("synchronized","我是被修饰的代码块" +i++);
                 }
        }

         public synchronized void Single() {
                 Log.d("synchronized","我是被修饰的方法" +i++);
         }

         public static void getInstance() {
                 synchronized (Utils.class) {
                           Log.d("synchronized","我是被修饰的类" +j++);
                  }
          }
}
```
以上三种大致代表的synchronized可以使用的地方,下面一一解释:

修饰代码块

放到代码前面的意思是同一时间内,能执行此段代码的只能有一个线程,举个例子:

![image](//upload-images.jianshu.io/upload_images/3481369-b5ef4e881db4ca01?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

我这里开启20个线程进行执行这段代码块,现在看看结果:

![image](//upload-images.jianshu.io/upload_images/3481369-57053cc05d2ad142?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

结果和我们预期的一样,不会出现i值异常的情况,那假设我们去掉了这段代码块的synchronized会这么样呢?修改为:

public void Change(){
{
Log.d("synchronized","我是被修饰的代码块" +i++);
}
}

查看结果:

![image](//upload-images.jianshu.io/upload_images/3481369-fa5997698ba9f065?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

i值的变化很明显不是我们想要的了。

那假设我们使用的时候用了俩个   private Utilsutils;会怎么样呢?更改使用代码为:

![image](//upload-images.jianshu.io/upload_images/3481369-e4ce4ebb0af11c50?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

再查看一下结果:

![image](//upload-images.jianshu.io/upload_images/3481369-a0ea541ed5f94336?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

结果貌似又不对了啊,怎么i的值变来变去的?

其实i的值是没有问题的,我们修饰的i同步代码块只能在一个对象中起作用,但是这里我们却建了俩个对象,所以看起来i的值随意变,但事实上在各自的对象里变化仍然是正常的。

我们说了代码块的使用和结果,那其实修饰方法其实也是一样的,这里我就不在重复了。

那接着说synchronized修饰类的作用。

那我们接着看使用方法:

![image](//upload-images.jianshu.io/upload_images/3481369-a69b45281e98ac7d?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

我们再看下结果:

![image](//upload-images.jianshu.io/upload_images/3481369-c8220b86a9cdc361?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

嗯,也是我们预期的效果。

那有的同学就要问了,为什么用synchronized修饰类的时候为什么要把修饰的方法变成static呢?

这个问题问的好,如果我们去掉了static,j也不是static,最终我们得到的结果俩个i的变化值就是从0到9,就和修饰代码块得到的结果一样了,这样做也是没有意义的,又有人问为什么synchronized为什么这么神奇啊,能做到这样,还记得我们刚学程序的时候老师告诉我们的话么,万物皆对象(那我就不怕没对象了)。每个对象里面都有一个内置锁,而用synchronized修饰就是为了得到这个这个内置锁,以此达到同步的效果。
```
Lock类**
```
最后说一说Lock这个类。这个类我们查看源码就知道这个类其实就是一个接口,带有以下的方法:
```
void lock();                                                         //获取锁
boolean tryLock();                                              //尝试获取锁       
boolean tryLock(long time, TimeUnit unit);       //尝试在一定时间内获取锁   
void unlock();                                                     //释放锁
```
下面看下正确的使用姿势:

![image](//upload-images.jianshu.io/upload_images/3481369-edf23006a08e8aeb?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

再看下结果:

![image](//upload-images.jianshu.io/upload_images/3481369-60fc7c2d193d3f9d?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

嗯,结果让人很满意,ReentrantLock是Lock的子类,我们一般也可以称它为互斥锁,我们可以用它来实例化Lock,除此之外,Lock还有俩个子类:ReentrantReadWriteLock.ReadLock,ReentrantReadWriteLock.WriteLock,分别是读写锁,主要用于文件的读写互斥操作,这里就不在赘述了,同学们可以自行尝试一下。

那Lock使用需要注意的是,我们使用Lock结束一定要记得调用unlock(),用于释放锁,不然可能会出现一个对象光拿着锁却不放的情况,其他对象也用不了,也就是所谓的死锁情况。

说了这么多,不知道你懂了没有,下次再有别人问你知不知道线程通讯的时候,你就可以一脸骄傲的告诉他:这种面试题还要我告诉你?

好了,下回再见。

猜你喜欢

转载自www.cnblogs.com/wanxuedong/p/10007814.html