Java并发(理论知识)—— 线程安全性
2019-01-06 23:06 GarfieldEr007 阅读(280) 评论(0) 编辑 收藏 举报1、什么是线程安全性
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
在线程安全类中封装了必要的同步机制,因此客户端无需进一步采取同步错失。
2、原子性
要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的和可变的状态的访问。当多个线程访问某个状态变量,并且其中有一个线程执行写入操作时,必须采用同步机制来协调这些线程对变量的访问。无状态对象一定是线程安全的。
如果我们在无状态的对象中增加一个状态时,会出现什么情况呢?假设我们按照以下方式在servlet中增加一个"命中计数器"来管理请求数量:在servlet中增加一个long类型的域,每处理一个请求就在这个值上加1。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public class UnsafeCountingFactorizer implements Servlet { private long count = 0 ; public long getCount() { return count ; } @Override public void service(ServletRequest arg0, ServletResponse arg1) throws ServletException, IOException { // do something count++; } } |
不幸的是,以上代码不是线程安全的,因为count++并非是原子操作,实际上,它包含了三个独立的操作:读取count的值,将值加1,然后将计算结果写入count。如果线程A读到count为10,马上线程B读到count也为10,线程A加1写入后为11,线程B由于已经读过count值为10,执行加1写入后依然为11,这样就丢失了一次计数。
在并发编程中,这种由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况,它有一个正式的名字:竞态条件。最常见的竞态条件类型就是“先检查后执行”操作,即通过一个可能失效的观测结果来决定下一步操作,延迟初始化是竞态条件的常见情形:
1
2
3
4
5
6
7
8
|
public class LazyInitRace { private SomeObject instance = null ; public SomeObject getInstance() { if (instance == null ) instance = new SomeObject(); return instance ; } } |
在LazyInitRace中包含竞态条件:首先线程A判断instance为null,然后线程B判断instance也为null,之后线程A和线程B分别创建对象,这样对象就进行了两次初始化,发生错误。
要避免静态条件,就必须在某个线程修改变量时,通过某种方式防止其他线程使用这个变量,从而确保其他线程只能在修改操作完成之前或之后读取和修改状态,而不是在修改状态的过程中。
在UnsafeCountingFactorizer 例子中,线程不安全的原因是count ++并非原子操作,我们可以使用原子类,确保加操作是原子的,这样类就是线程安全的了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public class CountingFactorizer implements Servlet { private final AtomicLong count = new AtomicLong( 0 ); public long getCount() { return count .get() ; } @Override public void service(ServletRequest arg0, ServletResponse arg1) throws ServletException, IOException { // do something count.incrementAndGet(); } } |
AtomicLong是java.util.concurrent.atomic包中的原子变量类,它能够实现原子的自增操作,这样就是线程安全的了。
3、加锁机制
除了使用原子变量的方式外,我们也可以通过加锁的方式实现线程安全性。还是UnsafeCountingFactorizer,我们只要在它的service方法上增加synchronized关键字,那么它就是线程安全的了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public class UnsafeCountingFactorizer implements Servlet { private long count = 0 ; public long getCount() { return count ; } @Override public synchronized void service(ServletRequest arg0, ServletResponse arg1) throws ServletException, IOException { // do something count++; } } |
在方法上增加synchronized关键字后,它能够保证,同一时间只会有一个线程进入方法体,这样每个线程就可以全部执行完方法后再退出,方法体内操作就相当于是原子操作了,避免了竞态条件错误。
以上代码是线程安全的,但是性能很糟糕,因为我们把整个service都给锁起来了,同一时刻只能一个线程执行service,并发任务变成了串行任务。其实我们本意只是想把count++变成原子操作,根本就没必要把整个方法锁住,只需锁住count++操作即可:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public class UnsafeCountingFactorizer implements Servlet { private long count = 0 ; public long getCount() { return count ; }<br> @Override public void service(ServletRequest arg0, ServletResponse arg1) throws ServletException, IOException { // do something synchronized ( this ){ count++; } } } |
我们缩小了锁的范围,这样可以更好的增加并发性。
4、可见性
每个线程内部都保有共享变量的副本,当一个线程更新了这个共享变量,另一个线程可能看的到,可能看不到,这就是可见性问题,以下面的代码为例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public class NoVisibility { private static boolean ready; private static int number; public static class ReadThread extends Thread { public void run() { while (!ready ) Thread. yield(); System. out.println(number); } } public static void main(String [] args) { new ReadThread().start(); number = 42 ; ready = true ; } } |
以上代码可能输出0或者什么也不能输出。为什么会什么也不能输出呢?因为我们在主线程中把ready置为true,但是ReadThread中却不一定能够读到我们设置的ready值,所以在ReadThread中Thread.yield()将一直执行下去。为什么可能为0呢?如果ReadThread能够读到我们的值,可能先读到ready值为true,还未读取更新number值,ReadThread就把保有的number值输出了,也就是0。
注意,上面的所有内容都是假设,在缺乏同步的情况下,ReadThread和主线程会如何交互,我们是无法预期的,以上两种情况只是两种可能性。那么如何避免这种问题呢?很简单,只要有数据在多个线程之间共享,就使用正确的同步。
4.1、加锁与可见性
内置锁可以用于确保某个线程以一种可预测的方式查看另一个线程的执行结果,当线程A进入某同步代码块时,线程B随后进入由同一个锁保护的同步代码块,此时,线程B执行由锁保护的同步代码块时,可以看到线程A之前在同一同步代码块中的所有操作结果,如果没有同步,那么就无法实现上述保证。
加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。
4.2、volatile变量
volatile是一种比synchronized关键字轻量级的同步机制,volatile关键字可以确保变量的更新操作通知到其他线程。
下面是volatile的典型用法:
1
2
3
4
|
volatile boolean asleep; ... while (!asleep) doSomeThing(); |
加锁机制既可以确保可见性,又可以确保原子性,而volatile变量只能确保可见性。
5、总结
编写线程安全的代码,其核心在于要对状态访问操作进行管理。编写线程安全的代码时,有两个关注点,一个是原子性问题,一个是可见性问题,要尽量避免竞态条件错误。
from:https://www.cnblogs.com/timlearn/p/4012501.html