万隆的笔记 万隆的笔记
博文索引
笔试面试
  • 在线学站

    • 菜鸟教程 (opens new window)
    • 入门教程 (opens new window)
    • Coursera (opens new window)
  • 在线文档

    • w3school (opens new window)
    • Bootstrap (opens new window)
    • Vue (opens new window)
    • 阿里开发者藏经阁 (opens new window)
  • 在线工具

    • tool 工具集 (opens new window)
    • bejson 工具集 (opens new window)
    • 文档转换 (opens new window)
  • 更多在线资源
  • Changlog
  • Aboutme
GitHub (opens new window)
博文索引
笔试面试
  • 在线学站

    • 菜鸟教程 (opens new window)
    • 入门教程 (opens new window)
    • Coursera (opens new window)
  • 在线文档

    • w3school (opens new window)
    • Bootstrap (opens new window)
    • Vue (opens new window)
    • 阿里开发者藏经阁 (opens new window)
  • 在线工具

    • tool 工具集 (opens new window)
    • bejson 工具集 (opens new window)
    • 文档转换 (opens new window)
  • 更多在线资源
  • Changlog
  • Aboutme
GitHub (opens new window)
  • JUC介绍
  • 并发编程核心概念与主要内容
  • Java线程创建与使用
  • 线程生命周期
  • synchronized关键字
  • wait与notify
  • 线程中断-interrupt
  • 线程优雅关闭
  • JMM内存模型
  • volatile关键字
  • final关键字
    • 构造方法溢出问题
    • final的happen-before语义
    • happen-before规则总结
  • Lock
  • JUC并发编程
2022-05-06
目录

final关键字

# final关键字

final关键字:

  • 用来修饰一个引用:
    • 如果引用为基本数据类型,则该引用为常量,该值无法修改;
    • 如果引用为引用数据类型,比如对象、数组,则该对象、数组本身可以修改,但指向该对象或数组的地址的引用不能修改。
    • 如果引用是类的成员变量,则必须当场赋值,否则编译会报错。
  • 用来修饰一个方法:当使用final修饰方法时,这个方法将成为最终方法,无法被子类重写。但是,该方法仍然可以被继承。使用final方法的原因主要有两个:
    • 把方法锁定,以防止继承类对其进行更改
    • 效率,在早期的Java版本中,会将final方法转为内嵌调用。但若方法过于庞大,可能在性能上不会有多大提升。因此在最近版本中,不需要final方法进行这些优化了。
  • 用来修饰类:当用final修饰类时,该类成为最终类,无法被继承,该类就不能被其他类所继承;简称为“断子绝孙类”。当我们需要让一个类永远不被继承,此时就可以用final修饰,但要注意:final类中所有的成员方法都会隐式的定义为final方法。

# 构造方法溢出问题

考虑下面的代码:

public class MyClass { 
  private int num1; 
  private int num2; 
  private static MyClass myClass; 
  
  public MyClass() { 
    num1 = 1; 
    num2 = 2; 
  }
  
  /**
  * 线程A先执行write() 
  */ 
  public static void write() { 
    myClass = new MyClass(); 
  }
  /**
  * 线程B接着执行write() 
  */ 
  public static void read() { 
    if (myClass != null) { 
      int num3 = myClass.num1;
      int num4 = myClass.num2; 
    } 
  } 
}

num3和num4的值是否一定是1和2?

num3、num4不见得一定等于1,2,和DCL的例子类似,也就是构造方法溢出问题。myClass = new MyClass()这行代码,分解成三个操作:

  1. 分配一块内存;
  2. 在内存上初始化i=1,j=2;
  3. 把myClass指向这块内存。

操作2和操作3可能重排序,因此线程B可能看到未正确初始化的值。对于构造方法溢出,就是一个对象的构造并不是“原子的”,当一个线程正在构造对象时,另外一个线程却可以读到未构造好的“一半对象”。

# final的happen-before语义

要解决这个问题,有多种办法:

  • 办法1:给num1,num2加上volatile关键字。
  • 办法2:为read/write方法都加上synchronized关键字。

如果num1,num2只需要初始化一次,还可以使用final关键字。之所以能解决问题,是因为同volatile一样,final关键字也有相应的happen-before语义:

  1. 对final域的写(构造方法内部)happen-before于后续对final域所在对象的读。
  2. 对final域所在对象的读,happen-before于后续对final域的读。

通过这种happen-before语义的限定,保证了final域的赋值,一定在构造方法之前完成,不会出现另外一个线程读取到了对象,但对象里面的变量却还没有初始化的情形,避免出现构造方法溢出的问题。

# happen-before规则总结

  1. 单线程中的每个操作,happen-before于该线程中任意后续操作。
  2. 对volatile变量的写,happen-before于后续对这个变量的读。
  3. 对synchronized的解锁,happen-before于后续对这个锁的加锁。
  4. 对final变量的写,happen-before于final域对象的读,happen-before于后续对final变量的读。

四个基本规则再加上happen-before的传递性,就构成JMM对开发者的整个承诺。在这个承诺以外的部分,程序都可能被重排序,都需要开发者小心地处理内存可见性问题。

myjmm5

上次更新: 5/30/2023, 12:05:21 AM
Lock

Lock→

最近更新
01
2025
01-15
02
Elasticsearch面试题
07-17
03
Elasticsearch进阶
07-16
更多文章>
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式