并发 编程

并发编程

并行与并发

并发(concurrent): 是同一时间应对多件事情的能力(串行)。

并行(parallel): 是同一时间动手做多件事情的能力。

程序,进程,线程

程序 (program): 是为了完成特定的任务,用某种语言编写的一组指令的集合. 即指一段静态事物代码.

进程 (process): 就是正在执行的程序,从Windows角度讲,进程是含有内存和资源并安置线程的地方(操作系统分配资源的单元,运行中的程序)

线程 (thread): 进程可进一步细化为线程,是cpu调度的最小单位,一个具体的执行单元

进程与线程的关系

一个进程可以包含多个线程,一个线程只能属于一个进程,线程不能脱离进程而独立运行,所以线程共享进程资源

java中的main就是主线程

在主线程中,可以创建并启动其他线程

创建线程

方式1 : 继承thread类,重写run()方法,将在进程中执行的任务写在run方法中

如果我们重写了run方法,当线程启动时,它将执行run方法

//在主线程中创建并启用其他线程
        ThreadDemo t = new ThreadDemo1();
                   t.start();//启动进程

方式2 : 实现Runnable接口,创建一个Thread对象来启动

 //创建了一个线程要执行的任务
        ThreadDemo t = new ThreadDemo();
        //创建线程,并将线程中需要执行的任务添加到线程中
        Thread thread = new Thread(t);
               thread.start();//启动线程

方式3: 实现callable接口, call();可以抛异常,有返回值


**CPU在一个时间节点上只能处理一个线程,CPU速度非常快,可以交替执行**

## 线程中用到方法

```java
 run()//编写线程中需要执行的任务代码
 start()//启动线程
 
 //Thread()类的构造方法
Thread(Runnable target)//利用Runnable对象创建一个线程,启动时将执行对象的run方法
Thread(Runnable target,String name)//利用Runnable对象创建一个线程,启动时将执行对象的run方法,为线程命名
 Thread.currentThread()//获得当前正在执行的线程
 getName();//返回线程名称
 setName();//设置线程名称
 getPriority();//返回线程的优先级
 setPriority();//设置线程的优先级

线程优先级

java中线程的优先级,用整数1~10表示

默认情况下,线程的优先级为5

getPriority();//返回线程的优先级
setPriority();//设置线程的优先级

优先级影响CPU的执行权

CPU的执行策略:

  1. 时间片:等同于排队,先来先执行
  2. 抢占式:对优先级高的先执行

Thread类有如下3个静态常量来表示优先级:

  • MAX_PRIORITY:取值为10,表示最高优先级。
  • MIN_PRIORITY:取值为1,表示最底优先级。
  • NORM_PRIORITY:取值为5,表示默认的优先级

线程状态

新建 : 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态.

就绪 : 处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配带CPU资源

运行 : 当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能

阻塞 : 在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态

死亡 : 线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束

在这里插入图片描述

多线程

多线程:在一个进程中,同时拥有多个线程,执行多个任务

多线程的应用场景和安全问题:买票,取款,秒杀,抢购

多线程能解决什么问题?

​ 程序需要同时执行多个任务(任务都是各自独立的)

多线程优点:

​ 提高了cpu的利用率,增强了程序的功能。

多线程缺点:

​ 对硬件(cpu,内存,硬盘)要求提高了

​ 多线程访问同一个共享资源,可能会出现线程安全情况

​ 解决方法:加锁,排队

Java内存模型

cpu—内存—I/O

三者之间读写速度有差别 cpu >> 内存 >> I/O

为平衡三者的速度差异:

  1. ​ cpu增加了缓存,均衡cpu与内存的速度差异
  2. ​ 操作系统增加了进程,线程 分时复用cpu,均衡cpu与I/O设备的差异
  3. ​ 编译程序优化指令执行次序,使缓存能够更加合理的利用

并发编程核心问题

可见性

定义:当多个线程访问一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值。

	在多线程环境下,一个线程对共享变量的操作默认情况下对其他线程是不可见的。Java提供了volatile
	来保证可见性,当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后
	他会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。当然,synchronize
	和Lock都可以保证可见性。synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代
	码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

解决:Volatile关键字修饰

​ 1.Volatile修饰后的变量在一个线程操作后,对其他线程立即可见

​ 2.Volatile修饰后的变量,禁止重排序

​ 3.Volatile不能解决原子性问题

原子性

定义:即一个操作或者多个操作要么全部执行,要么就都不执行,并且执行的过程不会被任何因素打断

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

CPU能保证的原子操作是CPU指令级别的,而不是高级语言的操作符。线程切换导致了原子性问题

解决: 为了保证原子性

​ 方式一:加锁:互斥(同一时刻,只能有一个线程执行)

​ Synchronized(互斥锁,独占锁)可以保证在一个线程执行时,其他线程不能操作共享数据,

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

​ synchronized一定能保证原子性,因为被synchronized修饰某段代码后,无论是单核CPU还是多核CPU,只有一个线程能够执行该代码,所以一定能保证原子操作.

​ Synchronized锁同时保证原子性,可见性和有序性

​ 方式二:JUC(java.util.concurrent包)原子变量

有序性

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

cpu在执行过程中,可能会对我们的代码执行顺序做出优化,重新排列指令。

有序性指的是程序按照代码的先后顺序执行。

编译器为了优化性能,有时候会改变程序中语句的先后顺序。

Java内存模型中,允许编译器和处理器对指令进行重排序,当然重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性.

解决:1.volatile禁止重排序

​ 2.通过Synchronized和Lock来保证有序性(Synchronized和Lock保证每一时刻有一个线程执行同步代码,相当于按线程顺序执行同步代码,自然保证了有序性)

总结

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

Volatile

是java提供的一种轻量级的同步机制。它不会引起线程上下文的切换和调度

  • 保证了变量的可见性,不保证原子性
  • 禁止指令重排

volatile原理:

​ 加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

(1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

(2)它会强制将对缓存的修改操作立即写入主存;

(3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

volatile不适用的场景:

​ 不适合复合操作

​ 解决:

​ 1.加Synchronized或Lock

​ 2.采用原子操作类(原子操作类是通过CAS循环的方式来保证其原子性的)

JUC

​ JUC(java.util.concurrent包)

JUC包里的Lock包和Atomic包,可以解决原子性问题

​ 加锁是一种阻塞式方式实现

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

Atomic实现原理

案例:

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

​ 1.读取i的值

​ 2.计算出i+1

​ 3.将计算出的i+1赋给i

​ 这样就无法保证了i++的原子性,即在i++过程中,可能会出现其他线程也读取了i,但读取到的并不是更改后的i的值

原子类原理(AtomicInteger为例):

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

​ 2.AtomicInteger类中的value是由volatile关键字修饰的,这保证了value的内存可见性,这为后续的CAS实现提供了基础

​ 3.适用于低并发的情况下

CAS

​ CAS(Compare-And-Swap):比较并交换,该算法是硬件对于操作系统的支持,是一种无锁实现的非阻塞式,适合低并发(缺点)

​ CAS采用乐观锁+自旋锁

​ 每次先将数据从主内存加载到工作内存 V(内存值)

​ 再次从主内存中读取值 A(预期值,比较时从内存中再次读到的值)

​ 当V==A时(表示在此期间没有线程再对数据进行修改),主内存数据没有其他线程修改,此时就把计算后的数据B写入到主内存中 B(更新值,更新后的值)

在这里插入图片描述

优点: 效率高于加锁,且不会阻塞,当判断不成功时,继续获得cpu执行权,继续判断执行

缺点:

缺点一:使用自旋锁的方式,由于该锁不会阻塞,不断地自旋会导致cpu的高消耗,当并发量较大时,容易导致cpu跑满

缺点二: ABA问题:先将内存值,由A改为B,再由B改为A,然后拿预期值和内存值比较发现相等,误以为没有修改过。

解决:可以通过添加版本号来避免ABA问题

​ 如原先的内存值为(A,1),线程将(A,1)修改为了(B,2),再由(B,2)修改为(A,3)。此时另一个线程使用预期值(A,1)与内存值(A,3)进行比较,只需要比较版本号1和3,即可发现该内存中的数据被更新过了。

concurrentHashMap

ConcurrentHashMap同步容器类是Java5增加的一个线程安全的哈希表

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

​ 此包还提供了设计用于多线程上下文中的Collection实现:ConcurrentHashMap,ConcurrentSkipListMap,ConcurrentSkipListSet.CopyOnWriteArrayList和CopyOnWriteArraySet。当期望许多线程访问一个给定collection时,ConcurrentHashMap通常优于同步的HashTableConcurrentSkipListMap通常优于同步的TreeMap。当读数和遍历远远大于列表的更新数时,CopyOnWriteArrayList优于同步的ArrayList

放弃分段锁的原因

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

​ 2.生产环境中,map在放入时竞争同一个锁的概率非常小,分段锁反而会造成更新等操作的长时间等待。

​ 3.为了提高GC的效率jdk8放弃了分段锁而是用了Node锁,减低锁的粒度,提高性能,并使用CAS操作来确保Node的一些操作的原子性,取代了锁。

现在的ConcurrentHashMap机制

​ 在put时,先使用CAS原则判断是否是第一个Node,如果是则直接添加到第一个结点

​ 如果不是第一个Node,则在第一个Node上添加Synchronized的锁,保证安全,提高效率

HashTable是线程安全的,直接给整个put方法加锁,锁粒度大,效率低

concurrentHashMap也是线程安全的哈希表,采用cas+锁的机制,保证安全,提升了效率

Java中的锁

锁分类

乐观锁

采用CAS原则,更新数据时,进行判断,是不加锁的实现(原子类)

​ 适用于读操作非常多的场景。

认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。

悲观锁

采用加锁更新数据(Synchronized & Lock)

​ 适用于写操作非常多的场景。

认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。

​ 乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度

公平锁

Fair Lock

等待锁的线程按顺序排队,一旦锁释放,排的时间最长的先获得锁

非公平锁

Nonfair Lock

不用排队,所释放后,那个线程抢到了,先获得锁( ReentrantLock默认是非公平锁 & Synchronized)。

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

​ 因为公平锁需要在多核的情况下维护一个线程等待队列,基于该队列进行锁的分配,因此效率比非公平锁低很多

在这里插入图片描述

可重入锁

(又名递归锁)当线程获取外层方法锁对象时,依然可以进入到内层同步方法中,获得内层同步方法锁,否则就会出现死锁问题(ReentrantLock & Synchronized)。

在这里插入图片描述

读写锁

ReadWriteLock,ReentrantReadWriteLock

​ 特点:1.多个读者可以进行同时读

​ 2.写数据时,只能有一个执行(也不能读写同时操作)

​ 3.写优先于读(一旦开始写,则后续的读操作必须等待,唤醒时优先考虑写)

在这里插入图片描述

分段锁

例如JDK8以前的ConcurrentHashMap,并不是一种锁,而是一种思想,提高了锁的粒度化,提升了并发效率

自旋锁

不断重试去抢占cpu,不会阻塞线程,但是数量很多时,对cpu的消耗增大

共享锁

多个线程共享一把锁,(读写锁中的读锁)

独占锁

一次只能有一个线程持有的锁(Synchronized & ReentrantLock & 读写锁中的写锁)

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

AQS

​ AQS(AbstractQueuedSynchronizer)抽象的队列式同步器,这个类在Java.util.concurrent。locks包

​ 没有获得到锁的其他线程,把他们加入到一个队列里(阻塞状态,cpu不会加载,减少cpu开销)当锁被释放后,队列里的第一个线程获得锁(按排列顺序获得锁)

AQS的核心思想:这个机制AQS是用CLH队列锁 (CLH同步队列是一个FIFO双向队列,AQS依赖它来完成同步状态的管理) 实现的,将暂时获取不到锁的线程加入到队列中。 AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。

在这里插入图片描述

锁的几种状态

无锁状态
偏向锁

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

同一个线程访问时,可以直接获取锁,效率高(现实中这种情况很少)

轻量级锁

第二个线程来访问时偏向锁状态升级为轻量级锁状态,其他线程(线程数量较少)通过自旋尝试获取锁,提高性能,不会阻塞,提高效率。

重量级锁

当前锁状态为轻量级锁时,并发访问量增多(线程较多),锁状态升级为重量级锁,其他线程进入阻塞状态,不再尝试自旋(cpu开销降低,但性能也降低了)。

​ 锁的状态是通过对象监视器在对象头中的字段来表明的。四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级。

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

Synchronized锁实现

Synchronized的实现依赖于JVM指令monitorenter和monitorexit。

​ 在Synchronized修饰的程序块前后会添加一个监视器(进入,退出)利用对象头来记录锁是否被使用,(计数器默认情况下为0)获取锁时计数器+1,退出监视器释放锁时计数器-1。

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

java中Synchronized通过在对象头中设置标记,达到获取锁和释放锁的目的。
在这里插入图片描述

Java对象头

​ 在Hotspot虚拟机中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充;Java对象头是实现synchronized的锁对象的基础,一般而言,synchronized使用的锁对象是存储在Java对象头里。它是轻量级锁和偏向锁的关键

在这里插入图片描述

Mark Word

​ MarkWord用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit),下面就是对象头的一些信息:

在这里插入图片描述

ReentrantLock

ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁

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

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

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

Synchronized和Lock的区别

synchronized是托管给JVM执行的,而lock是java写的控制锁的代码

Synchronized:自动锁,可以添加到方法和代码块上

Lock:手动锁:获取锁:lock();

​ 释放锁:unLock();

​ 只能添加到代码块上

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值