一、什么是并发
做饭的时候,你需要洗菜,切菜,烧水,炒菜,作为单核CPU的你,可以选择按照顺序完成这些任务,也可以在烧水的同时洗菜,切菜。对应操作系统,就是CPU在烧水进行的时候不等待(阻塞)水烧好了再做下一步,而是直接去(线程切换)洗菜切菜。好了,并发就是同时进行多件事情的操作(多线程),并发完成任务会更快,CPU利用率更高。
二、并发一定更快吗
还是烧水这个例子,从烧水到去洗菜的地方,也需要你走过去,这也是需要时间成本的,换言之,CPU在响应中断,切换线程的过程中也是需要消耗资源的,当消耗的资源大于并发带来的收益的时候,并发也就不如串行执行快了。
三、并发编程的陷阱
1、可见性问题
对于两个线程操作同一个变量,线程一已经把菜切过了,但是线程二不知道,还去切一次,这就蠢了,保证线程之间变量的可见性,就是可见性问题。
那么可见性问题如何引起的呢?我们知道CPU是有缓存的,单核时代所有线程都操作的一个CPU,都是操作的一个缓存,但是现在是多核的CPU,每个CPU都有自己的缓存。SO,CPU1上缓存的变量A变了,但是CPU2上的变量A还是原来的值,这就引起了可见性问题。 总结,一个线程修改了共享变量的值,其他线程要能读到修改后的值,这就是可见性。
测试代码:
public class test1 {
private static boolean stop = false;
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("A run..");
while (!stop);
System.out.println("A stop..");
}
}).start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("B run..");
stop = true;
System.out.println("B stop..");
}
}).start();
}
}
输出:
A run..
B run..
B stop..
A 线程并没有读到stop变量已经发生变化。
private volatile static boolean stop = false;
如果stop变量加上volatile(保证变量可见的关键字!!)来修饰stop,输出就变成
A run..
B run..
A stop..
B stop..
2、原子性问题
以count自加举例来说明,在java层面count++是一句话,但是由JVM翻译到机器语言上后,就不是一句话了,变量count从内存加载到cpu寄存器,然后在寄存器执行+1动作,在把结果写入内存。线程一刚把count=0加载到cpu寄存器上,线程二也来把count=0加载到cpu寄存器上,各自加了一个1,再先后把count写到内存。此时count就=1,而不是2。这就是原子性问题,在cpu执行过程中,不被中断,能够保证连续性,叫做原子性。
测试代码:10个线程并发对count自加,预期结果应该是100000
public class yuanziwenti {
private long count = 0;
private void adds(){
for (int i = 0; i < 10000; i++) {
count++;
// count.incrementAndGet();
}
}
public static void main(String[] args) {
final yuanziwenti yuanziwenti = new yuanziwenti();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
yuanziwenti.adds();
}
}).start();
}
try {
Thread.sleep(7000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(yuanziwenti.count);
}
}
输出结果:
98976 //而不是100000
如果把long改成AtomicLong,输出结果就是100000。(如果不是10000,可能是可见性问题导致的)
因为AtomicLong是专门保证long型变量原子性的!
import java.util.concurrent.atomic.AtomicLong;
public class yuanziwenti {
private AtomicLong count = new AtomicLong(0);
private void adds(){
for (int i = 0; i < 10000; i++) {
count.incrementAndGet();
}
}
public static void main(String[] args) {
final yuanziwenti yuanziwenti = new yuanziwenti();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
yuanziwenti.adds();
}
}).start();
}
try {
Thread.sleep(7000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(yuanziwenti.count);
}
}
3、有序性问题
有序性的问题是因为编译器为了优化性能,有可能改变程序中的执行顺序。JAVA实现单例模式的时候会通过双重检查创建单例对象