在实际工作中,错误使用多线程非但不能提高效率还可能使程序崩溃。以在路上开车为例: 在一个单向行驶的道路上,每辆汽车都遵守交通规则,这时候整体通行是正常的。『单向车道』意味着『一个线程』,『多辆车』意味着『多个job任务』。 如果需要提升车辆的同行效率,一般的做法就是扩展车道,对应程序来说就是『加线程池』,增加线程数。这样在同一时间内,通行的车辆数远远大于单车道。 然而成年人的世界没有那么完美,车道一旦多起来『加塞』的场景就会越来越多,出现碰撞后也会影响整条马路的通行效率。这么一对比下来『多车道』确实可能比『单车道』要慢。 防止汽车频繁变道加塞可以采取在车道间增加『护栏』,那在程序的世界该怎么做呢? 程序世界中多线程遇到的问题归纳起来就是三类: 线程安全问题有时候我们会发现,明明在单线程环境中正常运行的代码,在多线程环境中可能会出现意料之外的结果,其实这就是大家常说的『线程不安全』。那到底什么是线程不安全呢?往下看。 原子性 举一个银行转账的例子,比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元,两个操作都成功才意味着一次转账最终成功。 试想一下,如果这两个操作不具备原子性,从A的账户扣减了1000元之后,操作突然终止了,账户B没有增加1000元,那问题就大了。 银行转账这个例子有两个步骤,出现了意外后导致转账失败,说明没有原子性。
在并发编程中很多操作都不是原子操作,出个小题目: i = 0; // 操作1 上面这四个操作中有哪些是原子操作,哪些不是的?不熟悉的人可能认为这些都是原子操作,其实只有操作1是原子操作。
在单线程环境下上述四个操作都不会出现问题,但是在多线程环境下,如果不通过加锁操作,往往可能得到意料之外的值。 在Java语言中通过可以使用synchronize或者lock来保证原子性。 可见性 talk is cheap,先show一段代码:
线程1执行update方法将 i 赋值为100,一般情况下线程1会在自己的工作内存中完成赋值操作,却没有及时将新值刷新到主内存中。 这个时候线程2执行get方法,首先会从主内存中读取i的值,然后加载到自己的工作内存中,这个时候读取到i的值是50,再将50赋值给j,最后返回j的值就是50了。原本期望返回100,结果返回50,这就是可见性问题,线程1对变量i进行了修改,线程2没有立即看到i的新值。
如上图每个线程都有属于自己的工作内存,工作内存和主内存间需要通过store和load等进行交互。 为了解决多线程可见性问题,Java语言提供了 当然Java的锁机制如synchronize和lock也是可以保证可见性的,加锁可以保证在同一时刻只有一个线程在执行同步代码块,释放锁之前会将变量刷回至主存,这样也就保证了可见性。 关于线程不安全的表现还有『有序性』,这个问题会在后面的文章中深入讲解。 活跃性问题上面讲到为了解决 在说『死锁』前我们先引入另外一个概念:
概念是不是有点拗口,如果看不懂也没关系,你可以记住活跃性问题一般有这样几类: (1)死锁 死锁是指多个线程因为环形的等待锁的关系而永远的阻塞下去。一图胜千语,不多解释。 (2)活锁 死锁是两个线程都在等待对方释放锁导致阻塞。而 当多个线程都在运行并且修改各自的状态,而其他线程彼此依赖这个状态,导致任何一个线程都无法继续执行,只能重复着自身的动作和修改自身的状态,这种场景就是发生了活锁。 ![](/Users/ray/Library/Application Support/typora-user-images/image-20210408232019843.png) 如果大家还有疑惑,那我再举一个生活中的例子,大家平时在走路的时候,迎面走来一个人,两个人互相让路,但是又同时走到了一个方向,如果一直这样重复着避让,这俩人就是发生了活锁,学到了吧,嘿嘿。 (3)饥饿 如果一个线程无其他异常却迟迟不能继续运行,那基本是处于饥饿状态了。 常见有几种场景:
有一个非常经典的饥饿问题就是 性能问题前面讲到了线程安全和死锁、活锁这些问题会影响多线程执行过程,如果这些都没有发生,多线程并发一定比单线程串行执行快吗,答案是不一定,因为多线程有 创建线程是直接向系统申请资源的,对操作系统来说创建一个线程的代价是十分昂贵的,需要给它分配内存、列入调度等。 线程创建完之后,还会遇到线程 CPU是很宝贵的资源速度也非常快,为了保证雨露均沾,通常为给不同的线程分配 一般减少上下文切换的方法有: 有态度的总结多线程用好了可以让程序的效率成倍提升,用不好可能比单线程还要慢。 用一张图总结一下上面讲的: |
|