示例代码如下:
public class Main {
static boolean run = true;
static DateFormat format = new SimpleDateFormat("HH:mm:ss");
public static void main(String[] args) throws Exception {
new Thread(() -> {
long start = System.currentTimeMillis();
while (run) {
if (System.currentTimeMillis() - start >= 2000) {
start = System.currentTimeMillis();
System.out.println("当前时间:" + format.format(start));
}
}
}).start();
Thread.sleep(7000);
run = false;
System.out.println("已经设置run = " + run + ",当前时间:" + format.format(System.currentTimeMillis()));
}
}
运行结果如下:
当前时间:17:26:27
当前时间:17:26:29
当前时间:17:26:31
已经设置run = false,当前时间:17:26:32
当前时间:17:26:33
代码的功能为,在子线程中是一个死循环,每两秒打印一下当前时间,在main线程中睡眠7秒,则理论上子线程可以打印3个时间,因为每两秒打印一个,6秒打印3个,第7秒的时候main线程把run变量设置为false,理论上此时while循环就结束了,但是并没有,while循环一直在转,直到第8秒的时候打印了第4个时间之后while循环才结束,也就是说在子线程中,第7秒到第8秒的时候,while语句拿到的run变量一直是true,所以才没有退出循环。
同样的代码,复制到Android项目中运行又没这个问题。神奇。
问了公司同事,说是每个线程在访问run变量时,都会拷贝一份副本到各自线程的堆栈中,所以我们在主线程中修改run变量只是修改了一个副本,而子线程中的run是另一个副本,没有得到及时的更新,所以才出现了问题,解决方案就是在变量上加入volatile修饰符,如下:
volatile static boolean run = true;
再次运行就没问题了。volatile的功能可以简单的理解为不再使用副本了,所以不会有之前的问题。
如果不加volatile修饰符,代码稍作修改又没问题了,如下:
while (run) {
if (System.currentTimeMillis() - start >= 2000) {
start = System.currentTimeMillis();
System.out.println("当前时间:" + format.format(start));
}
if (run) {
}
}
代码很简单,就是在while中加入了一个if判断,其它代码都不变,但是运行是OK。或者我们把while获取时间的代码提取到一个end变量,然后运行也是没问题的,如下:
while (run) {
long end = System.currentTimeMillis();
if (end - start >= 2000) {
start = System.currentTimeMillis();
System.out.println("当前时间:" + format.format(start));
}
}
至于这些神奇的现象,我们就不管它了,总之就记得,在使用多线程的时候,如果多个线程要访问同一个数据,这个数据就加volatile修饰,这个数据一般指8大基本数据类型,如果是对象类型,可以不用加的,因为一般对象我们创建后就是访问这个对象中的属性,很少会再创建一个新的对象。
在懒汉式单例中,也是需要加入volatile修饰的,示例如下:
public class Singleton {
private volatile static Singleton instance;
private Singleton() { }
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
如果我们没加volatile的话,IntelliJ也会提示我们加的,如下:
对于基本数据类型的属性,如果需要在多线程中又读又写的话,尽量使用对应的同步对象:
这样的话就不需要加volatile修饰了,因为这些对象里面包装的基本数据已经加入了volatile修饰了。如果我们查看它们的源码,会发现对应的get和set方法并没有加入同步代码块,因为它包装的value已经是volatile修饰的了,所以简单的一个set赋值,其实就是一行语句的执行,没必要加入同步代码,影响效率,对于AtomicBoolean,它封装的value是用int类型的,用0表示false,1表示true。在addAndGet函数上,还看到了Unsafe的使用,据说它是sun公司留的后门,用这个类可以申请内存,且不受jvm控制,具体可百度。
使用AtomicBoolean修改前面的Demo,如下:
public class Main {
static AtomicBoolean run = new AtomicBoolean(true);
static DateFormat format = new SimpleDateFormat("HH:mm:ss");
public static void main(String[] args) throws Exception {
new Thread(() -> {
long start = System.currentTimeMillis();
while (run.get()) {
if (System.currentTimeMillis() - start >= 2000) {
start = System.currentTimeMillis();
System.out.println("当前时间:" + format.format(start));
}
}
}).start();
Thread.sleep(7000);
run.set(false);
System.out.println("已经设置run = " + run + ",当前时间:" + format.format(System.currentTimeMillis()));
}
}
在kotlin中,没有volatile关键字,可以在变量上加@Volatile注解实现同样的功能。