多线程(基础2)

Posted ohana!

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程(基础2)相关的知识,希望对你有一定的参考价值。

目录

一,线程通信 

1)wait()¬ify()

2)作用

3)实现

4)wait() 和 sleep() 的区别

二,阻塞队列

1)概念

2)生产者消费者模型

3)标准库中的阻塞队列

三,线程池

为什么要创建线程池

作用

为什么不能使用快捷创建的方式 

理解 ThreadPoolExecutor 构造方法的参数

线程池的工作流程

四,常见的锁策略

1.乐观锁&悲观锁

1)悲观锁的概念

2)乐观锁的概念

3)乐观锁的实现

2.读写锁

使用场景

实例

3.重量级锁&轻量级锁

解释

4.自旋锁

5.公平锁&非公平锁

6.可重入锁&不可重入锁

7.独占锁&共享锁

五,synchronized拓展(补充)

1.作用

2.原理

3.jvm对synchronized的优化

1)锁消除

2)锁粗化

4.synchronized和lock可以实现的锁策略

六,CAS

1.什么是cas

2.jdk是如何提供cas的?/cas的原理?

3.CAS的应用

4.CAS可能存在的问题,ABA问题

5.解决ABA问题

6.CAS总结


一,线程通信 

1)wait()&notify()

wait(总的来说就是让满足一定条件的线程等待,由运行态转变为等待状态)

notify&notifyall(即就是随机唤醒/全部唤醒正在等待的线程)

2)作用

  • 多线程并发并行执行,多个线程指令表现为随机的顺序
  • 为满足多线程指令具有一定的顺序性,就需要使用线程通信

3)实现

4)wait() 和 sleep() 的区别

  • wait用于线程通信,sleep用于让线程阻塞一段时间
  • wait需要搭配synchronized使用,sleep不需要
  • wait是object的方法,sleep是Thread的静态方法

二,阻塞队列

1)概念

  • 阻塞队列是一种特殊的队列. 也遵守 "先进先出" 的原则.
  • 阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:
  1. 当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.
  2. 当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素.

2)生产者消费者模型

  • 生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
  • 生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取
  1. 阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力
  2. 阻塞队列也能使生产者和消费者之间解耦

3)标准库中的阻塞队列

三,线程池

为什么要创建线程池

  • 虽然创建销毁线程比创建销毁进程更轻量, 但是在频繁创建销毁线程的时候还是会比较低效.
  • 线程池就是为了解决这个问题. 如果某个线程不再使用了, 并不是真正把线程释放, 而是放到一个 "池子"中, 下次如果需要用到线程就直接从池子中取, 不必通过系统来创建了

作用

  • 线程池最大的好处就是减少每次启动、销毁线程的损耗

为什么不能使用快捷创建的方式 

Executors 创建线程池的几种方式

  • newFixedThreadPool: 创建固定线程数的线程池
  • newCachedThreadPool: 创建线程数目动态增长的线程池.
  • newSingleThreadExecutor: 创建只包含单个线程的线程池.
  • newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer

主要原因:

1)没有设置拒绝策略,或使用jdk提供的这四种,都不合适,可能需要把队列满了的时候,提交的任务保存在日志或数据库

2)可能使用无边界的阻塞队列,如果生产速度过快,一定会在某个时间,导致内存不够,出现OOM(内存溢出)

3)最多使用CallerRunsPolicy(谁提交/让我执行,就让他自己执行)

理解 ThreadPoolExecutor 构造方法的参数

把创建一个线程池想象成开个公司. 每个员工相当于一个线程.

  • corePoolSize: 正式员工的数量. (正式员工, 一旦录用, 永不辞退)
  • maximumPoolSize: 正式员工 + 临时工的数目. (临时工: 一段时间不干活, 就被辞退).
  • keepAliveTime: 临时工允许的空闲时间.
  • unit: keepaliveTime 的时间单位, 是秒, 分钟, 还是其他值.
  • workQueue: 传递任务的阻塞队列
  • threadFactory: 创建线程的工厂, 参与具体的创建线程工作.
  • RejectedExecutionHandler: 拒绝策略, 如果任务量超出公司的负荷了接下来怎么处理.
  1. AbortPolicy(): 超过负荷, 直接抛出异常.
  2. CallerRunsPolicy(): 调用者负责处理
  3. DiscardOldestPolicy(): 丢弃队列中最老的任务.
  4. DiscardPolicy(): 丢弃新来的任务.

线程池的工作流程

四,常见的锁策略

1.乐观锁&悲观锁

1)悲观锁的概念

大多数时间看,存在线程冲突的(悲观的看),每次都先加锁,在释放锁

2)乐观锁的概念

  • 大多数时间看,没有线程冲突的(乐观的看),每次都不加锁(Java)层面看,从cpu层面加锁
  • 每次都直接修改数据的操作,返回修改是否成功的结果,程序自行处理逻辑

3)乐观锁的实现

2.读写锁

使用场景

读写锁适合读多写少的场景

实例

在ReadWriteLock接口下实现了一个读写锁的子类(读写锁是两个锁,不是一个锁)

public class 读写锁的策略 
    public static void main(String[] args) 
        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
        ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
        //
        readLock.lock();
        readLock.unlock();
        writeLock.lock();
        writeLock.unlock();
    

3.重量级锁&轻量级锁

解释

锁的核心特性 "原子性", 这样的机制追根溯源是 CPU 这样的硬件设备提供的.

  • CPU 提供了 "原子操作指令".
  • 操作系统基于 CPU 的原子指令, 实现了 mutex 互斥锁.
  • JVM 基于操作系统提供的互斥锁, 实现了 synchronized 和 ReentrantLock 等关键字和类

尽量不使用重量级锁,但是如果线程冲突严重的话还是会使用重量级锁 (主要区别涉及用户态和内核态的切换)

4.自旋锁

  • 实际上是Java层面无锁的操作
  • 以自旋的方式,执行乐观锁修改数据的操作
  • 会反复来进行尝试修改数据(不修改成功不退出)
  • 自旋锁一般都结合乐观锁来进行使用

5.公平锁&非公平锁

  • 公平锁与非公平锁的机制,就类似于,先来先服务,这是公平的,所有线程同时竞争,不讲究顺序,这就是不公平的
  • 不公平锁可能会引发饥饿现象

6.可重入锁&不可重入锁

  • 可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。
  • 比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也叫做递归锁)。
  • Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的。
  • 而 Linux 系统提供的 mutex 是不可重入锁.

7.独占锁&共享锁

  • 同一个时间只有一个线程能获取到锁,就是独占锁
  • 同一个时间,可以有多个线程同时获取到锁,就是共享锁

五,synchronized拓展(补充)

1.作用

基于对象头加锁的方式,实现同步互斥

字段:状态

状态的值:(锁可以升级,但是不能降级)

无锁:没有任何线程申请到该对象的锁

偏向锁:第一次进入的线程,或是这个线程再次申请到同一个对象锁

轻量级锁:出现线程冲突,但冲突概率比较小(cas+自旋锁)

重量级锁:线程冲突比较严重,真实的进行加锁(使用操作系统mutex加锁)

2.原理

3.jvm对synchronized的优化

1)锁消除

锁消除就是字面意思,直接将锁去掉不使用锁,但是前提就是只有一个线程可以操作

2)锁粗化

举个简单的栗子:老师给学生打电话布置课堂作业,打电话,布置作业1,2,3,挂断电话

原始情况其实应该是:打电话,布置任务1,挂电话

                                    打电话,布置任务2,挂电话

                                    打电话,布置任务3,挂电话

前提条件:同一个线程调用,但是这个变量是所有线程都可以获取到的

4.synchronized和lock可以实现的锁策略

六,CAS

1.什么是cas

  • CompareAndSwap 比较并交换
  • 是jdk提供的一种乐观锁的实现,能够满足线程安全的条件,并且以乐观锁的方式来修改变量

1)比较从主存读 与 写回主存时 是否相等。(比较)
2)如果比较相等,将工作内存中修改的值写入主存。(交换)
3)返回操作的结果

2.jdk是如何提供cas的?/cas的原理?

1)jdk提供了一个unsafe的类,来执行cas的操作

2)jdk中unsafe提供的cas方法,又是基于操作系统提供的cas方法

简单点说就是,unsafe时基于操作系统和cpu提供的cas来实现的

3.CAS的应用

比较常见的,java.util.concurrent.atomic包下都是使用了cas来保证线程安全的,如:++,--,!flag

4.CAS可能存在的问题,ABA问题

ABA 的问题:
假设存在两个线程 t1 和 t2. 有一个共享变量 num, 初始值为 A.
接下来, 线程 t1 想使用 CAS 把 num 值改成 Z, 那么就需要

  • 先读取 num 的值, 记录到 oldNum 变量中.
  • 使用 CAS 判定当前 num 的值是否为 A, 如果为 A, 就修改成 Z.

但是, 在 t1 执行这两个操作之间, t2 线程可能把 num 的值从 A 改成了 B, 又从 B 改成了 A

举个例子:

5.解决ABA问题

给要修改的值, 引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期.

  • CAS 操作在读取旧值的同时, 也要读取版本号.
  • 真正修改的时候,
  • 如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1.
  • 如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了). 

6.CAS总结

1)什么叫cas

  • 比较并交换
  • 比较从主存读 与 写回主存时 是否相等
  • 如果相等就交换

2)原理

  • 属于乐观锁的一种
  • jdk提供了一个叫unsafe的类,来实现cas的操作
  • unsafe又是基于操作系统和cpu的cas来实现的

3)应用

  • atomic包下的类

4)存在的问题

  • ABA问题
  • 解决方法:引入版本号,jdk提供了一个叫AtomicStampedReference类来管理版本号

 

以上是关于多线程(基础2)的主要内容,如果未能解决你的问题,请参考以下文章

.NET基础拾遗多线程开发基础1

java核心-多线程-线程类基础知识

Java基础学习之-多线程学习知识点的学习

Java多线程基础

java基础---多线程---JUC原子类

Java多线程之内存可见性