内存模型
由前一遍文章https://www.jianshu.com/p/623cf38cc4c7讲解了内存模型,但也带来线程的三个问题:原子性、可见性、有序性
因为CPU的运行速度特别快,而主存的运行的速度跟不上CPU的速度,造成CPU在读取主存的数据要等待很久时间,所以CPU增加了的高速缓存区把需要数据存起来,CPU需要数据的时候就从高速缓存区中获取数据的副本,虽然高速缓存区运行速度很快,但也很昂贵。高速缓存区的出现提高了CPU的执行效率,但在多线程中也随之出现数据的原子性、可见性问题
我们模仿两核CPU的执行多线程对同一数据的读取:
CPU1和CPU2分别从主存中获取a的副本,两个高速缓存区a=0,线程A通过CPU1的运算后把a赋值为3,然后把a=3的结果缓存到高速缓存区中,但是并没来的急把结果返回到主存时,线程B通过CPU2也运算操作a++,从CPU2的高速缓存区中拿到的a=0的值+1,而不是3+1,所以内存模型在多线程中会造成数据的不同步。也带出线程的三大特性:原子性、可见性、可序性。
线程原子性:在多线程的情况下,数据可能同时被多个线程上同时执行,这样就造成运算结果的不一致性,线程的原子性就是数据在被一个线程执行的时候,其他线程不可以同时再运行此数据。java中可以使用synchronized可以解决线程的原子性问题。
线程可见性:原子性解决了多线程同时访问数据的问题,但是CPU的高速缓存区会并不能执行后数据立刻的更新到主存中,这样会导致其他的线程对另一个线程的计算结果不可见。在java中volatile可以接口线程的可见性问题。
线程可序性:这个也是指令重排序问题,就是CPU为了内部的处理器单元的充分利用,会在对单线程结果正确的情况下,对代码指令进行非顺序执行。
原子性——Synchronized
Synchronized:每一个对象都有一个锁,在执行Synchronized就会为线程获取其锁,其他线程再访问Synchronized的内容时,如果没有对象的锁就不能访问。所以保证了多线程的原子性。
通过一个Bean数据类学习Synchronized的使用:
public class StudentBean {
// 每个对象都有一个锁
private String name;
private String age;
private String sex;
// 1.在方法中加锁
public synchronized void setNameAndAge(String name, String age) {
this.name = name;
this.age = age;
}
// 2.在代码块中加锁
public void setName(String name) {
synchronized (this) {
this.name = name;
}
}
// 3.在静态方法中加锁,这里锁的是"类的对象,即Class的对象"
public static synchronized void start() {
System.out.println("start");
}
// 执行完synchronized方法或代码块后,就会释放对象锁
// 没有获取对象锁的线程,可以访问没有被synchronized的方法或代码块
public void setAge(String age) {
this.age = age;
}
}
Synchronized可以修饰在方法、代码块、静态方法中,其中方法、代码块
锁的是java对象,静态方法
锁的是Class对象。执行完锁的方法后,拥有锁的线程就会释放对象锁。其他没有锁的线程就要么在CPU等一会,要么进入阻塞状态。
Synchronized原理
看看synchronized代码块的通过反编译后的字节码是怎样的。
public class SynchronizedTest {
public static void main(String[] args) {
//通过synchronized修饰代码块
synchronized (SynchronizedTest.class) {
System.out.println("this is in synchronized");
}
}
}
反编译字节码
同步代码块在monitorenter
之后开始,同步代码块在monitorenter
结束。
monitor:
锁可以理解为对象锁,它是java虚拟机实现的,底层依赖操作系统的Mutex Lock实现,每一个java对象都有都拥有monitor锁。
monitorenter:
执行monitorenter时,会先尝试获取锁,如果monitor没有被锁,或者已经拥有的monitor锁,锁的计数器就会+1,并开始执行同步代码。
monitorexit:
执行monitorexit时,锁的计数器就会-1,如果计数器为0时,线程就会释放monitor锁,其他线程就可以获取monitor锁。
- 静态方法:
public class SynchronizedTest {
public static void main(String[] args) {
doSynchronizedTest();
}
//通过synchronized修饰方法
public static synchronized void doSynchronizedTest(){
System.out.println("this is in synchronized");
}
}
反编译字节码
静态方法并没有出现monitorenter和monitorexit,而是执行了
ACC_SYNCHRONIZED
,ACC_SYNCHRONIZED在执行同步代码块之前会获取monitor锁,再执行完成同步方法后会释放monitor锁。
-
JDK1.6后的锁优化
1.6之前,如果其他线程获取不了锁,就会进入阻塞状态,阻塞状态就会涉及上下文切换,这将消耗很多的时间,所以1.6后优化了锁频繁的上下文切换问题。
对象头的锁信息
首先线程进来获取锁时,会先获取偏向锁
,偏向锁记录着线程Id,如果偏向锁已经有线程锁定,那就就会进入轻量级锁
,在轻量级锁的线程就会在CPU执行回旋操作,如果已经获取的锁的线程很快的执行完成,那么就到轻量级锁执行,如果等待的时间久了,就进入重量级锁
,那么线程就进入阻塞状态。所以这就优化了线程没有获取锁就直接阻塞问题。
可见性——volatile
被Volatile修饰的变量,在变量会修改后的值,对于其他的线程来说一样是及时可见的。
private var volatile hasLoadingHeader = false
它是如何实现的,被volatile修饰的变量在修改之后,会从高速缓存立即更新到主存中,并让主存发送一个通知给其他线程,告诉其他线程当前的变量已经更新,你高速缓存区的变量已经失效了,如果你要使用的话,请到主存拿去最新的变量值。
网友评论