并行 :
多个进程利用多个cpu核心
同时在多个地方完成多个任务。这解决了所谓的计算密集型问题,如果将程序分成多个部分并在不同的处理器上编辑不同的部分,程序可以运行得更快。
并发:
多个进程使用一个cpu核心(cpu分片)
同时完成多个任务。在开始处理其他任务之前,当前任务不需要完成。并发解决了阻塞发生的问题。当任务无法进一步执行,直到外部环境发生变化时才会继续执行。最常见的例子是I/O,其中任务必须等待一些input(在这种情况下会被阻止)。这个问题产生在I/O密集型。
多线程问题
线程安全问题
原子性
举一个银行转账的例子,比如从账户 A 向账户 B 转 1000 元,那么必然包括 2 个操作:从账户 A 减去 1000 元,往账户 B 加上 1000 元,两个操作都成功才意味着一次转账最终成功。
银行转账有两个步骤,出现意外后导致转账失败,说明没有原子性。
原子性:即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
原子操作:即不会被线程调度机制打断的操作,没有上下文切换。
可见性
当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
为了解决多线程的可见性问题,Java 提供了volatile
这个关键字。当一个共享变量被 volatile 修饰时,它会保证修改的值立即更新到主存当中,这样的话,当有其他线程需要读取时,就会从内存中读到新值。普通的共享变量不能保证可见性,因为变量被修改后什么时候刷回到主存是不确定的,因此另外一个线程读到的可能就是旧值。
当然 Java 的锁机制如 synchronized 和 lock 也是可以保证可见性的。
活跃性问题
死锁
死锁是指多个线程因为环形等待锁的关系而永远地阻塞下去
产生死锁的四个必要条件:
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求且等待:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3)不可剥夺:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之
一不满足,就不会发生死锁。
活锁
死锁是两个线程都在等待对方释放锁导致阻塞。而活锁
的意思是线程没有阻塞,还活着呢。当多个线程都在运行并且都在修改各自的状态,而其他线程又依赖这个状态,就导致任何一个线程都无法继续执行,只能重复着自身的动作,于是就发生了活锁。
饥饿
如果一个线程无其他异常却迟迟不能继续运行,那基本上是处于饥饿状态了。
常见的有几种场景:
高优先级的线程一直在运行消耗 CPU,所有的低优先级线程一直处于等待;
一些线程被永久堵塞在一个等待进入同步块的状态,而其他线程总是能在它之前持续地对该同步块进行访问;
性能问题
多线程有创建线程
和线程上下文切换
的开销。
创建线程是直接向系统申请资源的,对操作系统来说,创建一个线程的代价是十分昂贵的,需要给它分配内存、列入调度等。
线程创建完之后,还会遇到线程上下文切换
。
CPU 是很宝贵的资源,速度非常快,为了保证雨露均沾,通常会给不同的线程分配时间片
,当 CPU 从执行一个线程切换到执行另一个线程时,CPU 需要保存当前线程的本地数据,程序指针等状态,并加载下一个要执行线程的本地数据,程序指针等,也就是『上下文切换』。
一般减少上下文切换的方法有:
无锁并发编程:可以参照 ConcurrentHashMap 锁分段的思想,不同的线程处理不同段的数据,这样在多线程竞争的条件下,可以减少上下文切换的时间。
CAS 算法,利用 Atomic + CAS 算法来更新数据,采用乐观锁的方式,可以有效减少一部分不必要的锁竞争带来的上下文切换。
使用最少线程:避免创建不必要的线程,如果任务很少,但创建了很多的线程,这样就会造成大量的线程都处于等待状态。
协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换
评论区