一、并发编程理论
并发编程可谓是Java编程的一把双刃剑,用好了能够提升系统的性能,用不好严重影响系统的使用性,因此掌握并发编程,是每一个Java程序员必要的技能。
1. 共享性
数据共享性是线程安全的主要原因之一,如果所有的数据只是在线程内有效,那就不存在线程安全的问题,当然我们在写代码的时候,也会尽量去考虑数据的共享性以避免线程安全问题,但是在多线程环境中,数据共享是不可避免的。例如数据库中的数据,为了保证数据的一致性,我们通常要共享一个数据库中的数据,即使在主从热备的情况,访问的也是同一份数据,主从只是为了访问的效率和数据安全做的副本而已。示例如下:
public class DataShareTest {
// 共享数据
public static int count = 0;
public static void main(String[] args) {
final DataShareTest data = new DataShareTest();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
//进入的时候暂停1毫秒,增加并发问题出现的几率
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int j = 0; j < 100; j++) {
data.addCount();
}
System.out.print(count + " ");
}
}).start();
}
try {
//主程序暂停3秒,以保证上面的程序执行完成
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count=" + count);
}
public void addCount() {
count++;
}
}
理论上来讲,以上代码的执行结构应该是 count=1000,但实际上得到的结果并非如此:
155 455 155 555 355 655 755 855 255 955 count=955
当然,每次执行的结果可能不一样,这就是对共享变量操作,多线程编程造成的结果。
2. 互斥性
资源互斥是指同时只允许一个访问者对其进行访问,具有唯一性和排它性。我们通常允许多个线程同时对数据进行读操作,但同一时间内只允许一个线程对数据进行写操作。所以我们通常将锁分为共享锁和排它锁,也叫做读锁和写锁。如果资源不具有互斥性,即使是共享资源,我们也不需要担心线程安全。例如,对于不可变的数据共享,所有线程都只能对其进行读操作,所以不用考虑线程安全问题。但是对共享数据的写操作,一般就需要保证互斥性,上述例子中就是因为没有保证互斥性才导致数据的修改产生问题。Java 中提供多种机制来保证互斥性,最简单的方式是使用Synchronized。还是以上面的代码为例,稍加修改:
public class DataShareTest {
// 共享数据
public static int count = 0;
public static void main(String[] args) {
final DataShareTest data = new DataShareTest();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
//进入的时候暂停1毫秒,增加并发问题出现的几率
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int j = 0; j < 100; j++) {
data.addCount();
}
System.out.print(count + " ");
}
}).start();
}
try {
//主程序暂停3秒,以保证上面的程序执行完成
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count=" + count);
}
// 给方法加上synchronized
public synchronized void addCount() {
count++;
}
}
最终得到的结果:
500 854 700 630 500 500 500 500 1000 900 count=1000
3. 原子性
原子性就是对数据的操作是一个独立的、不可分割的整体。换句话说,就是一次操作,是一个连续不可中断的过程,数据不会执行到一半的时候被其他线程锁修改。保证原子性的最简单方式就是操作系统指令,如果一次操作对应一条操作系统指令,这样就可以保证原子性。但是很多操作不能通过一条指令就完成。例如,拿我们最常见的i++操作来看,这个操作分成三个步骤:
- 读取证书i的值;
- 对i进行一次加1操作;
- 将结果写回内存;
如果在多线程操作的情况下,如图所示:
image-20190727105332152.png
执行结果并没有达到我们所期望的结果。对于这种组合操作,要保证原子性,最常见的方式就是加锁,Java中可以通过synchronized或者Lock实现。除了锁意外,还有一种方式就是CAS(关于什么是CAS,在下一章讲解),也就是Java中的Atomic包。
4. 可见性
我们先来看看Java的内存模型图:
image-20190727110357575.png从这个图中我们可以看出,每个线程都有一个自己的工作内存(相当于CPU高级缓冲区,这么做的目的还是在于进一步缩小存储系统与CPU之间速度的差异,提高性能),对于共享变量,线程每次读取的是工作内存中共享变量的副本,写入的时候也直接修改工作内存中副本的值,然后在某个时间点上再将工作内存与主内存中的值进行同步。这样导致的问题是,如果线程1对某个变量进行了修改,线程2却有可能看不到线程1对共享变量所做的修改。
5. 有序性
JDK1.6以后,为了能提高程序性能,编译器和处理器可能对指令做重排序,重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,它不会影响单线程环境的执行结果,但是会破坏多线程的执行语义