【狂神说Java】多线程详解笔记
【狂神说Java】多线程详解笔记
线程状态
java类的方式
外部类: 在主函数所在的类的外部定义的类, 若是在同一文件, 直接new; 若是不同文件, 导包再new
静态内部类: 在主函数所在的类的内部且在主函数外部定义的类, 直接new
因为main是static, 所以他也要是static
局部类: 在主函数内定义的类, 直接new
匿名内部类:
Interface o = new Interface(){类的定义}
, 需先定义interfacelambda:
1
2
3
4Runnable as = () -> {
System.out.println("as");
};
as.run();1
new Thread(()->System.out.println("as")).start();
线程休眠sleep
Thread.sleep(1000);
单位为毫秒
线程礼让yield
调用方式: 在线程内
Thread.yield();
将线程由运行态转为就绪态, 此时重新决定哪个线程获取cpu, 可能还是刚才的进程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19package com.zq;
public class yield {
public static void main(String[] args) {
myYield y = new myYield();
new Thread(y, "a").start();
new Thread(y, "b").start();
}
}
class myYield implements Runnable {
public void run() {
System.out.println(Thread.currentThread().getName() + "->start");
Thread.yield();
System.out.println(Thread.currentThread().getName() + "->end");
}
}
join
阻塞其他线程, 强制该线程运行, 类似插队
调用:
thread.join();
注意是线程对象, 不是类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34package com.zq;
public class join implements Runnable {
public static void main(String[] args) throws InterruptedException {
join j = new join();
Thread thread = new Thread(j);
thread.start();
for (int i = 0; i < 10; i++) {
if (i == 5) {
thread.join();
}
System.out.println("main->" + i);
}
}
public void run() {
for (int i = 0; i < 20; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("vip->" + i);
}
}
}
线程优先级
默认为5, main线程优先级为5
守护线程
- 用户线程执行完后,虚拟机即关闭,尽管守护线程没有执行完毕
- 使用:
thread.setDaemon("true");
默认为false
线程同步
- 线程同步形成条件: ==队列+锁==
- sleep不会释放锁
synchronized
- 同步方法默认锁的对象是this
- 可以用同步块完全替代同步方法
死锁 活锁 饥饿
死锁: 两个或更多线程阻塞着等待其它处于死锁状态的线程所持有的锁
- 形象的例子: 两个小朋友分别拿着对方喜欢的玩具, 而且谁都不愿先把手中的玩具先给对方
- 死锁通常发生在多个线程同时但以不同的顺序请求同一组锁的时候
- 死锁会让你的程序挂起无法完成任务
- 解决方法: 只能通过中止并重启的方式来让程序重新执行
- 在程序中, 双方不会协商, 只会一直僵持, 程序一直阻塞
死锁的四个必要条件:
1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
2)请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
3)不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
4)环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
预防死锁–破坏死锁的四个必要条件
破坏互斥条件:使资源同时访问而非互斥使用,就没有进程会阻塞在资源上,从而不发生死锁。
破坏请求和保持条件:采用静态分配的方式,静态分配的方式是指进程必须在执行之前就申请需要的全部资源,且直至所要的资源全部得到满足后才开始执行,只要有一个资源得不到分配,也不给这个进程分配其他的资源。
破坏不剥夺条件:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源,但是只适用于内存和处理器资源。
破坏循环等待条件:给系统的所有资源编号,规定进程请求所需资源的顺序必须按照资源的编号依次进行。
活锁: 活锁指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败, 线程为了彼此间的响应而相互礼让,使得没有一个线程能够继续前进
- 形象的例子: 两条车道上, 两人在同一车道相向行走, 发现对方阻碍自己的道路时, 两人都向另一条车道偏移, 结果还是会阻碍对方的道路(传说中的神默契), 这样一直僵持
- 活锁有可能自行解开
- 活锁可以认为是一种特殊的饥饿
饥饿: 是指一个可运行的进程尽管能继续执行,但被调度器无限期地忽视,而不能被调度执行的情况
- 饥饿可以通过先来先服务资源分配策略来避免
- 优先级高的线程抢占资源, 导致优先级低的线程一直得不到资源
- 某个线程长期占用资源, 导致其他线程得不到资源
lock
1 | private ReentrantLock lock=new ReentrantLock(); |
- 性能比ssynchronized好
- 一般放在try-finally中, 不然容易出问题
线程通信
线程池
1 | package com.zq; |
如果线程池大小小于开启的线程数, 则等待之前的线程执行完毕释放, 再执行新线程
summary
1 | package com.zq; |
一些扩展
- java中的锁Lock就是基于AbstractQueuedSynchronizer来实现的
- 在大多数情况下,我们写并发代码使用synchronized就足够了,而且使用synchronized也是首选
- 但是lock更加灵活
- lockInterruptibly方法可以响应中断,lock方法会阻塞线程直到获取到锁,而tryLock方法则会立刻返回,返回true代表获取锁成功,而返回false则说明获取不到锁
- newCondition方法返回一个条件变量,一个条件变量也可以做线程间通信来同步线程。多个线程可以等待在同一个条件变量上,一些线程会在某些情况下通知等待在条件变量上的线程,而有些变量在某些情况下会加入到条件变量上的等待队列中去。
- 独占锁就是只能有一个线程获取到锁,其他线程必须在这个锁释放了锁之后才能竞争而获得锁
- 共享锁则可以允许多个线程获取到锁
ReentrantLock
- 是lock的子类
- 可重入性: 同一个线程可以多次获得锁,而不同线程依然不可多次获得锁
- 划分:
- 公平锁: 保证等待时间最长的线程将优先获得锁
- 非公平锁: 并不会保证多个线程获得锁的顺序,并发性能表现更好,ReentrantLock默认使用非公平锁
CopyOnWriteArrayList
- ArrayList的线程安全版本
- CopyOnWriteArrayList是在有写操作的时候会copy一份数据,然后写完再设置成新的数据。CopyOnWriteArrayList适用于读多写少的并发场景
- CopyOnWriteArraySet是线程安全版本的Set实现,它的内部通过一个CopyOnWriteArrayList来代理读写等操作,使得CopyOnWriteArraySet表现出了和CopyOnWriteArrayList一致的并发行为
- 使用了ReentrantLock来支持并发操作
多线程中的三大特性
- 原子性: 一个或多个操作,要么全部执行完成,要么就都不执行
- 可见性: 当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程就能够立即看到修改的值
- 有序性: 编译器可以对指令进行重排, 对单线程无影响, 但可能会影响多线程
wait notify
- 必须在synchronized 中执行
- wait 必须暂停当前正在执行的线程,并释放资源锁,让其他线程可以有机会运行
- notify/notifyall:唤醒锁池中的线程,使之运行
- 调用wait方法后, 线程会放弃对象锁, 进入等待此对象的等待锁定池, 只有再次调用此对象的notify方法, 本线程才会进入对象锁池准备, 才有可能获取对象锁进入运行状态
Volatile
在 java 中为了加快程序的运行效率,对一些变量的操作通常是在该线程的寄存器或是 CPU 缓存上进行的,之后才会同步到主存中,而加了 volatile 修饰符的变量则是直接读写主存
volatile 虽然具有可见性但是并不能保证原子性, 所以不能替代Synchronize
Volatile在某些情况下性能优于Synchronize