s1 基础知识
001 并发编程的优缺点
优点 | 充分利用多核cpu的计算能力 | 方便业务拆分,提升系统并发能力和性能,提升程序执行效率,提速 |
---|---|---|
缺点 | 内存泄漏 | 上下文切换 | 线程安全 | 死锁 |
002 并发编程三要素
三要素 | 安全问题的原因 | 具体原因 | 对策 |
---|---|---|---|
原子性 | 线程切换 | 一个或多个操作要么全部执行成功要么全部执行失败 | JDK Atomic开头的原子类、synchronized、LOCK |
可见性 | 缓存 | a线程对共享变量的修改,b线程能够立刻看到 | synchronized、volatile、LOCK |
有序性 | 编译优化 | 处理器可能会对指令进行重排序 | Happens-Before 规则 |
:::success
tip: java程序保证多线程运行安全
方法一:使用安全类,考虑atomic包下的类,比如 java.util.concurrent 下的类,使用原子类AtomicInteger
方法二:使用自动锁 synchronized
方法三:使用手动锁 Lock
方法四: 考虑atomic包下的类,保证操作的可⻅性
方法五: 涉及到对线程的控制,考虑CountDownLatch/Semaphore==
:::
003 并行并发区别
类别 | 并发 | 并行 | 串行 |
---|---|---|---|
定义 | 多个任务在同个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑看那些任务是同时执行 | 单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行” | 有n个任务,由一个线程按顺序执行. 不存在线程不安全情况,也就不存在临界区的问题 |
举例 | 两个队列,一个售票口 | 两个队列,2个售票口 | 1个队列,一个售票口 |
004 线程进程区别
类别 | 进程 | 线程 |
---|---|---|
本质 | 操作系统资源分配的基本单位 | 处理器任务调度和执行的基本单位 |
资源开销 | 有独立代码和数据空间,进程间切换开销大 | ,同一类线程共享代码和数据空间,每个线程有自己独立的运行栈和pc,开销小 |
包含关系 | 一个进程可以包含1-n个线程 | 线程是进程的一部分,轻量级的进程 |
内存分配 | 进程之间的地址空间和资源是相互独立 | 同一进程的线程共享本进程的地址空间和资源 |
影响关系 | 进程崩溃,保护模式下对其他无影响,健壮 | 线程崩溃,整个进程挂 |
执行过程 | 程序运行的入口、顺序执行序列和程序出口 | 线程不能独立执行,必须依存在应用程序中 |
005 上下文切换
任务从保存到再加载的过程就是一次上下文切换
1.CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
2.当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态
:::success
上下文切换可以认为是内核(操作系统的核心)在 CPU 上对于进程(包括线程)进行切换,上下
文切换过程中的信息是保存在进程控制块(PCB, process control block)中的。PCB 还经常被称
作“切换桢”(switchframe)。信息会一直保存到 CPU 的内存中,直到他们被再次使用。
:::
006 守护线程 | 用户线程
用户 (User) 线程 :运行在前台,执行具体的任务,如程序的主线程 | 连网的子线程等都是用户线程
守护 (Daemon) 线程 :运行在后台,为其他前台线程服务,一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作. 比如垃圾回收线程,
:::success
tip:
1. setDaemon(true) 必须在 start() 方法前执行,否则会抛出IllegalThreadStateException 异常
2. 在守护线程中产生的新线程也是守护线程
3. 不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算逻辑
4. 守护 (Daemon) 线程中不能依靠 finally 块的内容来确保执行关闭或清理资源的逻辑。因为daemon线程的finally语句块可能无法被执行.
5.优先级别:守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。
:::
007 死锁
百度百科 :死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)。
如图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
死锁的四要素:
1. 互斥条件:线程(进程)对于所分配到的资源有排它性,即一个资源只能被一个线程(进程)占用,直到被该线程(进程)释放
2. 请求与保持条件:一个线程(进程)因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
3. 不剥夺条件:线程(进程)已获得的资源在末用完之前不能被其他线程强行剥夺,只有自己用完后才释放资源。
4. 循环等待条件:当发生死锁时,所等待的线程(进程)必定会形成一个环路(类似于死循环),造成永久阻塞
避免线程死锁:(破环四要素任意一个即可)
破坏互斥条件
这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)
破坏请求与保持条件
一次性申请所有的资源
破坏不剥夺条件
占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源
破坏循环等待条件
靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件
- 尽量使用 tryLock(long timeout, TimeUnit unit)的方法(ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁。
- 尽量使用 Java. util. concurrent 并发类代替自己手写锁。
- 尽量降低锁的使用粒度,尽量不要几个功能用同一把锁。
- 尽量减少同步的代码块
1. 固定加锁的顺序,⽐如我们可以使⽤Hash值的⼤⼩来确定加锁的先后
2. 尽可能缩减加锁的范围,等到操作共享变量的时候才加锁。
3. 使⽤可释放的定时锁(⼀段时间申请不到锁的权限了,直接释放掉
008 创建线程的四种方式(池化资源)
创建线程有四种方式:
- 继承 Thread 类; 重写run方法,调用线程对象的start()方法开启线程
//Thread类本质是实现Runnablre接口的实例
public class Test extends Thread{
public void run(){
System.out.print("Test.run()");
}
}
Test test = new Test();
test.start();//start方法是一个native方法,启动新线程,执行run()方法
- 实现 Runnable 接口; 重写run方法,调用线程对象的start()方法开启线程
public class MyThread extends OtherClass implements Runnable {
public void run() {
System.out.println("MyThread.run()");
}
}
//启动 MyThread,需要首先实例化一个 Thread,并传入自己的 MyThread 实例:
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
//事实上,当传入一个 Runnable target 参数给 Thread 后,Thread 的 run()方法就会调用
target.run()
public void run() {
if (target != null) {
target.run();
}
}
-
实现 Callable 接口; 1. 创建实现Callable接口的类myCallable2. 以myCallable为参创建FutureTask对象
3. 将FutureTask作为参数创建Thread对象 4. 调用线程对象的start()方法
(对比runable,有返回值,且允许抛出异常,可获取异常信息)(5以后才有)
//创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(taskSize);
// 创建多个有返回值的任务
List<Future> list = new ArrayList<Future>();
for (int i = 0; i < taskSize; i++) {
Callable c = new MyCallable(i + " ");
// 执行任务并获取 Future 对象
Future f = pool.submit(c);
list.add(f);
}
// 关闭线程池
pool.shutdown(