一些最基本的基础知识就不总结了,参考之前写的如下几篇博客,阅读顺序从上到下,依次递进。
java 多线程 多线程概述及其三种创建方式 线程的常用方法
java 线程安全问题 三种线程同步方案 线程通信(了解)
java 线程池 并发并行 线程的生命周期
java 多线程乐观锁与悲观锁
目录
- 基础知识补充
- 新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?
- 在 java 中 wait 和 sleep 方法的不同?
- 如何停止一个正在运行的线程?
- 线程中并发锁知识补充
- synchronized关键字的底层原理
- 谈谈 JMM(Java 内存模型)
- CAS
- volatile
- AQS
- ReentrantLock的实现原理
- synchronized和Lock有什么区别 ?
- 死锁产生的条件是什么?如何避免死锁?如何如何进行死锁诊断?
- ConcurrentHashMap
- 并发程序出现问题的根本原因(并发三大特性)
- ThreadLocal详解
- ThreadLocal-内存泄露问题
基础知识补充
新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?
可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。
为了确保三个线程的顺序你应该先启动最后一个(T3调用T2,T2调用T1),这样T1就会先完成而T3最后完成
public class JoinTest {
public static void main(String[] args) {
// 创建线程对象
Thread t1 = new Thread(() -> {
System.out.println("t1");
}) ;
Thread t2 = new Thread(() -> {
try {
t1.join(); // 加入线程t1,只有t1线程执行完毕以后,再次执行该线程
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2");
}) ;
Thread t3 = new Thread(() -> {
try {
t2.join(); // 加入线程t2,只有t2线程执行完毕以后,再次执行该线程
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t3");
}) ;
// 启动线程
t1.start();
t2.start();
t3.start();
}
}
在 java 中 wait 和 sleep 方法的不同?
共同点
- wait() ,wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态
不同点
- 方法归属不同,sleep(long) 是 Thread 的静态方法,而 wait(),wait(long) 都是 Object 的成员方法,每个对象都有
- 醒来时机不同,首先它们都可以被interrupt()方法打断唤醒执行。正常情况sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来,wait(long) 和 wait() 还可以被 notify 唤醒,wait() 如果不唤醒就一直等下去。
- 锁特性不同(重点)
-
- wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制
-
- wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃cpu,但你们还可以用)
-
- 而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃cpu,你们也用不了)
如何停止一个正在运行的线程?
有三种方式可以停止线程
- 当run方法完成后线程终止
- 使用stop方法强行终止(不推荐,方法已作废)
- 使用interrupt方法中断线程
其中stop方法就是下面这样使用:
MyInterrupt2 t1 = new MyInterrupt2() ;
t1.start();
// 主线程休眠2秒
Thread.sleep(6000);
// 调用stop方法
t1.stop();
注意调用interrupt()方法并不会立即停止线程,它只是改变了线程的中断状态,具体是否能够停止线程,还需要看这个线程的具体代码实现。如果线程中没有处理中断的逻辑,那么即使调用了interrupt()方法,线程也不会停止。因此,编写多线程代码时,我们需要特别注意线程的中断处理。这就是"打断唤醒"的基本含义。
package com.itheima.basic;
public class MyInterrupt3 {
public static void main(String[] args) throws InterruptedException {
//2.打断正常的线程
Thread t2 = new Thread(()->{
while(true) {
Thread current = Thread.currentThread();
boolean interrupted = current.isInterrupted();
if(interrupted) {
System.out.println("打断状态:"+interrupted);
break;
}
}
}, "t2");
t2.start();
Thread.sleep(500);
// t2.interrupt();
}
}
线程中并发锁知识补充
synchronized关键字的底层原理
参考回答:
- Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】
- 它的底层由monitor实现的,monitor是jvm级别的对象( C++实现),线程获得锁需要使用对象(锁)关联monitor
- 在monitor内部有三个属性,分别是owner、entrylist、waitset
- 其中owner是关联的获得锁的线程,并且只能关联一个线程;entrylist关联的是处于阻塞状态的线程;waitset关联的是处于Waiting状态的线程
谈谈 JMM(Java 内存模型)
JMM(Java Memory Model)Java内存模型,是java虚拟机规范中所定义的一种内存模型。
Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。
特点:
- 所有的共享变量都存储于主内存(计算机的RAM)这里所说的变量指的是实例变量和类变量。不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。
- 每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。
- 线程对变量的所有的操作(读,写)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存完成。
CAS
CAS的全称是: Compare And Swap(比较再交换),它体现的一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性。
在JUC( java.util.concurrent )包下实现的很多类都用到了CAS操作
- AbstractQueuedSynchronizer(AQS框架)
- AtomicXXX类
基于刚才学习过的JMM内存模型进行说明
线程1与线程2都从主内存中获取变量int a = 100,同时放到各个线程的工作内存中
一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当旧的预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。如果CAS操作失败,通过自旋的方式等待并再次尝试,直到成功
volatile
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那
么就具备了两层语义:
保证线程间的可见性
保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的
值,这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即
写入主存。
禁止进行指令重排序
用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写
操作越过屏障,从而达到阻止重排序的效果
下面解释一下这两个含义,首先是保证线程间的可见性,如下案例所示
package com.itheima.basic;
// 可见性例子
// -Xint
public class ForeverLoop {
static boolean stop = false;
public static void main(String[] args) {
new Thread(() -> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
stop = true;
System.out.println("modify stop to true...");
}).start();
foo();
}
static void foo() {
int i = 0;
while (!stop) {
i++;
}
System.out.println("stopped... c:"+ i);
}
}
当执行上述代码的时候,发现foo()方法中的循环是结束不了的,也就说读取不到
共享变量的值结束循环。主要是因为在JVM虚拟机中有一个JIT(即时编辑器)给代码做了优化。在很短的时间内,foo中while循环执行的次数太多了,当达到了一个阈值,JIT就会优化此代码,直接将!stop转为true,当把代码优化成这样子以后,及时stop 变量改变为了false 也依然停止不了循环
解决方案:
第一:在程序运行的时候加入vm参数-Xint 表示禁用即时编辑器,不推荐,得不偿失(其他程序还要使用)
第二:在修饰stop 变量的时候加上volatile ,表示当前代码禁用了即时编辑器,问题
就可以解决
然后是禁止进行指令重排序,比如如下代码:
在去获取上面的结果的时候,有可能会出现4种情况
情况一:先执行actor2获取结果—>0,0(正常)
情况二:先执行actor1中的第一行代码,然后执行actor2获取结果—>0,1(正常)
情况三:先执行actor1中所有代码,然后执行actor2获取结果—>1,1(正常)
情况四:先执行actor1中第二行代码,然后执行actor2获取结果—>1,0(发生了指令重排序,影响结果)
解决方案:在变量上添加volatile,禁止指令重排序,则可以解决问题
屏障添加的示意图:
写操作加的屏障是阻止上方其它写操作越过屏障排到volatile变量写之下
读操作加的屏障是阻止下方其它读操作越过屏障排到volatile变量读之上
我们上面的解决方案是把volatile加在了int y这个变量上,我们能不能把它加在
int x这个变量上呢?如果使用volatile修饰了x变量,如下所示,这样显然是不行的
所以,现在我们就可以总结一个volatile使用的小妙招:
- 写变量让volatile修饰的变量的在代码最后位置
- 读变量让volatile修饰的变量的在代码最开始位置
AQS
全称是 Abstract Queued Synchronizer 它是构建锁或者其他同步组件的基础框架
synchronized | AQS |
---|---|
关键字,c++ 语言实现 | java 语言实现 |
悲观锁,自动释放锁 | 悲观锁,手动开启和关闭 |
锁竞争激烈都是重量级锁,性能差 | 锁竞争激烈的情况下,提供了多种解决方案 |
AQS常见的实现类
- ReentrantLock 阻塞式锁
- Semaphore 信号量
- CountDownLatch 倒计时锁
工作机制
- 在AQS中维护了一个使用了volatile修饰的state属性来表示资源的状态,0表示无锁,1表示有锁
- 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList
- 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的WaitSet
线程0来了以后,去尝试修改state属性,如果发现state属性是0,就修改state状态为1,表示线程0抢锁成功
线程1和线程2也会先尝试修改state属性,发现state的值已经是1了,有其他线程持有锁,它们都会到FIFO队列中进行等待,
FIFO是一个双向队列,head属性表示头结点,tail表示尾结点
如果多个线程共同去抢这个资源是如何保证原子性的呢?在去修改state状态的时候,使用的cas自旋锁来保证原子性,确保只能有一个线程修改成功,修改失败的线程将会进入FIFO队列中等待
AQS是公平锁吗,还是非公平锁?
如果新的线程与队列中的线程共同来抢资源,是非公平锁
如果新的线程到队列中等待,只让队列中的head线程获取锁,是公平锁
比较典型的AQS实现类ReentrantLock,它默认就是非公平锁,新的线程与队
列中的线程共同来抢资源
ReentrantLock的实现原理
ReentrantLock翻译过来是可重入锁,相对于synchronized它多具备以下特点(面试题,与synchronized的区别):
- 可中断
- 可以设置超时时间
- 可以设置公平锁
- 支持多个条件变量
- 与synchronized一样,都支持重入
ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似构造方法接受一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。
工作流程
- 线程来抢锁后使用cas的方式修改state状态,修改状态成功为1,则让exclusiveOwnerThread属性指向当前线程,获取锁成功
- 假如修改状态失败,则会进入双向队列中等待,head指向双向队列头部,tail指向双向队列尾部
- 当exclusiveOwnerThread为null的时候,则会唤醒在双向队列中等待的线程
- 公平锁则体现在按照先后顺序获取锁,非公平体现在不在排队的线程也可以抢锁
synchronized和Lock有什么区别 ?
- 语法层面
- synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现
- Lock 是接口,源码由 jdk 提供,用 java 语言实现
- 使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁
- 功能层面
- 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
- Lock 提供了许多 synchronized 不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量(重点 需要背)
- Lock 有适合不同场景的实现,如 ReentrantLock, ReentrantReadWriteLock
- 性能层面
- 在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖
- 在竞争激烈时,Lock 的实现通常会提供更好的性能
死锁产生的条件是什么?如何避免死锁?如何如何进行死锁诊断?
死锁:一个线程需要同时获取多把锁,这时就容易发生死锁
例如:t1 线程获得A对象锁,接下来想获取B对象的锁
t2 线程获得B对象锁,接下来想获取A对象的锁
产生死锁的四个必要条件:
1.互斥:该资源任意一个时刻只由一个线程占用。
2.占有且等待:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
3.不可强占用:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
4.循环等待:若干线程之间形成一种头尾相接的循环等待资源关系。
如何预防死锁?破坏死锁的产生的必要条件即可:
1.破坏占有且等待∶一次性申请所有的资源。
2.破坏不可强占用︰占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
3.破坏循环等待︰靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。
当程序出现了死锁现象,我们可以使用jdk自带的工具:jps和 jstack
第一:jps查看运行的线程
第二:使用jstack查看线程运行的情况,下图是截图的关键信息 运行命令: jstack -l 46032
其他解决工具,可视化工具
- jconsole
用于对jvm的内存,线程,类 的监控,是一个基于 jmx 的 GUI 性能监控工具
打开方式:java 安装目录 bin目录下 直接启动 jconsole.exe 就行 - VisualVM:故障处理工具
能够监控线程,内存情况,查看方法的CPU时间和内存中的对 象,已被GC的对象,反向查看分配的堆栈
打开方式:java 安装目录 bin目录下 直接启动 jvisualvm.exe就行
ConcurrentHashMap
JDK1.7中concurrentHashMap
底层采用分段的数组+链表实现
提供了一个segment数组,在初始化ConcurrentHashMap 的时候可以指定数组的长度,默认是16,一旦初始化之后中间不可扩容
在每个segment中都可以挂一个HashEntry数组,数组里面可以存储具体的元素,HashEntry数组是可以扩容的
在HashEntry存储的数组中存储的元素,如果发生冲突,则可以挂单向链表
存储流程:
先去计算key的hash值,然后确定segment数组下标
再通过hash值确定hashEntry数组中的下标存储数据
在进行操作数据的之前,会先判断当前segment对应下标位置是否有线程进行操作,为了线程安全使用的是ReentrantLock进行加锁,如果获取锁是被会使用cas自旋锁进行尝试
JDK1.8中concurrentHashMap
在JDK1.8中,放弃了Segment臃肿的设计,数据结构跟HashMap的数据结构是一样的:数组+红黑树+链表
采用 CAS + Synchronized来保证并发安全进行实现
- CAS控制数组节点的添加
- synchronized只锁定当前链表或红黑二叉树的首节点,只要hash不冲突,就不
会产生并发的问题 , 效率得到提升
并发程序出现问题的根本原因(并发三大特性)
Java并发编程三大特性
- 原子性 一个线程在CPU中操作不可暂停,也不可中断,要不执行完成,要不不执行
- 可见性 让一个线程对共享变量的修改对另一个线程可见
- 有序性 即指令重排
指令重排:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保
证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终
执行结果和代码顺序执行的结果是一致的
原子性解决方案
synchronized:同步加锁
JUC里面的lock:加锁
内存可见性解决方案
synchronized
volatile(推荐)
LOCK
有序性解决方案
volatile
ThreadLocal详解
ThreadLocal是多线程中对于解决线程安全的一个操作类,它会为每个线程都分配一个独立的线程副本从而解决了变量并发访问冲突的问题。ThreadLocal 同时实现了线程内的资源共享
案例:使用JDBC操作数据库时,会将每一个线程的Connection放入各自的ThreadLocal中,从而保证每个线程都在各自的 Connection 上进行数据库的操作,避免A线程关闭了B线程的连接
三个主要方法:set(value) 设置值 get() 获取值 remove() 清除值
ThreadLocal的实现原理
在ThreadLocal中有一个内部类叫做ThreadLocalMap,类似于HashMap,ThreadLocalMap中有一个属性table数组,这个是真正存储数据的位置
ThreadLocal-内存泄露问题
ThreadLocalMap中使用的 key为 ThreadLocal的弱引用,而value是强引用。所以,如果ThreadLocal没有被外部强引用的情况下,在垃圾回收的时候,key会被清理掉,而value 不会被清理掉。
这样一来,ThreadLocalMap中就会出现 key为null 的Entry。假如我们不做任何措施的话,value永远无法被GC回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用set() 、get() 、 remove()方法的时候,会清理掉key为 null 的记录。使用完ThreadLocal方法后最好手动调用removeO方法
static class Entry extends >WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v; //强引用,不会被回收
}
}
四种引用类型介绍这篇文章有:一文学完JVM所有知识点!