Java架构师交流群:793825326
java版本:jdk1.8
IDE:idea 18
详细信息可参考博客:https://www.cnblogs.com/dolphin0520/p/3920373.html
这里面只简单介绍下,看下面的代码(java版本:1.8):
public class Test {
private boolean flag = false;
public void setFlag() {
flag = true;
}
public void start() {
System.out.println("start");
while (!flag) ;
System.out.println("end");
}
}
在main函数里面添加如下代码:
public class Main {
public static void main(String[] args) {
Test t=new Test();
try {
new Thread(() -> t.start()).start();
Thread.sleep(100);
new Thread(() -> t.setFlag()).start();
}
catch (Exception ex)
{
}
}
}
这段代码会进入死循环,但如果用volatile修饰,就不会进入死循环了,由此可以看出volatile的作用,当变量被修改后,通知所有的线程修改变量缓存(java8里面有个主内存,同时每个线程还有个自己的缓存。当线程用到主内存里面的变量时,会主动将变量拷贝到缓存里面。)。
另外这里面还有一点需要注意下,看下面的代码:
public class Test {
private boolean flag = false;
public void setFlag() {
flag = true;
}
public void start() {
System.out.println("start");
while (!flag)
{
System.out.println("-ing");
}
System.out.println("end");
}
}
也就是在while循环里面加上
System.out.println("-ing");
这样线程就不会再进入死循环了。这是为什么呢,看下println的源码:
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
注意到代码里面加了一把锁,也就是说可能是因为这把锁导致了数据的更新,我们不妨尝试一下,修改代码如下:
public class Test {
private boolean flag = false;
public void setFlag() {
flag = true;
}
public void start() {
System.out.println("start");
while (!flag)
{
int i=0;
i++;
}
System.out.println("end");
}
}
这段代码仍然会进入死循环,那么我们在这里加把锁,代码如下:
public class Test {
private boolean flag = false;
public void setFlag() {
flag = true;
}
private Object lock=new Object();
public void start() {
System.out.println("start");
while (!flag)
{
synchronized(lock)
{
int i=0;
i++;
}
}
System.out.println("end");
}
}
这个时候线程再次不会进入死循环了。
那么如果我们把锁的级别再提高一点,放到循环外面,会怎样:
public class Test {
private boolean flag = false;
public void setFlag() {
flag = true;
}
private Object lock = new Object();
public void start() {
System.out.println("start");
synchronized (lock) {
while (!flag) {
int i = 0;
i++;
}
}
System.out.println("end");
}
}
答案是这段代码仍然会进入死循环。
这说明了synchronized的作用,也证实了我们的猜测,每次线程尝试获取锁的时候,会清空工作内存,同步主内存的数据进来,这个时候flag自然就会被同步过来了。
获取锁的过程大概如下:
1.获取锁
2.清空工作内存,copy主内存。
3.执行代码
4.更新主内存
5.释放锁
synchronized的原子性也就因此保证了。同时也说明volatile只保证内存可见性,并不保证原子性,比如它能够保证及时拿到最新的数据,但是不保证该数据被更新成功。而synchronized虽然保证内存可见性,但是这种内存可见性不像volatile那样是实时的,也就是说只有在尝试获取的锁的时候,清空一次缓存,后面如果涉及到的变量变更了,加锁的线程仍然是不知道的,而volatile是会收到通知的,这是它们两个之间的内存可见性的区别,它们两个的另一个区别是锁还保证原子性。
另外还有一种情况,也会导致循环退出,就是在while循环体内调用native修饰的方法,比如下面的三个方法随意调用一个,都会导致循环退出
new Object().hashCode();
Thread.sleep(10);
Thread.yield();
另外,虽然
TimeUnit.MICROSECONDS.sleep(100);
没有被native修饰,但是由于其内部实现最终调用的还是sleep方法,所以不管调用的方法有没有native修饰,只要内部有代码是native的,都会让循环正常退出。
这是什么原因呢。我们知道被native修饰的方法,其实就是jvm帮我们调用的非java实现的操作系统的方法。有就是虚拟机调用操作系统的方法。回到我们的问题中来,首先循环能够正常退出,就说明该线程的缓存更新了,那么为何调用native方法就要更新缓存呢,这个我猜测是为了保证native方法执行结果的准确性,所以在调用native方法的时候,重新更新了线程的缓存,具体是因为什么,我现在还没搞清楚。等我搞清楚了,再补充原因。
另外如果你很细心,你会发现,java的atomic包里面的方法,最终调用的都是native方法,由于native保证了可见性,所以atomic也保证了可见性,然而事实上atomic的方法并单单是因为这个,它还维护一个volatile的value变量。
所以native方法的机制,大概就是在调用前更新下缓存,至于是不是在中途也会随时更新缓存,我暂时还没找到方法验证,后面找到会补充。