native、volatile关键字作用 java中举例
(一)native
1.查看java object类的源码,发现hashCode方法是这样实现的
public native int hashCode();
使用native关键字修饰了hashCode方法,这有什么用呢?
回答:使用native关键字的方法说明这个方法是一个调用java本地接口(Java Native Interface,即JNI)实现的方法。
什么是JNI呢?简单来说就是使用其他汇编语言,如C或C++实现的接口。可以理解为native修饰的方法就是去调用了java底层实现的代码。
一个简单的JNI接口示例:
① 创建一个包含native接口的java类
public class HelloJNI {
static {
System.loadLibrary("hello"); // Load native library at runtime
// hello.dll (Windows) or libhello.so (Unixes)
}
// Declare a native method sayHello() that receives nothing and returns void
private native void sayHello();
// Test Driver
public static void main(String[] args) {
new HelloJNI().sayHello(); // invoke the native method
}
}
② 编译Java类并生成.h 文件
JDK 8 的编译命令
javac -h . HelloJNI.java
JDK 8之前的编译命令,分为两步
javac HelloJNI.java
javah HelloJNI
生成的 HelloJNI.h 如下所示
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloJNI */
#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: HelloJNI
* Method: sayHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
③ 创建C文件 HelloJNI.c
#include <jni.h>
#include <stdio.h>
#include "HelloJNI.h"
// Implementation of native method sayHello() of HelloJNI class
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject thisObj) {
printf("Hello World!\n");
return;
}
④ 生成本地类库文件,以Ubuntu 64位的操作系统为例
先导入JAVA_HOME的环境变量
export JAVA_HOME=/your/java/installed/dir
echo $JAVA_HOME
编译生成类库文件
gcc -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -o libhello.so HelloJNI.c
⑤ 运行java程序
java -Djava.library.path=. HelloJNI
注意:编写第1步的java程序时,不要加上包名,如果加上包名还按后面的步骤执行的话,最后会报一个Error: Could not find or load main class HelloJNI 的错误。不加包名是可以的。
(二)volatile
这个关键字涉及的内容很多,包括java内存模型、单线程、单例模式等等
1. volitale是Java虚拟机提供的一种轻量级的同步机制
三大特性(作用):保证可见性;不保证原子性;禁止指令重排
2.volatile工作原理
为什么 volatile 只能保证可见性,不能保证原子性呢?这跟它的工作原理有关。
-
线程写 volaitle 变量的步骤为:
-
改变线程工作内存中 volatile 变量副本的值
-
将改变后的副本的值从工作内存刷新到主内存
-
-
线程读 volatile 变量的步骤为:
-
从主内存读取 volatile 变量的最新值到线程的工作内存中
-
从工作内存中读取 volatile 变量的副本
-
3.1 可见性
1.概念:内存可见性是指当一个线程修改了某个变量的值,其它线程总是能知道这个变量变化。也就是说,如果线程 A 修改了共享变量 V 的值,那么线程 B 在使用 V 的值时,能立即读到 V 的最新值
2.下面我们来看一个多线程访问共享变量的例子
/**
* 变量的内存可见性例子
*/
public class VolatileExample {
/**
* main 方法作为一个主线程
*/
public static void main(String[] args) {
MyThread myThread = new MyThread();
// 开启线程
myThread.start();
// 主线程执行
for (; ; ) {
if (myThread.isFlag()) {
System.out.println("主线程访问到 flag 变量");
}
}
}
}
/**
* 子线程类
*/
class MyThread extends Thread {
private boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 修改变量值
flag = true;
System.out.println("flag = " + flag);
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
执行上面的程序,你会发现,控制台永远都不会输出 “主线程访问到 flag 变量” 这句话。我们可以看到,子线程执行时已经将 flag 设置成 true,但主线程执行时没有读到 flag 的最新值,导致控制台没有输出上面的句子
3. 出现上述情况的原因——Java内存模型(JMM)
Java 内存模型
JMM(Java Memory Model):Java 内存模型,是 Java 虚拟机规范中所定义的一种内存模型,Java 内存模型是标准化的,屏蔽掉了底层不同计算机的区别。也就是说,JMM 是 JVM 中定义的一种并发编程的底层模型机制。
JMM 定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。
JMM 的规定:
-
所有的共享变量都存储于主内存。这里所说的变量指的是实例变量和类变量,不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。
-
每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。
-
线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量。
-
不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存中转来完成。
JMM 的抽象示意图:
然而,JMM 这样的规定可能会导致线程对共享变量的修改没有即时更新到主内存,或者线程没能够即时将共享变量的最新值同步到工作内存中,从而使得线程在使用共享变量的值时,该值并不是最新的。
正因为 JMM 这样的机制,就出现了可见性问题。也就是我们上面那个例子出现的问题
怎么解决可见性问题?
两种方案:加锁 或者 使用volatile关键字
加锁
使用 synchronizer 进行加锁。
/**
* main 方法作为一个主线程
*/
public static void main(String[] args) {
MyThread myThread = new MyThread();
// 开启线程
myThread.start();
// 主线程执行
for (; ; ) {
synchronized (myThread) {
if (myThread.isFlag()) {
System.out.println("主线程访问到 flag 变量");
}
}
}
}
为什么加锁后就保证了变量的内存可见性了? 因为当一个线程进入 synchronizer 代码块后,线程获取到锁,会清空本地内存,然后从主内存中拷贝共享变量的最新值到本地内存作为副本,执行代码,又将修改后的副本值刷新到主内存中,最后线程释放锁。
这里除了 synchronizer 外,其它锁也能保证变量的内存可见性。
使用 volatile 关键字
使用 volatile 关键字修饰共享变量。
/**
* 子线程类
*/
class MyThread extends Thread {
private volatile boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 修改变量值
flag = true;
System.out.println("flag = " + flag);
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
使用 volatile 修饰共享变量后,每个线程要操作变量时会从主内存中将变量拷贝到本地内存作为副本,当线程操作变量副本并写回主内存后,会通过 CPU 总线嗅探机制告知其他线程该变量副本已经失效,需要重新从主内存中读取。
volatile 保证了不同线程对共享变量操作的可见性,也就是说一个线程修改了 volatile 修饰的变量,当修改后的变量写回主内存时,其他线程能立即看到最新值
至于CPU总线嗅探机制,太底层了,有兴趣自己百度
3.2 原子性
所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。简单来说就是:要么都成功,要么都失败
在多线程环境下,volatile 关键字可以保证共享数据的可见性,但是并不能保证对数据操作的原子性。也就是说,多线程环境下,使用 volatile 修饰的变量是线程不安全的。
要解决这个问题,我们可以使用锁机制,或者使用原子类(如 AtomicInteger)。
这里特别说一下,对任意单个使用 volatile 修饰的变量的读 / 写是具有原子性,但类似于 flag = !flag 这种复合操作不具有原子性。简单地说就是,单纯的赋值操作是原子性的
3.3 禁止指令重排
有序性的概念:在计算机执行程序时,为了提高性能,编译器和处理器常常会对指令做重排
一般分以下三种:
-
单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。
-
处理器在进行重排顺序是必须要考虑指令之间的数据依赖性
-
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性时无法确定的,结果无法预测 重排代码实例:声明变量:
int a,b,x,y=0
-
线程A 线程B x=a; y=b; b=1; a=2; 执行结果 x=0,y=0 如果编译器对这段程序代码执行重排优化后,可能出现如下情况:
线程A 线程B b=1; a=2; x=a; y=b; 执行结果 x=2,y=1 这个结果说明在多线程环境下,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的
volatile实现禁止指令重排,从而避免了多线程环境下程序出现乱序执行的现象
4.volatile使用场景
要在多线程中安全的使用 volatile 变量,必须同时满足:
-
对变量的设置操作不依赖其当前值
-
不满足举例:number++、count = count + 5
-
满足举例:boolean 变量等
-
-
该变量没有包含在具有其他变量的不等式中
在实际项目中,由于很多情况下都不满意 volatile 的使用条件,所以 volatile 使用的场景并没有 synchronized 广。
-
不满足举例:不变时 low < up
-
5. 总结
-
volatile 修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值;或者作为状态变量,如 flag = ture,实现轻量级同步。
-
volatile 属性的读写操作都是无锁的,它不能替代 synchronized,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以说它是低成本的。
-
volatile 只能作用于属性,我们用 volatile 修饰属性,这样编译器就不会对这个属性做指令重排序。
-
volatile 提供了可见性,任何一个线程对其的修改将立马对其他线程可见。volatile 属性不会被线程缓存,始终从主存中读取。
-
volatile 可以使纯赋值操作是原子的,如 boolean flag = true; falg = false。
-
volatile 可以在单例双重检查中实现可见性和禁止指令重排序,从而保证安全性。(举例略)