Java并发编程(1)-线程安全基础入门知识

释放双眼,带上耳机,听听看~!

文章目录

  • 一、线程安全性

  • 1.1、无状态类
    * 1.2、有状态类

    
    
    1
    2
    1  * 二、原子性
    2
  • 2.1、原子操作
    * 2.2、竞争操作
    * 2.3、复合操作

    
    
    1
    2
    1  * 三、锁
    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将永远等下去。

给TA打赏
共{{data.count}}人
人已打赏
安全技术

详解Node.js API系列 Crypto加密模块(2) Hmac

2021-12-21 16:36:11

安全技术

从零搭建自己的SpringBoot后台框架(二十三)

2022-1-12 12:36:11

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索