多线程基本理解

    我们提到多线程都会想到一个优点和和一个缺点:优点是CPU使用率提高了换句话说也就是速度正常情况下会变快(为什么说正常情况下?因为因为CPU使用率过高的情况下,你线程再多也没有用,甚至会降低),缺点就是线程不安全(线程不安全指一个变量在多个线程中进行读写,造成读写的内容不一致,无法判断当前读写的变量值是否为准确的)。
     那使用多线程一定会提高效率嘛,线程不安全的根本原因是什么呢,我们首先做一个形象的比喻便于后面理解:
     假设现在要在AB两点之间修一条路,我们可以从A点开始一直修改到B点,也可以从AB两点一起修。假设我们有甲乙两个团队,并且从一个方向修只需要一个团队(假设人多了也帮不了忙),这种情况下,我们肯定选择让甲乙团队分别从AB两点一起修,这样效率最高了,而每天修路需要各种材料,而要买哪些材料,去哪里买需要去总部C点获取信息,为了提高速度,甲乙两个团队都把购买原材料的信息自己记下来,然后直接去购买原材料,节省了去总部C点获取信息的时间。同时总部需要不断的知道工程总进度,所以甲乙两个团队需要不断地往总进度上累加,比如现在总进度为0,甲团队从总部获取到总进度0,然后累加自己的进度,在告诉总部,同时乙团队也要做同样的事情,从总部获取总进度,然后加上自己进度,在告诉总部,我们可以发现此时就出现了问题,甲乙同时在对总进度进行读写,而且读和写不是一个完整的原子操作,那就意味着总进度会出现问题,此时的解决方案就是,甲团队在修改总进度的时候,乙团队需要阻塞,等甲团队修改完自己再修改,或者改用其他方案。
     我们从上面个一个比喻可以很好的来理解多核情况下的多线程,甲乙两个团队可以理解为两台CPU,AB两点两点同时工作,我们可以理解为一个任务进行了 多线程处理,在多核的情况下,即为真实的多线程处理,两个CPU同时工作,我们可以看到在购买原材料的事情上,甲乙团队选择自己记住信息,然后直接去购买,这即为缓存,CPU为了提高读写速度,对于一些数据会缓存在自己的寄存器中,这样就不用管每次都去内存或者硬盘中读取数据了,当这种缓存并不是永远都那么好用,我们可以看到在计算总进度这件事上,就出现了问题,如果缓存,就意味着总部的总进度值有问题,这就是线程不安全。
     们上面讲了多核情况下的多线程和线程不安全,下面我们假设只有一个甲团队来修路,那我们怎么修呢,只有一个团队是不是多线程会反而慢呢,其实不然的,因为修路并不是仅仅修路这一件事情,比如修路需要先买原材料,很有可能一天时间购买和拉运原材料需要4小时,修路只需要2小时,并且修路和拉运原材料是绝对的顺序逻辑,不能一边运一边修,那就意味着修路两小时后,需要等四小时运材料,如果AB两点只需要15分钟的车程,那甲团队完全可以A点修完,去B点继续修,总体上来看,效率还是比从A一点来修快很多,可以算一下:甲在A点修路两小时,然后A点开始购买和运输原材料,然后甲去B点15分钟,然后修路两小时,然后B点开始购买运输原材料4小时,然后甲回到A点15分钟,一共花了两个半小时,等A点的材料购买运输完成,甲团队还可以休息一个半小时。如果把车程的半小时算休息的,甲团队的使用率仅50%。
     而甲一个团队的情况,即为我们的单核CPU的情况,而甲团队一会儿再A点工作一会儿在B点工作,即为CPU的上线文切换,而CPU工作的时候会遇上上面说的拉原材料的问题吗?答案是有的,因为整个系统并不是只有个CPU,电脑存在大量的IO阻塞一起其他情况,都会操作CPU的空闲,此时如果把CPU拿去做别的事情,会大大提高效率,上文中的甲团队的使用率即为50%,同样可以理解为CPU的使用率为50%。而真实的CPU中为了,让尽可能多的进程和线程工作,CPU并不是像上文那样,一件事全做完再切换,而是给没有个线程分配一个时间片,每个时间片大概在几到几十毫秒之间,一旦到了时间,或者没到时间线程已经进入了阻塞(比如该线程进入等待键盘录入数据),就会切换到其他的线程中去,从而这也就引起了单核情况下的线程不安全,假设A线程对变量a进行读取和修改,而当A线程在读a变量时,CPU切换到了B线程,同样对a变量进行读写,此时两个线程对a变量的修改就会存在问题了。

     上文我们通过修路的比喻,对多线程在单核和多核情况下的工作情况,我们可以总结如下:

  • 多核情况下是真正的多线程,因为有多台CPU在同时进行处理,而单核情况下是假并发,只不过是CPU的上线文切换给我们直观上一种并发的错觉。

  • 单核假并发同样也会提高效率,根本原因是计算机中除了CPU在工作,还有很多其他设备在工作,当其他设备工作时往往CPU需要等待(比如IO阻塞),所以此时把CPU安排去做别的事情可以提高CPU的使用率,从而提高效率;同样线程和进程也不可能过多,如果过多了即意味着CPU在上线文切换的消耗会不断增大,当CPU使用率已经很高的情况下,继续增加线程和进程,则意味着CPU疲于上线文切换,只会降低CPU的工作速度。

  • 从甲乙团队购买原材料的问题上可以看出,CPU为了提高效率,会把一些数据存在自己这里,工作时直接对自己这里的数据进行读取写入,这就引出了多线程三大问题之一的:可见性,CPU对当前线程缓存的读写,其他线程是延迟感知的,这就存在线程不安全的隐患,java中常用volatile来修饰指定变量从而解决这问题。

  • 从甲乙团队对总进度的修改问题上可以看到多线程三大问题的第二点:原子性。由于一个线程对一个公共变量的值进行读取和修改,整套过程并不是原子的,从而会引发线程不安全问题,解决这个问题我们只能通过加锁来实现,比如java的synchronize关键字。

  • 多线程的第三大问题,顺序一致性问题(也叫有序性问题),在上面的例子上不能很好的说明,这里简要说一下,顺序一致性是指两方面的,单核情况下,CPU和JVM本身为了提高执行效率,在不影响最终执行结果的条件下进行指令重排。什么是指令重排?看下面一段代码:

public static void main(String[] args) {
      TestEntity testEntity = new TestEntity();
      //线程1
      new Thread(()->{
          testEntity.setA(1);
          testEntity.setB(testEntity.getA()+1);
          testEntity.setC(1);
      }).start();
      //线程2
      new Thread(()->{
          while (testEntity.getC()==1){
              System.out.println(testEntity.getB());
          }
      }).start();
  }

如果线程1的执行顺序变成:testEntity .setC(1);testEntity.setA(1);testEntity.setB(testEntity.getA()+1);对于单线程来说完全没有影响,但是我们再看线程2的运行,如果线程1执行顺序不发生变化,那打印结果为2,如果线程1的执行顺序变成了我们前面描述的一样,那打印结果就变成0了。像上面这种执行顺序发生变化的情况即为指令重排。
    为什么会发生指令重排?
    这个又要说道CPU工作原理里,前面我们说了CPU是串行,一个时间里只能做一件事,对于线程和进程来说正确的,某一个时间里一个CPU只会处理某一个线程里的逻辑,但是到了指令层面这么说并不准确,CPU内部是会产生并行的,原因是CPU是由很多个子模块组成的,比如说加法器,减法器,他们是可以同时进行的,这样可以极大地提高效率,但是有时候这种并行会有问题,比如:a++;a–;如果同时进行最终的结果可能是a+1或者a-1,而实际的逻辑应该还是a,所以CPU面对这种情况不指令重排的话就会阻塞,等a++执行结束,再a–,也就是减法器是空闲的,为了提高效率,CPU有了指令重排,比如a++;a–;c–;为了避免加法器对a–的等待,所以实际指令可能是这样执行的a++;c–;a–;从而来提高效率。
    如何避免指令重排?
    上面我们讲了指令重排的原因和对多线程的影响,那我们如何避免指令重排,从而防止其对多线程的影响呢。在java中同样是volatile关键字,被这个关键字描述的变量相关的指令是不会发生指令重排的,也就是a++;a–;时CPU会选择等待。

发布了39 篇原创文章 · 获赞 9 · 访问量 1000

猜你喜欢

转载自blog.csdn.net/qq_30095631/article/details/103796074
今日推荐