欢迎您访问365答案网,请分享给你的朋友!
生活常识 学习资料

并发编程synchronized实现原理

时间:2023-07-01

深入学习并发编程中的synchronized

第一章:并发编程中的三个问题

1.1 可见性

是什么?

一个线程对共享变量进行修改,另一个线程立即得到修改后的新值。

private static boolean flag = true;@Testpublic 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();@Testpublic 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_WAITINGt2 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的区别

 

Copyright © 2016-2020 www.365daan.com All Rights Reserved. 365答案网 版权所有 备案号:

部分内容来自互联网,版权归原作者所有,如有冒犯请联系我们,我们将在三个工作时内妥善处理。