• 谈谈我对Java并发的理解——读《Java并发编程实战有感》
    # 谈谈我对Java并发的理解——读《Java并发编程实战有感》
    ## 线程安全
    先要谈一下最根本的问题,线程安全问题。可以说多线程编程带来的影响有利有弊,好处自然是提高处理器的利用率,加快任务执行速度,弊端是线程安全问题。我在阅读这本书之前已经学了一段时间的JavaWeb开发,学的时候对线程安全不太敏感,主要原因可以总结为两点: 
    - 很少显式使用多线程
    - 框架的屏蔽
    
    众所周知,不论是SpringMVC还是Struts框架,它们都是使用多线程的方式,对每一个请求都会创建一个线程去处理,所以我们平时不容易去主动地接触多线程,更不要说线程安全了。读这本书的时候当我发现书中的加锁等机制用得非常频繁的时候,我的内心是非常震惊的,因为我平时开发很少用到锁,尤其是对象的setter和getter都会加锁时内心简直崩溃。阅读完这本书之后,对线程安全的理解更深了一些,现总结如下:
    
     - 线程安全主要是多条具有关联的、对同一变量进行修改的语句不能以原子性执行的情况下,会出现线程安全问题。注意一条语句并不能保证其原子性(++i不是原子执行的),多条语句更不会有原子性。在多线程环境下,每一条语句都有可能被中断。
     - 如果整个Application不存在任何成员变量(包括实例成员变量和静态成员变量,以下提到的成员变量都包括这两种;如果没有指明是不可变的,那么默认为可变的),或者所有成员变量都是final的(包括成员对象的属性),那么一定是线程安全的。
     - 如果存在可变的成员变量,不论是基础数据类型还是引用类型,那么都是需要关注它的线程安全问题;如果存在并发写的情况,那么修改它的时候需要加锁。部
     - 局部变量一般情况下访问是不需要加锁的,因为它是栈封闭的,多线程不会并发访问到。但是如果局部变量是参数,并且它来自于成员变量,那么也是需要关心其线程安全问题的,甚至setter和getter在某些情况下也要加锁。
     - 成员变量是容器类的情况下,不仅需要关注容器的线程安全问题(通常可以使用一些线程安全的容器类),还需要关注容器中元素的线程安全问题(通常是实体类)。
     - 局部变量如果是基础数据类型,而非引用类型,那么一定线程安全的,因为传参是使用拷贝的方式,修改它不会影响成员变量。
    重点! 从数据库中取出的数据不是成员变量,而是局部变量,修改它不需要考虑线程安全问题。。对于并发修改数据库的问题,应该交由数据库来处理。由数据库来处理并发读写问题,这往往与数据库的隔离级别有关。 
    ![这里写图片描述](https://img-blog.csdn.net/20170528083849200?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvc29uZ3hpbmppYW5xd2U=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
    以上这四种隔离级别都是拒绝并发修改的,比如MySQL的InnoDB引擎,会在修改表的时候加表锁或行锁,此时其他修改被锁数据的请求是会被阻塞的,直到锁被释放才会执行。对于并发读写,这四种隔离级别的要求各不相同,前三种是允许并发读写的,即当有Session在修改表的时候,仍有Session可以读取数据,第四种串行化是拒绝并发读写的,无论是读还是写,都必须要以串行的方式执行(当然是一种非常低效的方式,很少使用,并发度非常低,但最大限度地保证了数据一致性)。
     - 如果成员变量不存在并发修改(多线程同时写),但存在并发读写(多线程同时读写),那么如果要求读到的结果是最新的,那么也要对成员变量的内存可见性有要求,这个问题会在下面继续讨论。
     - 如果我们编程时很少使用成员变量、主要数据都是从数据库从取出的,那么可以较少地关注并发修改产生的问题。但是如果是读-改-写回这种执行序列,当要求这几个操作是原子的话,即从数据库读到这个数据之后,在写回之前不允许其他事务写,仅依赖于数据库隔离级别是不可行的。前三种数据库隔离级别都无法避免丢失修改(两个事务交替修改同一数据,造成前一个事务的修改被覆盖掉),要解决这个问题有两种方式:
    	 - 在代码中使用锁,比如synchronized或者ReentrantLock
    	 - 在SQL中使用
    		 - 乐观锁:使用多版本并发控制,增加一列version,在写之前再读取一次,如果version相同,说明在第一次读之后其他事务没有写过,那么可保证读-改-写回是原子的。
    		 - 悲观锁:比如select ... for update,在读的时候就加上互斥锁,其他事务无法读写该数据,本事务写后释放互斥锁。 
     - 哪怕是有一个成员变量,不论是基础数据类型,还是引用类型,在存在并发修改的情况下,修改它的时候都需要加锁。尤其是对多条代码的原子性有要求时,非常经典的是“读取-修改-写回”这种指令序列时,如果后续的修改依赖于之前的读取结果时,那么这个指令序列必须不可被中断(加锁)。
     - 综上所述,就成员变量的访问而言,假如存在并发修改,就需要使用Java中的锁。就数据库的读-改-写回序列而言,假如有原子执行的需求,就需要使用Java中的锁或数据库的乐观锁或悲观锁。
    
    ## 内存可见性
    内存可见性主要是并发读写的情况下读线程要求读到的数据必须是刚被修改的数据,此时需要使用volatile关键字或原子变量或者加锁来保证这一点。加锁是一种较重的行为,而volatile关键字和原子变量是一种较为轻量的并发手段。
    
    当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或其他对处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。 
    而原子变量是使用无锁并行机制的,主要是CAS算法(compare-and-swap)。如果用一句话来解释CAS的话,就是:读的时候记录结果,写的时候检查是不是还是刚才读到的,如果是,那么说明读和写之间没有其他线程修改它的值,这段代码是原子执行的,可以进行修改操作;如果不是,那么说明其他线程修改了它的值,这段代码并没有原子执行,此时需要使用循环,重新读取,再检查,直至保证原子执行。 
    
    这种方式和锁有一些类似,都可以保证代码的原子执行,但是使用锁会涉及到一些线程的挂起和上下文切换问题,需要消耗资源,但是CAS仅是轮询,不涉及JVM级别。书中提到低度和中度竞争的情况下,CAS的代价是低于锁的,在高度竞争的情况下,CAS的代价是高于锁的(毕竟轮询也需要消耗资源,占用CPU),但高度竞争这种情况是比较少的。在一些细粒度的并发操作上,推荐还是使用CAS。
    
    ## 并发工具
    1. 同步容器类:如果将容器类型作为成员变量,那么容器必须是同步容器类。对List和Set而言,有Collections.synchronizedXXX对非同步容器类进行包装,也有CopyOnWriteXXX,CopyOnWrite适用于读多写少的情况,如果大量修改,会出现大量的内存拷贝行为,效率较低。对于Map而言,有非常高效的ConcurrentHashMap,比Collections.synchronizedMap包装的Map性能更好,主要是因为使用了CAS无锁并行机制。 
    2. 阻塞队列:并发的经典模式生产者——消费者模式的一种比较好的解决方案是使用阻塞队列,在Java中是ArrayBlockingQueue和LinkedBlockingQueue。使用阻塞队列而非原生的wait-nofity或者是显式锁的await-signal,会大大降低生产者——消费者模式的开发难度。阻塞队列的原理就是生产者线程(1或多)将原料放入阻塞队列,如果阻塞队列已满,那么put方法会被阻塞,直到阻塞队列不满;消费者线程(1或多)从阻塞队列中取出原料,进行消费,如果阻塞队列为空,那么take方法会被阻塞,直到阻塞队列不空。 
    另外,推荐使用有界的阻塞队列,避免生产者与消费者速度不匹配时不会无限扩展队列长度,造成OOM(OutOfMemeory异常)。 
    3. 还有一些其他的工具类,比如CountDownLatch闭锁、Semaphore信号量、Barrier栅栏等,这些可能没有之前的工具使用地那么频繁,这里不再过多介绍,很多书中都有介绍。
    ## 任务执行
    ###无限制创建线程的不足:
    	1. 线程生命周期的开销非常高
    	2. 资源消耗
    	3. 稳定性
    ###解决方法:线程池 Executor框架
    
    一般来说不推荐使用Executors工具类创建的那些线程池,通用性较差,推荐自己new一个ThreadPoolExecutor。注意创建时的一些参数需要特别关注,尤其是阻塞队列,一定要使用有界队列,理由同上。使用有界队列需要考虑的一个问题是当队列满了的时候如何处理加入的任务。
    
    ###饱和策略
    
    当有界队列被填满后,饱和策略开始发挥作用。ThreadPoolExecutor的饱和策略可以通过setRejectedExecutionHandler来修改。JDK提供了几种不同的RejectedExecutionHandler的实现,每种实现都包含有不同的饱和策略:#AbortPolicy、CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy。
    
    中止策略是默认的饱和策略,该策略将抛出未检查的RejectedExecutionException。调用者可以捕获这个异常,然后根据需求编写自己的处理代码,当新提交的任务无法保存到队列中执行时,抛弃策略会悄悄抛弃该任务。抛弃最旧的策略则会抛弃下一个将被执行的任务,然后尝试重新提交下一个将被执行的任务(如果工作队列是一个优先级队列,那么抛弃最旧的将抛弃优先级最高的任务)
    
    调用者运行策略实现了一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退给调用者,从而降低新任务的流量。它不会在线程池的某个线程中执行新提交的任务,而是在一个调用了execute的线程中执行该任务。为什么好?因为当服务器过载时,这种过载情况会逐渐向外蔓延开来——从线程池到工作队列到应用程序再到TCP层,最终达到客户端,导致服务器在高负载下实现一种平缓的性能降低。
    ## 任务取消
    ### 任务取消
    
    最通用的中断线程的方式是使用interrupt 
    使用boolean变量决定线程何时停止的方式不是很好,因为任务可能永远不会检查取消标志,因此永远不会结束。 
    interrupt方法能中断目标线程,而isInterrupted方法能返回目标线程的中断状态。静态方法interrupted方法将清除当前线程的中断状态,并返回它之前的值,这也是清除中断状态的唯一方法。
    
    ###中断
    
    当线程在非阻塞状态下中断,它的中断状态将被设置,然后根据将被取消的操作来检查中断状态以判断发生了中断。通过这样的方法,中断操作将变得有黏性——如果不触发InterruptedException,那么中断状态将一直保持,直到明确地清除中断状态。 
    调用interrupt并不意味着立即停止目标线程正在进行的工作,而只是传递了请求中断的消息。 
    另外线程应该由其所有者中断,所有者可以将线程的中断策略信息封装某个合适的取消机制种,例如关闭方法。 
    由于每个线程拥有各自的中断策略,因此除非你知道中断对该线程的含义,否则就不应该中断这个线程。
    
    这段代码是我认为取消线程最好的方式。
    
    ```
    public class PrimeProducer extends Thread {
        private final BlockingQueue queue;
    
        public PrimeProducer(BlockingQueue queue) {
            this.queue = queue;
        }
    
        public void run(){
            BigInteger i = BigInteger.ONE;
            try {
                while(!Thread.currentThread().isInterrupted()){
                    queue.put(i = i.nextProbablePrime());
                }
            } catch (InterruptedException e) {
            }
        }
        public void cancel(){
            Thread.currentThread().interrupt();
        }
    }
    ```
    
    这种方式可以解决在不存在阻塞的代码段的线程中止问题。如果存在阻塞的代码段,那么通常是先关闭阻塞的资源(比如套接字Socket),再中断线程。 
    下面这段代码是使用了NIO的服务器程序的监听线程,当关闭服务器时,会调用这个线程的shutdown方法,这个方法会关闭seletor,让线程从检查seletor的阻塞方法中退出,然后再中断该线程,此时可以正确地关闭该线程。
    
    ```
    private class ListenerThread extends Thread {
    
        @Override
        public void interrupt() {
            try {
                try {
                    selector.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            } finally {
                super.interrupt();
            }
        }
    
        @Override
        public void run() {
            try {
                //如果有一个及以上的客户端的数据准备就绪
                while (!Thread.currentThread().isInterrupted()) {
                    //当注册的事件到达时,方法返回;否则,该方法会一直阻塞  
                    selector.select();
                    //获取当前选择器中所有注册的监听事件
                    for (Iterator it = selector.selectedKeys().iterator(); it.hasNext(); ) {
                        SelectionKey key = it.next();
                        //删除已选的key,以防重复处理 
                        it.remove();
                        //如果"接收"事件已就绪
                        if (key.isAcceptable()) {
                            //交由接收事件的处理器处理
                            handleAcceptRequest();
                        } else if (key.isReadable()) {
                            //如果"读取"事件已就绪
                            //取消可读触发标记,本次处理完后才打开读取事件标记
                            key.interestOps(key.interestOps() & (~SelectionKey.OP_READ));
                            //交由读取事件的处理器处理
                            readPool.execute(new ReadEventHandler(key));
                        }
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        public void shutdown() {
            Thread.currentThread().interrupt();
        }
    }
    ```
    ## 锁与CAS对比
    锁的缺点:
    1. 在挂起和恢复线程等过程中存在着很大的开销,并且通常存在着较长时间的中断。
    2. volatile变量同样存在一些局限:虽然它们提供了相似的可见性保证,但不能用于构建原子的负责操作。
    3. 当一个线程正在等待锁时,它不能做任何其他事情。如果一个线程在持有锁的情况下被延迟执行,那么所有需要这个锁的线程都无法执行下去。
    4. 总之,锁定方式对于细粒度的操作(比如递增计数器)来说仍然是一种高开销的机制。在管理线程之间的竞争应该有一种粒度更细的技术,比如CAS。
    
    非阻塞算法可以使多个线程在竞争相同的数据时不会发生阻塞,因此它能在粒度更细的层次上进行协调,并且极大地减少调度开销。而且,在非阻塞算法中不存在死锁和其他活跃性问题。在基于锁的算法中,如果一个线程在休眠或自旋的同时持有一个锁,那么其他线程都无法执行下去,而非阻塞算法不会受到单个线程失败的影响。非阻塞算法常见应用是原子变量类(JDK1.8的ConcurrentHashMap也使用了CAS)。
    即使原子变量没有用于非阻塞算法的开发,它们也可以用作一个更好的volatile类型变量。原子变量提供了与volatile类型变量相同的内存语义,此外还支持原子的更新操作,从而使它们更加适用于实现计数器、序列发生器和统计数据收集等,同时还能比基于锁的方法提供更高的可伸缩性。
    
    ## 总结
    
    这篇文章主要是聊了一下自己对并发的一些看法,并不专注于介绍并发的具体知识点,可能部分观点也有点皮面,希望各位多加指教。 
    最后说一句:《Java并发编程实战》绝对是Java并发的Bible,推荐所有学习Java的人去阅读这本书。我读完感觉只掌握了其中的一半,一年以后我会重新读这本书,希望能掌握其更多的精妙之处!
    
    PS:我在读完这本书后动手写了一个使用了Java的多线程(线程池、阻塞队列、原子变量、内置锁等)和NIO的CS架构的聊天室程序(当然还有一些奇奇怪怪的功能),之后打算再写一篇博客来介绍这个程序,现在暂时把Github地址放上来,欢迎各位star和fork,如果发现代码有问题也望给予指教。除了代码之外还放上了我学习Java多线程的笔记,也一并分享给大家。 
    
    > https://github.com/songxinjianqwe/Chat
    
    谢谢大家!
    
    展开全文
  • 01 - Java并发编程与高并发解决方案笔记-基础篇 基础篇很重要!很重要!很重要!!!一定要理解和认真思考。 01 - Java并发编程与高并发解决方案笔记-基础篇 1.课程准备 2.并发编程基础 2-0 CPU多级缓存 2-1...
  • 文章目录并发模拟的四种方式一、Postman二、Apache Bench(AB)三、并发模拟工具JMeter四、代码模拟 并发模拟的四种方式 一、Postman Postman是一个款http请求模拟工具 首先演示一下postman最基本的使用 创建一个...
  • 什么是并发 在过去单CPU时代,单任务在一个时间点只能执行单一程序。之后发展到多任务阶段,计算机能在同一时间点并行执行多任务或多进程。虽然并不是真正意义上的“同一时间点”,而是多个任务或进程共享一个CPU,...
  • [超级链接:Java并发学习系列-绪论] Lock接口在之前的章节中多次提及: Java并发02:Java并发Concurrent技术发展简史(各版本JDK中的并发技术) Java并发12:并发三特性-原子性、可见性和有序性概述及问题示例 Java...
  • 原文地址:...最近后台和微信理有很多读者让我整理一些面试题,我就把这事放在心上了,于是在各大网站和其他公众号里面搜索面试相关的高质量文章或者信息,今天主要整理一下 Java 并发编程在...
  • Java 并发核心编程 2012-11-28 09:54:16
    1、关于java并发 2、概念 3、保护共享数据 4、并发集合类 5线程 6、线程协作及其他   1、关于java并发 自从java创建以来就已经支持并发的理念,如线程和锁。这篇指南主要是为帮助java多线程开发人员理解...
  • 同一时间,CPU只能处理1条线程,只有一条线程在工作(执行)多线程并发(同时)执行,其实质是CPU快速的在多线程之间调度(切换) 如果线程过多,会怎样? CPU在N多条线程中调度,会消耗大量的cpu资源每条线程...
  • [超级链接:Java并发学习系列-绪论] [系列概述: Java并发22:Atomic系列-原子类型整体概述与类别划分] 本章主要对原子累加器进行学习。 1.原子类型累加器 原子类型累加器是JDK1.8引进的并发新技术,它可以...
  • 第1章 课程介绍(Java并发编程进阶课程) 什么是Disruptor?它一个高性能的异步处理框架,号称“单线程每秒可处理600W个订单”的神器,本课程目标:彻底精通一个如此优秀的开源框架,面试秒杀面试官。本章会带领小...
  • 构建Java并发模型框架 2015-06-04 23:15:59
    原文来自:http://www.ibm.com/developerworks/cn/java/l-multithreading/构建Java并发模型框架Java的多线程特性为构建高性能的应用提供了极大的方便,但是也带来了不少的麻烦。线程间同步、数据一致性等烦琐的问题...
  • 在这里写写我学习到和自己所理解的 Java并发编程和高并发解决方案。现在在各大互联网公司中,随着日益增长的互联网服务需求,高并发处理已经是一个非常常见的问题,在这篇文章里面我们重点讨论两个方面的问题,一...
  • Java 并发面试题解 2019-01-10 14:06:00
    我发现,不论是哪个国家,什么背景的 Java 开发者,都对自己写的并发程序相当自信,但也会在出问题时表现得很诧异甚至一筹莫展。 可见,Java 并发编程显然不是一件能速成的能力,基础搭得越好,越全面,在实践中才...
  • 2019年最新Java学习路线图,路线图的宗旨就是分享,专业,便利,让喜爱Java的人,都能平等的学习。从今天起不要再找借口,不要再说想学Java却没有资源,赶快行动起来,Java等你来探索,高薪距你只差一步! java...
  • 2016最新Java学习计划 2018-04-02 23:05:53
    一、Java学习路线图 二、Java学习路线图——视频篇 六大阶段学完后目标知识点配套免费资源(视频+笔记+源码+模板)密码 第一阶段Java基础 入门学习周期:35天学完后目标:1.可进行小型应用程序开发2.对数据库进行...
  • [超级链接:Java并发学习系列-绪论] 本章主要对Java多线程实现的三种方式进行学习。 1.序言 在JDK5版本之前,提供了两种多线程的实现方式: 继承Thread类,重写run()方法 实现Runnable接口,实现run()方法 ...
  • java-并发-并发容器(4) 2016-07-23 19:30:38
    Set类型的ConcurrentSkipListSet和CopyOnWriteArraySet对应的非并发容器:HashSet 目标:代替synchronizedSet 原理:基于CopyOnWriteArrayList实现,其唯一的不同是在add时调用的是CopyOnWriteArrayList的...
  • Java并发代码篇 2018-10-26 16:57:44
    一、创建线程:  package com.dong.testThread; /** * * 1.线程的创建 * 2.线程的方法 * * @author liuD ... public static void main(String[] args) throws InterruptedException { //创建...
  • JAVA并发编程:内存模型 2018-11-25 17:13:47
    JAVA并发编程中,有一个很重要的东西需要搞清楚,那就是内存模型。 内存模型的目标: 定义了程序中变量的访问规则,以及虚拟机存储变量到内存,从内存中取出变量的底层细节。 这里所说的变量不包括局部变量、...
  • 一、为什么需要把并行?业务需求性能 二、了解下高手之间的过招(本人望尘莫及呀)linux之父炮轰并行开发,主张大容量缓存他说:硬件的性能无法永远提升,当前的趋势实际上趋于降低功耗。那么推广并行技术这个...
1 2 3 4 5 ... 20
收藏数 46,488
精华内容 18,595