java volatile 线程安全

本来这部分内容是放在上一篇文章中的,结果越写越多,觉得应该另起一篇再详细描述一下

不看不看只要答案党看这里

使用volatile关键字

—————————————————-

1. 只能保证读取一致性

2. 不能保证线程安全性(原子性)

—————————————————-

下面看几个例子

第一个例子用来证明读取一致性

package testgc;

public class Counter {
     
    public volatile static int count = 0;
 
    public static void setCount(int i) {
        count = i;
    }
 
    public static void main(String[] args) {
        new Thread(new Runnable() {
            public void run() {
                while(count==0){
                    
                }
            }
        }).start();
        
        new Thread(new Runnable() {
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Counter.setCount(1);
            }
        }).start();
        
    }

}

在这个例子中, 我使用成员变量public volatile static int count = 0; 作为一个标志位

在线程1中, 当count==0时, while会一直循环不会退出

在线程2中, 在程序开始2秒后将count标志位改成1 ( 期待线程1可以退出) , 测试结果为

使用volatile时, 线程2可以及时将值1 写入主内存(这也正是volatile要做的), 线程1也可以及时察觉到标志位变化从而退出循环

当不使用volatile时, 线程1会一直持有一个count标志位的copy, 在线程2修改了值后无法察觉变化, 你会看到while循环一直存在,程序会一直执行,无法退出


第二个例子用来证明volatile无法保证线程安全

这个测试方法是从另外一个哥们的博客中看到的, 但是实际上原程序有一个小bug, 使其并不能证明作者的结论(尽管他的结论是正确的)

因为原程序的输出位置在主线程中, 也就是说即使你在inc() 方法前加入synchronized 关键字使其同步, 你也不能观察到正确结果

因为当主线程启动子线程之后, 不管子线程是否运行结束

主线程都会去尝试读取结果值 , 如果这时子线程没有全部结束你会得到一个中间值结果

不加volatile你会得到错误结果

package testgc;

public class TestVolatitle {
     
    public static int count = 0;
 
    public  static void inc() {
 
        //这里延迟1毫秒,使得结果明显
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
        }
 
        count++;
        
        //我修改后的输出位置
        System.out.println("运行结果:Counter.count=" + TestVolatitle.count);
    }
 
    public static void main(String[] args) {
 
        //同时启动10个线程,去进行i++计算,看看实际结果
 
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                public void run() {
                    TestVolatitle.inc();
                }
            }).start();
        }

        //原程序的输出位置在这里
        //System.out.println("运行结果:Counter.count=" + TestVolatitle.count);
    }
}

结果为

运行结果:Counter.count=7
运行结果:Counter.count=7
运行结果:Counter.count=7
运行结果:Counter.count=7
运行结果:Counter.count=7
运行结果:Counter.count=7
运行结果:Counter.count=7
运行结果:Counter.count=9
运行结果:Counter.count=9
运行结果:Counter.count=9
 

加入volatile关键字之后的代码, 还是会得到错误结果

package testgc;

public class TestVolatitle {
     
    public volatile static int count = 0;
 
    public static void inc() {
 
        //这里延迟1毫秒,使得结果明显
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
        }
 
        count++;
        
        //我修改后的输出位置
        System.out.println("运行结果:Counter.count=" + TestVolatitle.count);
    }
 
    public static void main(String[] args) {
 
        //同时启动10个线程,去进行i++计算,看看实际结果
 
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                public void run() {
                    TestVolatitle.inc();
                }
            }).start();
        }

        //原程序的输出位置在这里
        //System.out.println("运行结果:Counter.count=" + TestVolatitle.count);
    }
}
运行结果:Counter.count=3
运行结果:Counter.count=5
运行结果:Counter.count=5
运行结果:Counter.count=6
运行结果:Counter.count=3
运行结果:Counter.count=3
运行结果:Counter.count=7
运行结果:Counter.count=9
运行结果:Counter.count=10
运行结果:Counter.count=9
 

如何让它正确打印1到10呢? 只能同步或者等待, 使用synchronized可以做到同步,使用join可以让主线程等待子线程结束(或者超时)

synchronized 使所有的子线程互相同步

join 使主线程等待子线程(或者超时)

使用synchronized的例子

package testgc;

public class TestVolatitle {
     
    public volatile static int count = 0;
 
    public synchronized static void inc() {
 
        //这里延迟1毫秒,使得结果明显
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
        }
 
        count++;
        
        //我修改后的输出位置
        System.out.println("运行结果:Counter.count=" + TestVolatitle.count);
    }
 
    public static void main(String[] args) {
 
        //同时启动10个线程,去进行i++计算,看看实际结果
 
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                public void run() {
                    TestVolatitle.inc();
                }
            }).start();
        }

        //原程序的输出位置在这里
        //System.out.println("运行结果:Counter.count=" + TestVolatitle.count);
    }
}
运行结果:Counter.count=1
运行结果:Counter.count=2
运行结果:Counter.count=3
运行结果:Counter.count=4
运行结果:Counter.count=5
运行结果:Counter.count=6
运行结果:Counter.count=7
运行结果:Counter.count=8
运行结果:Counter.count=9
运行结果:Counter.count=10

使用join

package testgc;

public class TestVolatitle {
     
    public volatile static int count = 0;
 
    public static void inc() {
 
        //这里延迟1毫秒,使得结果明显
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
        }
 
        count++;
        
        //我修改后的输出位置
        System.out.println("运行结果:Counter.count=" + TestVolatitle.count);
    }
 
    public static void main(String[] args) throws InterruptedException {
 
        //同时启动10个线程,去进行i++计算,看看实际结果
 
        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(new Runnable() {
                public void run() {
                    TestVolatitle.inc();
                }
            });
            t.start();
            t.join();
        }

        //原程序的输出位置在这里
        //System.out.println("运行结果:Counter.count=" + TestVolatitle.count);
    }
}
运行结果:Counter.count=1
运行结果:Counter.count=2
运行结果:Counter.count=3
运行结果:Counter.count=4
运行结果:Counter.count=5
运行结果:Counter.count=6
运行结果:Counter.count=7
运行结果:Counter.count=8
运行结果:Counter.count=9
运行结果:Counter.count=10

对于他的例子其实还有一个偷懒的办法, 让主线程在输出结果之前sleep两秒, 就可以观察到正确的结果

使用synchronized关键字, 结果正确, 使用volatile, 结果不正确