JAVA进阶--->并发编程
创始人
2024-05-29 13:28:04

文章目录

  • 线程回顾
  • 多线程
  • 并行与并发
  • 并发编程的根本原因
    • Java内存模型(JMM)
    • 多线程核心的根本问题
    • volatile关键字
    • 原子性
  • Java中的锁
    • synchronized锁
    • AQS(AbstractQueuedSynchronizer)
  • JUC常用类
    • ConcurrentHashMap
    • CopyOnWriteArrayList
    • CopyOnWriteArraySet
    • 辅助类CountDownLatch
  • 对象引用
  • 线程池
  • ThreadLocal

线程回顾

线程基本概念

​ 程序:静态的代码

​ 并发:一个CPU同时执行多个任务,指两个或多个事件在同一个时间段内发生。比如:秒杀、多个人做同一件事。

​ 并行:多个CPU同时执行多个任务,指两个或多个时间在同一时刻发生(同时发生)。比如:多个人同时做不同的事。

​ 进程:运行中的程序,被加载到内存中,是操作系统分配内存的基本单位,每个进程都有自己独立的一块内存空间,一个进程可以有多个线程(至少有一个)

​ 线程:线程是程序处理的基本最小单位,是cpu执行的单元,是CPU调度和分派的基本单位,它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

线程的创建方式

继承Thread 类重写run() 创建类的对象

​ 1.定义MyThread类继承Thread类

​ 2.重写run()方法,编写线程执行体

​ 3.创建线程对象,调用start()方法启动线程

​ 特点:编写简单,可直接操作线程,适用于单继承

实现Runnable接口 重写run() 任务 new Thread(任务)

​ 1.定义MyRunnable类实现Runnable接口

​ 2.实现run()方法,编写线程执行体

​ 3.创建线程对象,调用start()方法启动线程

​ 特点:避免单继承局限性,便于共享资源

​ 实现Callable接口 重写call() 有返回值,可以抛出异常

线程调度-------常用的方法

方法说明
setPriority()更改线程的优先级
sleep()在指定的毫秒内让当前正在执行的线程休眠
void join()等待该线程终止
yield()暂停当前正在执行的线程对象,并执行其他线程
interrupt()中断线程
isAlive()测试线程是否处于活动状态
run()如果这个线程使用单独的Runnable运行对象构造,则调用该Runnable对象的run方法; 否则,此方法不执行任何操作并返回。
start()导致此线程开始执行; Java虚拟机调用此线程的run方法

​ wait()

​ notify()

线程的生命周期(线程状态)

​ 创建---->start()----->就绪----->运行------->死亡

​ -----阻塞-----

​ 守护线程

多线程

什么是多线程

​ 一个程序中,支持同时运行多个线程

多线程的优点

​ 提高程序相应速度,提升硬件(CPU)利用率

多线程问题

​ 线程过多占内存

​ CPU需要处理线程,需要性能能够满足

​ 多线程访问同一个资源

并行与并发

并行:在同一个时间节点上,多件事情同时发生(真正意义的同时执行)

并发:在一段时间内,多件事情交替执行

并发编程:例如:买票,抢购,秒杀等场景,有大量的请求访问同一个资源。

​ 会出现线程安全的问题,所以需要通过编程来控制解决让多个线程依次访问资源,称为并发编程。

并发编程的根本原因

​ 单核CPU,线程是串行执行,操作系统中有一个叫任务调度器的组件,将CPU的时间片分给不同的线程使用,由于CPU在线程间的切换非常快,人类感觉是同时运行的。

​ 总结为一句话就是:微观串行,宏观并行,一般会将这种线程轮流使用 CPU

的做法称为并发,concurrent

​ 多核 CPU下,每个核(core)都可以调度运行线程,这时候线程可以是并

行的。

Java内存模型(JMM)

​ Java内存模型(Java Memory Model)是Java虚拟机规范的一种工作模式,规范了Java虚拟机与计算机内存是如何协同工作的。

Java主内存与工作内存

​ Java内存模型规定了所有的变量数据存储在主内存中,每条线程还有自己的工作内存,线程在操作变量时,会将主内存中的数据复制一份到工作内存,在工作内存中操作完成后,再写会到主内存中。线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存中的变量。

多线程核心的根本问题

基于java内存模型的设计,多线程操作一些共享的数据时,出现以下三个问题。

不可见性:多个线程分别同时对共享数据操作,彼此之间不可见,操作完成后写回主内存,有可能出现问题。一个线程对共享变量的修改,不能被另外一个线程立刻看到,如今的多核处理器,每个CPU内核都有自己的缓存,而缓存仅仅可以被自己所在的处理器内核可见,CPU缓存与内存的数据不容易保持一致。

无序性

​ 程序在执行过程中按照代码的先后顺序执行。

​ 为了性能,对一些代码指令的执行顺序重排,以提高速度。

​ int a = 从硬盘上实时读;

​ int b = 5;

​ int c = a+b;

非原子性

​ 线程切换带来原子性问题,原子的意思代表着-----“不可分”

​ 一个或多个操作在CPU执行的过程中不被中断的特性,称为原子性

​ 原子性是拒绝多线程交叉操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作

​ i++

​ int i = 0; 2 主内存

​ i++ i = 0 i = 1 工作内存

​ i++ i = 0 i = 1 工作内存

​ i++ 先从主内存加载数据到工作内存

​ 操作数据

​ 再从工作内存写回到主内存

总结

​ 缓存(工作内存)导致可见性问题,编译(指令重排)优化带来有序性问题,线程切换带来原子性问题。缓存、线程、编译优化的目的和我们写并发程序的目的是相同的,都是提高程序安全性和性能。但是技术在解决一个问题的同时必然会带来另外一个问题。

解决方法

让不可见变为可见

让无需变为----->不乱序/不重排/有序

非原子执行变为原子(加锁)由于线程切换执行导致

volatile关键字

volatile 修饰的变量被一个线程修改后,可以在其他线程中立即可见

volatile 修饰的变量,在执行过程中不会被重排序执行

volatile 不能解决原子性问题

volatile底层实现原理

​ 在底层指令级别来进行控制

​ volatile 修饰的变量在操作前,添加内存屏障,不让它的指令干扰

内存屏障:是一条指令,该指令可以对编译器(软件)和处理器(硬件)的指令重排做出一定的限制,比如,一条内存屏障指令可以禁止编译器和处理器将其后面的指令移到内存屏障指令之前。

​ volatile 修饰的变量添加内存屏障之外,还要通过缓存一致性协议(MESI)将数据写回到主内存,其他工作内存嗅探后,把自己工作内存数据过期,重新从主内存读取最新的数据

原子性

只有通过加锁的方式,让线程互斥执行来保证一次只有一个线程对共享资源访问

synchronized:关键字,修饰代码块、方法,自动获取锁,自动释放锁

ReentrantLock:类,只能对某段代码修饰,需要手动加锁,手动释放锁

​ synchronized是独占锁/排他锁,顾名思义有你没我,但是synchronized并不能改变CPU时间片切换的特点,要是有其他线程要访问这个资源,发现锁还没释放,就只能在外面等待。

​ synchronized一定能保证原子性,因为被synchronized修饰某段代码后,无论是单核CPU还是多核CPU,只有一个线程能够执行该代码。当然也能够保证可见性和有序性。

在Java中还提供一些原子类,在低并发情况下使用,是一种无锁实现,

​ 在JUC(java.util.concurrent)中,你可以见到java.util.concurrent.atomic和java.util.concurrent.locks。

​ 加锁是一种阻塞式方式实现,原子变量是非阻塞式方式实现

原子类的原子性是通过volatile+CAS实现原子操作的

AtomicInteger类中的value是有volatile关键字修饰的,这就保证了value的内存可见性

​ 低并发情况使用AtomicInteger

采用自旋思想

采用CAS机制(Compare-And-Swap)比较并交换

​ 第一次获取内存值到工作内存中,存储起来作为预期值,然后对象数据进行修改

​ 将工作内存中值写入到主内存,在写入之前需要做一个判断,用与气质与主内存中的值进行比较

​ 如果预期值与主内存中的值一致,说明没有其他线程修改,将更新数的值写入到主内存

​ 如果预期值与主内存中的值不一致,说明其他线程修改了主内存的值,这是就需要操作整个过程

在这里插入图片描述

概述:将第一次获取的值存入工作内存作为期望值,对数据进行修改,然后写入主内存,写之前判断与主内存中的值是否一致,一致直接写,不一致更新为新的值。

特点

  • 不加锁,所有的线程都可以对共享数据操作
  • 适合低并发使用
  • 由于不加锁,其他线程不需要阻塞,效率高

缺点:大并发时,不停自旋判断,导致CPU占用率高

ABA问题

​ 某个线程将内存值由A改为了B,再由B改为了A。当另外一个线程使用预期值进行判断时,与内存值相同,当前线程的CAS操作无法判断这个值是否发生过变化。

解决ABA问题可以设置版本号,每次操作改变版本号,可以进行判断

Java中的锁

synchronized:关键字,修饰代码块、方法,自动获取锁、释放锁

ReentrantLock:类,只能对某段代码修饰,需手动加锁、释放锁

Java中锁的名词

​ 每个名字并不都代表一个锁,有些锁的名字指的是锁的特性,锁的设计,锁的状态

乐观锁(不加锁):认为并发的操作,不加锁的方式实现是没有问题的,每次操作前判断(CAS,自旋)是否成立,不加锁实现。
悲观锁:认为对于同一个数据的并发操作,一定是会发生修改的,即使没有修改,也会认为修改。认为并发操作肯定会有问题,必须加锁,对于同一个数据的并发操作,悲观锁采用加锁的形式
总结:悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。

​ 悲观锁在Java中使用的话就是利用各种的锁

​ 乐观锁在Java中使用的话是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新

可重入锁:当一个线程获取到外层方法的同步锁对象后,可以获取到内部其他方法的同步锁,如果不使用,就容易出现死锁
在这里插入图片描述

读写锁(ReentrantReadWriteLock):支持读,写加锁,总的来说就是如果是读操作,就不加锁,如果一旦有写操作,那么读写互斥
特点:读读不互斥,读写互斥,写写互斥
加读锁是防止在另外的线程在此时写入数据,防止读取脏数据

​ 一个读写锁同时只能有一个写者或多个读者(与CPU数相关),不能同时有读者和写者

​ 在读写锁保持期间也是抢占失效的

​ 如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否者它必须自旋在那,直到没有读者或者写者。如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁。

分段锁(不是锁):是一种思想,将数据分段,并在每个分段上单独加锁,以此来将锁的粒度拆分,提高效率

自旋锁(不是锁):是一种自旋思想,是以自旋的方式重试获取,当线程抢锁失败后,重试几次,要是抢到锁了就继续,抢不到就阻塞线程,目的就是还是为了尽量不要阻塞线程。

​ 由此可见,自旋锁是比较消耗CPU的,因为要不断的循环重试,不会释放CPU资源。加锁时间普遍较短的场景非常适合自旋锁,可以极大提高锁的效率
共享锁/独占锁
​ 共享锁:读写锁中的读锁,该锁可被多个线程持有,并发访问共享资源
​ 独占锁:互斥锁,synchronized ReentrantLock都属于独占锁,一次之能被一个线程持有

公平锁/非公平锁
​ 公平锁:根据线程先来后到公平的获取锁,例ReentrantLock就可以实现公平锁,按照锁请求的顺序分配,拥有稳定获得锁的机会
​ 非公平锁:不按照锁请求顺序分配,没有先来后到,谁抢到谁获得执行权,synchronized 是非公平锁

​ ReentrantLock默认是非公平锁,但是底层可以通过AQS来实现线程调度,所以可以使其变成公平锁

synchronized锁

对象结构

​ 在Hotspot虚拟机中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充;

​ 对象头中有一块区域称为 Mark Word,用于存储对象自身的运行时数据,如哈 希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID 等等。

在synchronized锁的底层实现中,提供锁的状态,又来区别对待
这个锁的状态在同步锁的对象头中,有一个区域叫Mark Word中存储

偏向锁/轻量级锁/重量级锁(都是锁的状态):

这四种状态都不是Java语言中的锁,而是JVM为了提高锁的获取与释放效率而做的优化(使用cynchronized时)

​ 无锁
偏向锁
​ 一直是一个线程访问,线程ID被记录,快速获取锁
轻量级锁
​ 当锁状态为偏向锁时,又继续有其他线程访问,此时升级为轻量锁,没有获取到锁的线程,不会阻塞,继续不断尝试获取锁
重量级锁
​ 当锁的状态为轻量级锁时,线程自旋达到一定的次数,进入达到阻塞状态,锁状态升级为重量级锁,等待操作系统调度

synchronized锁的实现

​ synchronized锁是依赖底层编译后的指令来控制的,需要我们提供一个同步对象,来记录锁状态

​ 线程在进入synchronized代码块时候会自动获取内部锁,这个时候其他线程访问时会被阻塞,直到执行完或抛出异常或者调用了wait方法,都会释放锁资源,主内存把变量读取到自己工作内存,在退出时会把工作内存的值写入到主内存,保证原子性
​ synchronized基于进入和退出监视器对象来实现方法同步和代码块同步
​ 设置ACC_SYNCHRONIZED标记是否为同步方法,调用指令先检查是否设置,如果设置了,需要monitorenter表明线程进入该方法,使用monitorexit退出该方法
​ 在虚拟机执行monitorenter指令时,首先要尝试获取对象的锁,如果当前线程拥有了这个对象的锁,把锁的计数器+1,当执行monitorexit指令时将所得计数器-1,当计数器为0时,锁就被释放了
​ Java中synchronized通过在对象头设置标记,达到了获取锁和释放锁的目的

AQS(AbstractQueuedSynchronizer)

这 个 类 在 java.util.concurrent.locks 包下面。

AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效的构造出同步器,时JUC中核心的组件。

​ 抽象同步队列是JVC其他锁实现的基础

AQS实现原理
​ 在类中维护一个state变量,然后还维护一个队列,以及获取锁,释放锁的方法
​ 当线程创建后,先判断state的值,如果为0,则没有线程使用,将state=1,执行完成后将state=0,期间如果有其他的线程访问,state=1的话,将其他的线程放入到队列中

​ state由于是多线程共享变量,所以必须定义成volatile,用来保证state的可见性,同时虽然volatile能保证可见性,但不能保证原子性,所以AQS提供了对state的原子操作方法,保证了线程安全。

​ AQS中实现的FIFO队列是双向链表实现的

​ 可以通过getState(),setState(),compareAndSetState进行操作

​ 分别时获取锁状态,设置锁状态,使用CAS机制设置状态

AQS操作重点方法

​ acquire:表示一定能获取锁

​ tryAcquire:尝试获取锁,如果获取锁成功,那么tryAcquire(arg)为false,说明已经获取锁,不用参与排队,也就是不用再执行后续判断条件,直接返回。

​ addWaiter:尝试获取锁失败后,将当前线程封装到一个Node对象中,添加到队尾,并返回Node节点

​ acquireQueued:将线程添加到队列后,以自旋的方式去获取锁

​ release:释放锁

​ tryRelease:释放锁,将state值进行修改为0

​ unparkSuccessor:唤醒节点的后继者(如果存在)

AQS的锁模式分为:独占和共享

​ 独占锁:每次只能有一个线程持有锁,比如 ReentrantLock 就是以独占方式实 现的互斥锁。

​ 共 享 锁 : 允 许 多 个 线 程 同 时 获 取 锁 , 并 发 访 问 共 享 资 源 , 比 如 ReentrantReadWriteLock。

ReentrantLock锁的实现(等待精修):

​ ReentrantLock 是 java.util.concurrent.locks 包 下 的 类,ReentrantLock 基于 AQS,在并发编程中它可以实现公平锁和非公平锁来

​ ReentrantLock 类内部总共存在 Sync、NonfairSync、FairSync 三个类,

​ NonfairSync 与 FairSync 类 继 承 自 Sync 类 , Sync 类 继 承 自 AbstractQueuedSynchronizer 抽象类。

对共享资源进行同步,同时和 synchronized 一样,

​ ReentrantLock 支持可重入,

除此之外,ReentrantLock 在调度上更灵活,支持更多丰富的功能。

​ NonfairSync 类继承了 Sync 类,表示采用非公平策略获取锁,其实现 了 Sync 类中抽象的 lock 方法.

​ 非公平

NonfairSync
final void lock() {if (compareAndSetState(0, 1))//线程来到后,直接尝试获取锁,是非公平setExclusiveOwnerThread(Thread.currentThread());else//获取不到acquire(1);
}

​ FairSync 类也继承了 Sync 类,表示采用公平策略获取锁,其实现了 Sync 类中 的抽象 lock 方法.

公平实现

FairSync
final void lock() {acquire(1);
}

JUC常用类

ConcurrentHashMap

​ HashMap:多线程会报错
​ Hashtable:效率低

​ ConcurrentHashMap:不支持存储null键和null值,为了消除歧义

​ 原因:无法分辨是key没找到的null还是有key值的null,在多线程里面这是模糊不清的

​ ConcurrentHashMap对多线程操作,介于HashMap与Hashtable之间。内部采用“锁分段”机制(jdk8弃用了分段锁,使用CAS+synchronized)替代了Hashtable的独占锁,进而提高了性能

放弃分段锁的原因

1。加入多个分段锁浪费内存空间。

2。生产环境中, map 在放入时竞争同一个锁的概率非常小,分段锁反而

会造成更新等操作的长时间等待。

​ 因此jdk8放弃了分段锁而是用了Node锁,减低锁的粒度,提高性能,并使用CAS操作来确保Node的一些操作的原子性,取代了锁。put时首先通过哈市找到对应链表后,查看是否第一个Node,如果是,直接用CAS原则插入,无需加锁

如果不是链表第一个Node,则直接用链表第一个Node加锁,这里加的锁是synchronized

CopyOnWriteArrayList

​ 使用ArrayList:线程不安全,在高并发情况下可能会出现问题
​ 使用Vector可以解决线程安全问题,还有Collections中的synchronizedList可以解决
​ 使用Vector效率低,原因是get()方法也加了锁,读操作多的情况下,效率低。
​ 使用CopyOnWriteArrayList:
​ 可以在读的时候不加锁,写的时候加锁,提高读的效率。实现过程在进行add,set等修改操作时,先将数据进行备份,对备份数据进行修改,之后将修改后的数据赋值给原数组。

CopyOnWriteArraySet

基于CopyOnWriteArrayList,不能存储重复数据

辅助类CountDownLatch

​ 构造器需要输入一个计数器(数字),这个数字指的是线程的总量,操作一个线程执行完后执行另一个线程。

​ 允许一个线程等待其他线程各自执行完毕后再执行。底层实现是通过AQS来完成的,创建CountDownLatch对象时指定一个初始值是线程的数量。每当一个线程执行完毕后,AQS内部的state就-1,当state的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了

对象引用

强引用:即对象由引用指向的。Object obj=new…这种情况下new出来的对象不能进行垃圾回收。

软引用、弱引用、虚引用都可以在一定条件下被回收,强引用是造成Java内存泄漏的主要原因之一。这三者都是用来标记对象的一种状态。
当一些对象称为垃圾后,还需要有不同的状态,可以继承SoftReference,WeakReference,PhantomReference或者把自己的对象添加到软、弱、虚的对象中

软引用:如果内存充足的情况下,可以保留软引用的对象,如果内存不足,经过一次垃圾回收之后任然不够,那么将清除软引用的对象。

弱引用(本身已经是垃圾对象):弱引用管理的对象只能存活到下一次垃圾回收

虚引用:和没有任何引用一样,只是为了系统的检测。虚引用必须和引用队列一起使用,在创建时必须提供一个队列

线程池

数据库连接池

池的概念
我们以前使用线程时就去创建一个,这样实现非常简便,但是如果并发线程数量很多,并且每个线程都是执行一个很短的任务就结束了,频繁的创建数据库链接对象、销毁,在时间上开销较大
集合
Java中使用线程池解决此问题,线程池每一个线程代码结束后,不会死亡,二十再次回到线程池中成为空闲状态,等待下一个对象来使用。事先创建出一些连接对象,每次使用时,从集合中直接获取,用完不销毁,减少频繁创建、销毁。

​ 在jdk5之前,需手动实现自己的线程池,jdk5之后提供线程池的实现

​ 使用ThreadPoolExecutor类实现线程池的创建管理

​ 池的好处:减少频繁创建销毁时间,统一管理线程,提高速度

使用ThreadPoolExecutor类实现线程池创建管理
构造器中的七个参数

corePoolSize:核心线程池大小

maximumPoolSize:线程池最大数量

keepAliveTime:非核心线程池中的线程,在多久没有任务执行时,就终止

unit:为keepAliveTime设定单位

workQueue:一个阻塞队列,用来存储等待的任务

ArrayBlockingQueue有界的阻塞队列,必须给定最大容量

threadFactory:线程池工厂

handler:拒绝策略,核心线程池,阻塞队列,非核心线程池已满,继续有任务,如何执行

​ AbortPolicy(): 抛出异常,拒绝执行

​ DiscardOldestPolicy(): 丢弃等待时间最长的任务

​ DiscardPolicy(): 直接丢弃,不执行

​ CallerRunsPolicy(): 交由当前提交任务的线程执行

​ execute() 提交任务,没有返回值和 submit() 提交任务,可以有返回值

​ 关闭线程池

​ shutdownNow() 直接关闭

​ shutdown() 不再接收任务,等待任务执行完关闭

ThreadLocal

​ 在做项目时候,想要每个人进入这个项目是独立的,以前是每个人访问的话都是访问的想同的变量,需要每个人独立访问这个变量。

本地线程变量,可以为每一个线程创建一个变量副本,使得多个线程之间相互隔离不影响。

ThreadLocal底层实现

​ 为当前线程创建一个ThreadLocalMap,唯一的ThreadLocal对象作为key

ThreadLocal内存泄露问题

因为ThreadLocal与弱引用有关,key失效后,value还被强引用着,造成内存泄漏

正确用法,用完之后,及时调用remove()清除

相关内容

热门资讯

脑机接口遇到音乐治疗,AI真能... 志愿者体验“央音一号”。受访者供图 在走进中央音乐学院“央音一号”实验室之前,中青报·中青网记者对脑...
伊朗警告:若遭攻击必将还击 据外媒报道,伊朗议长卡利巴夫11日说,如果美国对伊朗发动打击,伊朗将把以色列以及美国在中东地区的军事...
SpaceX再部署7500颗星... 来源:@央视财经微博 【#SpaceX再部署7500颗星...
商络电子:向不特定对象发行可转... 商络电子公告,公司于2026年1月9日收到深圳证券交易所出具的《关于受理南京商络电子股份有限公司向不...
王毅原定访问索马里计划推迟 中... 新京报讯 据中国驻索马里使馆消息,有媒体报道,中共中央政治局委员、外交部长王毅原定1月9日访问索马里...