1.什么是虚假唤醒?(某个线程不应该被唤醒)
比如线程A和B同时陷入阻塞,线程C执行notifyAll(),线程A、B都被唤醒,但线程A唤醒执行后的结果应该导致B依旧阻塞,但B没有循环判断,导致也被唤醒!!下面有图解
以生产者-消费者模型举例。。
class Goods{
public int number=0;
//制作方法
public synchronized void made() throws InterruptedException {
//判断 用if的话会产生虚假唤醒
if (number != 0){
this.wait();
}
//干活
number++;
System.out.println(Thread.currentThread().getName()+" number="+number);
//通知
this.notifyAll();
}
//销售方法
public synchronized void sale() throws InterruptedException {
//判断
if(number == 0){
this.wait();
}
//执行
number--;
System.out.println(Thread.currentThread().getName()+" number="+number);
//通知
this.notifyAll();
}
}
public class MadeAndSale {
public static void main(String[] args) {
Goods goods = new Goods();
new Thread(()->{
for (int i = 0;i<10;i++){
try {
goods.made();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i=0;i<10;i++){
try {
goods.sale();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(()->{
for (int i = 0;i<10;i++){
try {
goods.made();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"c").start();
new Thread(()->{
for (int i=0;i<10;i++){
try {
goods.sale();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
按我们在资源类制定的规则是 生产者消费一个 消费者消费一个 所以number应该不是0就是1,,但是在有两个消费者两个生产者的情况下,number出现了2、3的情况,可见number被多加了 ,现在来详细分析为什么会被多加!
主要原因就是线程A、B被阻塞后又被同时释放,而且也没有再次判断number的值,导致number多加了一次
2.如何解决虚假唤醒?
官方文档提出,使用wait时,必须用while来循环判断,不能用if。。
3.三个线程,轮流打印1-100
class Count{
static int number = 0;
private int flag=1;
ReentrantLock reentrantLock = new ReentrantLock();
Condition condition1 = reentrantLock.newCondition();
Condition condition2 = reentrantLock.newCondition();
Condition condition3 = reentrantLock.newCondition();
public void add1(){
reentrantLock.lock();
try {
while (flag != 1){
condition1.await();
}
if (number<=100){
System.out.println(Thread.currentThread().getName()+" "+number);
number++;}
flag=2;
condition2.signal();
}catch (InterruptedException e){
e.printStackTrace();
}finally {
reentrantLock.unlock();
}
}
public void add2(){
reentrantLock.lock();
try {
while (flag !=2){
condition2.await();
}
if (number<=100){
System.out.println(Thread.currentThread().getName()+" "+number);
number++;}
flag=3;
condition3.signal();
}catch (InterruptedException e){
e.printStackTrace();
}finally {
reentrantLock.unlock();
}
}
public void add3(){
reentrantLock.lock();
try {
while (flag !=3){
condition3.await();
}if (number<=100)
System.out.println(Thread.currentThread().getName()+" "+number);
number++;
flag=1;
condition1.signal();
}catch (InterruptedException e){
e.printStackTrace();
}finally {
reentrantLock.unlock();
}
}
}
public class Study {
public static void main(String[] args) {
Count count = new Count();
while (count.number <= 100) {
new Thread(() -> {
count.add1();
}, "A").start();
new Thread(() -> {
count.add2();
}, "B").start();
new Thread(() -> {
count.add3();
}, "C").start();
}
}
}
四、FutureTask与CompletableFuture
1.异步调用
异步调用其实就是实现一个可无需等待被调用函数的返回值而让操作继续运行的方法。在 Java 语言中,简单的讲就是另启一个线程来完成调用中的部分计算,使调用继续运行或返回,而不需要等待计算结果。但调用者仍需要取线程的计算结果。FutureTask也实现了异步调用,但使用它的get()方法时,可能会发生阻塞
JDK5新增了Future接口,用于描述一个异步计算的结果。虽然 Future 以及相关使用方法提供了异步执行任务的能力,但是对于结果的获取却是很不方便,只能通过阻塞或者轮询的方式得到任务的结果。阻塞的方式显然和我们的异步编程的初衷相违背,轮询的方式又会耗费无谓的 CPU 资源,而且也不能及时地得到计算结果.
2.CompletableFuture
根据上图可见,CompletableFuture是FutureTask的加强版
使用get()方法获得返回值时会发生阻塞
解决阻塞的办法:
①将get()方法放到最后(不见不散)
②设置超时时间(过时不候)
③轮询(其实也是另一种形式的阻塞)
while(true){
if (vFutureTask.isDone()){
System.out.println(vFutureTask.get());
break;
}else{
System.out.println("不要催l");
}
}
④用CompletableFuture的静态方法来代替FutureTask
主要思想:回调通知:做完了在通知我
合并多个独立的异步计算,虽相互独立,但第二个依赖于第一个的结果
public class CompletableFutureTest2 {
public static void main(String[] args) {
CompletableFuture.supplyAsync(()->{
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 1;
}).thenApply((f)->{
return f+1;
}).whenComplete((v,e)->{
if (e == null){
System.out.println("result"+v);
}
}).exceptionally((e)->{
e.printStackTrace();
return null;
});
System.out.println("main");
//如果主线程关闭,那么默认的线程池会关闭,让主线程多跑5秒,让上面的result跑出来
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
五、说说Java的锁
1.悲观锁、乐观锁
悲观锁:认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。synchronized关键字和Lock的实现类都是悲观锁
适合写操作多的场景,先加锁可以保证写操作时数据正确
乐观锁:乐观锁则直接去操作同步资源,是一种无锁算法,得之我幸不得我命,再抢
适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。、
2.synchronized锁
①作用于代码块:
monitorinter----------------------------monitorexit ----------------------monitorexit
如果代码块中显示的抛出了异常
monitorinter----------------------------monitorexit
②作用于实例方法
字节码文件里有个标识 : ACC_SYNCHRONIZED
③作用域=于静态实例方法
字节码文件里有个标识 : ACC_STATIC ACC_SYNCHRONIZED
monitor(管程)底层使用objecMonitor(对象监视器)
每个对象都有一个objecMonitor
3.公平锁和非公平锁
按序排队公平锁,就是判断同步队列是否还有先驱节点的存在(我前面还有人吗?),如果没有先驱节点才能获取锁;先占先得非公平锁,是不管这个事的,只要能抢获到同步
为什么默认使用非公平锁?(性能好)
1恢复挂起的线程到真正锁的获取还是有时间差的
2.2使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。
非公平锁产生的问题
锁饥饿
什么时候使用非公平锁?
为了提高吞吐量
4.可重入锁(为了防止自己坑到自己)
还没释放这个锁就再次获得这个锁在语法上是可以的
一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。
StampedLock是不可重入锁
5.死锁
1.手写一个死锁`
public class Deadsuo {
static Object o = new Object();
static Object o2 = new Object();
public static void main(String[] args) {
new Thread(()->{
synchronized (o){
System.out.println("have o expect o2");
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
System.out.println("have o2");
}
}
},"A").start();
new Thread(()->{
synchronized (o2){
System.out.println("have o2 expect o");
synchronized (o){
System.out.println("have o");
}
}
},"B").start();
}
}
2.如何判断发生了死锁?(不能单纯的根据运行结果就判断死锁)
步骤一:打开cmd
步骤二:用jps命令查看进程号
步骤三:用jstack查看是否发生了死锁
6.自旋锁
手写一个自旋锁
class Zixuan{
AtomicReference<Thread> atomicReference = new AtomicReference<>();
//获得锁
public void lock(){
System.out.println(Thread.currentThread().getName()+"come in");
//没有得到就一直自旋
while (!atomicReference.compareAndSet(null,Thread.currentThread())){
}
System.out.println(Thread.currentThread().getName()+"得到了锁");
}
//解锁
public void unlock(){
//没有解锁就一直自旋
while (!atomicReference.compareAndSet(Thread.currentThread(),null)){
}
System.out.println(Thread.currentThread().getName()+"解锁成功");
}
}
public class Study {
public static void main(String[] args) {
Zixuan zixuan = new Zixuan();
new Thread(()->{
zixuan.lock();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
zixuan.unlock();
},"A").start();
new Thread(()->{
zixuan.lock();
zixuan.unlock();
},"B").start();
}
}
7.无锁、偏向锁、轻量级锁、重量级锁(锁升级)
为什么要有锁升级机制?
Synchronized会涉及到用户态和内核态转换,这对性能有极大的影响。为了提高性能
①无锁----->偏向锁------>轻量锁------->重量锁
0 01 1 01 0 00 0 10
②偏向锁:如果很长时间的情况下只有有一个线程持有这个锁,那么这个线程就会一直持有这个锁
③轻量级锁: 假设线程A持有偏向锁,此时线程A有两种情况:
A1:线程A正在执行锁里面的内容
A2:线程A已经执行完锁里的内容
这是线程B也来竞争锁,若线程A处于A1状态,则升级为轻量级锁
若线程A处于A2状态,线程B获得这个锁,若线程A不再CAS则线程B独占锁 ,为偏向锁
④重量锁:多线程竞争激励,升级为重量锁 。自旋到一定次数,空耗CPU,JVM进行自适应,升级成重量锁。
偏向锁一直持有锁,轻量级锁通过CAS来竞争锁,重量级锁涉及到用户态和内核态状态的转换
偏向锁可以变成无锁状态,但锁只能升级,不能降级
8.无锁→独占锁→读写锁→邮戳锁
StampedLock邮戳锁,读写共存,比读写锁速度更快。
先是了乐观读,如果数据发生了修改,升级成悲观读。
六、LockSupport与线程中断
1.如何停止、中断一个运行中的线程??
利用interru方法,设置该线程的中断标志位为true,然后程序员依靠这个标志位实现线程中断,线程应该自己中断,不应该有其他线程控制。
在需要中断的线程中不断监听中断状态,
一旦发生中断,就执行相应的中断处理业务逻辑。
2.三个重要方法
①public void **interrupt()**实例方法,实例方法interrupt()仅仅是设置线程的中断状态为true,不会停止线程
②public static boolean **interrupted()**静态方法。Thread.interrupted(); 判断线程是否被中断,并清除当前中断状态这个方法做了两件事:
1 返回当前线程的中断状态
2 将当前线程的中断状态设为false 这个方法有点不好理解,因为连续调用两次的结果可能不一样。
③public boolean isInterrupted()实例方法,判断当前线程是否被中断(通过检查中断标志位
3.LockSupport
以前写的
park与unpark是阻塞原语,
、优势是:无需和锁一起用
位置颠倒也可
七、JMM模型
1.简单说一下jmm
jmm模型有个主内存,每个线程有自己的工作内存,主内存存储着一些变量,线程要操作这些变量,需要拷贝一份到自己的工作内存进行操作,然后在返回给主内存
并发编程有三个特性:可见性、有序性、原子性
2.happens-before是什么?
JVM会对代码进行编译优化,会出现指令重排序情况,为了避免编译优化对并发编程安全性的影响,需要happens-before规则定义一些禁止编译优化的场景,保证并发编程的正确性
典型的happens-before规则:volatile变量规则:
3.简单说一下volatile
volatile可以保证可见性和禁止指令重排
volatile在多个线程间实现变量可见,可以实现线程中断
如何保证?
通过内存屏障:内存屏障就是4个JVM指令
loadload storestore loadstore storeload
在对volatile修饰的变量进行写操作之前会加入storestore指令,写操作之后加入storeload指令
在对volatile修饰的变量进行读操作之后会加入loadload,loadstore指令
八、CAS底层原理:
在多线程情况下,不使用原子类对基本数据类型进行保护的话,用volatile+synchronized
而使用原子类的底层是volatile+CAS+native方法
CAS不会阻塞,通过硬件保证
CAS依赖于unSafe类,,而Unsafe底层采用的native方法是直接由cpu硬件支持的原子操作,这使得java程序通过CAS来运行的效率会非常高。
CAS带来的问题?
- ABA问题:我用AtomicStampedReference类来解决ABA问题
- 自旋对于CPU来说开销很大
九、十六个原子操作类
首先我来默写一下这些原子类
①基本类型原子类
AtomicInteger
AtomicLong
AtomicBoolean:自认为线程中断要用到它
②数组类型原子类
AtomicIntegerArray
AtomiclongArray
AtomicReferenceArray
③引用类型原子类
AtomicReference
AtomicStampedReference:解决ABA问题,能知道修改了多少次
AtomicMarkableReference:只能判断出是否修改,不能解决ABA问题
④对象类型修改类原子类
AtomicIntegerFileUpdater:可被修改的属性要用public volatile修饰
用一段代码来看看是如何用它的:
class Person{
//1.属性必须用public volatile修饰
public volatile int x;
//2.用静态方法来制造一个newUpdater
AtomicIntegerFieldUpdater fieldUpdater = AtomicIntegerFieldUpdater.newUpdater(Person.class,"x");
//3.利用newUpdater来对数据进行操作
public void transfer(Person person){
fieldUpdater.incrementAndGet(person);
}
}
public class AtomicIntegerFileUpadterTest {
public static void main(String[] args) throws InterruptedException {
Person person =new Person();
CountDownLatch countDownLatch = new CountDownLatch(500);
for(int i = 0;i<500;i++){
new Thread(()->{
person.transfer(person);
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
System.out.println(person.x);
}
}
AtomicLongFileUpdater
AtomicReferenceUpdater
⑤增强型:基本数据原子类型的性能增强版
LongAdder:只能计算加法,且从0开始计算
LongAccumulator:提供了自定义的函数操作
DoubleAdder
DoubleAccumulator
下面来说说为什么是性能增强版!!!
首先为什么要性能增强?
原子类采用了CAS原理,在多线程的情况下,自旋情况造成的CPU浪费情况越来越严重。用这四个增强类,可以提升性能。
他们是如何实现性能增强的?(Striped64很强大,而LongAdder实现了它)
主要思想:base值+cell【value】数组(空间换时间)(分散热点)
看图说话
在并发量不大的情况下,他就和普通的原子类一样,当并发量大时,他会初始化一个cell类型的数组,数组长度为2,这些线程向往哈希表里放数据一样,选择自己要操作的下标,线程对其对应的value进行CAS操作。当并发量更大时,cell数组会扩容,扩容的规则是 (乘2)即 0、2、4、8…
最后将base和cell数组里的每一个value值加起来
十、ThreadLocal
四个大厂面试题:
1.Thread、ThreadLocal中ThreadLocalMap的数据结构和关系?
每一个Thread有一个ThreadLocal
ThreadLocal有一个静态内部类ThreadLocaMap,注意看ThreadLocaMap里的静态内部类Entry继承了弱引用,
Thread里有一个ThreadLocalMap类的实例引用
同一个ThreadLocal在不同的线程里的value是不一样的
整个代码
public class ThreadLocalDemo {
private static ThreadLocal<String> localVar = new ThreadLocal<String>();
private static ThreadLocal<Integer> MyLocal = new ThreadLocal<Integer>();
static void print() {
//打印当前线程中本地内存中本地变量的值
System.out.println(localVar.get());
System.out.println(MyLocal.get());
//清除本地内存中的本地变量
localVar.remove();
MyLocal.remove();
}
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName());
ThreadLocalDemo.localVar.set("local_A");
ThreadLocalDemo.MyLocal.set(100);
print();
//打印本地变量
System.out.println("after remove : " + localVar.get());
System.out.println("after remove : " + MyLocal.get());
}
},"A").start();
Thread.sleep(2000);
new Thread(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName());
ThreadLocalDemo.localVar.set("local_B");
ThreadLocalDemo.MyLocal.set(200);
print();
System.out.println("after remove : " + localVar.get());
System.out.println("after remove : " + MyLocal.get());
}
},"B").start();
}
}
2.ThreadLocal的key是弱引用,这是为什么
利于ThreadLocal对象的回收
3.ThreadLocal内存泄露问题你知道吗?
方法结束,强引用消失,弱引用遇到GC则回收对应的ThreadLocal对象,但是线程不结束,ThreadLocalMap还存在,而key的值为null,访问不到value的值,而且线程的创建现在大部分是在线程池里,线程不会立马结束,导致Entry越来越多,造成内存泄漏。
4.ThreadLocal中最后为什么要加remove方法?
防止内存泄漏
十一、AQS
1.用自己的话来说一说啥是AQS?
AQS是一个构建锁和同步器的框架,像 ReentrantLock、FutureTask、semaphore都是基于它构造出来的。他的结构应该是资源标志位state和一个CLH虚拟双向队列吧。当一个线程请求资源时,这个资源没有线程占用,则这个线程占用,state设为1,然后其他的线程请求资源,就会被封装成一个节点,加入到虚拟队列当中。
AQS的整体结构
上面说到AQS是实现一些锁、同步器的框架,这里以ReentraLock的lock来举例说明一下
ReentraLock有公平锁和非公平锁,看看他的结构
先以非公平锁的lock来看
进入到lock()方法后,先对state进行CAS,如果失败进入acquire()方法
首先这里有一个模板方法tryAcquire()要让继承AQS的类来实现它,不实现就会爆异常
这里看看ReentrantLock里对模板的实现类,这里以线程A已经抢占到资源,然后线程B来抢锁的i情况来说明这个方法:最后nonfairTryAcquire()方法返回false,然后就要判断acquireQueued()方法
这里的addwaiter()就是要将这个线程包装成节点加到队列里
加完以后的状态是
十二、线程不安全的集合类
1.ArrayList
ArrayList在多线程的情况下进行读写时可能会爆ConcurrentModificationException(并发修改异常)
public class ArrayListTest {
public static void main(String[] args) {
ArrayList arrayList = new ArrayList();
for (int i=0;i<500;i++){
new Thread(()->{
arrayList.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(arrayList);
}).start();
}
}
}
解决方法
①Vector
②Collections.synchronizedList()
③CopyOnWriteArrayLiat()
写的时候会复制一份新的数组来进行添加
十三、JUC辅助类
CountDownLatch:倒计时
CyclicBarrier:全部到齐再发车
public class ArrayListTest {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
System.out.println("集齐七颗龙珠,召唤神龙");
});
for (int i=0;i<7;i++){
final int a = i;
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"找到第"+a+"颗龙珠");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
semaphore:抢车位
public class ArrayListTest {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);//模拟有三个停车位
//模拟六个车抢停车位
for (int i=0;i<6;i++){
new Thread(()->{
try {
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"抢到了车位");
//每辆车都是五秒后离开车位
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
semaphore.release();
},String.valueOf(i)).start();
}
}
}
十四、ReentraReadWriteLock与StampedLock
ReentraReadWriteLock读写锁只能锁降级不能锁升级
锁降级:获得写锁-》获得读锁-》释放写锁-》释放读锁
锁升级:不行 在获得读锁后是无法获得写线程的(悲观读)
StampedLock允许读的时候写(乐观读):
为了解决锁饥饿:一直是读锁,无法获得锁饥饿
stamp的值位true时,就没人修改,为false时,就有人修改,在乐观读中通过判断stamp来看是否要从乐观读降成悲观读