多线程相关知识整理

java内存区域和内存模型是不一样的东西,内存区域是指Jvm运行时将数据分区存储,强调对内存空间的划分。而内存模型是定义了线程和主内存之间的关系,即JVM在计算内存中的工作方式,如果我们想要深入了解JAVA并发编程,就要先理解好JAVA内存模型。

——Java内存区域(运行时数据区域)和内存模型(JMM)

多线程编程的三个重要特性:

  • 原子性:多个线程对一段代码的操作是互斥的
  • 可见性:一个线程对于变量做出的修改能够及时被其他线程查看到
  • 有序性:除了满足happens-before原则的语句,其他的基本指令的顺序都是可以被编译器优化的

以上三个特性是实现线程安全的基础。

原子性

java中提供多种保证原子性的机制:

  • CompareAndSwap(CAS)

意即比较并交换,这里使用了CPU的CAS指令保证了操作的原子性,当一个线程要改变一个变量t的值,需要向cpu提供期待的t的值x,以及调整后的值y,CAS指令会比较主内存中t的真实值和x,如果两者相等,就把y赋值给t,否则不做任何操作。

java.util.concurrent.atomic(JUC)包中提供了支持CAS操作的工具类,包括针对基本数据类型的AtomicInteger、AtomicLong等,以及针对未知对象的AtomicReference,或者是针对对象的某个变量的AtomicReferenceFieldUpdater。它们的基本思路就是在一个while循环中调用CAS,直到修改成功。

这种方式能够保证修改的正确性,但是如果竞争较激烈,长时间的while循环将占用大量资源。

针对以上的问题,其中一个解决方案是分而治之,LongAdder就是采用了这个思想,将要访问的变量分为多块,不同的线程对不同的块做出修改,最终对所有修改结果进行汇总,即最终结果。但是这样也会有个问题,子线程上进行的操作有可能不会立即更新到主内存,这样当写操作比较频繁的时候,读取到的结果很有可能不会是最新的,但是这种机制能够保证最终的结果一定是正确的。

同时简单的CAS也会带来ABA的问题,当前线程仅仅是通过比较值是否相同来确定变量是否被修改,这是有问题的,例如对于变量x,有两个子线程A和B都能够对之进行修改,如果B线程将变量x修改为y,之后又修改为x,此时对于线程A来说,变量的值依旧是x,没有做出改变,所以可以通过CAS操作修改变量的值,但是此时的x是已经被B修改过的了。为了解决ABA问题,可以添加一个辅助变量,以记录变量的版本号,AtomicStampedReference即采用了这种思路,只有当变量的值以及版本号都一致,才可以认为变量未被修改。

java中,锁有多种实现方式,可以通过java自带的synchronized关键字实现对代码块、方法、类的锁定,也可以通过JUC中提供的lock加锁。

synchronized关键字实现的是非公平锁,相对于JUC中的锁,它使用起来更方便,不会出现因为忘记解锁而导致死锁的情况,但是相对于synchronized,JUC提供的锁有几个独特的点:

  1. 可以指定是否为公平锁
  2. 其中提供condition队列
  3. 可以中断等待获取锁的线程

现阶段实际编程过程中,如果没有用到只有JUC才能够提供的功能,最好是使用synchronized关键字加锁。

可见性

synchronized关键字和volatile关键字可以用来保证可见性。

在线程加锁时,会清空工作内存中共享变量的值,及时获取共享变量在主内存中的值;线程解锁前,工作内存中对共享变量做出的修改必须被刷新到主内存中。由此,synchronized关键字可以保证变量的可见性。

而volatile关键字则更加简单粗暴,被volatile关键字修饰的变量具有以下特性,当用到该变量的时候,必须从主内存中进行读取,而不是使用线程工作内存中的值,对变量做出的修改必须马上刷新到主内存中。基于此,可以利用volatile关键字实现一个信号量,一个线程中对于该信号量的修改可以及时被其他线程观测到。

有序性

现阶段对于有序性的理解仅在于双重判断的单例模式。线程A中初始化一个对象,具体执行顺序如下:

  1. 申请内存空间
  2. 初始化对象
  3. 发布对象

正常来看这个顺序是没问题的,但是JustInTime(JIT)会对指令执行顺序进行优化,例如可能优化成1-3-2,这样,线程A中对象还没有被初始化完成就被发布出去,一旦其他的线程B读取并使用了这个还没有被初始化的对象,就可能会造成一些问题,volatile关键字可以保证这段指令不会被优化。

 

待续~

 

posted @ 2021-02-20 15:09  sunnysgw  阅读(42)  评论(0)    收藏  举报