多线程的基础与高级

本文深入探讨了Java多线程的基础和高级主题,包括并发与并行的区别,进程与线程的概念,线程调度,线程同步(如synchronized和volatile关键字),死锁的条件及处理,阻塞队列,以及并发工具类如CountDownLatch和Semaphore的使用。此外,还详细介绍了线程池的工作原理和线程状态转换,以及Atomic包的原子操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一 多线程基础

1.1 关于并发与并行

并行:在同一时刻,有多个指令在多个CPU上同时执行
并发:在同一时刻,有多个指令在单个CPU上交替执行。

1.2 进程与线程

进程:是正在运行的程序(软件)
独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位
动态性:进程的实质是程序(代码)的一次执行过程,进程是动态产生,动态消亡的
并发性:任何进程都可以同其他进程一起并发执行

线程(进程中的各个部分):是进程中的单个顺序控制流,是一条执行路径
​ 单线程:一个进程如果只有一条执行路径,则称为单线程程序
​ 多线程:一个进程如果有多条执行路径,则称为多线程程序

1.3 关于线程的优先级

1.3.1 线程调度

线程调度是获取CPU的使用权的方式;

1.3.2 线程调度的两种方式

分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些
Java使用的是抢占式调度模型,该模型拥有一种特性叫做随机性;这种随机性是指假如计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令。所以说多线程程序的执行是有随机性,因为谁抢到CPU的使用权是不一定的

1.3.3 优先级相关方法

| final int getPriority() | 返回此线程的优先级
| final void setPriority(int newPriority) | 更改此线程的优先级线程默认优先级是5;线程优先级的范围是:1-10

1.4 关于守护线程

作用:为了守护普通线程存在
相关方法
void setDaemon(boolean on):将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出

1.5 关于线程同步以及处理同步代码的问题

线程同步带来的问题

安全问题出现的条件

  • 是多线程环境
  • 有共享数据
  • 有多条语句操作共享数据

如何处理这个问题

  • 同步代码块
synchronized(任意对象:锁关闭) { 
	多条语句操作共享数据的代码 
}
  • 同步方法
    • 普通同步方法:同步方法的锁对象是this
修饰符 synchronized 返回值类型 方法名(方法参数) { 
	方法体;
}
- 静态同步方法:同步静态方法的锁对象是类名.class
修饰符 static synchronized 返回值类型 方法名(方法参数) { 
	方法体;
}

同步代码块与同步方法的区别:
同步代码可以锁住制定代码,同步方法是锁住方法中的所有方法

同步代码块可以制定锁对象,同步方法不能制定锁对象(同步方法中的锁对象是this/类名.class)

1.6 关于死锁

1.6.1 死锁产生的必要条件

1、互斥条件
	某资源只能被一个进程使用,其他进程请求该资源时,只能等待,直到资源使用完毕后释放资源。
2、请求和保持条件
	程序已经保持了至少一个资源,但是又提出了新要求,而这个资源被其它进程占用,自己占用资源却保持不放。
3、不剥夺条件
	任何一个资源在没被该进程释放之前,任何其他进程都无法对他剥夺占用
4、循环等待条件
	当发生死锁时,所等待的进程必定会形成一个环路(类似于死循环),造成永久阻塞。

1.6.2 处理死锁思路

1. 预防死锁(破坏死锁的四个条件中的一个或多个来预防死锁。但不能破坏互斥条件,其他三个都可。)
	破坏请求和保持的条件
	破坏不可剥夺调价
	破坏循环等待条件
2. 避免死锁(在资源动态分配过程中,用某种方式阻止系统进入不安全状态。比如银行家算法。)
	银行家算法
3. 检测死锁
	允许系统在运行过程中发生死锁,但可已设置检测机构及时检测死锁的发生,并采取适当措施加以清除
4. 解除死锁(发生死锁后,采取适当措施将进程从死锁状态中解脱出来。解除死锁主要方法:资源剥夺法,撤销进程法,进程回退法。)
	1. 资源剥夺
		挂起某些死锁进程,并抢占它的资源,讲这些资源分配给其他的死锁进程。但应防止被挂起的进程长时间得不到进程,而处于资源匮乏状态。
	2. 终止进程
		强制将一个或多个死锁进程终止(撤销)并剥夺这些进程的资源,直至打破循环环路,使系统从死锁状态中解脱出来。撤销的原则可以按照进程的优先级和撤销进程代价的高低进行。
	3. 进程回退
		让一个或多个进程回退到足以避免回避死锁的地步,进程回退时资源释放资源而不是被剥夺。要求系统保持进程的历史信息,设置还原点。

1.7 关于阻塞队列

1.7.1 阻塞队列继承结构

请添加图片描述

1.7.2 常见BlockingQueue

ArrayBlockingQueue: 底层是数组,有界

LinkedBlockingQueue: 底层是链表,无界.但不是真正的无界,最大为int的最大值

1.7.3 BlockingQueue的核心方法

put(anObject): 将参数放入队列,如果放不进去会阻塞

take(): 取出第一个数据,取不到会阻塞

二 多线程高级

2.1 线程的几种状态

 		/* 新建 */
        NEW , 
        /* 可运行状态(就绪) */start
        RUNNABLE , 
        /* 阻塞状态 */ 
        BLOCKED , 
        /* 无限等待状态 */ wait
        WAITING , 
        /* 计时等待(睡眠状态) */ sleep
        TIMED_WAITING , 
        /* 终止 */
        TERMINATED;

2.1 各个状态的转换

请添加图片描述

2.3 线程池的设计思路

  1. 准备一个任务容器
  2. 一次性启动多个(2个)消费者线程
  3. 刚开始任务容器是空的,所以线程都在wait
  4. 直到一个外部线程向这个任务容器中扔了一个"任务",就会有一个消费者线程被唤醒
  5. 这个消费者线程取出"任务",并且执行这个任务,执行完毕后,继续等待下一次任务的到来

2.4 创建线程池对象的其中一种方法

ThreadPoolExecutor 创建线程池对象 :
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(核心线程数量,最大线程数量,空闲线程最大存活时间,任务队列,创建线程工厂,任务的拒绝策略);
// 参数一 corePoolSize:核心线程数量 ,不能小于0
// 参数二 maximumPoolSize:最大线程数,不能小于等于0,maximumPoolSize >= corePoolSize
// 参数三 keepAliveTime:空闲线程最大存活时间,不能小于0
// 参数四 unit:时间单位
// 参数五 workQueue:任务队列 不能为null:任务等待队列(new ArrayBlockingQueue<>(10), )
// 参数六:创建线程工厂 ,,不能为null (Executors.defaultThreadFactory(),)
// 参数七:任务的拒绝策略,不能为null
1、生么时候拒绝?当提交的任务 > 池子中最大线程数量 + 队列容量
2、如何拒绝?(abortPolicy:默认)

2.4.1 非默认任务拒绝策略

  1. ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常。是默认的策略。
  2. ThreadPoolExecutor.DiscardPolicy: 丢弃任务,但是不抛出异常 这是不推荐的做法。
  3. ThreadPoolExecutor.DiscardOldestPolicy: 抛弃队列中等待最久的任务 然后把当前任务加入队列中。
  4. ThreadPoolExecutor.CallerRunsPolicy: 调用任务的run()方法绕过线程池直接执行。

2.5 关于volatile关键字

==作用 ==:强制线程每次在使用的时候,都会看一下共享区域最新的值,但是volatile关键字不能保证原子性

2.6 Atomic包

2.6.1概述

Atomic包里一共提供了13个类,属于4种类型的原子更新方式
1. 原子更新基本类型
AtomicBoolean: 原子更新布尔类型
AtomicInteger: 原子更新整型
AtomicLong: 原子更新长整型
2. 原子更新数组
3. 原子更新引用和
4.原子更新属性(字段)

2.6.2 内存解析

AtomicInteger原理 :自旋锁 + CAS 算法

2.6.2.1 CAS算法:

有3个操作数(内存值V, 旧的预期值A,要修改的值B),当旧的预期值A == 内存值 此时修改成功,将V改为B ,当旧的预期值A!=内存值 此时修改失败,不做任何操作 ,并重新获取现在的最新值(这个重新获取的动作就是自旋)

2.6.3 源码解析:

weakCompareAndSetInt(o, offset, v, v + delta)重点

2.6.4 悲观锁和乐观锁

synchronized和CAS的区别
相同点:在多线程情况下,都可以保证共享数据的安全性。
不同点:synchronized总是从最坏的角度出发,认为每次获取数据的时候,别人都有可能修改。所以在每 次操作 共享数据之前,都会上锁。(悲观锁)
悲观锁
synchronized总是从最坏的角度出发,认为每次获取数据的时候,别人都有可能修改。所以在每 次操作 共享数据之前,都会上锁。(悲观锁)
乐观锁
cas是从乐观的角度出发,假设每次获取数据别人都不会修改,所以不会上锁。只不过在修改共享数据的时候,会 检查一下,别人有没有修改过这个数据。如果别人修改过,那么我再次获取现在最新的值。 如果别人没有修改过,那么我现在直接修改共享数据的值.(乐观锁)

2.7 并发工具类

2.7.1 并发工具类-Hashtable

Hashtable出现的原因 : 在集合类中HashMap是比较常用的集合对象,但是HashMap是线程不安全的(多线程环境下可能会存在问题:出现null值)。为了保证数据的安全性我们可以使用Hashtable,但是Hashtable的效率低下。

Hashtable 如何保证安全(采取悲观锁)?为啥效率低(只要有线程访问,锁整张表)?

Hashtable底层也是哈希表(数组(数组长度是:16 加载因子是0.75 到12就要扩容)加链表):

2.7.2 并发工具类-ConcurrentHashMap

出现的原因: 在集合类中HashMap是比较常用的集合对象,但是HashMap是线程不安全的(多线程环境下可能会存在问题)。为了保证数据的安全性我们可以使用Hashtable,但是Hashtable的效率低下。

体系结构:
请添加图片描述
三个map总结 :
​ 1 ,HashMap是线程不安全的。多线程环境下会有数据安全问题

​ 2 ,Hashtable是线程安全的,但是会将整张表锁起来,效率低下
​ 3,ConcurrentHashMap也是线程安全的,效率较高。 在JDK7和JDK8中,底层原理不一样。

2.7.2.1 ConcurrentHashMap的底层原理

jdk1.7
分析: 底层也是哈希表结构 也有数组(Sement)16,加载因子0.75,HashEntey[]:小数组(长度为2)给大数组索引为0,作为模版使用,二次哈希(也要扩容 扩容两倍,加载到)

线程安全如何保证:锁小表,最多允许16个线程同时访问

请添加图片描述
jdk1.8
底层哈希表,数据+链表+当链表的长度大于的8的时候转位红黑树

CAS + synchronized同步代码块

线程安全:锁链表,锁对象为头结点

请添加图片描述

2.7.2.1 ConcurrentHashMap的底层原理的总结
1. 如果使用空参构造创建ConcurrentHashMap对象,则什么事情都不做。     在第一次添加元素的时候创建哈希表
2. 计算当前元素应存入的索引。
3. 如果该索引位置为null,则利用cas算法,将本结点添加到数组中。
4. 如果该索引位置不为null,则利用volatile关键字获得当前位置最新的结点地址,挂在他下面,变成链表。		
5. 当链表的长度大于等于8时,自动转换成红黑树6,以链表或者红黑树头结点为锁对象,配合悲观锁保证多线程操作集合时数据的安全性

2.7.1 并发工具类-CountDownLatch

  1. public CountDownLatch(int count) |:参数传递线程数,表示等待线程数量
  2. public void await() : 让线程等待 :当计数器为0时,会唤醒等待的线程
  3. public void countDown():当前线程执行完毕,会将计数器-1

2.7.1 并发工具类-Semaphore

//获得了通行证
semaphore.acquire();
//归还通行证
semaphore.release();

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值