在springCloud gateway网关层解决跨域问题

mysql 学习之基础架构

  返回  

并发编程与常见的锁

2021/7/21 19:42:09 浏览:

并发编程与锁

线程,进程,并行,并发

线程是cpu调度的最小单位(是一个具体的执行单元)

进程是操作系统分配资源的单元

进程含有内存和资源并安置线程的地方

并行:同时做多件事情(多人同时做多事)

并发:同一时间段内做多件事(一个人在一个时间段内,一件一件做事情)

多线程

多线程是一个进程中允许有多个线程同时执行

优点:提高CPU的利用率,增强程序的功能

缺点:对硬件要求提高(CPU,内存,硬盘)

多线程产生的问题:多线程共同访问同一个共享资源,会出现线程安全问题

线程的创建方式

①继承Thread类

②实现Runnable接口

③实现Callable接口,对call()方法进行重写

相对于前两个可以抛出异常,有返回值

线程状态(生命周期)

在这里插入图片描述

多线程安全问题

产生原因:随着计算机硬件的发展,CPU逐渐由单核变为多核(因为现在的cpu是多核的,可以同时执行多个线程)。

经常在买票,取款,秒杀,团购中产生

主要问题:安全性,性能方面,产生死锁,可见性,原子性,有序向

解决方法:加锁,线程排队

守护线程

线程间通信

Java内存模型(JMM)

问题:

cpu—>内存—>IO(硬盘)三者之间读写速度有差别.为了平衡这三者的速度差异,做了如下优化:

cpu 提供了缓存

任务细化到线程, 切换执行

操作系统增加了进程、线程,以分时复用 CPU,进而均衡 I/O 设备与 CPU 的速度差异;

cpu对我们指令代码的顺序进行优化(重排),使得缓存能够得到更加合理地利用。

JMM

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

是一个完整的计算机的一个模型,因此这个模型自然也包含一个内存模型——又称为 Java 内存模型。

JMM 主内存与工作内存

Java 内存模型中规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程对变量的所有操作都

必须在工作内存中进行,而不能直接读写主内 存中的变量。

就像每个处理器内核拥有私有的高速缓存,JMM 中每个线程拥有私有的本地内存。

不同线程之间无法直接访问对方工作内存中的变量,线程间的通信一般有两种方式进行,一是通过消息传递,二

是共享内存。Java 线程间的通信采用的是共享内存方式,线程、主内存和工作内存的交互关系如下图所示:

在这里插入图片描述

并发编程核心问题–可见性,原子性,有序性

1.可见性(解决数据不可见问题)

不同线程中,有一个缓存,缓存要操作的变量,为了提升效率会等所有操作完成后,再讲数据存入主内存中,当数据还未写到主存时,其他线程对修改的数据才可见。

在这里插入图片描述

解决方案:volatile 关键字

一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰之后:

1.保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立

即可见的。 (操作中会将数据存入线程的缓存)

2.禁止进行指令重排序。

3.volatile 不能保证对变量操作的原子性。

2.原子性

此问题由线程切换带来(一行代码本来是不应该被拆分执行的,但是线程切换导致其他线程也会执行,最终导致结果与预期不同)

原子性是一个或多个操作在CPU执行的过程中不被中断的特征

原子性拒绝多线程交叉操作,同一时刻只有一个线程对它操作。
在这里插入图片描述

例如:

如 count++,至少需要三条 CPU指令。

指令1:首先,需要把变量 count 从内存加载到工作内存;

指令2:之后,在工作内存执行 +1 操作;

指令3:最后,将结果写入内存;

还是以上面的 count++ 为例。两个线程 A 和 B同时执行 count++,即便 count 使用 volatile 修辞,我们预期的结果值是2,但实际可能是 1。

在这里插入图片描述

解决方案1:加锁

锁是一种通用的技术方案,Java 语言提供的 synchronized 关键字,就是锁的一种实现。

**synchronized 是独占锁/排他锁,**synchronized 并不能改变 CPU 时间片切换的特点,只是当其他线程要访问这个资源时,发现锁还未释放,所以只能在外面等待。

synchronized 一定能保证原子性。synchronized 也能够保证可见性和有序性。

解决方案2:JUC–原子变量

下 J.U.C(java.util.concurrent 包),包含两个包一个是 locks 包,一个是 atomic 包,它们可以解决原子性问题。

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

AtomicXXX

案例:

一个简单的 i++可以分为三步:

1.读取 i 的值

0.计算 i+1

将计算出 i+1 赋给 i

这就无法保证 i++的原子性,即在 i++过程中,可能会出现其他线程也读取了 i

的值,但读取到的不是更改过后的 i 的值。

原子类原理(AtomicInteger 为例)

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

AtomicInteger 类中的 value 是有 volatile 关键字修饰的,这就保证了 value

的内存可见性,这为后续的 CAS 实现提供了基础。

低并发情况下:使用 AtomicInteger。

3.有序性

CPU在执行过程中可能会对我们的代码执行顺序做出优化,重新排序指令(要求是代码间无数据依赖:)导致结果出现问题。

在这里插入图片描述

在这里插入图片描述

4.总结

缓存导致可见性问题,线程切换导致原子性问题,编译优化代码顺序带来有序性问题。

CAS比较并交换

CAS 是乐观锁的一种实现方式,他采用的是自旋锁的思想,是一种轻量级的锁机制。

自旋锁:所谓自旋其实指的就是自己重试,当线程抢锁失败后,重试几次, 要是抢到锁了就继续,要是抢不到就

阻塞线程。说白了还是为了尽量不要阻塞线程

CAS 包含了三个操作数:

①内存值V

②预估值A (比较时,从内存中再次读到的值)

③更新值B (更新后的值)

当且仅当预期值 A==V,将内存值 V=B,否则什么都不做。

这种做法的效率高于加锁,当判断不成功不能更新值时,不会阻塞,继续获得 cpu 执行权,继续判断执行

在这里插入图片描述

缺点:

1.非阻塞式的,其他线程依然可以执行,还要自旋.导致cpu消耗比较高.

ABA问题

ABA 问题,即某个线程将内存值由 A 改为了 B,再由 B 改为了 A。当另外一个线程使用预期值去判断时,预期值

与内存值相同,误以为该变量没有被修改过而导致的问题。

ConcurrentHashMap

ConcurrentHashMap是安全的HashMap

HashTable也是安全直接将put()方法整个加锁

ConcurrentHashMap放弃了分段分页(将某个整体加锁),而采用CAS原则+synchronized

在put()时,先判断添加的数据节点是不是第一个节点,如果是,采用CAS原则(判断比较),加入数据,

Java中的锁

乐观锁/悲观锁

共享锁/独占锁

公平锁/非公平锁

可重入锁

读写锁

分段锁

偏向锁/轻量级锁/重量级锁

自旋锁

上面是很多锁的名词,这些分类并不是全是指锁的状态,有的指锁的特性,有的指锁的设计,下面总结的内容是对每个锁的名词进行一定的解释。

1.乐观锁/悲观锁

乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时

候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操

作是没有事情的。

悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,

也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观

的认为,不加锁的并发操作一定会出问题。

从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操

作非常多的场景,不加锁会带来大量的性能提升。

悲观锁在 Java 中的使用,就是利用各种锁。

乐观锁在 Java 中的使用,是无锁编程,常常采用的是 CAS 算法,典型的例子就

是原子类,通过 CAS 自旋实现原子操作的更新。

2.共享锁/独占锁

共享锁是指该锁可被多个线程所持有,并发访问共享资源。

独占锁也叫互斥锁,是指该锁一次只能被一个线程所持有。

对于 Java ReentrantLock,Synchronized 而言,都是独享锁。但是对于 Lock

的另一个实现类 ReadWriteLock,其读锁是共享锁,其写锁是独享锁。

读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。

独享锁与共享锁也是通过 AQS 来实现的,通过实现不同的方法,来实现独享或

者共享。

3.公平锁/非公平锁

**公平锁(Fair Lock)**是指在分配锁前,检查是否有线程在排队等待获取该锁,优先

将锁分配给排队时间最长的线程。

**非公平锁(Nonfair Lock)**是指在分配锁时不考虑线程排队等待的情况,直接尝试

获取锁,在获取不到时再排到队尾等待.

因为公平锁需要在多核的情况下维护一个线程等待队列,基于该队列进行锁的分

配,因此效率比非公平锁低很多.

对于 synchronized 而言,是一种非公平锁。

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

以可以使其变成公平锁。

在这里插入图片描述

4.可重入锁(递归锁)

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。

5.读写锁(ReadWriteLock

读写锁特点:

a)多个读者可以同时进行读

b)写者必须互斥(只允许一个写者写,也不能读者写者同时进行)

c)写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)

在这里插入图片描述

6.分段锁

分段锁并非一种实际的锁,而是一种思想,用于将数据分段,并在每个分段上都会单独加锁,把锁进一步细粒度化,以提

高并发效率

例如:jdk8以前的Concurrent HashMap

7.自旋锁( SpinLock)

自旋锁其实并不属于锁的状态,从 Mark Word (用于存储对象自身的运行时数据)的说明可以看到,并没有一

个锁状态叫自旋锁。所谓自旋其实指的就是自己重试,当线程抢锁失败后,重试几次,要是抢到锁了就继续,要是

抢不到就阻塞线程。说白了还是为了尽量不要阻塞线程。

由此可见,自旋锁是是比较消耗 CPU 的,因为要不断的循环重试,不会释放 CPU

**资源。**另外,加锁时间普遍较短的场景非常适合自旋锁,可以极大提高锁的效率。

锁的几种状态

偏向锁, 当一直只有一个线程,一直获取锁对象,此时对象头中的锁状态改为 偏向锁,并记录线程id,

同一个线程访问时,可以直接获取锁,效率高

轻量级锁

当第二个线程访问时,偏向锁状态 升级为 轻量级锁状态.

其他线程自旋,尝试获取锁,不会阻塞,提高效率 (线程数量较少)

**重量级锁 **

当前锁状态为轻量级锁时, 并发访问量增多,锁状态升级为重量级.其他线程进入到阻塞状态,不再自旋尝试.

Synchronized 实现

Synchronized 是 由 JVM 实 现 的 一 种 实 现 互 斥 同 步 的 一 种 方 式 , 被Synchronized 修饰过的程序块,译前

后被编译器生成了 monitorenter 和 monitorexit 两个字节码指令

在synchronized修饰的程序块前后会添加一个监视器(进入,退出),利用对象头来记录锁是否被使用.

获取锁,计数器+1, 退出监视器,释放锁,计数器-1

特点:使用一个唯一的对象,作为锁状态的标记.

ReentrantLock

在内部有一个锁的状态默认是0,如果有线程获取到了锁,将状态改为1.

其他线程有两种处理方式,公平锁和非公平锁.

**重量级锁 **

当前锁状态为轻量级锁时, 并发访问量增多,锁状态升级为重量级.其他线程进入到阻塞状态,不再自旋尝试.

Synchronized 实现

Synchronized 是 由 JVM 实 现 的 一 种 实 现 互 斥 同 步 的 一 种 方 式 , 被Synchronized 修饰过的程序块,译前

后被编译器生成了 monitorenter 和 monitorexit 两个字节码指令

在synchronized修饰的程序块前后会添加一个监视器(进入,退出),利用对象头来记录锁是否被使用.

获取锁,计数器+1, 退出监视器,释放锁,计数器-1

特点:使用一个唯一的对象,作为锁状态的标记.

ReentrantLock

在内部有一个锁的状态默认是0,如果有线程获取到了锁,将状态改为1.

其他线程有两种处理方式,公平锁和非公平锁.

如果使用公平锁,会将等待线程添加同步等待队列中.

联系我们

如果您对我们的服务有兴趣,请及时和我们联系!

服务热线:18288888888
座机:18288888888
传真:
邮箱:888888@qq.com
地址:郑州市文化路红专路93号