【高并发系列】1.Java并发编程必知的概念

Java 高并发编程中必须知道的一些概念~

1、并发与并行

并发 Concurrency

        在单核 CPU 或多核 CPU 中交替处理多个任务。

并行 Parallelism

        多核 CPU 同时处理多个任务。注意,单核 CPU 是不存在并行的。

区别  

        并发与并行的概念,重要区别在于“一段时间”和“同一时刻”。

        并发偏重于多个任务交替执行,在一段时间内同时做多个事情,比如在周末上午,一会哄娃,一会玩手机,娃闹了哄,安静了玩手机……,如此反复(头疼~)。

        而并行指的是同一时刻执行多个任务。比如我双手各拿一支笔,左手画方,同时右手画圆(秀儿~)。

2、同步与异步

二者用来形容用一次方法调用。

同步 Synchronous

        即调用方法开始,一旦调用就必须等待方法执行完返回才能继续后面的操作。

        举个例子,你去银行ATM取钱,你必须等到ATM吐完钱你拿到钱取完卡你才能离开。

异步Asynchronous

        更像是一种消息传递,不用关心方法具体执行过程,一旦触发会立即返回结果,调用者可继续后面的操作。

        举个例子,你今天要取钱,数量较大,直接电话或者 APP 预约银行说你要取多少现金,这段时间银行会为你准备钱,而这准备过程与你都没什么关系,然后你只要按预定的时候去取就行了,对你于而言,你们是触发了一个异步动作或传递了一个消息而已。

3、进程与线程

概念

进程:操作系统进行资源分配的最小单位,其中资源包括:CPU、内存空间、磁盘IO等。同一进程中的多条线程共享该进程中的全部系统资源,而进程与进程之间相互独立。

线程:CPU调度的最小单位,必须依赖进程而存在。

区别

  • 定义:进程是程序运行的一个实体的运行过程,是系统进行资源分配和调度的独立单位;线程是进程运行和执行的最小调度单位。
  • 活泼性:进程不活泼(只是线程的容器);线程活泼,随时可以创建和销毁。
  • 系统开销:进程创建、撤销、切换开销大,资源要重新分配和收回。线程相对于进程仅保存少了寄存器内容,开销小,在进程的地址空间执行代码。
  • 拥有资产:进行是资源拥有的基本单位。线程相对于进程来说基本上不拥有资源,但会占用CPU。
  • 调度:进程仅是资源分配的基本单位。线程是独立调度、分派的基本单位。
  • 安全性:进程之间相对比较独立,彼此不会互相影响。线程共享同一个进程下的资源,可以相互通信和影响。

4、临界区

        临界区用来表示一种公共资源或者说共享数据,可以被多个线程使用,但是每一次只能有一个线程使用它。

         一旦临界区资源被占用,其他线程想要使用这个资源,就必须等待。

5、 阻塞与非阻塞

        阻塞和非阻塞通常用来形容多线程间的影响。

阻塞 Blocking

        如果一个线程占用了一个公共资源而没有释放对它的锁,另外别的一些线程想要继续执行就只能等它释放锁,等待会导致线程挂起,这时候就造成阻塞了。

非阻塞 Non-Blocking

        就是没有阻塞,线程可以自由运行,没有锁定公共资源,不相互阻塞运行。

6、线程安全

对 Java 线程安全的理解:

        当多个线程并发访问某个 Java 对象时,无论系统如何调度这些线程,也无论这些线程将如何交替操作,这个对象都能表现出一致的、正确的行为,那么对这个对象的操作是线程安全的。如果这个对象表现出不一致的、错误的行为,那么对这个对象的操作不是线程安全的,发生了线程的安全问题。

7、Java 高并发编程

        Java5 之后引入高级并发特性,大多数的特性在 java.util.concurrent 包中,是专门用于多线程发编程的,充分利用了现代多处理器和多核心系统的功能以编写大规模并发应用程序。主要包含原子量、并发集合、同步器、可重入锁,并对线程池的构造提供了强力的支持。

意义及优势

  1、充分利用CPU的资源;

  2、加快响应用户的时间;

  3、使代码模块化,异步化,简单化。

 需要注意的问题

  1、线程之间的安全性;

  2、线程之间的死循环过程;

  3、线程太多了会将服务器资源耗尽形成死机宕机。

8、并发编程中的三大问题

原子性

定义:是指一个线程的操作是不能被其他线程打断,同一时间只有一个线程对一个变量进行操作。

        在多线程情况下,每个线程的执行结果不受其他线程的干扰。比如说多个线程同时对同一个共享成员变量n++了100次,如果n初始值为0,n最后的值应该是100,所以说它们是互不干扰的,这就是原子性。

        实际上,n++并不是原子性的操作,可以使用 AtomicInteger 保证其原子性。

可见性

定义:是指某个线程修改了某一个共享变量的值,而其他线程是否可以看见该共享变量修改后的值。

        每个线程都有自己的工作内存,线程先把共享变量的值从主内存读到工作内存,形成一个副本,当计算完后再把副本值刷回主内存,从读取到最后刷回主内存这是一个过程,当还没刷回主内存的时候这时候对其他线程是不可见的,所以其他线程从主内存读到的值是修改之前的旧值。

        CPU 的缓存优化、硬件优化、指令重排及对JVM编译器的优化,都会出现可见性的问题。

有序性

定义:程序执行的顺序按照代码的先后顺序执行。

        如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存中主内存同步延迟”现象。

        Java volatile 关键字本身就包含了禁止指令重排序的语义,而 ynchronized 则是由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则来获得的,这个规则决定了持有同一个锁的两个同步块只能串行地进入。

9、Java内置锁及状态

描述

        在 Java6 之前,所有 Java 内置锁都是重量级锁,而重量级锁会造成 CPU 在用户态和核心态之间频繁切换,代价高,效率低。为了减少获得锁和释放锁所带来的性能消耗,Java6 引入了偏向锁和轻量级锁的实现。因此,Java 内置锁一共有4种状态:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这些状态会随着竞争情况逐渐升级。内置锁可升级但不能降级,这意味着偏向锁升级成轻量级锁后不能再降级成偏向锁,这样的策略可以提高获得锁和释放锁的效率。

无锁状态

        Java 对象刚创建的时候无任何线程来竞争它,说明该对象处于无锁状态,这时偏向锁标识位是0,锁状态是01。

偏向锁状态

        偏向锁是指一段同步代码一直被同一个线程所访问,那么该线程会自动获取锁,从而降低获取锁的代价。如果内置锁处于偏向状态,当有一个线程来竞争锁时,先用偏向锁,表示内置锁偏爱这个线程,这个线程要执行该锁关联的同步代码时,不需要再做任何检查和切换。偏向锁在竞争不激烈的情况下效率非常高,是因为偏向锁状态的 Mark Word 会记录内置锁自己偏爱的线程 ID,内置锁会将该线程当作自己的熟人。

轻量级锁状态

        当有两个线程开始竞争这个锁对象时,情况就发生变化了,不再是偏向(独占)锁了,锁会升级为轻量级锁,两个线程公平竞争,哪个线程先占有锁对象,锁对象的Mark Word就指向哪个线程的栈帧中的锁记录。

        当锁处于偏向锁,又被另一个线程企图抢占时,偏向锁就会升级为轻量级锁。企图抢占的线程会通过自旋的形式尝试获取锁,不会阻塞抢锁线程,以便提高性能。

        自旋原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要进行内核态和用户态之间的切换来进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免了用户线程和内核切换的消耗。

        但是,线程自旋是需要消耗 CPU 的,如果一直获取不到锁,那么线程也不能一直占用CPU自旋做无用功,所以需要设定一个自旋等待的最大时间。JVM 对于自旋周期的选择,Java6 之后引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不是固定的,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定的。线程如果自旋成功了,下次自旋的次数就会更多,如果自旋失败了,自旋的次数就会减少。

        如果持有锁的线程执行的时间超过自旋等待的最大时间仍没有释放锁,就会导致其他争用锁的线程在最大等待时间内还是获取不到锁,自旋不会一直持续下去,这时争用线程会停止自旋进入阻塞状态,该锁膨胀为重量级锁。

重量级锁状态

        重量级锁会让其他申请的线程之间进入阻塞,性能降低。重量级锁也叫同步锁,这个锁对象 Mark Word 再次发生变化,会指向一个监视器对象,该监视器对象用集合的形式来登记和管理排队的线程。

总结

使用 Java 内置锁时,不需要通过 Java 代码显式地对同步对象的监视器进行抢占和释放,这些工作由 JVM 底层完成,而且任何一个 Java 对象都能作为一个内置锁使用,所以 Java 的对象锁使用起来非常方便。但是,Java 内置锁的功能相对单一,不具备一些比较高级的锁功能,比如限时抢锁,可中断抢锁,多等待队列。除了这些功能问题之外,Java 对象锁还存在性能问题。在竞争稍微激烈的情况下,Java 对象锁会膨胀为重量级锁,而重量级锁的线程阻塞和唤醒操作需要进程在内核态和用户态之间来回切换,导致其性能非常低。所以,迫切需要提供一种新的锁来提升争用激烈场景下锁的性能。

10、JUC显式锁

        与 Java 内置锁不同,JUC 显式锁是一种非常灵活的、使用纯 Java 语言实现的锁,这种锁的使用非常灵活,可以进行无条件的、可轮询的、定时的、可中断的锁获取和释放操作。由于 JUC 锁加锁和解锁的方法都是通过 Java API 显式进行的,因此也叫显式锁。

        Java5 引入了 Lock 接口,Lock 是 Java 代码级别的锁。为了与 Java 对象锁区分,Lock 接口叫作显式锁接口,其对象实例叫作显式锁对象。后面会详细介绍。

11、独占锁与共享锁

        在访问共享资源之前进行加锁操作,在访问完成之后进行解锁操作。按照“是否允许在同一时刻被多个线程持有”来区分,锁可以分为共享锁与独占锁。

        独占锁也叫排他锁、互斥锁、独享锁,是指锁在同一时刻只能被一个线程所持有。一个线程加锁后,任何其他试图再次加锁的线程都会被阻塞,直到持有锁线程解锁。通俗来说,就是共享资源某一时刻只能有一个线程访问,其余线程阻塞等待。

        如果是公平地独占锁,在持有锁线程解锁时,如果有一个以上的线程在阻塞等待,那么最先抢锁的线程被唤醒变为就绪状态去执行加锁操作,其他的线程仍然阻塞等待。Java 中的 Synchronized 内置锁和 ReentrantLock 显式锁都是独占锁。

        共享锁就是在同一时刻允许多个线程持有的锁。当然,获得共享锁的线程只能读取临界区的数据,不能修改临界区的数据。JUC 中的共享锁包括 Semaphore(信号量)、ReadLock(读写锁)中的读锁、CountDownLatch 等等。

12、悲观锁与乐观锁

        独占锁其实就是一种悲观锁,Java 的synchronized 是悲观锁。悲观锁可以确保无论哪个线程持有锁,都能独占式访问临界区。虽然悲观锁的逻辑非常简单,但是存在不少问题。

        悲观锁总是假设会发生最坏的情况,每次线程读取数据时,也会上锁。这样其他线程在读取数据时就会被阻塞,直到它拿到锁。传统的关系型数据库用到了很多悲观锁,比如行锁、表锁、读锁、写锁等。

悲观锁机制存在以下问题:

(1)在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。

(2)一个线程持有锁后,会导致其他所有抢占此锁的线程挂起。

(3)如果一个优先级高的线程等待一个优先级低的线程释放锁,就会导致线程的优先级倒置,从而引发性能风险。

        解决以上悲观锁的这些问题的有效方式是使用乐观锁去替代悲观锁。与之类似,数据库操作中的带版本号数据更新、JUC 包的原子类,都使用了乐观锁的方式提升性能。

13、AQS抽象同步器

        在争用激烈的场景下,使用基于 CAS 自旋实现的轻量级锁,会存在 CAS 恶性空自旋从而浪费大量的 CPU 资源。解决这种问题方案有两个:分散操作热点和使用队列削峰。JUC 并发包使用的是队列削峰的方案解决 CAS 的性能问题,并提供了一个基于双向队列的削峰基类——抽象基础类AbstractQueuedSynchronizer(抽象同步器类,简称为 AQS)。

        AQS 是 JUC 提供的一个用于构建锁和同步容器的基础类。JUC包内许多类都是基于 AQS 构建的,例如ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock、FutureTask 等。AQS 解决了在实现同步容器时设计的大量细节问题。

        AQS 队列内部维护的是一个 FIFO 的双向链表,这种结构的特点是每个数据结构都有两个指针,分别指向直接的前驱节点和直接的后继节点。所以双向链表可以从任意一个节点开始很方便地访问前驱节点和后继节点。每个节点其实是由线程封装的,当线程争抢锁失败后会封装成节点加入AQS队列中;当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的线程。

最后~

Java 高并发编程涉及的必知概念比较多。这里,总结和梳理了13个比较重要的概念,而且只做了简单的概念说明,并不涉及代码示例,底层实现原理等内容。

【不积硅步无以至千里,共同进步吧~】

猜你喜欢

转载自blog.csdn.net/qq_29119581/article/details/129271987