Java代码---编译--->Java字节码---类加载器加载到JVM中--->汇编指令(在CPU上执行)
Java中的并发机制依赖于JVM的实现和CPU指令
1.volatile
轻量级synchronized,使用成本低,不会引起上下文切换和调度。
1.1 主要使用的场合
是在多个线程之间感知实例变量的修改,并且可以获得最新值使用,当线程想要访问volatile修饰的变量时强制从公共堆栈中进行读取。
volatile变量在线程内存中被修改之后要立即同步回主内存中,以保证其他线程使用该volatile关键字修饰的变量时获取到的是最新的变量值。
volatile可以保证每次线程从主内存中刷新到最新的变量值,但是不能保证变量值被加载到线程内存之后对该变量做的修改操作是原子性的。也就是说,volatile关键字保证的是变量在不同线程之间的可见性,但是无法保证原子性,对于多个线程访问同一个实例变量还是需要加锁同步。
1.2 volatile变量的两个特性
1、保证可见性:此变量对于所有线程的可见性,指的是当一个线程修改了这个变量的值,新值对于其他线程来说是立即可见的。
2、禁止指令重排序优化(内存屏障,也会牺牲掉一些性能)
1.2.1 指令重排优化
在保证可以得到程序正确执行结果的前提下,CPU允许将多条指令不按程序规定的顺序分开发送到各个相应电路单元处理。
1.3 某个变量定义为volatile的应用场景
① 运算结果不依赖于变量的当前值或者能够确保只有一个线程修改这个变量的值;
② 变量不需要与其他的状态变量共同参与不变约束。
1.4 Java内存模型对volatile变量定义的特殊规则
① 在工作内存中每次使用变量前都需要从主内存中刷新最新值;
② 每次修改变量的值之后都必须立刻同步到主内存中;
③ 要求volatile修饰的变量不会被指令重新排序。
1.5 不支持原子性,非线程安全
对一个volatile修饰的变量的读写操作不是原子性的。
因为如果在第一个线程加载某个volatile修饰的变量值到工作内存之后有其他线程修改了这个变量值,那么第一个线程是感知不到这个值的变化的。这个时候就会出现线程安全的问题,所以为了保证线程安全问题还是需要synchronized关键字。
1.6 volatile变量的使用
并不建议过多地依赖volatile变量。如果在代码中过多地依赖volatile变量来控制状态可见性,通常比使用锁的代码更脆弱,也更难以理解。
仅当volatile变量能简化代码的实现以及对同步策略的验证,才应使用volatile变量。
如果在验证正确性时需要对可见性进行复杂的判断,那么就不要使用volatile变量。
volatile变量的正确使用方式包括:
- 确保它们自身状态的可见性;
- 确保它们所引用的对象的状态的可见性;
- 标识一些重要的程序生命周期事件的发生(比如初始化或关闭)。
2.synchronized
synchronized关键字保证在同一时刻,只有一个线程可以执行某个对象内某一个方法或某一段代码块。
重量级锁。包含两个特征:互斥性和可见性。
synchronized可以解决一个线程看到对象处于不一致的状态,可以保证进入同步方法或者同步代码块的每个线程都可以看到由同一个锁保护之前所有的修改效果。
实现同步的基础:Java中每个对象都可作为锁。
方法 | static方法 | 锁的是当前类的class对象,该类的所有对象访问这个static方法都被阻塞。 |
非static方法 | 锁的是当前实例对象,必须持有该对象锁才能访问对象内的同步方法。 | |
代码块 | this对象 | 与锁非static方法一样,都是给对象上锁。 |
非this对象x | 给对象x上锁,想访问这个代码段必须持有这个对象x的锁才可以。 | |
class对象 | 与给static方法加同步关键字一样,锁的都是class对象。 |
注意:class锁和对象锁不是同一种锁。
- 持有class锁则可以访问同步的static方法和锁定class对象的代码段;
- 持有对象锁则可以访问同步的非static方法和锁定this对象的代码段。
- 只有多个线程持有同一个对象的锁时,访问该对象内的同步方法才会被阻塞,如果持有的不是同一个对象的锁则异步执行。
2.1 synchronized关键字可以用于同步方法和同步代码块。
同步方法又可分为同步static方法和非static方法
2.1.1 如果是给static方法加上synchronized关键字
则说明同步的是当前类的.class类,那么后面所有对这个static方法的访问都会被阻塞,但是此时可以访问其他没有加synchronized关键字的方法或者是加了synchronized关键字的非static方法;
2.1.2 如果给非static方法加上synchronized关键字
则同步的是当前对象,这样的话其他想访问同一个对象下的synchronized同步方法就会被阻塞,但是不影响访问synchronized同步的static方法,因为synchronized非static方法是某个对象实例加锁,而synchronized static方法是给.class对象加锁,
但是class锁是对类的所有对象都有效,也就是说如果现在有个static方法加上了synchronized关键字,则这个类的所有对象都会对这个方法进行同步操作。
2.2 synchronized同步代码块
synchronized同步代码块的时候分为synchronized(this 对象)、synchronized(非this 对象)、synchronized(class对象)。
2.2.1 synchronized(this 对象)
synchronized(this 对象)同步的也是当前对象,而synchronized(非this 对象)则是对某个非this对象进行同步即锁定。
2.2.2 synchronized(非this 对象)
synchronized(非this 对象)同步代码块的方法在进行同步操作时,对象监视器必须是同一个对象。如果不是同一个对象监视器,运行的结果就是异步调用了。
2.2.3 synchronized(class对象)
synchronized(class对象)与给static方法加上synchronized关键字是一样的。
2.4 使用synchronized需要注意的地方
1、synchronized锁可重入:持有某个对象的锁可继续访问需要持有该对象锁才可访问的方法和代码段;
2、可重入锁也支持在父子类继承的环境中:调用子类中同步的方法时也可以访问父类中需要对象锁的方法和代码段;
3、 同步方法内出现异常时将会自动释放持有的对象锁;
4、同步不能继承,所以还需要在子类中需要同步的方法上加同步关键字。
2.5 Monitor对象
JVM基于进入和退出Monitor对象来实现方法同步和代码块同步
2.5.1 代码块同步
monitorenter指令:在编译后插入到同步代码块的开始位置;
monitorexit指令:下入到方法结束出和异常处。
Java保证每个1后必有对应的2。任何一个对象都有一个monitor与之关联。
2.5.2方法同步
另一种方法,也用这两个指令。
2.6
synchronized用的锁是存在Java对象头中的。
若对象是数组,则三个字宽:hashcode/锁/分代年龄、指针、数组长度
若对象不是数组,则只用存前两个。
3.volatile和synchronized的区别
volatile | synchronized |
线程同步的轻量级实现 | |
只能用于同步变量 | 可以用于修饰方法和代码块 |
多线程访问volatile不会发生阻塞 | 会发生阻塞 |
可保证数据的可见性,但是不能保证数据的一致性(原子性) | 可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数据做同步。 |
解决的是多线程之间的可见性 | 解决的是多个线程之间访问资源的同步性 |
4.并发编程中的三个概念(原子、可见、有序)
Java内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性三个特征建立的
4.1原子性
一个操作要么全部执行完毕 ,要么根本就不执行。
Java内存模型直接保证的原子性变量操作有read、load、assign、use、store和write。
4.2可见性
多个线程访问同一个变量时一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
Java语言中的volatile、synchronized和final三个关键字都可保证操作时变量的可见性。
4.3有序性
即程序执行的顺序按照代码的先后顺序执行。
Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性。
总之,要想让并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。
5.先行发生原则
先行发生是Java内存模型中定义的两项操作之间的偏序关系:如果说操作A先行发生于操作B,那么操作A产生的影响将会被操作B观察到。“影响”包括修改了内存中共享变量的值、发送了消息、修改了变量等。
5.1 Java内存模型中定义的一些“天然的”先行发生关系
① 程序次序原则(控制流中的先后顺序)
② 管程锁定原则(同一个锁的unlock发生在下一次lock)
③ volatile变量规则(禁止指令重排优化)
④ 线程启动原则(线程的start操作优先于线程内部的其他所有操作)
④ 线程终止规则(线程内部的所有操作优先于线程终止操作)
⑥ 线程中断原则(对线程interrupt()方法的调用先行发生于被中断检测代码检测到中断的发生)
⑦ 对象终结原则(对象初始化操作先行于finalize操作的发生)
⑧ 传递性(A先行发生于B,B先行发生于C,则A先行发生于C)。
时间先后顺序与先行发生原则之间基本没有太大关系,衡量并发问题的时候不要受到时间上先后发生的干扰,一切以先行发生原则为准。