JAVA多线程、高并发梳理

来源:互联网 发布:淘宝评论为垃圾评论 编辑:程序博客网 时间:2024/06/05 21:02

首先从概念上讲:JAVA 多线程,高并发。

  • 为什么是多线程而不是单线程
    思考一下Servlet容器,会同时有多个用户访问,如果是单线程的话,只有一个服务线程来处理多个用户的请求,这样的服务器响应会特别差。
  • 为什么多线程之间是并发执行,而不是并行执行
    从CPU的角度讲,单线程在获得CPU的执行权期间,如果因为I/O或等待其他资源比如数据库连接,而浪费CPU资源。关于高并发,是让多个线程之间共享CPU时间片段,实现多线程交替执行,而不是一个线程执行完了,再由其他线程执行,这样做,在CPU时间片上看各线程是交替执行,而从宏观的时间角度来看,是多个任务同时执行的,就好像有多个CPU分别处理一个任务。

  • 为什么要采取合适的执行策略,异步或同步。

    这牵扯到线程之间的的交互,有的线程在执行到某一过程的时候需要等待其他线程的计算结果才能继续自己的执行,这时候就需要同步,而如果不需要等待其他线程的结果就能继续向下执行当前线程,则可以采用异步策略,在一个合适的点,比如在当前线程计算完毕之后,再获取其他线程的计算结果。
    举一个我开发中遇到的实际例子,场景,一个页面存在多个form表单,至于为什么用多个form而不用一个,牵扯到复杂的业务场景,在此不做赘述。这些form表单需要一个按钮同时触发 使用ajax的异步方式提交,也就是有多个ajax按顺序(可以理解为单线程)触发,但是ajax的回调函数success是异步调用的。也就是不知道什么时候会调用success回调函数。 需求是在所有success执行完毕后,执行另外一个逻辑。


说起多线程不得不提线程在JVM中的内存模型

  • 从抽象的角度看
    多个线程共享的数据放在主存中,线程操作的数据只是主存中数据的备份,每一个线程有自己的工作内存,来存储共享数据的备份。这样就又引出了另外一个问题,如何保证主存中数据的有效性,可见性、正确性。

    • 临界区(可以理解为主存)
      为了解决主存中数据的共享问题,JAVA内存模型提供了8种原子操作,锁定主存,解锁主存。从主存中的共享变量读取数据,将读取到的数据复制到工作内存中,将工作内存的数据交给线程,将计算结果返回工作内存,将工作内存的数据传递给主存,将主存中的数据存入变量。

    • volatile关键字
      可以抽象的理解为 不保存工作内存的数据,直接读写工作内存中的数据,当然实际情况复杂的多,但是达到了这样的效果。


JAVA 锁体系

  • Synchronize 同步代码块

    提到锁,最基本的Synchronized 同步代码块,进入同步代码块的线程需要持有一个锁,在java中,每一个对象都可以作为一个锁,如何来保证多线程场景下的共享变量的安全性呢,就是所有线程都是用同一把锁来守护共享变量。
    浅析JVM中的锁,上面提到每个对象都可以作为锁,那么这个抽象的锁在JVM中的表现形式(实现原理)到底是什么样的呢,java对象头 java获取锁,锁升级的过程

简单来讲,就是每个对象在JVM中都由对象头这么个东西,32位或64位,不同的位代表不同的含义,比如最后两位代表锁的状态,当前锁对象是无锁的,还是轻量锁,还是重量锁,还是偏向锁。在不同的状态下, 各个位数又代表不同的意思。在JVM与jdk的优化下,并不是所有的线程获取锁的时候都直接获取重量级锁,线程进入同步代码块的时候,首先检查当前锁对象的状态,根据不同的锁对象的状态,尝试不同的锁策略,其中偏量锁适用于单线程进出同步代码块的场景,轻量锁适用于多个线程交替进入同步代码块的场景,重量级锁则发生于轻量锁不能满足条件的场景下,多个线程同时竞争执行同步代码块。

  • Lock
    相比于Synchronize,线程等待一个锁的时候或者说是处于阻塞状态时是不可中断的,Lock提供可更多的特性:可中断的锁,可定时的锁,非公平的锁。

  • 可重入锁 ReentrantLock

    底层实现是即AQS框架,分为公平锁与非公平锁,公平锁策略下,所有的请求锁的线程都进入FIFO的等待队列,依次等待;非公平锁下,如果当前锁处于可用状态,如果此时有新的线程来请求该锁,则该线程悠闲获取该锁,相当于它插队在所有等待队列的线程前面。这样做的好处是,更好的利用了等待队列中的线程唤醒锁占用的时间。尤其是唤醒一个等待线程消耗的时间很大的时候。


  • 同步工具类(实现原理 AQS)

    • CountDownLatch 闭锁
    • Sempahore 信号量
    • Barrier
    • Exhanger
  • AQS(java同步器框架AbstractQueuedSynchronizer)

    • volatile变量 state

    • FIFO线程队列

    • 当前 state策略,排他、共享

    • CAS 操作state变量


  • 同步容器框架

  • 最基本的vector、hashtable、以及Collections.synchronizedXXX

  • 底层实现:Synchronize, 内置锁。

  • 同步容器的迭代问题,以及及时失败策略。

  • 并发容器框架

    • ConcurrentHashMap
      • 分段锁
    • CopyOnWriteArrayList
      • 读写分离
    • Queue
      • BlockingQueue
  • ThreadLocal
    不得不提的ThreadLocal,线程封闭思想,将变量的引用封存在当前线程中,使当前线程可以在不同的地方获取该变量,例子:数据库连接池。

    内部实现

    • 变量并不是维护在ThreadLocal中的,而是维护在当前线程中的。 在当前的Thread实例中维护了一个Map,这个Map是ThreadLocal的内部类,Map<ThreadLocal,Object>(一个线程可以使用多个ThreadLocal)
      这里写图片描述

    • 线程如果想获取封闭在自己当中的变量引用,完全可以通过反射来将线程实例中的ThreadLocalMap获取到,然后以ThreadLocal的实例为Key,通过get()来获取封闭在线程中的变量引用。那么,ThreadLocal存在的意义是什么的,我认为ThreadLocal提供的get、set等方法就是封装了线程获取目标变量的方法,简化了通过Thread获取目标变量的过程。

    • 内存泄露问题。
      如上图所示,因为目标变量的引用是存储在线程中的,即使调用ThreadLocal.remove()方法,也只是斩断了Thread跟ThreadLocal之间的关联,而目标变量还存储在Thread的Map中,只是没有了Key而已,所以该目标变量只有在当前Thread消亡后才可能会被GC回收。
原创粉丝点击