java 并发编程

JUC编程

一、并发编程中的挑战

1、上下文切换

​ 在多线程编程中,操作系统是利用时间片让多条线程抢占cup,来执行的,当每条线程的时间片执行完毕后,就要切换到其他就绪等待的线程,当下次该线程得到时间片并抢占cpu时,cup要接着上次执行的点,继续执行该线程,这就需要系统要保存每条线程执行的上下文,并在执行时完成上下文的切换,但这是需要耗费系统资源,并需要时间的;而大量的上下文不仅消耗系统资源,而且会拖慢系统执行的效率;所以并不是线程数越多越好;

​ 开发者需要判断:

  • 执行的任务需不需要使用多线程
  • 合理的创建线程

2、死锁

​ 多线程并发,当线程需要对系统的共享资源进行操作时,为了避免数据的丢失、数据的紊乱等,需要同步处理,即每次只能允许一个线程访问共享资源;在使用同步时,需要使用锁机制,而不合理的使用锁,不但不会保证系统更加健壮,反而会造成死锁问题

3、资源的限制

​ 加快代码执行的速度,将串行的代码,使用多个线程并发执行,而系统资源是有限的,不能无限制创建线程,来并发执行代码,如cup的核数、网络带宽、内存等,当超过系统所能承载的线程数,其他的线程还是时间片抢占cup,所以实际上还是串行的,并且多了上下文的系统消耗,系统性能更慢;

硬件资源的限制

​ 考虑集群

软件资源的限制

​ 池化技术,进行资源共享,如:线程池、数据库连接池等

二、线程创建

三种方法:

  • 继承Thread类
  • 实现runnable接口
  • 实现Callable接口的Call

callable的实现是由返回值的,可用Future对象的get方法接收

三、JAVA的内存模型

image-20201112175409322

​ 如图JAVA的内存模型(JMM),在并发编程中,在多个线程访问共享资源或者变量时,是将共享变量(通过四组操作)拷贝到各自的本地内存中进行修改,修改完成后再刷新回主内存,而这个过程早些时候的java版本中,其他线程是不可见的,也就是说,其他线程是不知道该共享变量已经发生变化;

四、volatile

​ volatile是轻量级的synchronized

作用:

  • 用volatile修饰的共享变量,保证了所有线程的可见性,保证所有线程看到的共享变量值是一致的

    在加锁前,线程需要读取最新的值到本地内存中

    在解锁前,将结果刷回主内存

    加锁、解锁是同一把锁

  • 不保证原子性

    即执行要么成功,要么失败,不可再中途打断

  • 禁止指令重排

    什么是指令重排?

​ 在java虚拟机中,java的源码被编译字节码,解释器、编译器又将其转换为机器码,在执行过程中会对指令执行顺优化,进行重排

有一下几种重排序:

  • 编译器优化的重排序
  • 指令级并行的重排序(不存在数据依赖情况下)
  • 内存系统的重排序

在这里插入图片描述

如何保证原子性(不使用锁)

使用原子类

image-20201112181507768

这些类都和操作系统底层直接挂钩,再内存中修改值

五、锁机制

4.1、synchronized

是java提供的关键字

在jdk1.6之前,synchronized是jvm级别的重量级锁,在1.6之后做了优化,不那么重量了

使用synchronized实现同步,有以下几种方式

  • 普通同步方法 锁的是当前的实例对象

  • 静态同步方法 锁的是当前类的class对象

  • 同步代码块 锁的是括号里的对象

    代码块同步使用的是monitorenter、monitorexit;在同步代码块的首尾会自动插入这两种指令实现同步,而且指令一一对应

在使用synchronized进行同步时,要注意锁的对象要一致,即使用同一把锁

注意:八锁现象

4.2、lock

Lock锁是JUC包下的接口

image-20201112184314397

lock具有和synchronized相同的同步功能,具备了多种synchronized所不具备的功能,只是需要显示的加锁、释放锁

lock锁的实现依靠队列同步器

通过判断、设置当前的同步状态来实现同步

使用CAS(compareAndState)设置当前状态,并保证原子性

同步状态(就是锁):

  • 独占式状态
  • 独占式释放状态
  • 共享式状态
  • 共享式释放状态
  • 是否被当前线程独占

独占:同一时刻,只能有一个线程获得到锁,而其他获取锁的线程只能处于同步队列中等待,只有获得锁的线程,释放了锁,后继的线程才能获得锁

在使用lock锁时,同步队列器中维护了一个同步队列,线程在tryAcquire中通过CAS获取同步状态,tryRelease将同步状态设置为0,即恢复到独占使释放状态,此时其他线程可尝试去获得独占锁,获取同步状态失败,会被加入到同步队列中等待

什么是CAS?

CAS : 比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么则执行操作!如果不是就 一直循环!

缺点: 1、 循环会耗时

​ 2、一次性只能保证一个共享变量的原子性

​ 3、ABA问题

什么是ABA问题

在多线程并发中,线程在本地内存中将共享变量,进行了修改,在刷回主内存时又改了了回去,其他线程是不知道的

解决ABA问题的办法:使用原子引用

4.3、原子引用

思想:与乐观锁原理相同

理解为:为共享变量设置了版本号,在修改后对应的版本号+1

使用:AtomicStampedReference对象

4.4、同步队列

1.什么是同步队列?

一个先入先出的双向队列

2.基于同步队列的lock原理

获取同步状态失败后,同步器将当前线程阻塞,把其信息以及线程状态等构成一个节点加入到同步队列,释放时又将其唤醒,在此尝试获取同步状态

既然同步队列是节点构成(其实就是双向链表),便具有前驱节点、后继节点

且没有获得同步状态的线程会使用基于CAS的方法,保证线程安全,从队尾加入队列,同步器中有两个地址应用(一个队列头、一个队列尾)

每次只有头节点会获得同步状态,头节点释放后,会唤醒后继节点,后继节点会判断自己的前驱是不是头节点,如果是后继节点尝试获得同步状态后,将自己设置为头节点

4.5、lock锁与synchronized的区别

  • synchronized是关键字,lock是juc下的类
  • synchronized是隐式锁,会自动释放锁,lock是显式锁,必须手动加锁、解锁否则陷入死锁
  • synchronized无法判断锁的状态,lock可判断是否获得了锁
  • synchronized获得锁,阻塞后,其他线程只能等,lock锁不一定,比如异常后,自动释放锁
  • synchronized可重入锁,非公平锁,不可中断,lock可重入锁,默认非公平锁,也可自己设置
  • synchronized适合少量代码,lock可适合大段代码

4.6、读写锁

image-20201112194303533

在这里插入图片描述

读锁是共享锁,写锁是独占锁

  • 允许多个线程同时读,只能由一个线程同时写
  • 加了读锁,写操作就会阻塞
  • 加了写锁,读操作就会阻塞,避免脏读

image-20201112195321025

4.7、可重入锁

获得锁的线程可继续加锁,如同步方法的嵌套等

不可重入锁,线程只能获取一次锁,第二次加锁线程便会阻塞

在lock锁进行重入时,是将同步状态+1,在释放锁时,将同步状态-1,在同步状态为0时,恢复为释放状态

4.8、公平/非公平锁

公平锁:按顺序执行,每次等待时间最长的线程执行

非公平锁:不按顺序执行,可插队

非公平锁的效率比公平锁的效率高

刚刚执行完的线程有机会重新抢占cup继续执行,相比省略了线程的上下文切换

4.9、偏向锁

锁的级别:

无锁状态------>偏向锁------>轻量级锁------->重量级锁

会随着竞争情况逐渐升级,锁会升级,但不会降级(为了提高获得锁与释放锁的效率)

级别越高,获得锁的代价就越高

偏向锁,级别最低的锁,使线程获得锁的代价最低

研究:大多数情况下,不存在多线程竞争锁,而是总是一个线程获得锁,因此使用高级别的锁,代价越高

在对象头、栈帧里存放着锁偏向的线程id,在进出同步块时,只需要判断一下对象头中有无指向当前线程的偏向锁,如果有,获取锁成功,否则失败;如果失败判断当前是否是偏向锁,不是,使用cas竞争,是,使用cas将偏向锁指向当前线程

竞争出现时,才会释放偏向锁;

首先暂停拥有偏向锁的线程,判断持有偏向锁的线程是否在获得,不活动,将对象头设置为无锁状态;活动,拥有偏向锁的栈会被执行,要么恢复到无锁状态或者标记为不合适作为偏向锁,最后唤醒其他线程;

六、并发容器和框架

5.1、并发容器

juc报下提供了大量的并发容器
在这里插入图片描述

在java的util包下提供的集合容器大多是线性不安全的,在并发编程中,使用线性不安全的容器会发生并发修改异常,或者数据丢失

在并发中使用容器由以下几种选择:

  1. 使用collecions工具类将普通容器转换成线性安全的
  2. 使用java提供的并发容器
  3. 使用vector,hashtable等容器,但效率较低

5.2、队列

在这里插入图片描述

中有7个阻塞队列

4中处理方式:

  • 抛出异常:队列满\空时,再加入\删除时会抛出异常
  • 返回特殊值:队列满\空时,再加入\删除时会返回true或false
  • 一直阻塞:队列满\空时,再加入\删除时一直阻塞
  • 超时退出:队列满\空时,再加入\删除时会阻塞一段时间,如果超过设定时间,就会退出
方法\处理方式抛出异常返回特殊值一直阻塞超时退出
插入方法addofferputoffer
移除方法removepolltakepoll
检查方法elementpeek--

5.3、Fork\join框架

在这里插入图片描述

说白了,所谓的Fork\join框架就是一种分而治之的思想

  • Fork:将任务分解为多个小任务
  • join:将分解的多个小任务的结果进行聚合

再如今的大数据场景中有许多类似的框架:

如MapReduce、Flink、spark等

七、线程池

三大方法、7大参数、4种拒绝策略

6.1、线程池的执行流程

在这里插入图片描述

6.2、三大方法

image-20201112213215236

  • ThreadPoolExecutor

    • CachedThreadPool

      大小无界限的线程池

    • FixedThreadPool

      创建固定线程数的线程池

    • SingleThreadPoolExecutor

      创建单个线程数的线程池

  • ScheduledThreadPoolExecutor适应周期任务

    • ScheduledThreadPoolExecutor

      创建若干

    • SingleThreadScheduledExecutor

      创建一个

6.3、七大参数

image-20201112214706609

image-20201112214730806

根据源码分析和阿里巴巴java参考书册,在使用线程池时最好手动创建线程池,负责可能会导致OOM

八、死锁问题排查

1、使用 jps -l 定位进程号

2、使用 jstack 进程号 找到死锁问题

3、使用第三方可视化工具

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值