深入学习并发编程中的synchronized
第一章:并发编程中的三个问题
1.1 可见性
是什么?
一个线程对共享变量进行修改,另一个线程立即得到修改后的新值。
private static boolean flag = true;
@Test
public void testSynthonized() throws InterruptedException {
new Thread(()->{
while (flag){
}
}).start();
Thread.sleep(2000);
new Thread(() -> {
flag = false;
System.out.println("线程修改了共享变量flag的值为" + flag);
log.info("线程修改了共享变量flag的值为:{}",flag);
}).start();
}
结果:
可能会出现第一个线程一直while 循环
但是第二个线程已经修改了flag的值,改为了 false
所以第一个线程未读取到 第二个线程修改的变量的值
结果: 可能会出现第一个线程一直while 循环 但是第二个线程已经修改了flag的值,改为了 false 所以第一个线程未读取到 第二个线程修改的变量的值
总结:
在并发编程的时候,会出现可见性问题,当一个线程对共享变量进行了修改,另外的线程并没有立即看到修改后的最新值。
为什么?
怎么用?
1.2 原子性
是什么?
在一次或多次操作中,要么所有的操作都执行并且不受其他因素干扰而中断,要么所有的操作都不执行。
为什么?
例如:
num初始化 0,5个线程 同时循环1000次执行 num++ 操作,按理来说最终num的值应该为5000
结果:可能为5000,可能比5000小
为什么会比5000小? 因为num++ 里面是有4个步骤,读取值,修改值。5个线程之间有一些操作会交替执行,读取的不是最新的值,修改之后最终的值也就不符合了,这就是并发编程不能保证原子性问题。
怎么用?
1.3 有序性
是什么?
程序代码在执行过程中的先后顺序,由于Java在编译期以及运行期的优化,导致了代码的执行顺序未必就是开发着编写代码时的顺序。
num = 0;
flage = true;
上面的这 2 句代码,在实际的执行过程中,因为他们之间没有之间的关联关系,所以cpu可能会对其 重新排序 (cpu指令重排),顺序可能会变成
flage = true;
num = 0;
代码的顺便被打乱了,所有可能会出现不能的执行结果。这就是并发编程的时候不能保证程序的有序性。
为什么?
怎么用?
第二章:Java内存模型(JMM)
注意:
1.如果对一个变量执行lock操作,将会清空工作内存中此变量的值(先清空值,再lock)
2.对一个变量执行unlock操作,必须把此变量同步到主内存中(先把工作内存的变量值同步到主内存中,再执行unlock操作)
synchronized保证原子性的原理:
对num ++ 增加同步代码块后,保证同一时间只有一个线程操作number++;就不会出现线程安全的情况。
小结:synchronized保证原子性的原理,synthronized保证只有一个线程拿到锁,能够进入同步代码块。
synchronized保证可见性的原理:
volatile 修饰的属性可以解决可见性问题,
原理:volatile存在一个缓存一致性协议,当有线程修改共享变量的值的时候,会去将其他线程从主内存读取到的备份变量设置为失效,当其他线程需要使用改变量的时候,会重新到主内存读取最新的变量值、11
使用synchronized 来保证戏线程的可见性
原理:
当使用到synthronized的时候,底层实现是 lock 和 unlock操作,所以就能够保证可见性。
使用打印语句 sout 的时候也可以保证可见性:
原理:因为打印语句底层的实现也是使用了synchronized关键字,所以会主内存中读取最新的变量值
小结:
synchronized保证可见性的原理,执行synchronized时,会对应lock原子操作,会刷新工作内存中共享变量的值。
synchronized保证有序性的原理:
为什么要重排序?
为了提高程序的执行效率,我们所写的代码顺序, 不一定是CPU执行代码的最佳顺序,所以CPU和编译器会对程序中代码重排序
重排序的规则?
as-if-serial 语义
定义:不管编译器和CPU如何对代码重排序,必须保证在单线程的情况下程序的结果是正确的。
synchronized后,虽然进行了重排序,保证只有一个线程进入同步代码块,也能保证有序性。
小结:
synchronized保证有序性的原理,我们加synchronized后,依然会发生重排序,只不过,我们有同步代码块,可以保证只有一个线程执行同步代码块中的代码,保证有序性。
synchronized保证可重入的原理:
可重入是什么?
原理:
synthronized的锁对象中有一个计数器(recurSion变量)会记录线程获得几次锁。
可重入锁的好处?
1.避免死锁
2.方便我们更好的的封装代码
流程:
1.线程1 先取到MyThread.class的锁,对应的程序计数器会 +1 ,变为1
2.线程1 进入到加锁的同步代码块里面(它已经拿到MyThread.class的锁,所以能访问,)此时程序计数器+1,变为2。
3.当执行完,退出对应的代码块的时候 程序计数器会 -1,-1。程序计数器 = 0 的时候代表没有线程获取到MyThread.class的锁,然后其他线程就可以竞争获取到这个锁
4.线程2 获取到MyThread.class的锁,然后走线程1的逻辑。
不管加锁的是同步代码块是在,在不能的方法中,都可以取到锁
总结:
synchronized是可重入锁,内部锁对象中会有一个计数器记录线程获取几次锁,在执行完同步代码块时,计数器的数量会 -1 ,直到计数器的值为0,就会释放该锁。
synchronized的不可中断特性:
学习synchronized的不可中断特性
学习lock的可中断特性
是什么(什么是不可中断)?
一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直阻塞或等待,不可被中断。
为什么?
怎么用?
synchronized是不可中断,处于阻塞状态的线程会一直等待锁。
lock锁是不可中断的锁
private static Lock lock = new ReentrantLock();
/**
* lock的不可中断和可中断
*/
@Test
public void testLockInte() throws InterruptedException {
Runnable runnable = ()->{
try {
lock.lock();
String name = Thread.currentThread().getName();
System.out.println(name + "获得锁,进入锁执行");
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println("释放锁");
}
};
Thread t1 = new Thread(runnable);
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(runnable);
t2.start();
System.out.println("停止线程t2前");
t2.interrupt();
System.out.println("停止线程t2后");
Thread.sleep(1000);
System.out.println("t1" + t1.getState());
System.out.println("t2" + t2.getState());
}
结果:
第一个线程拿到锁
第二个线程拿不到锁,然后就会进入waitting 状态,就会一直等待或者阻塞
Thread-1获得锁,进入锁执行
停止线程t2前
停止线程t2后
t1 TIMED_WAITING
t2 WAITING
小结:
不可中断是指,当一个线程获得锁后,另一个线程一直处于阻塞或等待状态,前一个线程不释放锁,后一个线程会一直阻塞或等待,不可被中断。
synchronized属于不可被中断
Lock的lock方法是不可中断的,
Lock的tryLock方法是可中断的
Java反汇编
每一个对象都会和一个监视器monitor关联。监视器被占用时会被锁住,其他线程无法来获取该monitor。当jvm执行某个线程的方法内部的monitorenter时,它会尝试去获取当前对象对应的monitor的所有权。过程如下:
1.当monitor的进入数为0,线程可以进入monitor,并将monitor的进入数设置为1。当前线程成为monitor的ower(所有者)
2.若线程已拥有monitor的所有权,允许它重入monitor,则monitor的进入数加1
3.若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直到monitor的进入数变为0,才能重新获取monitor的所有权。
monitor小结:
synchronized的锁对象会关联monitor,这个monitor不是我们主动创建的对象,是jvm的线程执行到这个同步代码块,发现锁对象没有monitor就会创建monitor(c++对象)
monitor的内部有2个重要的成员变量 1.owner;拥有这把锁的线程,2.recursions:会记录线程拥有锁的次数,当一个线程拥有monitor后其他线程就只能等待
monitorexit
面试题:synchronized出现异常会释放锁?
会
同步方法
同步方法在反汇编之后,会增加ACC_SYNCHRONIZED修饰,会隐式调用monitorenter,和monitorexit,
在执行同步方法前会调用monitorenter,
在执行完同步方法之后会调用monitorexit。
小结
过javap反汇编我们可以看到synchronized使用编程了monitorenter和monitorexit两个指令,每个锁对象都关联一个monitor(监视器,它就是真正的锁对象),
它内部有两个重要的成员变量owner会保存获得锁的线程,
recursions会保存线程获取锁的次数。
当执行到monitorexit的时候,recursions会-1,当计数器减到0 的时候,线程会释放锁。
面试题:synchronized 和 lock的区别