石一歌的多线程进阶(JUC)笔记 [狂神]
Java多线程进阶JUC
JUC
其实就是Java.Util.concurrent
包的缩写文档地址
回顾
线程的开启三种方式
- Thread 单继承会有
oop
问题 - Runnable 没有返回值、效率相比入 Callable 相对较低
- Callable 推荐
线程与进程
-
进程
? 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
-
线程
? 线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
- Java 本身无法开启线程,需要调动本地方法
并发编程
-
并发
- 单核 ,模拟出来多条线程
-
并行
- 多核 ,多个线程可以同时执行; 线程池
-
查看电脑线程数
public class Test {
public static void main(String[] args) {
// 获取电脑的线程数,不是cpu的核心数
// CPU 密集型,IO密集型
System.out.println(Runtime.getRuntime().availableProcessors());
}
}
- 线程状态
public enum State {
// 新生
NEW,
// 运行
RUNNABLE,
// 阻塞
BLOCKED,
// 等待,死死地等
WAITING,
// 超时等待
TIMED_WAITING,
// 终止
TERMINATED;
}
-
wait 与 sleep 的区别
-
来源
//Object.wait() public final void wait() throws InterruptedException { wait(0); } //Thread.sleep() //注:实际开发,会使用TimeUnit.DAYS.sleep(1L),底层仍为Thread.sleep() public static native void sleep(long millis) throws InterruptedException;
-
锁的释放
-
wait() 会释放锁:wait 是进入线程等待池等待,出让系统资源,其他线程可以占用 CPU。
-
sleep() 不出让系统资源;
-
-
捕获异常
- 都需要捕获异常
-
使用范围
- wait() 需要在同步代码块、同步方法中使用
- sleep() 可以在任何地方使用
-
作用对象:
- wait() 定义在 Object 类中,作用于对象本身
- sleep() 定义在 Thread 类中,作用当前线程。
-
方法属性:
- wait() 是实例方法
- sleep() 是静态方法 有
static
-
锁
-
synchronized
- 同步方法
- 同步代码块
-
Lock
-
常用语句
Lock l = ...; l.lock(); try { // access the resource protected by this lock } finally { l.unlock(); }
-
ReentrantLock
可重入锁实现- 默认为非公平锁,可在构造函数选择
- 默认为非公平锁,可在构造函数选择
-
-
Synchronized 和 Lock 区别
详细区别,参照文末链接 **Java并发编程:Lock**
- Lock获取锁的其他方式
-
尝试非阻塞的获取锁
tryLock()
:当前线程尝试获取锁,如果该时刻锁没有被其他线程获取到,就能成功获取并持有锁 -
能被中断的获取锁
lockInterruptibly()
:获取到锁的线程能够响应中断,当获取到锁的线程被中断的时候,会抛出中断异常同时释放持有的锁 -
超时的获取锁
tryLock(long time, TimeUnit unit)
:在指定的截止时间获取锁,如果没有获取到锁返回 false
-
生产者消费者问题
synchronized 版本
- 单生产者单消费者
public class ProducerWithSynchronized {
public static void main(String[] args) {
SynchronizedData data = new SynchronizedData();
new Thread(() - {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"ADD").start();
new Thread(()-{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"MINUS").start();
}
}
//判断等待
//业务代码
//通知其他线程
//数字:资源类
public class SynchronizedData {
//属性
private int number = 0;
public synchronized void increment() throws InterruptedException {
while (number != 0) {
//等待
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName() + "--" + number);
//加完了通知其他线程
this.notifyAll();
}
public synchronized void decrement() throws InterruptedException {
while (number == 0) {
//等待
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName() + "--" + number);
//减完了通知其他线程
this.notifyAll();
}
}
- 多生产者多消费者
-
出现虚假唤醒问题
- 一旦线程被唤醒,并得到锁,就不会再判断if条件,而执行if语句块外的代码,所以建议,凡是先要做条件判断,再wait的地方,都使用while循环来做
-
解决方法
- 将
if
判断换为while
,线程被再次唤醒后会继续判断条件
- 将
-
注意
- 单纯将
notifyAll()
换为notify()
,不再唤醒等待的多个线程,而是随机唤醒单个线程,不会解决虚假唤醒问题。
- 单纯将
-
Lock版本
通过 Lock 找到 condition 来配合控制对线程的唤醒
public class ProducerWithLock {
public static void main(String[] args) {
LockData data = new LockData();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
public class LockData {
private int number = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void increment() throws InterruptedException {
lock.lock();
try {
while (number != 0) {
//等待
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName() + "-->" + number);
//加完了通知其他线程
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decrement() throws InterruptedException {
lock.lock();
try {
while (number == 0) {
//等待
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName() + "-->" + number);
//减完了通知其他线程
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
Lock精准唤醒版本
这里我用的例子还是刚才的生产者消费者,方便展示问题。
刚才解决虚假唤醒的时候说过不能直接将notifyAll()
换为notify()
,也就是说我们必须使用全部唤醒。但是转到Lock时分离出来的Condition可以使用signal()
来实现精准唤醒,不会有资源的浪费。
弹幕说notifyAll()
和单个Condition的signalAll()
也可以的。我只能说精准唤醒是为了提升性能,不是为了实现功能。
public class AwakeByCondition {
public static void main(String[] args) {
AwakeInOrderByCondition data = new AwakeInOrderByCondition();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
public class AwakeInOrderByCondition {
private int number = 0;
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
public void increment() throws InterruptedException {
lock.lock();
try {
while (number != 0) {
//等待
condition1.await();
}
number++;
System.out.println(Thread.currentThread().getName() + "-->" + number);
//加完了通知其他线程
condition2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decrement() throws InterruptedException {
lock.lock();
try {
while (number == 0) {
//等待
condition2.await();
}
number--;
System.out.println(Thread.currentThread().getName() + "-->" + number);
//减完了通知其他线程
condition1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
8 个代码加锁问题
通常在两个线程启动中加入主线程休眠语句,以保证先发线程抢到锁。
TimeUnit.SECONDS.sleep(1);
单实例双同步方法
public class Lock1 {
/**
* 标准情况下 是先sendEmail() 还是先callPhone()?
* 答案:sendEmail
* 解释:被 synchronized 修饰的方式,锁的对象是方法的调用者
* 所以说这里两个方法调用的对象是同一个,先调用的先执行!
*/
public static void main(String[] args) {
Phone1 phone = new Phone1();
//锁的存在
new Thread(() -> {
phone.sendSms();
}, "A").start();
new Thread(() -> {
phone.call();
}, "B").start();
}
}
class Phone1 {
public synchronized void sendSms() {
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
}
资源类休眠
public class Lock2 {
/**
* sendEmail()休眠三秒后 是先执行sendEmail() 还是 callPhone()
* 答案: sendEmail
* 解释:被 synchronized 修饰的方式,锁的对象是方法的调用者
* 所以说这里两个方法调用的对象是同一个,先调用的先执行!
*/
public static void main(String[] args) {
Phone2 phone = new Phone2();
//锁的存在
new Thread(() -> {
phone.sendSms();
}, "A").start();
new Thread(() -> {
phone.call();
}, "B").start();
}
}
class Phone2 {
public synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
}
单实例普通方法混同步方法
public class Lock3 {
/**
* 被synchronized 修饰的方式和普通方法 先执行sendEmail() 还是 callPhone()
* 答案: callPhone
* 解释:新增加的这个方法没有 synchronized 修饰,不是同步方法,不受锁的影响!
*/
public static void main(String[] args) {
Phone3 phone = new Phone3();
//锁的存在
new Thread(() -> {
phone.sendSms();
}, "A").start();
new Thread(() -> {
phone.call();
}, "B").start();
}
}
class Phone3 {
public synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public void call() {
System.out.println("打电话");
}
}
双实例双同步方法
public class Lock4 {
/**
* 被synchronized 修饰的不同方法 先执行sendEmail() 还是callPhone()?
* 答案:callPhone
* 解释:被synchronized 修饰的不同方法 锁的对象是调用者
* 这里锁的是两个不同的调用者,所有互不影响
*/
public static void main(String[] args) {
Phone4 phoneA = new Phone4();
Phone4 phoneB = new Phone4();
//锁的存在
new Thread(() -> {
phoneA.sendSms();
}, "A").start();
new Thread(() -> {
phoneB.call();
}, "B").start();
}
}
class Phone4 {
public synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
}
单实例双静态方法
public class Lock5 {
/**
* 两个静态同步方法 都被synchronized 修饰 是先sendEmail() 还是callPhone()?
* 答案:sendEmail
* 解释:只要方法被 static 修饰,锁的对象就是 Class模板对象,这个则全局唯一!
*/
public static void main(String[] args) {
Phone5 phone = new Phone5();
//锁的存在
new Thread(() -> {
phone.sendSms();
}, "A").start();
new Thread(() -> {
phone.call();
}, "B").start();
}
}
class Phone5 {
public static synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public static synchronized void call() {
System.out.println("打电话");
}
}
单实例静态方法混同步方法
public class Lock6 {
/**
* 被synchronized 修饰的普通方法和静态方法 是先sendEmail() 还是 callPhone()?
* 答案:callPhone
* 解释:只要被static修饰锁的是class模板, 而synchronized 锁的是调用的对象
* 这里是两个锁互不影响,按时间先后执行
*/
public static void main(String[] args) {
Phone6 phone = new Phone6();
//锁的存在
new Thread(() -> {
Phone6.sendSms();
}, "A").start();
new Thread(() -> {
phone.call();
}, "B").start();
}
}
class Phone6 {
public static synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(6);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
}
双实例双静态方法
public class Lock7 {
/**
* 两个静态同步方法 都被synchronized 修饰 是先sendEmail() 还是callPhone()?
* 答案:sendEmail
* 解释:只要方法被 static 修饰,锁的对象就是 Class模板对象,这个则全局唯一!
*/
public static void main(String[] args) {
Phone7 phoneA = new Phone7();
Phone7 phoneB = new Phone7();
//锁的存在
new Thread(() -> {
phoneA.sendSms();
}, "A").start();
new Thread(() -> {
phoneB.call();
}, "B").start();
}
}
class Phone7 {
public static synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(7);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public static synchronized void call() {
System.out.println("打电话");
}
}
双实例静态方法混同步方法
public class Lock8 {
/**
* 一个被static+synchronized 修饰的方法和普通的synchronized方法,先执行sendEmail()还是callPhone()?
* 答案:callPhone()
* 解释: 只要被static 修饰的锁的就是整个class模板
* 这里一个锁的是class模板 一个锁的是调用者
* 所以锁的是两个对象 互不影响
*/
public static void main(String[] args) {
Phone8 phoneA = new Phone8();
Phone8 phoneB = new Phone8();
//锁的存在
new Thread(() -> {
phoneA.sendSms();
}, "A").start();
new Thread(() -> {
phoneB.call();
}, "B").start();
}
}
class Phone8 {
public static synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(8);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
}
总结
- 同步方法锁对象,可以被多实例绕开
- 静态同步方法锁类,无法被多实例绕开
- 两者混合,不是相同锁
- 不建议使用通过类实例访问静态成员,应该直接使用类访问静态成员
不安全的集合类
List
public class UnSafeList {
public static void main(String[] args) {
List list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0, 5));
System.out.println(list+"("+Thread.currentThread().getName()+")");
}, String.valueOf(i)).start();
}
}
}
-
java.util.ConcurrentModificationException
异常不同线程同时操作了同一 list 索引元素抛出的异常。
-
解决方法
-
集合自带的线程安全的list
List
-
Collections工具类强行上锁
List
-
用
JUC
包下的读写数组CopyOnWriteArrayList
,读写分离List
-
-
CopyOnWriteArrayList
- 介绍
CopyOnWriteArrayList
,写数组的拷贝,支持高效率并发且是线程安全的, 读操作无锁的ArrayList
。所有可变操作都是通过对底层数组进行一次新的复制来实现。CopyOnWriteArrayList
,适合使用在读操作远远大于写操作的场景里,比如缓存。它不存在扩容的概念,每次写操作都要复制一个副本,在副本的基础上修改后改变 Array 引用。CopyOnWriteArrayList
中写操作需要大面积复制数组,所以性能差。CopyOnWriteArrayList
,慎用 ,因为谁也没法保证CopyOnWriteArrayList
到底要放置多少数据,万一数据稍微有点多,每次 add/set 都要重新复制数组,代价高昂。
- 缺点
- 由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致
young gc
或者full gc
。- young gc :年轻代(Young Generation):对象被创建时,内存的分配首先发生在年轻代(大对象可以直接被创建在年老代),大部分的对象在创建后很快就不再使用,因此很快变得不可达,于是被年轻代的
GC
机制清理掉(IBM 的研究表明,98% 的对象都是很快消亡的),这个GC
机制被称为Minor GC
或叫Young GC
。 - 年老代(Old Generation):对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次
Young GC
后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC
次数也比年轻代少。当年老代内存不足时,将执行Major GC
,也叫Full GC
- young gc :年轻代(Young Generation):对象被创建时,内存的分配首先发生在年轻代(大对象可以直接被创建在年老代),大部分的对象在创建后很快就不再使用,因此很快变得不可达,于是被年轻代的
- 不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set 操作后,读取到数据可能还是旧的, 虽然
CopyOnWriteArrayList
能做到最终一致性, 但是还是没法满足实时性要求;
- 由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致
- 总结
CopyOnWriteArrayList
这是一个ArrayList
的线程安全的变体,其原理大概可以通俗的理解为: 初始化的时候只有一个容器,很长一段时间,这个容器数据、数量等没有发生变化的时候,大家 (多个线程),都是读取(假设这段时间里只发生读取的操作) 同一个容器中的数据,所以这样大家读到的数据都是唯一、一致、安全的,但是后来有人往里面增加了一个数据,这个时候CopyOnWriteArrayList
底层实现添加的原理是先 copy 出一个容器(可以简称副本),再往新的容器里添加这个新的数据,最后把新的容器的引用地址赋值给了之前那个旧的的容器地址,但是在添加这个数据的期间,其他线程如果要去读取数据,仍然是读取到旧的容器里的数据。
- 介绍
Set
public class UnSafeSet {
public static void main(String[] args) {
Set