Java线程:线程安全与不安全

当查看JDK API的时候,总会发现一些类说明写着,线程安全或者线程不安全,比如说到StringBuilder中,有这么一句,“将StringBuilder 的实例用于多个线程是不安全的。如果需要这样的同步,则建议使用StringBuffer。”,提到StringBuffer时,说到“StringBuffer是线程安全的可变字符序列,一个类似于String的字符串缓冲区,虽然在任意时间点上它都包含某种特定的字符序列,但通过某些方法调用可以改变该序列的长度和内容。可将字符串缓冲区安全地用于多个线程。可以在必要时对这些方法进行同步,因此任意特定实例上的所有操作就好像是以串行顺序发生的,该顺序与所涉及的每个线程进行的方法调用顺序一致”。StringBuilder是一个可变的字符序列,此类提供一个与StringBuffe兼容的API,但不保证同步。该类被设计用作StringBuffer的一个简易替换,用在字符串缓冲区被单个线程使用的时候(这种情况很普遍)。如果可能,建议优先采用该类,因为在大多数实现中,它比StringBuffer要快。将StringBuilder的实例用于多个线程是不安全的,如果需要这样的同步,则建议使用StringBuffer。

   根据以上JDK文档中对StringBuffer和StringBuilder的描述,得到对String、StringBuilder与StringBuffer三者使用情况的总结:
   1、如果要操作少量的数据用String
   2、单线程操作字符串缓冲区下操作大量数据StringBuilder
   3、多线程操作字符串缓冲区下操作大量数据StringBuffer

   那么下面手动创建一个线程不安全的类,然后在多线程中使用这个类,看看有什么效果。

Count.java:

public class Count {
    private int num;
    public void increment() {
        num++;
    }
    public int get() {
        return num;
    }
}


        在这个类中的increment方法是累加num值,步长为1。
        ThreadTest.java:

public class ThreadTest {
    public static void main(String[] args) {
        Count count = new Count();
        Runnable runnable = new Runnable() {
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    count.increment();
                }
            }
        };
        List<Thread> threads = new ArrayList<>(10);
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(runnable);
            threads.add(thread);
            thread.start();
        }
        while (true) {
            if (allThreadTerminated(threads)) {// 所有线程运行结束
                System.out.println(count.get());
                break;
            }
        }
    }
    private static boolean allThreadTerminated(List<Thread> threads) {
        for (Thread thread : threads) {
            if (thread.isAlive()) {
                return false;
            }
        }
        return true;
    }
}


        这里启动了10个线程,每个线程累加1万次,我们期望的最终结果是10万,看一下输出结果:

95388


 

这就涉及到内存模型的知识了,我们都知道Cpu运算速度极快,但是每次读取数据都要直接访问内存,会严重拖慢Cpu的速率,所以内存就有了一层高速缓存,在JAVA中,每次线程读取到一个数据,会将这份数据copy到高速缓存中,然后写操作在高速缓存中进行,然后在"空余"时间,将缓存结果刷新到内存中。 回到问题,博主得到的结果大概率少于10000,就可以这样解释,比如A线程读取到Num的值为100 然后进行+1操作,写入缓存中,但是还没来得及同步到主存中,而B线程同理读取到100,进行+1操作,更新缓存,刷新到内存中。这样内存中的num为101,但是实际上做了2次 +1操作,所以结果会少于10000。 而你执行了打印操作,那么cpu获得一定的空余时间,线程每次都有空余时间,将缓存的值刷新到内存,所以你可以每次得到10000. JAVA的 volatile 关键字就是用于线程可见性的,强制线程读取内存中的值。这样就可以避免线程不安全了。

从内存读取数据=同时copy一份到告诉缓存-》写操作在高速缓存中进行,利用空余时间,然后刷新到内存中去。

多线程并发访问时,如未加锁,可能导致数据还未刷新到内存,原数据就从内存中读出,导致读取到原数据,而不是新计算后的数据。

猜你喜欢

转载自blog.csdn.net/qq_39507276/article/details/87902727