多线程核心 JUC

一级目录

二级目录

三级目录

学几个快捷键 可能用到

记住几个快捷键,

抽取代码来包围:ctrl+alt+t

抽取代码为方法:Ctrl + Alt + M

volatile

是轻量级的同步机制,他有三个特征:保证可见性、禁止指令重排+不保证原子性

第二问:深入问

可见性:主内存的值修改,线程都知道了这件事

1. 对可见性的理解+代码

JMM–Java内存模型

1.本身根本不存在,是一种约定/规范,规定了程序中变量的访问方式

2.同步的内容:

​ 线程解锁前,要把共享变量的值刷新回主内存

​ 线程加锁前,要从主内存读取最新的值到自己的工作内存

​ 加锁解锁用的是同一把锁

代码于idea

保证可见性,某线程对主存数据的修改,其他线程也知道

加volatile可以保证可见性

2. 不保证原子性

不保证

例子就是用 多个线程,每个线程是调用addNum方法多次

产生不一致结果

原因:出现了丢失写值的情况,el:写值的时候,线程一写回停住,线程二的修改完成,线程1继续写回,出现重复写

分析:

对num++命令进行java - p

查看jvm指令,可以知道,可能出现这么一个情况,在底层

putfield的时候,写回值时某几个线程执行过快,后面的值把前面的值覆盖,就是写丢了

这里的重复put可以用两个线程num++的例子

num++三步走:取值 ,++,写回

重复的写回,写丢了

https://blog.csdn.net/xdzhouxin/article/details/81236356

以上是理论,下面解释如何解决原子性

解决1:
用synchronized

解决二:

JUC 下面的atomic原子

使用包装的整型类

默认是0

atomic不加synchronized,实现原子性,原理是CAS,乐观锁(自旋锁)

3. 禁止指令重排

在保证数据依赖性的前提下,编译器、cpu对指令优化,重新排列底层指令的执行顺序 导致结果不一致,一般在多线程的环境下

计算机为了提高性能

在这里插入图片描述
注意,考虑数据依赖性,不可能源代码没问题,但编译器和处理器让还未定义的变量,优先执行

重排,有些时候需要有些时候不需要,加了volatile,他来决定要不要重排

其次,

在这里插入图片描述

总结:

原理:volatile静止指令重排,在加了这个关键字后,就会插入一个内存屏障,在这个内存屏障的前后,不允许cpu和编译器对代码进行重排优化

拓展,volatile与单例设计模式

我们知道,懒汉式的单例模式,可能存在线程安全问题(饿汉不会)

1.出现原因:

在这种并发的情况下,第一个进入的线程进行实例化,在其他线程中可能也刚好进入实例化。

2.解决:加synchronized关键字

无疑可以解决,但是太重了,锁住了整个方法,严重影响效率

3.优化:双端锁版本

public static SingletonDemo getInstance() {
        if (instance == null) {
            synchronized (SingletonDemo.class) {
                if (instance == null) {
                    instance = new SingletonDemo();
                }
            }
        }
        return instance;
    }

在加锁的前后都进行判断,有了保证

是可以,但是不保险,还是可能出现指令重排的可能

具体原因就是:

实例化这个代码可以大致分三步,1.分配空间;2.创建对象;3.指向对象引用;

2-3步之间没有依赖关系,有可能颠倒顺序,出现这样的情况;

线程1执行了由于指令重排,第1步和第3步,2还未执行,指向了一个未分配的内存空间

线程2线程调用的时候,发现不为空,return却取不到

3.再优化:加入了volatile,会插入一个内存屏障,在这个内存屏障的前后,不允许cpu和编译器对代码进行重排优化

private static volatile SingletonDemo instance = null;

CAS

compare

比较交换

不保证原子性,用AtomicInterger,原理就是CAS

unsafe类里面很多native方法,unsafe类在启动类加载器rt.jar上,他有很多是调用系统原语,这种调用原语是执行连续,不会造成数据不一致问题

[

var1是原子类对象的本身

var2内存地址偏移量

var4是需要变动的数字

var5是从var1+var2中得到的

读时候不加锁

修改的时候,是一个dowhile循环,

先从内存中读取一个值,作为原来的值,然后在要正确修改的时候,再从内存中读一次,看看这个新的值和原来的值一样不一样,一样就修改,不一样就出现读,直到成功为止

更加底层,靠的就是底层汇编

1.缺点:

不同于synchronized,没加锁,循环时间长开销大

只能保证一个共享变量的原子操作

ABA问题(重点)

卡里10万块,你取5万,此时公司发工资5万到卡里,你老婆又取走5万,如果你不知道发工资了,你还以为凭空多5万呢

2.原子引用

AtomicReference

一个user类

new AtomicReference();

3.ABA的解决

改为用 AtomicStampedReference

1.线程不安全问题

List

new ArrayList<>();

出现的异常叫:java.util.ConcurrentModification

并发修改异常

Vector

Collections.synchronized(new ArrayList<>());

JUC有

CopyOnWriteArrayList<>();

写时复制,读写分离

具体见自己的笔记

Set

Collections.synchronizedSet();

JUC有

CopyOnWriteArraySet

实际上底层还是CopyOnWriteArrayList

面试题:

HashSet底层是什么?

HashMap

为什么add 只加一个 ,放在key,因为他的value是没有很大作用的,是一个叫PRESENT的恒定Object

Map

Hashtable

synchronizedHashmap

ConcurrentHashMap

各种锁

公平锁与非公平锁

公平与不公平——>排序策略

公平锁是队列,每个线程在获取锁,就看看维护的等待队列,

如果为空,获取锁,如果不是空,加入等待队列的队尾,按照FIFO的规则从队列中取出自己

不公平所是可以插队的,

上来直接就尝试获取锁,如果失败了,在采用公平锁的机制

优点是吞吐量大(相对于公平锁)

synchronized而言,是非公平锁

new ReentrantLock();//可重入锁,默认非公平

如果为new ReentrantLock(true);公平锁

可重入锁与递归锁(同一个)

一个线程可以进入任何一个已经拥有锁的所同步着的代码块

可重入锁最大作用(优点)是避免死锁。缺点:必须手动开启和释放锁。

见代码

自旋锁

CAS自旋锁前面有伏笔了

CAS,用AtomicInterger,原理就是CAS,就是自旋锁,

每次都尝试去获取锁,不阻塞,而是循环获取锁

见代码

读写锁(读是共享,写是独自)

类似于前面的CopyOnWrite*

读读 可以共存

读写 不能共存

写写 可以

读写分离的思想

同步器

同步器是一些使线程能够等待另一个线程的对象,允许它们协调动作。最常用的同步器是CountDownLatch和Semaphore,不常用的是Barrier 和Exchanger

A. semaphore:信号量。用于表示共享资源数量。用acquire()获取资源,用release()释放资源。

B. CyclicBarrier 线程到达屏障后等待,当一组线程都到达屏障后才一起恢复执行

C. CountDownLatch 初始时给定一个值,每次调用countDown值减1,当值为0时阻塞的线程恢复执行

CountDownLatch

倒计数发生器 同步计数器

使用方法如下:

1.新建一个CountDownLatch有一个默认数值(如6)

2.每次调用countDownLatch.countDown()都会减一

3.countDownLatch.await()一般阻塞着,countDownLatch.countDown()执行满6次(举例)。就会继续执行。

下拉选项可以用这个

具体实习见代码

CyclicBarrier

可循环屏障(当障碍器的屏障被打破后,会重置计数器,因此叫做循环屏障)

设置了一个内存屏障,内部也有一个count,每次调用await方法都去调用dowait方法,这个方法会判断,当count==0,就会调用“Condition变量trip的signalAll” (还不知道是啥 忽略先)唤醒其他线程正阻塞的线程。同时自己(最后一个到达临界点的线程)也继续执行

https://blog.csdn.net/zy1994hyq/article/details/83587640

见代码

Semaphore

信号灯

(抢车位)一个等一个进入,当写车位(资源)唯一,退化为synchronozed

多个资源互斥使用,另一个用于并发线程数控制

见代码

不同于前面两个,他更灵活,可以伸缩

通过
semaphore.acquire();
+
semaphore.release();
循环利用资源

(题外话:线程池——>高并发系统

dubbo底层——>RPC底层——>NIO)

阻塞队列

为什么要用,什么好处?

某些情况,不得不阻塞,它的存在就是为了实现阻塞时的管理。

多线程领域,某些情况需要挂起线程(比如生产者生产满了,消费者没的消费了)。阻塞队列管理,减去烦躁操作。

程序员就不需要控制什么时候阻塞 什么时候唤醒,BlockingQueue一手包办

体系结构

种类

BlockingQueue是Queue的子接口,是Collection下的子接口

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vbVSimPe-1586435941977)(C:\Users\user\Desktop\Course\学无止境\有可能的面试点\阻塞队列种类.png)]

synchronousQueue,就是生产一个消费一个,一方不消费,另一方不生产

操作

在这里插入图片描述

在这里插入图片描述

synchronousQueue代码

线程 操作 资源类

判断 干活 通知(在加锁的时候操作)

要使用while 不能用if

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1QXIzhmB-1586435941991)(C:\Users\user\Desktop\Course\学无止境\有可能的面试点\线程通信old.png)]

public void increment() throws Exception{
    lock.lock();//最外层 上锁
    try{
        /** 这是生产的,所以当资源不是为0,叫醒其他线程操作*/
        while(number != 0){//改为if,就少了一次确实,可能导致错误唤醒,在2个以上的线程就可能出错,只有两个线程一个生产员工消费就不会
            condition.await();
        }
        number++;
        sout();
        condition.signAll();//生产完毕,唤醒其他线程去消费
    }catch(){
        
    }finally{
        lock.unlock();//最外层 解锁
    }
}

疯狂讲义753线程通信

判断

几个问题

lock与synchronized区别

synchronized底层是靠monitor实现,每次判断monitor进入数为0,那就设置为一,自己进入,

占有有只能重入,再加一,

其他对象就处于阻塞状态了,直到退出就减一,其他对象才能尝试重新获得这个锁

synchronized是属于jvm层面,

1.底层是monitor,进1退2(第2是异常)——》保证不会死锁

2.不用自己释放

lock

1.lock是api级别的

2.手动释放

在这里插入图片描述
在这里插入图片描述

三角形替代后有什么好处

代码

一把锁,多个condition

体会第5点

生产者消费者(new)

相比旧版本的 使用volatile+cas+atomic+blockqueue

更加方便,我们不需要手动释放/加锁,还要兼顾性能和效率

这一切由组设队列实现

volatile/cas/atomic/block queue

(代码我下次打)

Callable接口

线程池

抛弃策略

AbortPolicy 从头到尾,达到最多数(队列+最大运行线程数)抛出异常

CallerRunsPolicy 达到最多数,交给调用者执行

DiscardOldestPolicy 等得最久的,抛弃它

DiscardPolicy 直接抛弃

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值