Java多线程与并发-原理

目录

9-1 synchronized

9-2 synchronized底层实现原理

9-3 Synchronized和ReentrantLock的区别

9-4 Java内存模型JMM的内存可见性

9-5 CAS

9-6 Java线程池


9-1 synchronized

线程安全问题的主要诱因

  • 存在共享数据(临界资源)
  • 存在多条线程共同操作这些共享数据

解决问题的根本方法:

  • 同一时刻有且只有一个线程在操作共享数据,其他线程必须等待该线程处理完数据后再对共享数据进行操作。

互斥锁的特性

  • 互斥性---即在同一时间只允许一个线程持有某个对象的锁,通过这种特性来实现多线程的协调机制,这样同一个时间内只有一个线程对有需要的代码块进行访问。也称为原子性
  • 可见性---必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一线程时可见的。

synchronized锁的不是代码,锁的是对象

根据获取的锁的分类:获取对象锁和获取类锁

获取对象锁的两种方法

  1. 同步代码块(synchronsized(this),synchronized(类实例对象)),锁是小括号()中的实例对象
  2. 同步非静态方法(synchronized method),锁是当前对象的实例对象

获取类锁的两种用法

  1. 同步代码块(synchronized(类.class)),锁是小括号中的类对象
  2. 同步静态方法(synchronized static method),锁是当前对象的类对象

对象锁和类锁的总结

  1. 有线程访问对象的同步代码块时,另外的线程可以访问该对象的非同步代码块
  2. 若锁住的是同一个对象,一个线程在访问对象的同步代码块的时候,另一个访问对象的同步代码块的线程会被阻塞
  3. 若锁住的是同一个对象,一个线程在访问对象的同步方法的时候,另一个访问对象的同步方法的线程会被阻塞
  4. 若锁住的是同一个对象,一个线程在访问对象的同步代码块的时候,另一个访问对象的同步方法的线程会被阻塞,反之亦然
  5. 同一类的不同对象的对象锁互不干扰
  6. 类锁由于也是一种特殊的对象锁,因此表现和上述1234一致,由于一个类只有一把对象锁,所以同一个类的不同对象使用类锁将会是同步的
  7. 类锁和对象锁互不干扰

9-2 synchronized底层实现原理

实现synchronized的基础

对象在内存中的布局:对象头,实例数据,对齐填充

  • Java对象头:
    • Mark Word——用于存储对象自身运行时数据
    • ClassMetadataAddress——指针,JVM通过这个指针确定该对象是哪个类的实例
  • Monitor
    • 每个java对象天生自带的内部锁
    • synchronized 同步代码块的实现是通过 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。
    • 其内部包含一个计数器,当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止

自旋锁与自适应自旋锁

自旋锁

  • 许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得
  • 通过让线程执行忙循环等待锁的释放,不让出CPU
  • 缺点:若锁被其他线程长时间占用,会带来许多性能上的开销

自适应自旋锁

  • 自旋的次数不固定
  • 由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定

锁消除

  • JIT编译的时候,对运行上下文进行扫描,取出不可能存在竞争的锁

锁粗话

  • 通过扩大加锁的范围,避免反复加锁和解锁

synchronized的四种状态

  • 无锁、偏向锁、轻量级锁、重量级锁
  • 也是锁膨胀的方向

锁升级

  • synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。

偏向锁:减少同一线程获取锁的代价

  • 大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得
  • 核心思想——如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也变成了偏向锁结构,当线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查Mark Word的锁标记位为偏向锁以及当前线程id等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作。

轻量级锁

  • 轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程进入锁竞争的时候,偏向锁会升级为轻量级锁
  • 适应场景:线程交替执行同步块
  • 若存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁

锁的内存语义

  • 当线程释放锁的时候,Java内存模型会把该线对应的本地内存中的共享变量刷新到主内存中
  • 而当线程获取锁的时候,Java内存模型会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。

9-3 Synchronized和ReentrantLock的区别

ReentrantLock(再入锁)

  • 基于AQS实现
  • 能够实现比synchronized更细粒度的控制,比如控制fairness
  • 调用lock()后,必须使用unlock()释放锁
  • 性能未必比sunchronized高,并且也是可重入的

ReentrantLock公平性的设置

  • ReentrantLock fairLock = new ReentranLock(true);
  • 参数为true的时候,倾向于将锁赋予等待时间最久的线程
  • 公平锁:获取锁的顺序按先后调用lock方法的顺序
  • 非公平锁
  • Synchronized是非公平锁

ReentrantLock将锁对象化

  • 判断是否有线程,或者某个特定线程,在排队等待获取锁
  • 带超时的获取锁的尝试
  • 感知有没有成功获取锁

总结

  1. Synchronized是关键字,ReentrantLock是类;
  2. ReentrantLock可以对获取锁的等待时间进行设置,避免死锁;
  3. ReentrantLock可以获取各种锁的信息;
  4. ReentrantLock可以灵活地实现多路通知;
  5. 机制不同:Synchronized操作Mark Word,ReentrantLock调用Unsafe类的park方法

使用选择

  • 除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。
  • synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放

9-4 Java内存模型JMM的内存可见性

JMM中的主内存

  • 存储Java实例对象
  • 包括成员变量、类信息、常量、静态变量等
  • 属于数据共享区域,多线程并发操作会引发线程安全问题

JMM的工作内存

  • 存储当前方法的所有本地变量信息,本地变量对其他线程不可见
  • 属于线程私有数据区域,不存在线程安全的问题。

JMM与内存区域划分是不同的概念层次

  • JMM描述的是一组规则,围绕原子性,有序性、可见性展开
  • 相似点:存在共享区域和私有区域

主内存与工作内存的数据存储类型以及操作方式归纳

  • 方法里的基本数据类型本地变量将直接存储在工作内存的栈帧结构中
  • 引用类型的本地变量:引用存储在工作内存中,实例存储在主内存中
  • 成员变量、static变量、类信息均会被存储在主内存中
  • 主内存共享的方式是线程各拷贝一份数据到工作内存,操作完成后刷新回主内存

指令重排序需要满足的条件

  • 在单线程环境下不能改变程序运行的结果
  • 存在数据依赖关系的不允许重排序
  • 总结---无法通过happens-before原则推导出来的,才能进行指令的重排序

A操作的结果需要对B操作可见,则A与B存在happens-before关系

happens-before的八大原则

  1. 程序次序原则
  2. 锁定原则
  3. volatile原则
  4. 传递原则
  5. 线程启动原则
  6. 线程中断原则
  7. 线程终结原则
  8. 对象终结原则

 如果两个操作不满足上述任意一个happens-before规则,那么这两个操作就没有顺序的保障,JVM可以对这两个操作进行重排序。

volatile:JVM提供的轻量级同步机制

  • 保证被volatile修饰的共享变量对所有线程总是可见的,即变量被修改的时候,其他线程是可以发现的,有可能存在线程安全的问题
  • 禁止指令的重排序优化

volatile变量为何立即可见?

  • 当写一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量刷新到主内存中。
  • 当读取到一个volatile变量时,JMM会把该线程对应的工作过内存置为无效

volatile如何禁止重排优化

内存屏障(Memory Barrier):一条CPU指令

  1. 保证特定操作的执行顺序
  2. 保证某些变量的内存可见性

通过插入内存屏障指令禁止在内存屏障前后的指令执行重排序优化

volatile和sunchronized的区别

  1. volatile本质是在告诉JVM当前变量在寄存器(工作内存)中的值时不确定的,需要从主存中获取;synchronized则时锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住直到该线程完成变量操作为止
  2. volatile 仅能使用在变量级别;synchronized 则可以使用在 变量. 方法. 和类级别的 
  3. volatile 仅能实现变量的修改可见性,不能保证原子性;而synchronized 则可以 保证变量的修改可见性和原子性 
  4. volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。 
  5. volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。

9-5 CAS

synchronized属于悲观锁,CAS属于乐观锁

CAS(Compare and Swap)是一种高效实现线程安全性的方法

  1. 支持原子更新操作,适用于计数器,序列发生器等场景;
  2. 属于乐观锁机制,号称lock-free
  3. CAS操作失败时由开发者决定是继续尝试,还是执行别的操作

CAS思想

  1. 包含三个操作数:内存位置(V)、预期原值(A)、新值(B)
  2. 当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的 值,否则不会执行任何操作(他的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。)

CAS的缺点:

  1. 若循环时间长,则开销很大
  2. 只能保证一个共享变量的原子操作
  3. 无法避免ABA问题 解决:AtomicStampedReference

9-6 Java线程池

利用Excutors创建不同的线程池满足不同场景的需求

  1. newFixedThreadPool----创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。
  2. newCachedThreadPool——创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
    1. 工作线程的创建数量几乎没有限制
    2. 如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
  3. newSingleThreadExecutor——创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,如果这个线程异常结束,会有另一个取代它,保证顺序执行。
  4. newScheduleThreadPool——创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。

Fork/Join框架

  • 把大任务分割成若干个小任务并行执行,最终汇总每个小任务结果后得到大任务结果的框架

为什么要使用线程池

  1. 降低资源消耗
  2. 提高线程的可管理性
  3. 提高响应速度

线程池的核心参数

  1. orePoolSize : 核心线程大小。线程池一直运行,核心线程就不会停止。
  2. maximumPoolSize :线程池最大线程数量。非核心线程数量=maximumPoolSize-corePoolSize
  3. keepAliveTime :非核心线程的心跳时间。如果非核心线程在keepAliveTime内没有运行任务,非核心线程会消亡。
  4. workQueue :阻塞队列。ArrayBlockingQueue,LinkedBlockingQueue等,用来存放线程任务。
  5. defaultHandler :饱和策略。ThreadPoolExecutor类中一共有4种饱和策略。通过实现RejectedExecutionHandler接口。
    1. AbortPolicy : 线程任务丢弃报错。默认饱和策略。
    2. DiscardPolicy : 线程任务直接丢弃不报错。
    3. DiscardOldestPolicy : 将workQueue队首任务丢弃,将最新线程任务重新加入队列执行。
    4. CallerRunsPolicy :线程池之外的线程直接调用run方法执行。
  6. ThreadFactory :线程工厂。新建线程工厂。

线程池的流程

  1. 线程池执行execute/submit方法向线程池添加任务,当任务小于核心线程数corePoolSize,线程池中可以创建新的线程。
  2. 当任务大于核心线程数corePoolSize,就向阻塞队列添加任务。
  3. 如果阻塞队列已满,需要通过比较参数maximumPoolSize,在线程池创建新的线程,当线程数量大于maximumPoolSize,说明当前设置线程池中线程已经处理不了了,就会执行饱和策略。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值