深入理解static、volatile关键字

static

意思是静态的,全局的。被修饰的东西在一定范围内是共享的,被类的所有实例共享,这时候需要注意并发读写的问题。

只要这个类被加载,Java虚拟机就能根据类名在运行时数据区的方法区内找到他们。所以,static对象可以在他的任何对象创建之前访问,无需引用任何对象。

static可以修饰变量、方法和代码块。

static修饰类变量的时候,被修饰的变量叫做静态变量或者类变量;如果该变量的访问权限是public的话,表示该变量,可以被任何类调用,无需初始化。直接使用 类名.static变量名这种形式访问即可。

static和final一起修饰的变量可以理解为 “全局常量”,一旦赋值就不可更改,并且可以通过类名访问。对于static final修饰的方法不可以覆盖,并且可以通过类名直接访问。

这时候我们非常需要注意的就是线程安全问题了,当多个线程同时对共享变量进行读写时,很可能会出现并发问题

一般有两种解决方法:

  1. 把线程不安全的变量,例如修饰的是ArrayList替换成线程安全的CopyOnWriteArrayList;
  2. 每次访问这个变量,手动加锁。

静态变量和实例变量的区别
对于静态变量,在内存中只有一个拷贝,JVM只为静态分配一次内存,在加载类的过程中完成静态变量的内存分配,可以用类名直接访问,可以用对象来访问(不推荐)。

对于实例变量,每次创建一个实例,JVM就会为其分配一次内存,它在内存中有多个拷贝,互不影响。

static修饰方法时,该方法内部只能调用static方法,不能调用普通方法,但是普通方法可以调用static方法,也可以在普通方法中访问静态变量。我们常用的util类里面的方法,大多数是被static修饰的方法,在调用的时候很方便。
因为任何实例对象都可以调用静态方法,所以静态方法中不能出现this或者super关键字。

static修饰代码块时,我们叫做静态代码块或者静态方法块,一个类中可以有多个静态代码块,他不在任何方法的内部,JVM加载类的时候会执行这些静态代码块,如果静态代码块有多个,JVM将按照他们在类中的顺序依次执行他们,每个代码块只会执行一次。他常常在类启动之前初始化一些值。

private static Integer age;

static{
    age = 18;
}

初始化时机

对于被static修饰的变量、方法块和方法的时机。


/*子类*/
@Slf4j
public class ChildStaticClass  extends ParentStaticClass{
    public ChildStaticClass() {
      log.info("子类构造方法初始化...");
    }

    public static List<String> list = new ArrayList(){{log.info("子类静态变量初始化...");}};

    static{
        log.info("子类静态代码块初始化。。。");
    }

    public static void testStatic(){
        log.info("子类静态方法执行。。。");
    }

    public static void main(String[] args) {
        log.info("main 方法执行");
        new ChildStaticClass();
    }
}
/*父类代码*/
@Slf4j
public class ParentStaticClass {

    public static List<String> list = new ArrayList() {
        {
            log.info("父类静态变量初始化...");
        }
    };

    static {
        log.info("父类静态代码块初始化...");
    }

    public ParrentStaticClass() {
        log.info("父类构造方法初始化...");
    }

    public static void testStatic(){
        log.info("父类静态方法执行...");
    }

}

执行子类的main方法,打印结果是:

在这里插入图片描述
这里需要注意的是,如果将静态代码块放在静态变量前面,那么先加载静态代码块。这与静态顺序有关。

在这里插入图片描述
从结果上看,父类的静态代码块和静态变量比子类优先初始化;
静态变量和静态代码块比类构造器先初始化。

如果父类和子类都还有非静态代码块的话,执行顺序是:

父类静态代码块->子类静态代码块->父类非静态代码块->父类构造方法->子类非静态代码块->子类构造方法

static 总结

静态代码块内容先执行,接着执行父类非静态代码块和构造方法,然后在执行子类非静态代码块和构造方法。

注意

如果父类没有不带参数的构造方法,那么子类必须用super关键字调用父类带参数的构造方法,否则编译不通过。

final

final一般用于以下三种场景

  1. 被final修饰的类,表名该类是无法继承的。
  2. 被final修饰的方法,表名该方法是无法复写的;
  3. 被final修饰的变量是,说明该变量在声明的时候就必须初始化,而且以后不能修改其内存地址

需要注意的是,无法更改的是内存地址,被final修饰的集合,例如Map,List等,可以修改里面元素的值,但是无法修改初始化时的内存地址。

volatile

Java中的volatile用于将变量标记为“存储在主内存中”,对于volatile变量的每次读操作都会直接从计算机的主内存中读取,同样对于volatile变量的写操作只写入主存,而不是仅仅写入CPU缓存。

可见性的保证

volatile是轻量级的synchronized,他在多处理器中保证共享变量的可见性。可见性的意思是:当一个线程修改一个共享变量时,另一个线程能读到这个修改的值。volatile之所以比synchronized执行成本更低是因为他不需要切换上下文和调度。
当写一个volatile变量的时候,Java内存模型(JMM)会把线程对应的本地内存中的共享变量刷新到主内存中;
当读一个volatile变量时,JMM会把线程对应的本地内存无效化,接下来线程会从主存中读取这个volatile变量。

实现原理

Java代码如下

private volatile Object instance = new Singleton();

通过工具转变成汇编代码(window下下载这个压缩包,解压到你jdk/jre/bin/server下)
https://sourceforge.net/projects/fcml/files/fcml-1.1.1/hsdis-1.1.1-win32-amd64.zip/download
Linux下https://sourceforge.net/projects/fcml/files/fcml-1.1.3/fcml-1.1.3.tar.gz/download
解压,切换目录,

  • ./configure && make && sudo make install
  • cd example/hsdis && make && sudo make install
  • sudo ln -s /usr/local/lib/libhsdis.so /lib/amd64/hsdis-amd64.so
  • sudo ln -s /usr/local/lib/libhsdis.so /jre/lib/amd64/hsdis-amd64.so
    接下来便可以使用

执行main函数之前需要加上虚拟机参数
-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly XX:CompileCommand=compileonly,*类名.方法名
之后有一行指令会有 lock前缀,Lock前缀的指令在多喝处理器下会引发两件事。

  1. 当前处理器缓存行的数据写回到系统内存

  2. 写回内存的操作会使其他CPU里面缓存了该内存地址的变量无效。

  3. Lock前缀指令导致在执行指令期间,声言处理器的Lock#信号。他在声言信号期间会独占共享内存(直接锁总线,导致其他CPU不能访问总线,不能访问总线就意味着不能访问内存)。但是在最新的处理器,一般不锁总线,锁总线开销会很大,直接锁的是缓存。这个操作被称为“缓存锁定”,缓存一致性机制会阻止同时修改由两个以上的处理器缓存的内存区域数据。

  4. 为什么写回内存的数据就会使其他CPU里面的相同内存地址数据的缓存失效呢?
    IA-32和Intel64位的处理器会使用MESI(修改,独占,共享,无效)控制协议去维护内部缓存和其他处理器缓存的一致性。这两种处理器会嗅探其他处理器访问系统内存和他们的内部缓存。处理器使用嗅探技术保证内部缓存,其它处理器缓存,系统缓存在总线上是一致的。如果嗅探到一个处理器在写内存地址,而这个地址当前处于共享状态(也就是被volatile修饰),那么正在嗅探的处理器将使他自身的缓存失效,在下次访问相同的内存地址时,强制执行缓存填充。

什么时候使用volatile

如果两个线程都对共享变量进行读写,那么只是用关键字volatile就不能满足要求了。这种情况你需要使用synchronized保证读写变量的原子性。对volatile变量的读写不会阻塞其他线程,如果需要阻塞可以换成synchronized。

如果只有一个线程读写volatile变量,其他线程只读取,那么只读线程一定能看到最新写入到volatile变量的值。

transient

transient关键字是我们常用来修饰类变量的,意思是当前变量是无需进行序列化的。在序列化时,就忽略该变量,这些序列化工具在底层对transient进行了支持。

default

以前的接口中是不能有方法实现的,但是从java8引入default开始,这件事就改变了。default一般用于接口里的方法上,意思是对于该接口,子类无需强制实现该方法,因为有默认的实现。SpringBoot2.x中的一些接口采用了这种实现方式。子类无需实现也可正常使用。

default

public interface DefaultDemo {

    default void test(){
        //todo something 
        System.out.println("Hello");
    }
}

public class DefautDemoImpl implements DefaultDemo {


}

参考:
Java并发编程的艺术
强烈推荐:https://www.cnblogs.com/xrq730/p/7048693.html

原文地址:https://www.cnblogs.com/itjiangpo/p/14181392.html