提出问题
说到并发,我们首先应该给自己提出下面这三个问题:
- 产生并发的根本原因是什么?
- 会造成什么后果?
- 怎么去控制,处理并发达到我们预期的结果。
在线程的角度来说,内存分为共享内存和私有内存两个部分。线程访问一个共享区的资源时,会copy一份到私有内存操作栈中,进行计算处理,处理完成之后会在线程消亡前的某一个时机,将最终的结果刷新到共享内存中。在多线程的情况下,计算机允许多个线程同时运行。这样就会出现一个问题,缓存不一致性问题。
举例说明:A,B两个线程都需要访问数据data = 0,但是访问的时机是不可控的。现在A去共享内存区域读取了data,copy一份到私有内存,开始处理比如说+1操作,处理到一半的时候,B也去共享内存区域读取了data,也copy一份到了私有内存区域开始处理,因为A还没有做完+1操作,还没有将最新结果刷新到共享内存,所以B读取到的不是data=1而是0,这时候B也开始做+1操作,得到的结果也是data=1。现在A处理完了,将数据刷新到共享内存,data = 1,B也做完了,将数据刷新到共享内存data=1。最终结果data=1,如果说A,在B读取之前就已经处理完成,并刷新了共享内存中的数据,那B读取到的是data=1,那么结果就是2。最后得出结论:多线程访问共享内存中数据时,得到的结果是不可控制的。
通过上面的案例,我们解答了上面的1,2两个问题。1。产生并发的原因是,线程访问共享内存中的数据,需要copy一份处理完成后在不确定时机刷新共享内存,并且A,B之间更改数据的时候,彼此不知道,还有A,B谁先执行,什么时候执行都不可控。2。造成的结果就是最终结果不可控,不一定得到我们预期的结果。
下面给出一个具体案例,可以自己运行试试看结果:
线程:
/** * Created by PICO-USER on 2017/11/9. */ public class AdditionRun implements Runnable { public int count = 0; @Override public void run() { try { //休眠10毫秒,模拟耗时操作,以便等待其他线程也启动起来了 Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } count += 1; } }
程序入口:
public class MyClass { public static void main(String[] args0) throws InterruptedException { AdditionRun additionRun = new AdditionRun(); Thread thread = null; for (int i = 0; i < 1000; i++) { thread = new Thread(additionRun); thread.start(); } //休眠2秒,以便1000个线程已经全部执行完成。 Thread.sleep(2000); System.out.print("Count :" + additionRun.count); } }
很简单的案例,启动了1000个线程都对count进行+1操作,最后1000个线程运行完之后,打印出结果。我运行了10次,每一次的结果都不一样。
并发三个重要概念
要解决并发的问题,需要先了解一下三个概念
- 原子性 一个或多个操作要么全部执行完并且执行过程中不会被打断,要么都不执行。
举例说明:
1.int i = 2; int a = i; int b = i+1;
上面三个例子中是否都保证原子性呢?第一个保证原子性,直接将2赋值给常量i,整个过程不能再分,直接一步完成整个操作。第二个不保证原子性,因为它其实是分为了几个步骤,首先给a开辟内存,然后取出i的值,然后将i的值赋给a,这个过程是可以被中断的,不能保证整个过程能全部执行完毕,所以不能保证原子性。第三个不保证原子性,首先给b开辟内存,然后取出i的值,然后做+1操作得到返回值赋给b,这个过程同样可能被中断,不能保证整个过程能全部执行完。 - 可见性 可见性是说线程之间都需要访问同一个共享数据,当这个共享数据发生改变的时候,所有的线程都能自动知道,这儿不在给予举例说明了,上面的案例中两个线程处理count的时候,就是不可见的。
- 有序性 有序性是指代码的执行能根据我们书写代码的顺序进行执行。在编译器编译代码的时候,不一定会会有一个代码优化过程,我们称为“指令重排序”,编译器不保证代码的执行顺序是按照书写代码的顺序执行,但是会保证执行结果跟书写代码顺序结果一致。于多线程中来说,我们不能保证线程的执行顺序。
举例:int a =1;a = 2; a=3;这三句代码,编译器在编译的时候,并不会每一条语句都进行编译,只会编译a = 3,因为前两条之间并没有跟a的值产生依赖关系,所以是无效代码,所以前两句代码编译器不会进行编译。