zoukankan      html  css  js  c++  java
  • 线程安全

     线程安全

    通过这篇博客你能学到什么:


    编写线程安全的代码,本质上就管理状态的访问,而且通常是共享的、可变的状态.

    状态:可以理解为对象的成员变量.

    共享: 是指一个变量可以被多个线程访问

    可变: 是指变量的值在生命周期内可以改变.

    保证线程安全就是要在不可控制的并发访问中保护数据.

    如果对象在多线程环境下无法保证线程安全,就会导致脏数据和其它不可预期的后果


    在多线程编程中有一个原则:
    无论何时,只要有对于一个的线程访问给定的状态变量,而且其中某个线程会修改该变量,此时必须使用同步来协调线程对该变量的访问

    Java中使用synchronized(同步)来确保线程安全.在synchronized(同步)块中的代码,可以保证在多线程环境下的原子性(操作原子的执行)可见性(不会有过期数据).

    不要忽略同步的重要性,如果程序中忽略了必要的同步,可能看上去是可以运行,但是它仍然存在隐患,随时都可能崩溃.

    在没有正确同步的情况下,如果多线程访问了同一变量(并且有线程会修改变量,如果是只读,它还是线程安全的),你的程序就存在隐患,有三种方法修复它:
    1. 不要跨线程共享变量
    2. 使状态变为不可变的
    3. 在任何访问状态变量的时候使用同步

    虽然可以用上述三类方法进行修改,但是会很麻烦、困难,所以一开始就将一个类设计成是线程安全的,比在后期重新修复它更容易

    封装可以帮助你构建线程安全你的类,访问特定变量(状态)的代码越少,越容易确保使用恰当的同步,也越容易推断出访问一个变量所需的条件.总之,对程序的状态封装得越好,你的程序就越容易实现线程安全,同时有助于维护者保持这种线程安全性.

    设计线程安全的类时,优秀的面向技术--封装、不可变性(final修饰的)以及明确的不变约束(可以理解为if-else)会给你提供诸多的帮助

    虽然程序的响应速度很重要,但是正确性才是摆在首位的,你的程序跑的再快,结果是错的也没有任何意义,所以要先保证正确性然后再尝试去优化,这是一个很好的开发原则.

    1.线程安全性

    一个类是线程安全的,是指在被多个线程访问时,类可以持续进行正确的行为.

     

     2 一个无状态的(stateless)的servlet

    public class StatelessServlet implements Servlet {
    
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
    BigInteger i = extractFromRequest(servletRequest);
    BigInteger[] factors = factor(i);
    encodeIntoResponse(servletResponse,factors);
    }
    
    }

     

    我们自定义的StatelessServlet是无状态对象(没有成员变量保存数据),在方法体内声明的变量i和factors是本地变量,只有进入到这个方法的执行线程才能访问,变量在其他线程中不是共享的,线程访问无状态对象的方法,不会影响到其他线程访问该对象时的正确性,所以无状态对象是线程安全的.

    这里有重要的概念要记好:无状态(成员变量)对象永远是线程安全的

     

    3 原子性


    在无状态对象中,加入一个状态元素,用来计数,在每次访问对象的方法时执行行自增操作.

    public class StatelessServlet implements Servlet {
    private long count = 0;
    
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
    BigInteger i = extractFromRequest(servletRequest);
    BigInteger[] factors = factor(i);
    count++;
    encodeIntoResponse(servletResponse,factors);
    }

    在单线程的环境下运行很perfect,但是在多线程环境下它并不是线程安全的.为什么呢? 因为count++;并不是原子操作,它是由"读-改-写"三个操作组成的,读取count的值,+1,写入count的值,我们来想象一下,有两个线程同一时刻都执行到count++这一行,同时读取到一个数字比如9,都加1,都写入10,嗯 平白无故少了一计数.

    现在我们明白了为什么自增操作不是线程安全的,现在我们来引入一个名词竞争条件.

     

    4 竞争条件

    当计算的正确性依赖于运行时相关的时序或者多线程的交替时,会产生竞争条件.

    除了上面的自增,还有一种常见的竞争条件--"检查再运行".

    废话不多说,上代码.

    /**
     * @author liuboren
     * @Title: RaceCondition
     * @ProjectName multithreading
     * @Description: TODO
     * @date 2018/10/7 15:54
     */
    public class RaceCondition {
    
        private boolean state = false;
    
        public void test(){
            if (state){
                //做一些事
            }else{
                // 做另外一些事
            }
        }
    
        public void changeState(){
            if(state == false){
                state = true;
            }else{
                state = false;
            }
        }
    }

     

    代码很简单,test()方法会根据对象的state的状态执行一些操作,如果state是true就做一些操作,如果是false执行另外一些操作,在多线程条件下,线程A刚刚执行test()方法的,线程B可能已经改变了状态值,但其改变后的结果可能对A线程不可见,也就是说线程A使用的是过期值.这可能导致结果的错误.

     

    5. 示例: 惰性初始化中的竞争条件

    这个例子好,多线程环境下的单例模式.

    /**
     * @author liuboren
     * @Title: Singleton
     * @ProjectName multithreading
     * @Description: TODO
     * @date 2018/10/7 16:29
     */
    public class Singleton {
        private Singleton singleton;
    
        private Singleton() {
        }
    
        public Singleton getSingleton(){
            if(singleton ==null){
                   singleton = new Singleton();
                          }
            return singleton;
        }
        
    }

    看这个例子,我们把构造方法声明为private的这样就只能通过getSingleton()来获得这个对象的实例了,先判断这个对象是否被实例化了,如果等于null,那就实例化并返回,看似很完美,在单线程环境下确实可以正常运行,但是在多线程环境下,有可能两个线程同时走到new对象这一行,这样就实例化了两个对象,这可能不是我们要的结果,我们来小小修改一下

    /**
     * @author liuboren
     * @Title: Singleton
     * @ProjectName multithreading
     * @Description: TODO
     * @date 2018/10/7 16:29
     */
    public class Singleton {
        private Singleton singleton;
    
        private Singleton() {
        }
    
        public Singleton getSingleton(){
            if(singleton ==null){
                synchronized (this) {
                    if (singleton == null) {
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
        
    }


    限于篇幅,这里直接改了一个完美版的,之所以不在方法声明 synchronized是为了减少同步快,实现更快的响应.

    6 复合操作

    为了避免竞争条件,必须阻止其他线程访问我们正在修改的变量,让我们可以确保:当其他线程想要查看或修改一个状态时,必须在我们的线程开始之前或者完成之后,而不能在操作过程中

    将之前的自增操作改为原子的执行,可以让它变为线程安全的.使用Synchronized(同步)块,可以让操作变为原子的.

    我们也可以使用原子变量类,是之前的代码变为线程安全的.

        private final AtomicLong count = new AtomicLong(0);
    
        @Override
        public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
            BigInteger i = extractFromRequest(servletRequest);
            BigInteger[] factors = factor(i);
            count.incrementAndGet();
            encodeIntoResponse(servletResponse, factors);
        }

     

    7 锁

    Java提供关键字Synchronized(同步)块,来保证线程安全,可以在多线程条件下保证可见性和原子性.

    可见性: 一个线程修改完对象的状态后,对其他线程可见.

    原子性: 可以把复合操作转换为不可再分的原子操作.一个线程执行完原子操作其它线程才能执行同样的原子操作.


    让我们看看另一个关于线程安全的结论:当一个不变约束涉及多个变量时,变量间不是彼此独立的:某个变量的值会制约其他几个变量的值.因此,更新一个变量的时候,要在同一原子操作中更新其他几个.

    觉得过于抽象?我们来看看实际的代码

    /**
     * @author liuboren
     * @Title: StatelessServlet
     * @ProjectName multithreading
     * @Description: TODO
     * @date 2018/10/7 15:04
     */
    public class StatelessServlet implements Servlet {
        private final AtomicReference<BigInteger> lastNumber
                = new AtomicReference<>();
    
        private final AtomicReference<BigInteger[]> lastFactors
                = new AtomicReference<>();
    
    
        @Override
        public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
            BigInteger i = extractFromRequest(servletRequest);
            if (i.equals(lastNumber.get())) {
                encodeIntoResponse(servletResponse, lastFactors.get());
            } else {
                BigInteger[] factors = factor(i);
                lastFactors.set(factors);
                encodeIntoResponse(servletResponse, lastFactors.get());
    /        }
        }

    简单说明一下,AtomicLong是Long和Integer的线程安全holder类,AtommicReference是对象引用的线程安全holder类. 可以保证他们可以原子的set和get.

    我们看一下代码,根据lastNumber.get()的结果取返回lastFactors.get()的结果,这里存在竞争条件.因为很有可能线程A执行完lastNumber.set()且还没有执行lastFactors.set()的时候,另一个线程重新调用这个方法进行条件判断,lastNumber.get()取到了最新值,通过判断进行响应,但这时响应的lastFactors.get()却是过期值!!!!


    FBI WARNING: 为了保护状态的一致性,要在单一的原子操作中更新相互关联的状态变量.

     

    8 内部锁

    每个对象都有一个内部锁,执行线程进入synchronized快之前获得锁;而无论通过正常途径退出,还是从块中抛出异常,线程在放弃对synchronized块的控制时自动释放锁.获得内部锁的唯一途径是:进入这个内部锁保护的同步块或方法.

    内部锁是互斥锁,意味着至多只有一个线程可以拥有锁,当线程A尝试请求一个被线程B占有的锁时,线程A必须等待或者阻塞,直到B释放它,如果B永远不释放锁,A将永远等待下去

    内部锁对提高线程的安全性来说很好,很perfect,but但是,在上锁的时间段其他线程被阻塞了,这会带来糟糕的响应性.

    我们再来看之前的单例模式

    /**
     * @author liuboren
     * @Title: Singleton
     * @ProjectName multithreading
     * @Description: TODO
     * @date 2018/10/7 16:29
     */
    public class Singleton {
        private Singleton singleton;
    
        private Singleton() {
        }
    
     /*   public Singleton getSingleton(){
            if(singleton ==null){
                synchronized (this) {
                    if (singleton == null) {
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }*/
    
        public synchronized Singleton getSingleton() {
            if (singleton == null) {
                singleton = new Singleton();
            }
            return singleton;
        }
    }

     

    在方法上加synchronized可以保证线程安全,但是响应性不好,上面注解掉的是之前优化后的方法.


    9 用锁来保护状态

    下面列举了一些需要加锁的情况.

    1. 操作共享状态的复合操作必须是原子的,以避免竞争条件.例如自增和惰性初始化.

    2. 并不是所有数据都需要锁的保护---只有那些被多个线程访问的可变数据.

    3. 对于每一个涉及多个变量的不变约束,需要同一个锁保护其所有变量


    10 活跃度与性能
    虽然在方法上声明 synchronized可以获得线程安全性,但是响应性变得很感人.

    限制并发调用数量的,并非可用的处理器资源,而恰恰是应用程序自身的结构----我们把这种运行方式描述为弱并发的一种表现.

    通过缩小synchronized块的范围来维护线程安全性,可以很容易提升代码的并发性,但是不应该把synchronized块设置的过小,而且一些很耗时的操作(例如I/O操作)不应该放在同步块中(容易引发死锁)

    决定synchronized块的大小需要权衡各种设计要求,包括安全性、简单性和性能,其中安全性是绝对不能妥协的,而简单性和性能又是互相影响的(将整个方法声明为synchronized很简单,但是性能不太好,将同步块的代码缩小,可能很麻烦,但是性能变好了)

    原则:
    通常简单性与性能之间是相互牵制的,实现一个同步策略时,不要过早地为了性能而牺牲简单性(这是对安全性潜在的妥协).

    最后,使用锁的时候,一些耗时非常长的操作,不要放在锁里面,因为线程长时间的占有锁,就会引起活跃度(死锁)与性能风险的问题.

    嗯,终于写完了.以上是博主<<Java并发编程实战>>的学习笔记,如果对您有帮助的话,请点下推荐,谢谢.

       

  • 相关阅读:
    java架构解密——实时动态aop
    guice基本使用,配置模块的两种方式(三)
    guice基本使用,三种注入方式(二)
    guice的基本使用(一)
    Java 集合系列10之 HashMap详细介绍(源码解析)和使用示例
    java泛型(二)、泛型的内部原理:类型擦除以及类型擦除带来的问题
    java泛型(一)、泛型的基本介绍和使用
    java泛型学习(2)
    jquery,从后台查数据,给页面上添加树形。
    Java Annotation认知(包括框架图、详细介绍、示例说明)
  • 原文地址:https://www.cnblogs.com/xisuo/p/9751129.html
Copyright © 2011-2022 走看看