任务描述
在我们进行应用开发的时候,常常会关注网站的并发,如果网站的用户量很多,当这些用户同时访问一个服务的时候,我们的服务器就会接收到大量的并发请求,处理好这些并发请求是一个合格程序员必须要完成的工作。
理解并发编程的三个概念对于我们更好的开发高并发的Web
应用有很大的帮助。
本关的任务就是理解并发编程的三个重要概念并完成右侧选择题。
相关知识
1.原子性
原子性:即一个操作或者多个操作,要么全部执行并且在执行过程中不会被任何因素打断,要么就不执行。
我们来看看下面这段代码:
x = 10; //语句1
y = x; //语句2
x++; //语句3
x = x + 1; //语句4
现在请你判断,这段代码哪些是原子操作。
可能你会觉得四个语句都是原子操作,可是实际上只有语句1是原子性操作。 语句1
是直接将10
的值赋值给x
,所以也就是说执行这个语句会直接将数值10
写入到内存中,所以这是原子性的,语句2其实是两个操作,先读取x
的值,然后赋值给y
,这两个步骤是原子性的,但是他们合起来就不是原子性操作了,后面两个语句也是同样的道理。
也就是说,只有简单的读取,赋值(必须是将数字赋值给某个变量,变量之间的赋值不是原子性操作),才是原子性操作。
从上面可以看出,Java
内存模型只保证了基本读取和赋值是原子性操作,如果要实现大范围的原子性,可以通过synchronized
和lock
来实现,lock
(锁)和synchronized
(同步)在后面的关卡会介绍。
synchronized
和lock
可以保证在任何时候只有一个线程执行该代码块,所以就保证了原子性。
2.可见性
可见性是当多个线程访问一个变量时,一个线程改变了变量的值,其他线程立马可以知道这个改变。
举个例子:
//线程1执行的代码
int i = 0;
i = 10;
//线程2执行的代码
j = i;
假若执行线程1
的是CPU1
,执行线程2
的是CPU2
。由上面的分析可知,当线程1
执行i =10
这句时,会先把i
的初始值加载到CPU1
的高速缓存中,然后赋值为10
,那么在CPU1
的高速缓存当中i
的值变为10
了,却没有立即写入到主存当中。
此时线程2
执行 j = i
,它会先去主存读取i
的值并加载到CPU2
的缓存当中,注意此时内存当中i
的值还是0
,那么就会使得j
的值为0
,而不是10
。
这就是可见性问题,线程1
对变量i
修改了之后,线程2
没有立即看到线程1修改的值。
对于可见性,Java
提供了Volatile
关键字来保证,当一个变量被Volatile
修饰时,它会保证修改的值会被立即重新写入到主内存,当其他线程要调用该共享变量时,会去主内存中重新读取。
但是普通的共享变量是不能保证可见性的,因为普通变量会被读入到线程自己的内存,当一个线程修改了之后,可能还没来得及刷新到主内存,其他线程就从主存中读取了该变量。所以其他线程读取的时候可能还是原来的值,所以普通共享变量是无法保证可见性的。
关于保证可见性还可以通过Synchronized
和lock
的方式来实现。
3.有序性
有序性:即程序的执行是按照代码编写的先后顺序执行的,例如下面这个例子:
int a;
boolean flag;
i = 10; //语句1
flag = false; //语句2
上述代码定义了一个整形的变量a
,布尔类型变量flag
,使用语句1
和语句2
对变量i
和flag
进行赋值,看起来语句1
是在语句2
之前的,但是在程序运行的时候语句1
一定会在语句2
之前执行吗?是不一定的,因为这里可能会发生指令重排序(Instruction Reorder
)。
什么是指令重排序呢?
一般来说,处理器为了提升执行效率,会对输入代码进行优化,它不保证代码执行的顺序和代码编写的顺序一致,但是它会保证程序的输出结果和代码的顺序执行结果是一致的。
比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。
但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢?再看下面一个例子:
int a = 3; //语句1
int b = 5; //语句2
a = a + 3; //语句3
b = b + a + 4; //语句4
上述程序的执行结果就可能是:语句2 => 语句1 => 语句3 => 语句4
。
那有没有可能是:语句2 => 语句1 => 语句4 => 语句3
呢?
不可能,因为处理器在执行语句的时候会考虑数据之间的依赖性,上述代码语句4
是要依赖语句3
的结果的,所以处理器会保证语句3
在语句4
之前执行。
如果一个指令Instruction 2
必须用到Instruction 1
的结果,那么处理器会保证Instruction 1
会在Instruction 2
之前执行。
虽然指令重排序不会影响单个线程的最终执行结果,但是多线程的情况下会不会影响呢?我们来看一个例子:
//线程1:
context = loadContext(); //语句1
inited = true; //语句2
//线程2:
while(!inited ){
sleep();
}
doSomethingwithconfig(context);
可以发现语句1
和语句2
并没有数据依赖性,所以按照指令重排序的规则,可能语句2
在语句1
之前执行,语句2
执行完之后,语句1
还没开始执行,可能线程2
就开始执行了,这个时候inited
为true
,会跳出while
循环转而执行doSomethingwithconfig(context)
而这个时候语句1
还没执行,context
还没有初始化,就会造成程序报错。
从上述例子可以看出,指令重排序不会影响单个线程的执行,但是会影响多线程的执行。
也就是说,要保证多线程程序执行的正确性,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。
编程要求
略
测试说明
略
题
-
1、在并发编程中,我们需要以下哪几个特性来保持多线程程序执行正确(
A
)A、可见性
B、原子性
C、并发性
D、有序性
-
2、请分析以下语句哪些是原子操作(
AB
)A、int a = 3;
B、boolean flag = false;
C、a–;
D、a =a *a
-
3、以下代码的执行结果是(
E
)public class Test { public int inc = 0; public void increase() { inc++; } public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start(); } while(Thread.activeCount()>1) //保证前面的线程都执行完 Thread.yield(); System.out.println(test.inc); } }
A、10000
B、9870
C、大于10000
D、小于10000
E、不一定,大概率小于一万