JVM进阶——资深面试官必问的Java内存模型,Java面试题目

// A 线程

i = 1;

// B 线程

j = i;

// C 线程

j = 2;

如果 A 先行发生于 B,且 C 没有登场,那么,j 的值一定是 1。如果 C 登场了,仍旧只是 A 先行发生于 B,那么 j 可能是 2,也可能是 1

Java 内存模型中有一些天然的先行发生原则,其中介绍下面两条;

  • 管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。 这里必须强调的是同一个锁,而“后面”是指时间上的先后顺序。

  • volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序。

volatile变量规则

一起吹水聊天

对于volatile变量的读和写而言,如果在实际执行时间上有写在读前的话

(如线程A的assign在先于线程B的use执行,前面说了assign、use是原子操作)

,那么就有写在读前的先行发生关系,这样就保证了一切对于volatile变量写操作可见的变量(即happens before volatile写操作的其他变量操作所造成的一切影响),对于后面的volatile变量读操作也是可见的。如果换成可普通变量,即使是有时间上的写在读前,但如不是同一线程就没有happens关系,这样就不能保证可见性。

private int value = 0;

public void setValue(int value) {

this.value = value;

}

public int getValue() {

return value;

}

  • 假设有线程 A 和 B ,在时间顺序上,A 先调用 setValue(2),B 再调用 getValue(),那么,B 得到的值还是不确定的。

  • 可以把 value 修饰为 volatile 类型,由于 setter 方法对 value 的修改不依赖于原值,所以将会是线程安全的。

线程安全


Java中的线程安全排序:

  • 不可变

  • 绝对线程安全

  • 相对线程安全

  • 线程兼容

  • 线程对立

this逃逸

在构造器构造还未彻底完成前(即实例初始化阶段还未完成),将自身this引用向外抛出并被其他线程复制(访问)了该引用。

class ThisEscape {

int i;

static ThisEscape obj;

public ThisEscape() { // 由于指令重排序,所以不能确定这两部谁先进行

i = 1;

obj = this;

}

}

// 如果线程A还没来得及为i赋值,线程B就使用了这个obj.i;会导致空指针。或者其他情况下会导致对象不完整。

什么情况下会This逃逸?

(1)如上述的明显将this抛出

(2)在构造器中内部类使用外部类情况:内部类访问外部类是没有任何条件的,也不要任何代价,也就造成了当外部类还未初始化完成的时候,内部类就尝试获取为初始化完成的变量

  • 在构造器中启动线程:启动的线程任务是内部类,在内部类中xxx.this访问了外部类实例,就会发生访问到还未初始化完成的变量

  • 在构造器中注册事件,这是因为在构造器中监听事件是有回调函数(可能访问了操作了实例变量),而事件监听一般都是异步的。在还未初始化完成之前就可能发生回调访问了未初始化的变量。

不可变

  • final修饰:只要一个对象被正确地构建出来(即没有发生this引用逃逸)

  • 如果多线程共享的是一个基本数据类型,那只要再定义时使用final关键字修饰就可以保证它是不可变的。如果是一个对象,那就需要保证它自己不可变(如:String无论用什么方法都不会影响它原来的值,它是由final修饰的)

一起吹水聊天

绝对线程安全

再Java API中标注自己是线程安全的类,大多数都不是绝对的线程安全。

如:Vector类,它的add()、get()等方法都是由synchronized修饰的,但不意味着调用它的时候不需要原子操作(指要么都做要么都不做)了。

public void method(Vector vec) {

synchronized(vec) {

vec.set(…);

vec.get(…);

}

}

相对线程安全

指我们通常意义上的线程安全,它需要保证这个对象单次的操作是线程安全的。如:Vector类。

线程兼容

指我们通常所说的线程不安全。指对象本身并不安全,但可以通过调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用。

线程对立

指不管怎样操作都无法在多线程环境中并发使用。由于Java本身就具备支持多线程地特性,线程对立地情况很少出现。

线程安全的实现方法


互斥同步

同步是指在多线程并发访问共享数据时,保证共享数据同一时刻只被一条线程使用(或者是一些,当使用信号量时)。

互斥是同步的一种手段:临界区、互斥量和信号量都是常见的互斥实现方法。

互斥同步这四个字里:互斥是因,同步是果。互斥是方法,同步是目的。

我们拿Reentrantsynchronized多了一些高级功能:

  • 我们可以认为synchronized是Reentrant的一个子集,但是经过JDK6优化后,他们性能差不多,而synchronized使用方便

  • 公平锁:他们都是使用非公平锁(Reentrant可以改为公平锁,但性能会下降)

  • 锁绑定多个条件:Reentrant可以绑定多个Condition对象,而synchronized不行。

  • 等待可中断:当持有锁的线程长期不释放锁时,正在等待的线程可以放弃等待,去处理其他的事情。(synchronized则会一直阻塞,如果阻塞或者唤醒一条线程,则需要操作系统在用户态到核心态之间的转换,这样很耗资源)

一起吹水聊天

非阻塞同步

互斥同步面临的问题是线程阻塞和唤醒带来的性能开销,这称为阻塞同步,是一种悲观的发展策略,因为不管是否出现竞争都进行加锁。

非阻塞同步:是一种乐观发展策略,就是不管风险先进行操作(如CAS算法),但是它需要硬件指令集的发展(使多个步骤的操作具备原子性),使某些看起来的多步操作只要一步就可以完成。不过它无法解决ABA问题,如果要解决ABA问题,改用传统的互斥同步可能会更快。

无同步方案

同步和线程安全没有必然的联系。如果一个方法本来就不涉及共享数据,那自然就不需要任何同步措施就能保证其正确性。

  • 可重入代码

  • 线程本地存储(如:使用ThreadLocal类)

锁优化


从 JDK5 到 JDK6 虚拟机开发团队实现的各种锁优化技术

自旋锁与自适应自旋

互斥同步对性能最大的影响是阻塞的实现,需要用户态与内核态切换(需要消耗的不止CPU资源)。

如果机器上有多个处理器或者处理器核心,能让两个线程并行执行,我们就会请求其中一个线程等一会儿,但不放弃处理器的执行时间,这里的等一会就是忙循环(自旋)。默认忙循环10次,也可以自行设置。

如果,循环的时间不再是固定的,而是它自己决定的,那就是自适应自旋。

锁消除

对被检测到不可能存在共享数据竞争的锁进行消除。你可能觉得自己没有加锁呀,可是编译器会加锁。如:

public String add(String s1, String s2, String s3) {

return s1 + s2 + s3;

}

// javac转换后

public String add(String s1, String s2, String s3) {

StringBuffer sb = new StringBuffer();

sb.append(s1);

sb.append(s2);

sb.append(s3);

return sb.toString();

}

一起吹水聊天

锁粗化

如果一系列的操作都是对同一个对象进行反复加锁,甚至加锁是出现在循环体中的,那即使线程没有竞争,频繁地进行同步操作也会导致不必要地性能损耗。

如:上面的append()方法。虚拟机就会就会把锁扩展到第一个append()操作之前直至到最后一个append()操作之后。

轻量级锁

偏向锁

《深入理解Java虚拟机》

伪共享


什么是伪共享

为了解决计算机系统中主内存与CPU之间运行速度差的问题,会在CPU与主内存之间添加一级或多级高速缓冲存储器(Cache)。在Cache内部是按行存储的。

当CPU访问某个变量时,首先去缓存里看有没有该变量,如果有就直接获取,如果没有就去主内存中获取。

CPU读取数据通常以一块连续的块为单位,即缓存行(Cache Line)。一个缓存行里可能有多个变量。所以通常情况下访问连续存储的数据会比随机访问要快,访问数组结构通常比链结构快,因为通常数组在内存中是连续分配的。(PS. JVM标准并未规定“数组必须分配在连续空间。)

缓存行的大小通常是64字节,这意味着即使只操作1字节的数据,CPU最少也会读取这个数据所在的连续64字节数据。

缓存失效:根据主流CPU为保证缓存有效性的MESI协议的简单理解,如果一个核正在使用的数据所在的缓存行被其他核修改,那么这个缓存行会失效,需要重新读取缓存。

False Sharing:如果多个核的线程在操作同一个缓存行中的不同变量数据,那么就会出现频繁的缓存失效,即使在代码层面看这两个线程操作的数据之间完全没有关系。这种不合理的资源竞争情况学名伪共享(False Sharing),会严重影响机器的并发执行效率。

如何避免伪共享

在Java8之前,可以使用填充的方式来避免该问题,也就是创建一个变量时使用填充字段填充该变量所在的 Cache Line。如:

public static void FilledLong() {

public long value = 0L;

public long a1, a2, a3, a4, a5, a6;

}

// 假如Cache Line是64个字节的,而 long 是 8 个字节的,那么现在有 56 个字节,类对象FilledLong占用8个字节,刚好64个字节。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
img

最后

每年转战互联网行业的人很多,说白了也是冲着高薪去的,不管你是即将步入这个行业还是想转行,学习是必不可少的。作为一个Java开发,学习成了日常生活的一部分,不学习你就会被这个行业淘汰,这也是这个行业残酷的现实。

如果你对Java感兴趣,想要转行改变自己,那就要趁着机遇行动起来。或许,这份限量版的Java零基础宝典能够对你有所帮助。

也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!**

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-JOpUwd80-1712094981614)]

最后

每年转战互联网行业的人很多,说白了也是冲着高薪去的,不管你是即将步入这个行业还是想转行,学习是必不可少的。作为一个Java开发,学习成了日常生活的一部分,不学习你就会被这个行业淘汰,这也是这个行业残酷的现实。

如果你对Java感兴趣,想要转行改变自己,那就要趁着机遇行动起来。或许,这份限量版的Java零基础宝典能够对你有所帮助。

[外链图片转存中…(img-REUkvXdP-1712094981615)]

  • 26
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值