当前位置: 首页 > 科技观察

10张图告诉你多线程的事情

时间:2023-03-11 23:46:33 科技观察

本文转载自微信公众号《爱笑的建筑师》,作者雷小帅。转载本文,请联系LoveSmile的架构师公众号。头发很多的程序员:“大师,这个批处理界面太慢了,有什么办法可以优化吗?”架构师:“尝试使用多线程优化。”为什么多线程会导致界面变慢?”架构师:“去给我买杯咖啡,我写篇文章告诉你”……哎呀哎呀,我去买咖啡了。实际工作中,多线程的错误使用不能提高效率,还可能使程序崩溃。以在路上行驶为例:在单行道上,每辆车都遵守交通规则,此时整体交通正常。”一-waylane”是指“一个线程”,“multiplevehicles”是指“多个jobtasks”。单线程平滑行驶如果需要提高车辆的行驶效率,一般的做法是扩展车道。对于对应的程序,就是“加线程池”,增加线程数,这样一来,同一时间通过的车辆数量远大于单车道,多线程运行流畅。不过,大人的世界并不是那么完美,一旦车道多了,“堵”的场景就会越来越多,而碰撞也会影响整条道路的通行效率。这样一比较,“多车道”可能确实比“单车道”慢。多线程故障可以通过在车道之间加“护栏”来防止汽车频繁变道阻塞交通,那么在程序世界中应该怎么做呢?多线程在程序世界中遇到的问题可以概括为三类:“线程安全问题”、“Activity问题”和“性能问题”,接下来将对这些问题和相应的解决方案进行说明。线程安全问题有时候我们会发现,在单线程环境下正常运行的代码,在多线程环境下可能会出现意想不到的结果。其实这就是大家常说的“线程不安全”。那么到底什么是线程不安全的呢?继续阅读。原子性以银行转账为例,比如从A账户转1000元到B账户,那么它必须包括2个操作:从A账户减去1000元,向B账户增加1000元。表示两次操作都成功然后一次转账终于成功了。试想一下,如果这两个操作不是原子的,A账户扣除1000元后,操作突然终止,B账户没有增加1000元,那问题就严重了。银行转账的例子有两步,发生意外后转账失败,说明没有原子性。原子性:即一个操作或多个操作要么全部执行并且执行过程不会被任何因素打断,要么根本不执行。原子操作:即不会被线程调度机制打断的操作,不需要上下文切换。并发编程中的许多操作都不是原子操作。这里有一个小题目:i=0;//Operation1i++;//Operation2i=j;//Operation3i=i+1;//Operation4以上四个操作,哪些是原子操作,哪些不是?不熟悉的人可能会认为这些都是原子操作,但实际上只有操作1是原子操作。操作1:给基本数据类型变量赋值是原子操作;操作2:包含三个操作,读取i的值,给i加1,给i赋值;操作3:读取j的值,将j赋值给i;操作4:包含三个操作,读取i的值,给i加1,给i赋值;以上四种操作在单线程环境下不会出问题,但是在多线程环境下,如果不进行锁操作,往往会得到意想不到的值。在Java语言中,可以使用synchronize或者lock来保证原子性。Visibilitytalkischeap,先展示一段代码:classTest{inti=50;intj=0;publicvoidupdate(){//线程1执行i=100;}publicintget(){/线程2执行j=i;returnj;}}线程1执行update方法给i赋值100,一般情况下,线程1是在自己的工作内存中完成赋值操作,但未能及时将新值刷新到主存中。这时线程2执行get方法,先从主存中读取i的值,然后加载到自己的工作内存中。这时候读到的i的值是50,然后把50赋值给j,最后返回j的值是50,本来期望返回100,结果返回了50,这就是可见性问题。线程1修改了变量i,线程2并没有立即看到i的新值。可见性:当多个线程访问同一个变量时,一个线程修改变量的值,其他线程可以立即看到修改后的值。如上图,每个线程都有自己的工作内存,工作内存和主内存需要通过store和load进行交互。为了解决多线程可见性的问题,Java语言提供了关键字volatile。当一个共享变量被volatile修改时,它会保证修改后的值会立即更新到主存中,当其他线程需要读取它时,它会去内存中读取新的值。普通的共享变量不能保证可见性,因为变量修改后什么时候刷回主存是不确定的,另一个线程可能会读到旧值。当然Java的synchronize、lock等锁机制也可以保证可见性。加锁可以保证同一时刻只有一个线程在执行同步代码块。在释放锁之前,变量会被刷回主存,这样也保证了可见性。性别。关于线程不安全的表现也有“有序性”,后面的文章会深入讲解。活跃度问题上面提到,为了解决可见性问题,我们可以通过加锁来解决,但是如果锁使用不当,很容易引入其他问题,比如“死锁”。在讲“死锁”之前,我们先介绍另一个概念:活性问题。活跃意味着正确的事情最终会发生。当某个操作不能继续时,就会出现活性问题。这个概念是不是有点啰嗦?看不懂也没关系。你可以记住,活性问题通常分为以下几类:死锁、活锁和饥饿问题。(1)死锁死锁是指多个线程因为循环等待锁的关系而永远阻塞。一图抵千言,不多解释。(2)Livelock死锁是两个线程在等待对方释放锁,导致阻塞。活锁的意思是线程没有被阻塞,还活着。当多个线程都在运行并修改自己的状态,而其他线程又依赖于这个状态,这样任何一个线程都无法继续执行,只能重复自己的动作,修改自己的状态。锁。![](/Users/ray/Library/ApplicationSupport/typora-user-images/image-20210408232019843.png)如果你还有疑惑,我再举一个生活中的例子。来了一个人,两人互相让开,却是同时朝同一个方向走去。就这么躲着不放,两个人就闹僵了。学习了,呵呵。(3)饥饿如果一个线程没有其他异常但不能长时间持续运行,那么它基本上处于饥饿状态。有几种常见的场景:高优先级线程一直在运行消耗CPU,所有低优先级线程一直在等待;一些线程永久阻塞在等待进入同步块的状态,而其他线程总是可以领先于它持续访问同步块;有一个很经典的饥饿问题就是哲学家的用餐问题,如下图,有五个哲学家在吃饭,他们每人必须同时拿两把叉子才能开始吃饭。如果哲学家1和哲学家3同时开始进食,那么哲学家2、4和5就得饿着肚子等待。性能问题前面提到,线程安全、死锁、活锁都会影响到多线程的执行过程。如果这些都没有发生,多线程并发会比单线程串行执行更快吗?答案不一定,因为多线程有线程创建和线程上下文切换的开销。创建线程就是直接向系统申请资源。对于操作系统来说,创建一个线程的代价是非常昂贵的,需要为其分配内存,纳入调度等等。线程创建后,也会遇到线程上下文切换。CPU是一种非常宝贵的资源,它的速度是非常快的。为了保证雨露的均匀性,通常会把时间片分配给不同的线程。当CPU从执行一个线程切换到执行另一个线程时,CPU需要保存当前线程的本地数据。程序指针等状态,以及加载下一个要执行的线程的本地数据,程序指针等,这种切换称为“上下文切换”。一般减少上下文切换的方法有:无锁并发编程、CAS算法、使用协程等。抱持态度总结一下。如果多线程用得好,程序的效率可以成倍提高。如果使用不好,可能会比单线程慢。用一张图总结一下上面的内容: