JMM 内存可见性

原创
04/04 16:54
阅读数 110

一、什么是可见性

Java中,在一个线程中,修改主内存变量,修改同步到主内存(堆区+方法区)中方,然后立即后通知其他线程进行缓存同步。

二、JMM是如何实现可见性的?

在java中,锁相关的问题基本是原子性、可见性、重排序的相关讨论

  • 实现可见性的关键字只有volatile和synchronized,实现可见性的技术有CAS。
  • 实现可重排序的“关键字”只有volatile和final ,(CAS未知)
  • 实现原子性的只有CAS和synchronized

final 虽然和volatile具备内存屏障的功能,但final并不具备可见性。

如何证明final没有可见性呢?

我们来一段代码存在可见性问题的代码

public class ThreadWatcher {
    public int A = 0;
    public static void main(String[] args) {
        final ThreadWatcher threadWatcher = new ThreadWatcher();
        WorkThread t = new WorkThread(threadWatcher);
        t.start();
        while (true) {
            if (threadWatcher.A == 1) {
                System.out.println("Main Thread exit");
                break;
            }
        }
    }
}

class WorkThread extends Thread {
    private ThreadWatcher threadWatcher;
    public WorkThread(ThreadWatcher threadWatcher) {
        super();
        this.threadWatcher = threadWatcher;
    }
    @Override
    public void run() {
        super.run();
        System.out.println("sleep 1000");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.threadWatcher.A = 1;
        System.out.println("WorkThread exit");

    }
}

这个线程大概率会阻塞主线程,输出结果如下,显然主线程未能退出。

sleep 1000
WorkThread exit

我们解决方法当然是加volatile修饰变量A,但其实还有一种方式,利用其他volatile变量让A的缓存失效

public class ThreadWatcher {
    public int A = 0;
    private volatile int B = 0;
    public static void main(String[] args) {
        final ThreadWatcher threadWatcher = new ThreadWatcher();
        WorkThread t = new WorkThread(threadWatcher);
        t.start();
        while (true) {
            int c = threadWatcher.B;
            if (threadWatcher.A == 1) {
                System.out.println("Main Thread exit");
                break;
            }
        }
    }
}

class WorkThread extends Thread {
    private ThreadWatcher threadWatcher;
    public WorkThread(ThreadWatcher threadWatcher) {
        super();
        this.threadWatcher = threadWatcher;
    }
    @Override
    public void run() {
        super.run();
        System.out.println("sleep 1000");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.threadWatcher.A = 1;
        System.out.println("WorkThread exit");

    }
}

经过上述改造,我们运行代码,发现主线程正常退出。

sleep 1000
WorkThread exit
Main Thread exit

同样的,我们把volatile替换为final,发现主线程并不能正常退出,因此可以知道,final只能禁止重排序,多线程情况一般使用final修饰集合,这时下需要和synchronized一起使用。

此外,我们还可以使用数组进行测试,发现B变量的对象以及数组中方的元素值被修改同样对其他线程不可见

public final int[] B = new int[2];


 

一个有趣的实验——刷新线程缓冲区

sycnhornized也能达到相同的效果(将缓冲区TLAB刷新)

我们利用可见性实验,看看如果利用“空锁、volatile 无意义操作、cas无意义操作”能否让线程最终退出,当然volatile已经展示了,我们只需要验证空锁和cas

public class ThreadWatcher2 {

    private  static boolean flag = true;
    public static void main(String[] args) {

         new Thread(new Runnable() {
             @Override
             public void run() {
                 while (true){
                     synchronized (ThreadWatcher2.class){}
                     if(!flag){
                         break;
                     }
                 }
                System.out.println("work thread exit");
             }
         }).start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = false;

        System.out.println("main thread exit");
    }
}

 

CAS的调用也可以实现线程缓冲区内存刷新

public class ThreadWatcher2 {
    private  static boolean flag = true;
    private static AtomicBoolean atomicBoolean = new AtomicBoolean();
    public static void main(String[] args) {

         new Thread(new Runnable() {
             @Override
             public void run() {
                 while (true){
                     atomicBoolean.get();
                     if(!flag){
                         break;
                     }
                 }
                System.out.println("work thread exit");
             }
         }).start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = false;
        System.out.println("main thread exit");
    }
}

因此,我们要明白,编程中刷新缓冲区不给指定的变量加锁或者修饰也是可以的,但是原子性的话就无法保证了。

 

平时编码可能注意不到的问题

  1. System.out.println()
  2. Thread.sleep()

这俩方法的调用,也会刷新其所在线程的缓冲区,因为其内部使用了synchronized,因此,我们编程应该尽量避免使用,防止多次无效的线程缓冲区刷新。

 

三、ReentrantLock可见性问题

ReentrantLock作为JDK API锁,经常用来和synchronized进行比较。ReentrantLock + AQS作为J.U.C的一部分在JDK 1.5中发布,优异的表现迅速给JVM提供了参考,也使得JVM将synchornized参考ReentrantLock进行了实现。

共同点:

  • 【1】都有自旋锁,而且都是通过CAS进行自旋
  • 【2】都有锁资源监控机制,ReentrantLock通过修改ReentrantLock state实现,JVM通过修改锁资源的Mark Word实现(关于Mark Word接下来补充)。
  • 【3】都具备重量级锁,但ReentrantLock是通过三次自选以上,逐渐转为重量级锁(悬挂或休眠),而sycnhornized是自适应锁,要么自选升级为重量级锁,要么直接从无锁升级为重量级锁,最终进入ObjectMonitor
  • 【4】都使用了集合,ReentrantLock使用了双链,先从head开始进行给next指针赋值,在从tail给prev赋值,连续两遍,同时也会自旋。ObjectMonitor 具备两个集合,其中一个EntrySet,和ReenttrantLock一样,另一个是WaitSet,用于存储信号量、wait、sleep指定的线程

可见,sychornized借鉴了很多ReentrantLock的实现,当然还要说下不同点。

不同点:

  • 【1】ReentrantLock 具备公平锁,当然,synchornized也有多种方式来实现,比如join,信号量,单线程池。
  • 【2】ReentrantLock具备超时锁、中断,synchornized也可以通过wait超时机制实现。
  • 【3】性能上,无竞争情况下,synchornized支持偏向锁、锁粗放、锁消除,而ReentrantLock每次都会通过【读写state】刷新缓存
  • 【4】性能上,synchronized不支持禁止重排序,而ReentrantLock 的state字段可做到禁止重排序,如果是全时段锁,两者区别不大,多次重入,那么lock...unlock之间的可排序代码段将会越来越无法重排
  • 【5】性能上,sycnhornized支持自适应锁,ReentrantLock锁升级比较机械,直接由轻量级锁经过三次以上自旋进入重量级锁,而synchornized根据mark word状态判断要不要自旋、自旋锁多少次,相较而言,synchronized相对更加灵活。

 

四、Mark Word

我们经常可能被问到的问题,Java引用指针占几个字节,new Object()占几个字节?

在32位操作系统中,java引用指针占4个字节,在64位系统中,引用指针占8个字节。

在32位操作系统中,new Object()占8个字节+8个字节进行填充,所以16个字节,在64位系统,即便压缩指针,也要占也会填充,16个字节

参考:https://zhuanlan.zhihu.com/p/245583335

 

这些和Mark Word有什么关系?

我们需要明白,对象中存在一些元信息,这些元信息在Java运行中必不可少

下面是对象的布局

其中第一部分是Mark Word,对于synchornized自适应锁而言,这种实现方式很巧妙,而且还节省空间。

 

五、对象内存的分配过程

工作线程中,对象内存的分配。

【1】线程的栈内存实际上和TLAB的一部分,属于私有空间。

【2】大对象通常意义上是占内存很大的对象,比如数组就是大对象,而且数组的虚拟内存必须连续,如果没有足够连续的内存空间,一般会OOM。

【3】无论是分配对象到栈内存还是TLAB区域,一般需要JIT 逃逸分析和可见性分析

 

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部