Java并发编程(1)-线程安全基础概述

版权声明: https://blog.csdn.net/pbrlovejava/article/details/83216917


Java并发编程第一篇博客,主要讲解线程的安全性,有无状态类是什么,以及原子性,原子操作,竞争操作,复合操作机制,最后讲解锁机制,使用内部锁以及内部锁的解读。
本文内容均总结自《Java并发编程实践》第二章 线程安全 的内容 ,详情可以查阅该书。

一、线程安全性

当多个线程同时访问一个类时,如果无需考虑这些线程在运行环境下的调度和交替执行,并且不需要进行额外的同步处理操作,这个类的执行结果仍然正确,那么可以称这个类的线程安全类。

1.1、无状态类

无状态类可以理解为在多个线程同时访问的情况下,每个线程都能得到正确的相应结果,因为无状态类中的变量和数据都是无状态(stateless)的,下面通过一个Servlet来说明什么是无状态类:

public class StateLessServlet extends HttpServlet{
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        String paramName = servletRequest.getParameter("paramName");
        servletResponse.getWriter().write(paramName);
    }
}

这个自定义的Servlet类做的事情很简单,即接受前台的一个特定的参数并且向前台响应,怎么判断这个类是不是无状态的类呢?我们可以设想有多个线程去访问这个类,无论是线程同步或者不同步访问,都会获得和输出特定的、正确的结果,所以这个类是一个典型的无状态类。其实绝大多数是Servlet都可以实现无状态,只有当该Servlet需要处理一些特定的请求并记录信息时,才会产生线程安全的需求。

所以,无状态类一定是线程安全类,它永远线程安全。

1.2、有状态类

有状态类,即包含了有状态变量或者有状态对象的类,这样的类一般是线程不安全类。以下自定义Servlet类可说明什么是有状态类。

public class StatefulServlet extends HttpServlet{
    //用于记录请求该Servlet的次数
    private long count  = 0;
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        String paramName = servletRequest.getParameter("paramName");
        count++;//请求次数加1
        servletResponse.getWriter().write(paramName+count);
    }
}

这个Servlet中有一个有状态量count,在单一线程下,这个类是安全的,因为在每次请求时,count都唯一且确定,请求后自增即可。但是在多线程情况下,这个类是线程不安全的,因为在线程同时访问时,这个有状态量count会出现错误,比如线程Thread1刚读到count为1时,线程Thread2此时进来也读到count为1,那么这两个线程的响应结果都为2,可是这明显不对,正确的结果是有一个线程应该响应结果为3,即该Servlet被访问了3次。
在这里插入图片描述
现在可以下一个小小的结论:
无状态类一定是线程安全的,而有状态类一般线程不安全

二、原子性

从上面的计数例子可以看出,每个线程对count这个变量的自增并不是一次操作完成的,而分成了三步:读-改-写,首先读取了count的当前值,然后再将其值加一,并且写入原先变量中,它并不是一个原子操作, 这就是导致了线程不安全的原因

2.1、原子操作

所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,类似于MySql数据库中的事务操作:要不就完成该语句后Commit,中间出现错误就回滚(RollBack)。

扫描二维码关注公众号,回复: 3660990 查看本文章

2.2、竞争操作

用以下注册器的例子来讲解什么是竞争操作:

public class LazyInitRace(){
        //注册对象
        private RegisterObject instance = null;
        //获得注册对象
        public static RegisterObject getInstance(){
            if(instance == null){
                //注册对象为null
                return new RegisterObject();
            }
            //注册对象不为null
            return instance;
        }
    }

当有多个线程去请求这个类的getInstance()方法时,每个进程争夺注册对象的条件为instance是否为null,这就是线程的竞争条件,这就是一个竞争操作。竞争操作会引发线程的不安全,比如当进程Thread1和进程Thread2同时执行到getInstance,1看到instance是null,并且实例化一个新的注册对象。同时2也在检查instance是否为null,此时刻instance是否为null,这依赖于时序,是无法预期的。它包括调度的无常性,以及1初始化注册对象并设置instance域的耗时。如果2检查到instance为null,两个getInstance的调用者会得到不同的结果,然而,我们期望getInstance总是返回相同的实例,而不论线程的差异。上面的程序,又称为惰性初始化

2.3、复合操作

在上面的计数器例子中,若count++这个自增是原子操作,那么就不会发生线程的不安全,那么每次自增都会产生预期的结果,即计数器准确地加一。这个读-改-写操作的全部执行过程可以看作是复合操作:为了保证线程的安全,必须让这一系列的复合操作原子地执行。

public class StatefulServlet extends HttpServlet{
    //使用concurent并发包中的atomic工具包下的原子变量类,保证多线程请求下的原子操作
    private AtomicLong count = new AtomicLong(0);
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        String paramName = servletRequest.getParameter("paramName");
        count.incrementAndGet();//自增1
        servletResponse.getWriter().write(paramName+count);
    }
}

java.util.concurrent是多线程开发常用的并发包,其中的atomic是并发包的原子变量工具类,使用它们代替基本变量或者对象,可以保证在多线程请求的情况下,每次都执行的是原子操作。

三、锁

先来看下这段用于缓存前台数字的查询,它一共有两个有状态量,但是这两个量都已经使用了原子变量(Atomic Variable)代替了,并且使用了原子变量的setget方法,那么这段查询是线程安全的吗?

  public class SychronizedFactorizer extends  HttpServlet{
        private AtomicReference<Integer> cacheNumber = new AtomicReference<Integer>();
        private AtomicReference<List<Integer>> cacheNumbers = new AtomicReference<List<Integer>>();
        @Override
        public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
            String paramNumber = servletRequest.getParameter("paramNumber");
            if(Integer.parseInt(paramNumber) == cacheNumber.get()){
                //是最新的缓存
                servletResponse.getWriter().write(paramNumber);
            }else{
                //不是最新的缓存,将其替换成缓存
                cacheNumber.set(Integer.parseInt(paramNumber));
                //计入缓存集合中
                List<Integer> integers = cacheNumbers.get();
                //缓存数组尾部插入新缓存
                integers.add(Integer.parseInt(paramNumber));
            }
        }
    }

答案是这段缓存程序并不是线程安全的,因为线程和线程之间仍存在竞争操作,虽然每个set调用都是原子的,但是程序无法保证会同时更新cacheNumber和cacheNumbers;当某个线程只修改了cacheNumber而另一个变量还没开始修改的时候,其他线程将看到Servlet违反了不变约束,这样会形成一个程序漏洞,所以为了保护状态的一致性,要在单一的原子操作中更新相互关联的状态变量

那么,应该如何让两个set量子操作合并为1个量子操作呢?这就涉及到代码块的量子操作,需要用锁(lock)来实现。

3.1、使用内部锁

Java提供了强制原子性的内部锁机制:synchronized块。一个锁对象有两部分,分别是对锁synchronized的引用,以及锁需要保护的代码块。当synchronized关键字放在方法声明时,那么表明对整个方法的代码进行强制原子性,当synchronized关键字单独使用时,表明是对{}中的代码块进行强制原子性,即上锁。

(1)对整个方法上锁
public synchronized void function(){...}
(2)对特定代码块上锁
synchronized(this){...}

上述的缓存代码,可以改造成这样:

ublic class SychronizedFactorizer extends  HttpServlet{
        private long cacheNumber = 0;
        private List<Integer> cacheNumbers = new ArrayList<Integer>();
        @Override
        public synchronized void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
            String paramNumber = servletRequest.getParameter("paramNumber");
            if(Integer.parseInt(paramNumber) == cacheNumber){
                //是最新的缓存
                servletResponse.getWriter().write(paramNumber);
            }else{
                //不是最新的缓存,将其替换成缓存
                cacheNumber = Integer.parseInt(paramNumber);
                //缓存数组尾部插入新缓存
                cacheNumbers.add(Integer.parseInt(paramNumber));
            }
        }
    }

3.2、内部锁解读

执行线程进入synchronized块之前会自动获得锁(可以理解为获得了该段代码的控制权):而无论通过正常控制路径退出。还是从块中抛出异常,线程都会在放弃对synchronized块的控制时自动释放锁(可以理解为放弃对该段代码的控制权),从而能让其他线程去获得该锁。获得内部锁的唯一途径是:进入这个内部锁保护的同步块或方法。

在这里插入图片描述

内部锁在Java中扮演了互斥锁的角色,意味着至多只有一个线程可以拥有锁,如图所示,当线程Thread2尝试请求一个被线程Thread1占用的锁时,线程Thread2必须等待或者阻塞,直到Thread1释放锁,Thread2将永远等下去。

猜你喜欢

转载自blog.csdn.net/pbrlovejava/article/details/83216917