不论什么时候,只要您将修改的变量接下来可能被另一个线程读取,或者您将读取的变量最后是被另一个线程写入的,那么您必须考虑并发问题,并采取合适的同步方式。

我们可能并没有多少机会写并发的东西,或者在非常精通之前最好还是优先使用最熟悉的,起码应该保证正确性才能讨论性能问题,所以很多概念是理解性的。但理解这些概念会帮助我们理解优秀源码(要不然别人的代码都看不懂(・ε・))以及者写程序时会有更多的思考。

接下来将对常见的并发知识进行知识梳理总结:

java内存模型

简单的讲,Java 内存模型将内存分为共享内存和本地内存。共享内存又称为堆内存,指的就是线程之间共享的内存,包含所有的实例域、静态域和数组元素。每个线程都有一个私有的,只对自己可见的内存,称之为本地内存。java内存模型中的内存结构如下图所示:

内存模型.png

共享内存中共享变量虽然由所有的线程共享,但是为了提高效率,线程并不直接使用这些变量,每个线程都会在自己的本地内存中存储一个共享内存的副本,使用这个副本参与运算。由于这个副本的参与,导致了线程之间对共享内存的读写存在可见性问题。

重排序

在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序,指令重排序包括下面三种:

  • 编译器优化重排序,在不改变单线程程序语义的前提下。
  • 指令级并行的重排序,如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。- 内存系统重排序,由于处理器可以使用缓存和读写缓冲区,这使得加载和存储操作看起来可能是乱序执行的。这些重排序可能会导致多线程出现的内存可见性问题。
  • 对于编译器,JMM的编译器重排序会禁止特定类型的重排序- 对于处理器重排序,JMM的处理器重排序规则会要求java编译器在生成执行序列时,插入特定类型的内存屏障(Menory Barriers)指令,通过内存屏障指令来禁止特定类型的处理器重排序。

JMM属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台上,通过禁止特定类型的编译器重排序和处理器重排序为程序员提供一致的内存可见性保证。

上下文切换

目前流行的CPU在同一时间内只能运行一个线程,超线程的处理器(包括多核处理器)可以同一时间运行多个线程,linux将多核处理器当作多个单独CPU来识别的。每个进程都会分到CPU的时间片来运行,由于这个时间片非常的短,所以我们感觉好像就是多个线程在同时执行一样。

当某个进程(线程是轻量级进程,他们是可以并行运行的,并且共享地使用他们所属进程的地址空间资源,比如:内存空间或其他资源)用完时间片或者被另一个优先级更高的进程抢占的时候,CPU会将该进程备份到CPU的运行队列中,其他进程被调度在CPU上运行,当cpu再次切换到原来的线程时,需要先读取之前的任务的一个状态,然后再继续执行,这样从保存到再加载就是一个上下文切换的过程。

上下文的切换时需要开销的,所以并不见得多线程就比单个线程快,而是应该根据具体的任务与硬件的配置来控制多线程的数量。线程过多可能造成CPU利用率达到100%。如果能够减少上下文切换必然能提高程序的运行效率:

  1. 无锁并发编程(比如:取模分段)
  2. CAS算法,Java的Atomic包采用此算法
  3. 使用最少线程。避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态

CAS(比较与交换,Compare and swap,是一种有名的无锁算法函数) :对竞争资源不用加锁,而是假设没有冲突去完成某项操作,如果因为冲突失败就不断重试,直到成功为止。以此来减少上下文切换。上面所说的循环CAS操作就是上述所说的乐观锁。

参考:如何使用 volatile, synchronized, final 进行线程间通信