1、synchronized和Lock锁的区别
1、Synchronized 是内置的 Java关键字,Lock是一个Java类
2、Synchronized 无法判断获取锁的状态, Lock 可以判断是否获取到了锁
3、Synchronized 会自动释放锁,Lock 必须要手动释放锁!如果不释放锁,会造成死锁问题
4、Synchronized 线程1(获取锁,阻塞),线程2(等待锁释放),Lock 就不一定会等待下去
5、Synchronized 可重入锁,不可以中断,非公平锁,Lock 可重入锁,可以判断锁,非公平(可以自己设定)
6、Synchronized 适合锁少量的代码同步问题,Lock适合锁大量的同步代码!
2、集合类不安全
List 不安全
List、ArrayList 等在并发多线程条件下,不能实现数据共享,多个线程同时调用一个list对象时候就会出现并发修改异常ConcurrentModificationException 。
// 并发下 ArrayList 不安全的吗,Synchronized;
/*
* 解决方案;
* 方案1、List<String> list = new Vector<>();
* 方案2、List<String> list = Collections.synchronizedList(new ArrayList<>());
* 方案3、List<String> list = new CopyOnWriteArrayList<>();
*/
/* CopyOnWrite 写入时复制 COW 计算机程序设计领域的一种优化策略;
* 多个线程调用的时候,list,读取的时候,固定的,写入(覆盖)
* 在写入的时候避免覆盖,造成数据问题!
* 读写分离
* CopyOnWriteArrayList 比 Vector Nb 在哪里?
*/
Set 、Hash不安全
Set、Hash 等在并发多线程条件下,不能实现数据共享,多个线程同时调用一个set对象时候就会出现并发修改异常ConcurrentModificationException 。
/*
* Set解决方案;
* 方案1、Set<String> set = Collections.synchronizedSet(new HashSet<>());
* 方案2、Set<String> set = new CopyOnWriteArraySet<>();
*/
/*
* HashMap解决方案;
* 方案1、Map<String,Object> map = Collections.synchronizedMap(new HashMap<String,Object>());
* 方案2、Map<String,Object> map = new ConcurrentHashMap<>();
*/
扩展:hashSet 底层是什么?
底层就是new了一个HashMap,add时,将数据存于map的key,而value位置存的是常量Object,由于是map,所以不存在重复的值
3、ConcurrentHashMap原理,以及在jdk7和jdk8版本的区别
-
ConcurrentHashMap是线程安全的数组,是HashTable的替代品,同为线程安全,其性能要比HashTable更好
-
HashMap不是线程安全: 在并发环境下,可能会形成环状链表(扩容时可能造成,具体原因自行百度google或查看源码分析),导致get操作时,cpu空转,所以,在并发环境中使用HashMap是非常危险的
-
HashTable是线程安全的: HashTable和HashMap的实现原理几乎一样, 差别: HashTable不允许key和value为null; HashTable是线程安全的。
HashTable线程安全的策略实现代价却比较大,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,见下图:
这个时候,ConcurrentHashMap诞生了,不但是线程安全的,而且由于分段锁的存在, 性能上也很非常优秀
JDK7 :
数据结构: ReentrantLock + Segment + HashEntry, 一个Segment中包含一个HashEntry数组,每个HashEntry又是一个链表结构
元素查询: 二次hash,第一次定位Segment,第二次定位元素所在的链表的头部
锁 : Segment分段锁, Segment继承了ReentrantLock ,锁定操作的Segment,其他的Segment不受影响,并发度为Segment的个数,
可以通过构造函数指定,数组扩容不会影响其他的segment,get无需加锁,volatile保证内存可见性
使用 volatile 修饰共享变量后,每个线程要操作变量时会从主内存中将变量拷贝到本地内存作为副本,当线程操作变量副本并写回主内存后,会通过 CPU 总线嗅探机制告知其他线程该变量副本已经失效,需要重新从主内存中读取。
volatile 保证了不同线程对共享变量操作的可见性,也就是说一个线程修改了 volatile 修饰的变量,当修改后的变量写回主内存时,其他线程能立即看到最新值。
接下来我们就聊聊一个比较底层的知识点:总线嗅探机制
。
总线嗅探机制
在现代计算机中,CPU 的速度是极高的,如果 CPU 需要存取数据时都直接与内存打交道,在存取过程中,CPU 将一直空闲,这是一种极大的浪费,所以,为了提高处理速度,CPU 不直接和内存进行通信,而是在 CPU 与内存之间加入很多寄存器,多级缓存,它们比内存的存取速度高得多,这样就解决了 CPU 运算速度和内存读取速度不一致问题。
由于 CPU 与内存之间加入了缓存,在进行数据操作时,先将数据从内存拷贝到缓存中,CPU 直接操作的是缓存中的数据。但在多处理器下,将可能导致各自的缓存数据不一致(这也是可见性问题的由来),为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,而嗅探是实现缓存一致性的常见机制。
注意,缓存的一致性问题,不是多处理器导致,而是多缓存导致的。
嗅探机制工作原理:每个处理器通过监听在总线上传播的数据来检查自己的缓存值是不是过期了,如果处理器发现自己缓存行对应的内存地址修改,就会将当前处理器的缓存行设置无效状态,当处理器对这个数据进行修改操作的时候,会重新从主内存中把数据读到处理器缓存中。
注意:基于 CPU 缓存一致性协议,JVM 实现了 volatile 的可见性,但由于总线嗅探机制,会不断的监听总线,如果大量使用 volatile 会引起总线风暴。所以,volatile 的使用要适合具体场景。
使用 volatile 和 synchronized 锁都可以保证共享变量的可见性。相比 synchronized 而言,volatile 可以看作是一个轻量级锁,所以使用 volatile 的成本更低,因为它不会引起线程上下文的切换和调度。但 volatile 无法像 synchronized 一样保证操作的原子性。
volatile 的原子性问题
所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。
在多线程环境下,volatile 关键字可以保证共享数据的可见性,但是并不能保证对数据操作的原子性。也就是说,多线程环境下,使用 volatile 修饰的变量是线程不安全的。
要解决这个问题,我们可以使用锁机制,或者使用原子类(如 AtomicInteger)。
这里特别说一下,对任意单个使用 volatile 修饰的变量的读 / 写是具有原子性,但类似于 flag = !flag
这种复合操作不具有原子性。简单地说就是,单纯的赋值操作是原子性的。
容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这 样便可以有效地提高并发效率。这就是ConcurrentHashMap所采用的”分段锁”思想,见下图:
JDK8 : 数据结构: Synchronized + CAS +Node +红黑树.Node的val和next都用volatile保证,保证可见性,查找,替换,赋值操作都使用CAS
为什么在有Synchronized 的情况下还要使用CAS
因为CAS是乐观锁,在一些场景中(并发不激烈的情况下)它比Synchronized和ReentrentLock的效率要高,当CAS保障不了线程安全的情况下(扩容或者hash冲突的情况下)转成Synchronized 来保证线程安全,大大提高了低并发下的性能.
锁 : 锁是锁的链表的head的节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容时,阻塞所有的读写操作(因为扩容的时候使用的是Synchronized锁,锁全表),并发扩容.
读操作无锁 :
-
Node的val和next使用volatile修饰,读写线程对该变量互相可见
-
数组用volatile修饰,保证扩容时被读线程感知
4、Callable
Callable 和 Runable 对比:
-
Callable 是 java.util 包下 concurrent 下的接口,有返回值,可以抛出被检查的异常
-
Runable 是 java.lang 包下的接口,没有返回值,不可以抛出被检查的异常
-
二者调用的方法不同,run()/ call()
细节:
1、有缓存(call()方法结果会被缓存,提高效率,因此只打印1个call)
2、调用get方法获取结果时可能需要等待,会阻塞!
5、CyclicBarrier、CountDownLatch 和 Semaphore
-
定义
CyclicBarrier 计时器(增加),它的作用就是会让所有线程都等待完成后才会继续下一步行动。
CountDownLatch 计时器(减少),这个类使一个线程等待其他线程各自执行完毕后再执行。
Semaphore 类似通行证,通常我们叫它信号量, 可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。
-
CyclicBarrier与CountDownLatch区别
CountDownLatch 是一次性的,CyclicBarrier 是可循环利用的
CountDownLatch 参与的线程的职责是不一样的,有的在倒计时,有的在等待倒计时结束。CyclicBarrier 参与的线程职责是一样的。
-
Semaphore原理
semaphore.acquire();
获得,假设如果已经满了,等待,等待被释放为止!semaphore.release();
释放,会将当前的信号量释放 + 1,然后唤醒等待的线程!作用: 多个共享资源互斥的使用!并发限流,控制最大的线程数!
6、阻塞队列
BlockingQueue 不是新的东西
什么情况下我们会使用 阻塞队列?:多线程并发处理,线程池用的较多 !
学会使用队列
添加、移除
四组API
方式 | 抛出异常 | 有返回值,不抛出异常 | 阻塞等待 | 超时等待 |
---|---|---|---|---|
添加 | add | offer() | put() | offer(,) |
移除 | remove | poll() | take() | poll(,) |
检测队首元素 | element | peek() | - | - |
7、线程池(重点)
线程池:3大方法、7大参数、4种拒绝策略
池化技术
程序的运行,本质:占用系统的资源! (优化资源的使用 => 池化技术)
线程池、连接池、内存池、对象池///… 创建、销毁。十分浪费资源
池化技术:事先准备好一些资源,有人要用,就来我这里拿,用完之后还给我。
线程池的好处:
-
1、降低系统资源的消耗
-
2、提高响应的速度
-
3、方便管理
线程复用、可以控制最大并发数、管理线程
线程池:3大方法
线程池:3大方法
// Executors 工具类、3大方法
// Executors.newSingleThreadExecutor();// 创建单个线程的线程池
// Executors.newFixedThreadPool(5);// 创建一个固定大小的线程池
// Executors.newCachedThreadPool();// 创建一个可伸缩的线程池
线程池:7大参数
7大参数
public ThreadPoolExecutor(int corePoolSize, // 核心线程池大小
int maximumPoolSize, // 最大核心线程池大小
long keepAliveTime, // 超时没有人调用就会释放
TimeUnit unit, // 超时单位
BlockingQueue<Runnable> workQueue, // 阻塞队列
ThreadFactory threadFactory, // 线程工厂:创建线程的,一般 不用动
RejectedExecutionHandler handle )// 拒绝策略
线程池:4种拒绝策略
4种拒绝策略
/**
* 四种拒绝策略:
*
* new ThreadPoolExecutor.AbortPolicy()
* 银行满了,还有人进来,不处理这个人的,抛出异常
*
* new ThreadPoolExecutor.CallerRunsPolicy()
* 哪来的去哪里!比如你爸爸 让你去通知妈妈洗衣服,妈妈拒绝,让你回去通知爸爸洗
*
* new ThreadPoolExecutor.DiscardPolicy()
* 队列满了,丢掉任务,不会抛出异常!
*
* new ThreadPoolExecutor.DiscardOldestPolicy()
* 队列满了,尝试去和最早的竞争,也不会抛出异常! */
小结和拓展
池的最大容量如何去设置!
了解:IO密集型,CPU密集型:(调优)
// 自定义线程池!工作 ThreadPoolExecutor
// 最大线程到底该如何定义
// 1、CPU 密集型,几核,就是几,可以保持CPu的效率最高!
// 2、IO 密集型 > 判断你程序中十分耗IO的线程,
// 比如程序 15个大型任务 io十分占用资源!
// IO密集型参数(最大线程数)就设置为大于15即可,一般选择两倍
// 获取CPU的核数
System.out.println(Runtime.getRuntime().availableProcessors());
8、JMM
请你谈谈你对 Volatile 的理解
Volatile 是 Java 虚拟机提供轻量级的同步机制,类似于synchronized 但是没有其强大。
1、保证可见性
2、不保证原子性
3、防止指令重排
什么是JMM
JMM : Java内存模型,不存在的东西,概念!约定!
关于JMM的一些同步的约定:
1、线程解锁前,必须把共享变量立刻刷回主存。
2、线程加锁前,必须读取主存中的最新值到工作内存中!
3、加锁和解锁是同一把锁。
线程 工作内存 、主内存
8 种操作:
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和writ操作在某些平台上允许例外)
-
lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
-
unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
-
read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
-
load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
-
use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
-
assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
-
store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
-
write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
JMM 对这八种指令的使用,制定了如下规则:
-
不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
-
不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
-
不允许一个线程将没有assign的数据从工作内存同步回主内存
-
一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作
-
一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
-
如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
-
如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
-
对一个变量进行unlock操作之前,必须把此变量同步回主内存
问题: 程序不知道主内存的值已经被修改过了
##
9、Volatile
保证可见性
不保证原子性
原子性 : 不可分割
线程A在执行任务的时候,不能被打扰的,也不能被分割。要么同时成功,要么同时失败。
如果不加 lock 和 synchronized ,怎么样保证原子性
使用原子类,解决原子性问题。
// volatile 不保证原子性
// 原子类的
Integer private volatile static AtomicInteger num = new AtomicInteger();
public static void add(){
// num++;
// 不是一个原子性操作
num.getAndIncrement();
// AtomicInteger + 1 方法, CAS
}
这些类的底层都直接和操作系统挂钩!在内存中修改值!Unsafe类是一个很特殊的存在!
指令重排
什么是指令重排?:我们写的程序,计算机并不是按照你写的那样去执行的。
源代码 —> 编译器优化的重排 —> 指令并行也可能会重排 —> 内存系统也会重排 ——> 执行
处理器在执行指令重排的时候,会考虑:数据之间的依赖性
int x = 1; // 1
int y = 2; // 2
x = x + 5; // 3
y = x * x; // 4
我们所期望的:1234 但是可能执行的时候会变成 2134 或者 1324
但是不可能是 4123!
前提:a b x y 这四个值默认都是 0:
可能造成影响得到不同的结果:
| 线程A | 线程B |
| ----- | ----- |
| x = a | y = b |
| b =1 | a = 2 |
正常的结果:x = 0; y = 0; 但是可能由于指令重排出现以下结果:
| 线程A | 线程B |
| ----- | ----- |
| b = 1 | a = 2 |
| x = a | y = b |
指令重排导致的诡异结果: x = 2; y = 1;
非计算机专业
volatile 可以避免指令重排:
内存屏障。CPU指令。作用:
-
保证特定操作的执行顺序!
-
可以保证某些变量的内存可见性 (利用这些特性volatile 实现了可见性)
volatile 是可以保证可见性。不能保证原子性,由于内存屏障,可以保证避免指令重排的现象产生!
volatile 内存屏障在单例模式中使用的最多!