• synchronized,volatile关键字


    目录

    一,synchronized的特性

    1.1 互斥性

    1.2 可重入性

    二, 死锁

    2.1 死锁产生的原因

    三,volatile 关键字

    3.1 能保证内存可见性

    3.2 无原子性 


    一,synchronized的特性

    1.1 互斥性

    当两个线程对同一个对象加锁时,后加锁的线程要 "阻塞等待" ,直到第一个线程的锁释放。

    1.2 可重入性

    我们都知道当两个不同的线程正对同一个对象进行 "加锁" 时,会出现锁竞争,那么如果当一个线程对同一个对象连续 "加锁" 时,会怎么样呢?,比如下面的代码:

    1. public class Demo2 {
    2. public static void main(String[] args) throws InterruptedException {
    3. Object locker = new Object();
    4. Thread t1 = new Thread(() -> {
    5. synchronized (locker){
    6. synchronized (locker){
    7. System.out.println("t1执行");
    8. }
    9. }
    10. });
    11. t1.start();
    12. t1.join();
    13. }
    14. }

    按照之前说的,当 t1 线程第一次 "加锁" 成功后,此时的 locker 就属于是 "被锁定" 状态,那么当 t1 线程进行第二次 "加锁" 时,原则上是要 "阻塞等待" ,等到锁(locker)被释放了之后,才能再次 "加锁" ,但是在上述的代码中,只要第二次(locker)不加锁,第一个锁(locker)就不会被释放,而第一个锁(locker)不释放,第二次(locker)又不能加锁,这样的话,在逻辑上,上述代码中的线程 t1 就会产生死锁从而卡死,很明显,这是一个BUG。

    但是在日常开发中,这个BUG又很难避免,这时候有人会说,你上面的代码不是一眼就看出来了,这还不好避免?我在这里再举一个例子:

    1. class Test1{
    2. Object locker = new Object();
    3. public void fun1(){
    4. synchronized (locker){
    5. fun2();
    6. }
    7. }
    8. public void fun2(){
    9. fun3();
    10. }
    11. public void fun3(){
    12. fun4();
    13. }
    14. public void fun4(){
    15. synchronized (locker){
    16. System.out.println("4444");
    17. }
    18. }
    19. }
    20. //当出现上述代码时,当我们调用 fun1 方法,根本看不出来有什么问题!!!

    所以为了解决上述的BUG,设计Java的那群人就把 synchronized 设计成 "可重入锁" ,就是说当一个线程对同一个对象连续加锁时,这个锁会自己记录是哪个线程给它 "加锁" ,这样后续再次加锁时,如果加锁线程就是当前持有锁的线程,就直接加锁成功。

    这里我要提出一个问题:synchronized 虽然是可重入锁,避免了死锁,但是当一个线程对同一个对象加 N 层锁时,我们的锁什么时候释放?以及计算机如何判断锁释放的时机?第一个问题很简单,肯定是要第一次加锁的{}执行结束锁才能释放,第二个问题:锁对象不光会统计是谁拿到了锁,还会记录锁被加了几次,每一次加锁,计数器+1;每一次解锁,计数器-1。当出了最后一个{},计数器恰好为0,释放锁。

    二, 死锁

    产生死锁的情况:

    1. 如果 synchronized 没有可重入性,对同一个对象连续加锁

    2. 两个线程,两把锁,synchronized嵌套使用(可能产生!!!)。例如:

    1. public class Demo2 {
    2. public static void main(String[] args) throws InterruptedException {
    3. Object locker1 = new Object();
    4. Object locker2 = new Object();
    5. Thread t1 = new Thread(() -> {
    6. synchronized (locker1){
    7. try {
    8. Thread.sleep(1);//为了让t1拿到locker1,t2拿到locker2
    9. } catch (InterruptedException e) {
    10. throw new RuntimeException(e);
    11. }
    12. synchronized (locker2){
    13. System.out.println("t1结束");
    14. }
    15. }
    16. });
    17. Thread t2 = new Thread(() -> {
    18. synchronized (locker2){
    19. synchronized (locker1){
    20. System.out.println("t2结束");
    21. }
    22. }
    23. });
    24. t1.start();
    25. t2.start();
    26. t1.join();
    27. t2.join();
    28. }
    29. }

    3.  M个线程,N把锁(相当于2的推论)

    2.1 死锁产生的原因

    四个必要条件(只要下面4个条件中有一个不成立,那么就不会形成死锁)

    1. 互斥使用(锁的基本特性)即当两个线程对同一个对象加锁时,后加锁的线程要 "阻塞等待" ,直到第一个线程的锁释放。
    2. 不可抢占(锁的基本特性)与第一点相同
    3. 请求保持,即锁和锁之间可以嵌套使用
    4. 循环等待/环路等待,即等待的依赖关系形成了环,比如:车钥匙锁家里了,家钥匙锁车里了。与上述代码一个情况

    那么我们如何解决死锁? 

    因为上述的4个条件中,前两个条件是锁自带的属性,无法干预,因此我们只能从后两个条件入手。对于条件3:只要我们避免编写锁嵌套的逻辑就行。(但是有的情况下,这是无法避免的)对于条件4:给锁编号,约定加锁的顺序,比如:约定先加编号大的锁,后加编号小的锁,所有的线程都要遵守。

    三,volatile 关键字

    3.1 能保证内存可见性

    什么是内存可见性?

    在计算机运行代码时,要经常访问数据,这些数据往往存储在内存中(定义一个变量,这个变量就存储在内存中)。而cpu读取内存的这个操作,比cpu读取寄存器慢了几万倍,这时就会出现,cpu在解决大部分的情况时,速度很快,一旦读取内存,速度瞬间就慢下来了的情况。

    为了解决上述问题,提高运行效率,此时编译器就会对代码做出优化,把一些原本读取内存的操作,优化成读取寄存器,这样就减少了读内存的次数,从而提高了整体的效率。举一个例子:

    1. public class Demo3 {
    2. static boolean flag = true;
    3. public static void main(String[] args) throws InterruptedException {
    4. Thread t1 = new Thread(() -> {
    5. while(flag){
    6. }
    7. System.out.println("循环结束!");
    8. });
    9. t1.start();
    10. Thread.sleep(10);
    11. flag = false;
    12. t1.join();
    13. }
    14. }

     很明显,即使我们将flag改成false,线程也没有停止循环,这就是 "内存可见性" 引起的,由于该循环没有任何其他操作,循环速度非常快,就会进行大量的 load(将flag读取到内存) ,cmp 操作。此时编译器发现,虽然进行了多次 load 操作,但是 flag 的值没有改变,而 load 操作又很浪费时间,所以编译器直接就在第一次循环的时候,读内存,将flag存到寄存器中,后续就不读内存了,直接从寄存器中取出 flag,这就是导致 "内存可见性" 问题。

    而 volatile 关键字可以解决这一问题,只要被 volatile 修饰的变量,编译器就不可以对其进行优化,也就是说,该变量必须从内存中读取。例如:

    1. public class Demo3 {
    2. volatile static boolean flag = true;
    3. public static void main(String[] args) throws InterruptedException {
    4. Thread t1 = new Thread(() -> {
    5. while(flag){
    6. }
    7. System.out.println("循环结束!");
    8. });
    9. t1.start();
    10. Thread.sleep(10);
    11. flag = false;
    12. t1.join();
    13. }
    14. }

     还有一点需要注意的是,编译器优化的触发是不确定的,我们不知道它什么时候触发,什么时候不触发。所以最好使用 volatile 关键字!!!

    3.2 无原子性 

    volatile 关键字不能像 synchronized 关键字那样让 count++ 这类操作变成原子操作!!例如:

    1. public class Test {
    2. static volatile int count = 0;
    3. public static void main(String[] args) throws InterruptedException {
    4. Thread t1 = new Thread(() -> {
    5. for (int i = 0; i < 10000; i++) {
    6. count++;
    7. }
    8. });
    9. Thread t2 = new Thread(() -> {
    10. for (int i = 0; i < 10000; i++) {
    11. count++;
    12. }
    13. });
    14. t1.start();
    15. t2.start();
    16. t1.join();
    17. t2.join();
    18. System.out.println("count = " + count);
    19. }
    20. }

     3.3 禁止指令重排序

    比如说我们常常使用的 new 操作,它会大概分成三步 : 1. 向内存申请一块空间   2. 实例化变量   3. 返回该空间的引用,它可以是 1 -> 2 -> 3 ,也可以是 1 -> 3 -> 2,在有些情况中,我们必须要保持 1 -> 2 -> 3 的顺序,这时候我们就可以使用 volatile 关键字。

  • 相关阅读:
    随便写写之——CSDN个人主页布局
    Python && JAVA 去除字符串中空格的五种方法
    CODEFORCES --- 630A. Again Twenty Five!
    Java实现进度条加载效果
    vue项目启动npm install和npm run serve时出现错误Failed to resolve loader:node-sass
    软件开发模型与软件测试模型
    【教3妹学编程-算法题】高访问员工
    简单讲解Android Fragment(一)
    python3读取yaml文件
    ARMday03(寄存器读写、栈、程序状态寄存器、软中断和异常、混合编程)
  • 原文地址:https://blog.csdn.net/m0_74859835/article/details/132779308