• 【JAVA】从java线程来看java内存模型


    前言

    本来是想写两个线程,线程1输出1-98的奇数,线程2输出1-98的偶数,交替执行,在测试的时候发现线程安全问题,之后又引入到java内存模型,下面是几个demo。

    1.版本1

    //to print 1 ,3, 5...by thread1, print 2,4,6,8,10... by thread2 by turns
    public class Circle {
    
      public static boolean flag = true;
    
      public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
          int i = 1;
    
          @Override
          public void run() {
            while (i < 99) {
              if (flag == true) {
                System.out.println(Thread.currentThread().getName() + ": " + i);
                i = i + 2;
                flag = false;
              }
            }
          }
        });
    
        Thread t2 = new Thread(new Runnable() {
          int i = 2;
    
          @Override
          public void run() {
            while (i < 99) {
              if (flag == false) {
                System.out.println(Thread.currentThread().getName() + ": " + i);
                i = i + 2;
                flag = true;
              }
            }
          }
        });
    
        t1.start();
        t2.start();
    
      }
    }

    版本1很多次结果输出正常,偶尔会出现线程停留在中间某步不继续执行。

    2.版本2, 在版本1的基础上给其中一个线程加上sleep时间

    //to print 1 ,3, 5...by thread1, print 2,4,6,8,10... by thread2 by turns
    public class Circle {
    
      public static boolean flag = true;
    
      public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
          int i = 1;
    
          @Override
          public void run() {
            while (i < 99) {
              try {
                Thread.sleep(3);
              } catch (Exception e) {
                e.printStackTrace();
              }
              if (flag == true) {
                System.out.println(Thread.currentThread().getName() + ": " + i);
                i = i + 2;
                flag = false;
              }
    
            }
          }
        });
    
        Thread t2 = new Thread(new Runnable() {
          int i = 2;
    
          @Override
          public void run() {
            while (i < 99) {
              if (flag == false) {
                System.out.println(Thread.currentThread().getName() + ": " + i);
                i = i + 2;
                flag = true;
              }
            }
          }
        });
    
        t1.start();
        t2.start();
    
      }
    }

    结果随机,可能停留在Thread-0: 1 ,也可能停留在中间某步。

    3.版本3 给两个线程都sleep一段时间

    //to print 1 ,3, 5...by thread1, print 2,4,6,8,10... by thread2 by turns
    public class Circle {
    
      public static boolean flag = true;
    
      public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
          int i = 1;
    
          @Override
          public void run() {
            while (i < 99) {
              try {
                Thread.sleep(3);
              } catch (Exception e) {
                e.printStackTrace();
              }
              if (flag == true) {
                System.out.println(Thread.currentThread().getName() + ": " + i);
                i = i + 2;
                flag = false;
              }
    
            }
          }
        });
    
        Thread t2 = new Thread(new Runnable() {
          int i = 2;
    
          @Override
          public void run() {
            while (i < 99) {
              try {
                Thread.sleep(3);
              } catch (Exception e) {
                e.printStackTrace();
              }
              if (flag == false) {
                System.out.println(Thread.currentThread().getName() + ": " + i);
                i = i + 2;
                flag = true;
              }
            }
          }
        });
    
        t1.start();
        t2.start();
    
      }
    }

    结果正常(在我的机器上没演示出不正常的现象,理论上是会出现的)

    4.再看版本4  在版本2的基础上加上volatile

    //to print 1 ,3, 5...by thread1, print 2,4,6...by thread2 by turns
    public class Circle {
    
      public static volatile boolean flag = true;
    
      public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
          int i = 1;
    
          @Override
          public void run() {
            while (i < 99) {
              try {
                Thread.sleep(3);
              } catch (Exception e) {
                e.printStackTrace();
              }
              if (flag == true) {
                System.out.println(Thread.currentThread().getName() + ": " + i);
                i = i + 2;
                flag = false;
              }
    
            }
          }
        });
    
        Thread t2 = new Thread(new Runnable() {
          int i = 2;
    
          @Override
          public void run() {
            while (i < 99) {
              if (flag == false) {
                System.out.println(Thread.currentThread().getName() + ": " + i);
                i = i + 2;
                flag = true;
              }
            }
          }
        });
    
        t1.start();
        t2.start();
    
      }
    }

    结果执行正常,这是就涉及java volatile关键字了,volatile是保证线程之间的可见性,是保证全局变量flag对线程1和线程2的可见性。

    分析;

    1.背景知识

    多线程三大特性:

    原子性:保障线程安全问题

    可见性:java内存模型 — 不可见

    有序性: join方法  wait和notify

    java内存模型:

    主内存(存放共享的全局变量)

    私有本地内存(本地线程私有变量

    本地内存存放共享数据副本

     

    2.本案例中,两个线程线程1和线程2共享一个全局变量flag,这两个线程的内存称为私有本地内存,主内存main方法称为主内存;

    刚开始,主内存flag为true,主内存通知线程1和线程2的本地内存flag的值,两个线程获取到flag的值为true;

    之后,两个线程进行判断,线程1满足条件,线程2不满足条件,线程1执行,执行完后设定flag=false;

    再之后,线程1刷新flag的值为false到主内存;

    之后,主内存通知线程2拿到flag的值,flag拿到为flase,开始执行,执行完毕设定flag为true;

    在之后,线程2刷新flag的值为false到主内存;

    再之后,主内存通知线程1拿到flag的值,依次执行.

     

    重点是线程刷新flag最新的值到主内存的时间点,并不确定,借用别人一篇博客的摘录,这本书我没看过(打脸)

     

    在多线程的环境下,如果某个线程首次读取共享变量,则首先到主内存中获取该变量,然后存入工作内存中,以后只需要在工作内存中读取该变量即可。同样如果对该变量执行了修改的操作,则先将新值写入工作内存中,然后再刷新至主内存中。但是什么时候最新的值会被刷新至主内存中是不太确定的,这也就解释了为什么VolatileFoo中的Reader线程始终无法获取到init_value最新的变化。
    · 使用关键字volatile,当一个变量被volatile关键字修饰时,对于共享资源的读操作会直接在主内存中进行(当然也会缓存到工作内存中,当其他线程对该共享资源进行了修改,则会导致当前线程在工作内存中的共享资源失效,所以必须从主内存中再次获取),对于共享资源的写操作当然是先要修改工作内存,但是修改结束后会立刻将其刷新到主内存中。
    · 通过synchronized关键字能够保证可见性,synchronized关键字能够保证同一时刻只有一个线程获得锁,然后执行同步方法,并且还会确保在锁释放之前,会将对变量的修改刷新到主内存当中。
    · 通过JUC提供的显式锁Lock也能够保证可见性,Lock的lock方法能够保证在同一时 刻只有一个线程获得锁然后执行同步方法,并且会确保在锁释放(Lock的unlock方法)之前会将对变量的修改刷新到主内存当中。

      摘自:《Java高并发编程详解:多线程与架构设计》 — 汪文君

     

    所以说,使用volatile能够在修改结束后会立刻将其刷新到主内存中,所有的程序就是这样

    Ride the wave as long as it will take you.
  • 相关阅读:
    [LeetCode] 875. Koko Eating Bananas 科科吃香蕉
    [LeetCode] 874. Walking Robot Simulation 走路机器人仿真
    [LeetCode] 995. Minimum Number of K Consecutive Bit Flips 连续K位翻转的最小次数
    [LeetCode] 873. Length of Longest Fibonacci Subsequence 最长的斐波那契序列长度
    [LeetCode] 872. Leaf-Similar Trees 叶结点相似的树
    [LeetCode] 870. Advantage Shuffle 优势洗牌
    [LeetCode] 869. Reordered Power of 2 重新排序为2的倍数
    [LeetCode] 868. Binary Gap 二进制间隙
    [LeetCode] 867. Transpose Matrix 转置矩阵
    [LeetCode] 866. Prime Palindrome 质数回文数
  • 原文地址:https://www.cnblogs.com/jianpanaq/p/10264845.html
Copyright © 2020-2023  润新知