多线程学习笔记1volatile和synchronized

21次阅读

共计 1819 个字符,预计需要花费 5 分钟才能阅读完成。

今天开始整理学习多线程的知识,谈谈最重要的两个关键字:volatile 和 synchronized。

一、三个特性

1、原子性

所谓原子性操作就是指这些操作 是不可中断的 ,要么 执行过程中不被中断,要么不做。在 Java 中对基本数据类型的读取和赋值操作是原子性操作,比如 i ++ 就不是原子性操作,分为三步:读取 i,i++,写回。

2、可见性

这个特性可以通过 Java 的内存模型来理解。
链接描述

Java 的内存模型 如上图所示,可以看到,一个线程对变量进行操作时,首先需要将变量从主存拷贝一个副本到工作内存中,操作完之后再给工作内存中的变量赋值,再传回主存。但是这样一个过程比较慢,在使用多线程的时候就会出现问题。

而所谓的 内存可见性,就是在使用 volatile 关键字之后,对变量的修改会立即刷回主存。

3、有序性

有序性是指多线程执行结果的正确性。这里介绍一下指令重排的概念。

指令重排:这一机制是指 JMM 允许对指令进行重排,但是这种重排不会影响程序的结果。比如 a ++; b++; 两条指令之间没有联系,因此可以调换顺序。
这种机制在多线程中会出现问题,因此可以通过 volatile 来禁止重排。

二、volatile

1、volatile 关键字的特性:

(1)保证了不同线程对被修饰变量操作的 内存可见性
(2)禁止了指令的 重排序

从内存的语义上看,写 volatile 变量时,会将更新的值直接写回到主存;读 volatile 变量时,会直接从主存中读取变量。

2、volatile 关键字的特点

volatile 关键字可以保证 有序性 可见性 ,但是无法保证 原子性

举一个例子,现在内存中有一个 count 为 0,现在 A 进程和 B 进程想对 count 做加一操作,首先 A 和 B 都将 count 取出,A 和 B 的工作内存中的 count 值都为 0,此时 A 做加一操作,并写回主存,而 B 也只会对 0 做加一操作,因为 B 已经读完了,他并不知道主存中的 count 已经被修改过了,因此可见无法保证原子性。

3、volatile 的底层实现

volatile 主要是通过 lock指令前缀 来实现的:
(1)重排序时不能把后面的指令重排到前面
(2)使本 CPU 的 cache 写入主存
(3)写入时,其他 CPU 的 cache 无效,也就是直接从主存中读取,对外部可见

三、synchronized 关键字

1、概念

(1)一个非常直观的解释是:
Java 语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在 同一时刻 最多只有 一个线程 执行该段代码。
(2)官方的解释是:
Java 中的 synchronized,通过使用 内置锁 ,来实现对变量的同步操作,进而实现了对变量操作的 原子性 和其他线程对变量的 可见性 ,从而确保了并发情况下的 线程安全

2、特点

比起 volatile 关键字,synchronized 关键字不仅能够保证可见性,还可以保持原子性。

3、实现

synchronized 关键字的原理,就是通过锁,线程拿到锁之后,其他线程就会阻塞等待。

在 Java 中每一个对象都有一个与之关联的锁,称为 内置锁
当我们使用 synchronized 修饰非静态方法时,用的是调用该方法的实例的内置锁,也就是 this;
当我们使用 synchronized 修饰静态方法时,用的是调用该方法的所在的类对象的内置锁;
更多时候,我们使用的是 synchronized 代码块,我们经常用的是synchronized(this),也就是把对象实例作为锁。如下

    public void process() {doProcess();
        synchronized (this) {count ++;}
    }

4、重入问题

public class Widget {public synchronized void doSomething() {...}
}

public class LoggingWidget extends Widget {public synchronized void doSomething() {System.out.println(toString() + ": calling doSomething");
        super.doSomething();}
}

值得注意的是,在这个程序中,两个 doSomething 函数都有 synchronized 关键字,那么在执行过程中是否会发生死锁呢,答案是否定的,因为获得锁的是线程,因此这两个函数都用的是同一个锁,因此又称为 可重入锁

参考:
https://hzy38324.gitbooks.io/…《Java 并发编程实践》

正文完
 0