文章目录
-
一、线程安全性
-
1.1、无状态类
* 1.2、有状态类1
21 * 二、原子性
2 -
2.1、原子操作
* 2.2、竞争操作
* 2.3、复合操作1
21 * 三、锁
2 -
3.1、使用内部锁
* 3.2、内部锁解读
Java并发编程第一篇博客,主要讲解线程的安全性,有无状态类是什么,以及原子性,原子操作,竞争操作,复合操作机制,最后讲解锁机制,使用内部锁以及内部锁的解读。
本文内容均总结自《Java并发编程实践》第二章 线程安全 的内容 ,详情可以查阅该书。
一、线程安全性
当多个线程同时访问一个类时,如果无需考虑这些线程在运行环境下的调度和交替执行,并且不需要进行额外的同步处理操作,这个类的执行结果仍然正确,那么可以称这个类的线程安全类。
1.1、无状态类
无状态类可以理解为在多个线程同时访问的情况下,每个线程都能得到正确的相应结果,因为无状态类中的变量和数据都是无状态(stateless)的,下面通过一个Servlet来说明什么是无状态类:
1
2
3
4
5
6
7
8
9 1public class StateLessServlet extends HttpServlet{
2 @Override
3 public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
4 String paramName = servletRequest.getParameter("paramName");
5 servletResponse.getWriter().write(paramName);
6 }
7}
8
9
这个自定义的Servlet类做的事情很简单,即接受前台的一个特定的参数并且向前台响应,怎么判断这个类是不是无状态的类呢?我们可以设想有多个线程去访问这个类,无论是线程同步或者不同步访问,都会获得和输出特定的、正确的结果,所以这个类是一个典型的无状态类。其实绝大多数是Servlet都可以实现无状态,只有当该Servlet需要处理一些特定的请求并记录信息时,才会产生线程安全的需求。
所以,无状态类一定是线程安全类,它永远线程安全。
1.2、有状态类
有状态类,即包含了有状态变量或者有状态对象的类,这样的类一般是线程不安全类。以下自定义Servlet类可说明什么是有状态类。
1
2
3
4
5
6
7
8
9
10
11
12 1public class StatefulServlet extends HttpServlet{
2 //用于记录请求该Servlet的次数
3 private long count = 0;
4 @Override
5 public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
6 String paramName = servletRequest.getParameter("paramName");
7 count++;//请求次数加1
8 servletResponse.getWriter().write(paramName+count);
9 }
10}
11
12
这个Servlet中有一个有状态量count,在单一线程下,这个类是安全的,因为在每次请求时,count都唯一且确定,请求后自增即可。但是在多线程情况下,这个类是线程不安全的,因为在线程同时访问时,这个有状态量count会出现错误,比如线程Thread1刚读到count为1时,线程Thread2此时进来也读到count为1,那么这两个线程的响应结果都为2,可是这明显不对,正确的结果是有一个线程应该响应结果为3,即该Servlet被访问了3次。
现在可以下一个小小的结论:
无状态类一定是线程安全的,而有状态类一般线程不安全
二、原子性
从上面的计数例子可以看出,每个线程对count这个变量的自增并不是一次操作完成的,而分成了三步:读-改-写,首先读取了count的当前值,然后再将其值加一,并且写入原先变量中,它并不是一个原子操作, 这就是导致了线程不安全的原因。
2.1、原子操作
所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,类似于MySql数据库中的事务操作:要不就完成该语句后Commit,中间出现错误就回滚(RollBack)。
2.2、竞争操作
用以下注册器的例子来讲解什么是竞争操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 1public class LazyInitRace(){
2 //注册对象
3 private RegisterObject instance = null;
4 //获得注册对象
5 public static RegisterObject getInstance(){
6 if(instance == null){
7 //注册对象为null
8 return new RegisterObject();
9 }
10 //注册对象不为null
11 return instance;
12 }
13 }
14
15
当有多个线程去请求这个类的getInstance()方法时,每个进程争夺注册对象的条件为instance是否为null,这就是线程的竞争条件,这就是一个竞争操作。竞争操作会引发线程的不安全,比如当进程Thread1和进程Thread2同时执行到getInstance,1看到instance是null,并且实例化一个新的注册对象。同时2也在检查instance是否为null,此时刻instance是否为null,这依赖于时序,是无法预期的。它包括调度的无常性,以及1初始化注册对象并设置instance域的耗时。如果2检查到instance为null,两个getInstance的调用者会得到不同的结果,然而,我们期望getInstance总是返回相同的实例,而不论线程的差异。上面的程序,又称为惰性初始化。
2.3、复合操作
在上面的计数器例子中,若count++这个自增是原子操作,那么就不会发生线程的不安全,那么每次自增都会产生预期的结果,即计数器准确地加一。这个读-改-写操作的全部执行过程可以看作是复合操作:为了保证线程的安全,必须让这一系列的复合操作原子地执行。
1
2
3
4
5
6
7
8
9
10
11
12 1public class StatefulServlet extends HttpServlet{
2 //使用concurent并发包中的atomic工具包下的原子变量类,保证多线程请求下的原子操作
3 private AtomicLong count = new AtomicLong(0);
4 @Override
5 public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
6 String paramName = servletRequest.getParameter("paramName");
7 count.incrementAndGet();//自增1
8 servletResponse.getWriter().write(paramName+count);
9 }
10}
11
12
java.util.concurrent是多线程开发常用的并发包,其中的atomic是并发包的原子变量工具类,使用它们代替基本变量或者对象,可以保证在多线程请求的情况下,每次都执行的是原子操作。
三、锁
先来看下这段用于缓存前台数字的查询,它一共有两个有状态量,但是这两个量都已经使用了原子变量(Atomic Variable)代替了,并且使用了原子变量的set、get方法,那么这段查询是线程安全的吗?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 1 public class SychronizedFactorizer extends HttpServlet{
2 private AtomicReference<Integer> cacheNumber = new AtomicReference<Integer>();
3 private AtomicReference<List<Integer>> cacheNumbers = new AtomicReference<List<Integer>>();
4 @Override
5 public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
6 String paramNumber = servletRequest.getParameter("paramNumber");
7 if(Integer.parseInt(paramNumber) == cacheNumber.get()){
8 //是最新的缓存
9 servletResponse.getWriter().write(paramNumber);
10 }else{
11 //不是最新的缓存,将其替换成缓存
12 cacheNumber.set(Integer.parseInt(paramNumber));
13 //计入缓存集合中
14 List<Integer> integers = cacheNumbers.get();
15 //缓存数组尾部插入新缓存
16 integers.add(Integer.parseInt(paramNumber));
17 }
18 }
19 }
20
21
答案是这段缓存程序并不是线程安全的,因为线程和线程之间仍存在竞争操作,虽然每个set调用都是原子的,但是程序无法保证会同时更新cacheNumber和cacheNumbers;当某个线程只修改了cacheNumber而另一个变量还没开始修改的时候,其他线程将看到Servlet违反了不变约束,这样会形成一个程序漏洞,所以为了保护状态的一致性,要在单一的原子操作中更新相互关联的状态变量。
那么,应该如何让两个set量子操作合并为1个量子操作呢?这就涉及到代码块的量子操作,需要用锁(lock)来实现。
3.1、使用内部锁
Java提供了强制原子性的内部锁机制:synchronized块。一个锁对象有两部分,分别是对锁synchronized的引用,以及锁需要保护的代码块。当synchronized关键字放在方法声明时,那么表明对整个方法的代码进行强制原子性,当synchronized关键字单独使用时,表明是对{}中的代码块进行强制原子性,即上锁。
(1)对整个方法上锁
public synchronized void function(){…}
(2)对特定代码块上锁
synchronized(this){…}
上述的缓存代码,可以改造成这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 1ublic class SychronizedFactorizer extends HttpServlet{
2 private long cacheNumber = 0;
3 private List<Integer> cacheNumbers = new ArrayList<Integer>();
4 @Override
5 public synchronized void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
6 String paramNumber = servletRequest.getParameter("paramNumber");
7 if(Integer.parseInt(paramNumber) == cacheNumber){
8 //是最新的缓存
9 servletResponse.getWriter().write(paramNumber);
10 }else{
11 //不是最新的缓存,将其替换成缓存
12 cacheNumber = Integer.parseInt(paramNumber);
13 //缓存数组尾部插入新缓存
14 cacheNumbers.add(Integer.parseInt(paramNumber));
15 }
16 }
17 }
18
19
3.2、内部锁解读
执行线程进入synchronized块之前会自动获得锁(可以理解为获得了该段代码的控制权):而无论通过正常控制路径退出。还是从块中抛出异常,线程都会在放弃对synchronized块的控制时自动释放锁(可以理解为放弃对该段代码的控制权),从而能让其他线程去获得该锁。获得内部锁的唯一途径是:进入这个内部锁保护的同步块或方法。
内部锁在Java中扮演了互斥锁的角色,意味着至多只有一个线程可以拥有锁,如图所示,当线程Thread2尝试请求一个被线程Thread1占用的锁时,线程Thread2必须等待或者阻塞,直到Thread1释放锁,Thread2将永远等下去。