当查看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一份到告诉缓存-》写操作在高速缓存中进行,利用空余时间,然后刷新到内存中去。
多线程并发访问时,如未加锁,可能导致数据还未刷新到内存,原数据就从内存中读出,导致读取到原数据,而不是新计算后的数据。