预测锁冲突的概率非常高 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。 乐观锁:假设数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。 举例说明: 小红和小明去向老师请教问题
这两种思路不能说谁优谁劣,而是看当前的场景是否合适. 如果当前老师确实比较忙,那么使用悲观锁的策略更合适,使用乐观锁会导致"白跑很多趟"耗费额外的资源. 如果当前老师确实比较闲,那么使用乐观锁的策略更合适,使用悲观锁会让效率比较低.
加锁的开销的角度 重量级锁的加锁开销比较大,要做更多的工作 轻量级锁的加锁开销比较小,要做的工作相对更少
挂起等待锁,就是悲观锁/重量级锁的一种典型实现. 自旋锁,则是乐观锁/轻量级锁的一种典型实现. 举例说明: 小明追求小红 小红我喜欢你,我想和你在一起(尝试对小红加锁) 小红表示我有男票了(小红表示她这把锁已经被别的线程给加了)
公平锁:遵守"先来后到".B比C先来的.当A释放锁的之后,B就能先于C获取到锁. 非公平锁:不遵守"先来后到".B和C都有可能获取到锁.
之前我们学习到synchronized是一把可重入锁,也就是说当对一个对象进行多次重复加锁时,由于编译器的优化就会只加一把锁,此时synchronized是可重入锁; 也就是说当出现死锁问题时,如果一个线程,针对一把锁,连续加锁两次,就可能出现死锁.如果把锁设定成"可重入"就可以避免死锁了. 可重入锁工作原理
读写锁就是把“加锁操作”分成两种情况:读加锁和写加锁 读写锁提供了两种加锁的api,加读锁和加写锁,解锁的api都是一样的
在实际开发中对于大部分场景下读操作的频次本身就比写操作频次要高 读写锁也是Java的内置锁,Java标准库提供了ReentrantReadwriteLock类,实现了读写锁 此处就需要把unlock放到finally 中确保能够执行到.
synchronized 1)乐观悲观,自适应2)重量轻量,自适应3)自旋挂起等待,自适应4)非公平锁 5)可重入锁6)不是读写锁
锁升级:锁升级的过程,刚开始使用synchronized加锁,首先锁会处于偏向锁的状态(偏向锁,本质上,是在推迟加锁的时机),遇到线程之间的锁竞争就会升级到轻量级锁,进一步的统计出现的频次,达到一定程度后就会升级到“重量级锁” synchronized加锁的时候,会经历无锁=>偏向锁=>轻量级锁=>重量级锁 偏向锁->轻量级锁:出现竞争 轻量级锁->重量级锁:竞争激烈 理解偏向锁:偏向锁不是真正的加锁(真的加锁开销可能会比较大),偏向锁只是做个标记(标记的过程非常轻量高效) 上述锁升级的过程,主要也是为了能够让synchronized这个锁很好的适应不同的场景. 对于当前JVM的实现来说,上述锁升级的过程,属于"不可逆" 锁消除(编译器的优化策略):编译器会对你写的 synchronized 代码,做出判定,判定这个地方,是否确实需要加锁.如果这里没有必要加锁的,就能够自动把synchronized给干掉. Vector, StringBuffer .....自带synchronized 锁粗化(编译器的优化策略):锁的粒度;代码越多,就是"粒度越粗"代码越少,就是"粒度越细"

全称为Compare and swap 比较内存和cpu寄存器中的内容.如果发现相同,就进行交换(交换的是内存和另一个寄存器的内容)
一个内存的数据和两个寄存器中的数据进行操作(寄存器1和寄存器2) 比较内存和寄存器1中的值,是否相等. 如果不相等,就无事发生. 如果相等,就交换内存和寄存器2的值.
此处一般只是关心,内存交换后的内容 此处虽然叫做"交换"实际上,希望达成的效果是‘赋值' 不关心寄存器2交换后的内容 CAS伪代码
//下⾯写的代码不是原⼦的,真实的CAS是⼀个原⼦的硬件指令完成的.
boolean CAS(address, expectValue, swapValue) {
if (&address == expectedValue) {
&address = swapValue;
return true;
}
return false;
}
基于CAS实现“原子类” int/long在进行++ --的时候,都不是原子的 基于CAS实现的原子类对int/long等这些类型进行了直接的封装,从而可以原子的完成++ --等操作 原子类在Java标准库中也有现成的实现

import java.util.concurrent.atomic.AtomicInteger;
public class Demo4 {
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() ->{
for (int i = 0; i < 50000; i++) {
count.getAndIncrement();
}
});
Thread t2 = new Thread(() ->{
for (int i = 0; i < 50000; i++) {
count.getAndIncrement();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count.get());
}
}
如何通过CAS实现原子类 针对不同的操作系统,JVM用到了不同的CAS 实现原理,简单来讲: 1)java的CAS利用的的是unsafe这个类提供的CAS操作 2)unsafe的CAS依赖了的是jvm 针对不同的操作系统实现的Atomic:cmpxchg; 3)Atomic..cmpxchg的实现使用了汇编的CAS操作,并使用cpu硬件提供的lock机制保让具原子性。
class AtomicInteger {
private int value;
public int getAndIncrement() {
int oldValue = value;
while ( CAS(value, oldValue, oldValue+1) != true) {
oldValue = value;
}
return oldValue;
}
}

ABA的问题: 假设存在两个线程t1和t2.有一个共享变量num,初始值为A.接下来,线程t1想使用CAS把num值改成Z,那么就需要 1)先读取num的值,记录到oldNum变量中. 2)使用CAS判定当前num的值是否为A,如果为A,就修改成Z. 但是,在t1执行这两个操作之间, t2线程可能把num的值从A改成了B,又从B改成了A 线程t1的CAS是期望num不变就修改.但是num的值已经被t2给改了.只不过又改成A了.这个时候t1究竟是否要更新num的值为Z呢?

通过引入CAS版本号来解决此问题:

Callable是一个interface,相当于把线程封装了一个“返回值”,方便我们借助多线程的方式计算结果 比如说创建一个线程来实现1+2+3+4+...+1000
不使用Callable方法: 我们需要 额外创建一个result变量来接收,因为我们所创建的线程没有返回值
public class Demo1 {
private static int result;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() ->{
int sum = 0;
for (int i = 0; i < 1000; i++) {
sum += i;
}
result = sum;
});
t.start();
t.join();
System.out.println(result);
}
}使用Callable版本:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Demo1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 1000; i++) {
sum += i;
}
return sum;
}
};
FutureTask<Integer> future = new FutureTask<>(callable);
Thread t = new Thread(future);
t.start();
t.join();
System.out.println(future.get());
}
}理解Callable: Callable和Runnable相对,都是描述一个"任务".Callable描述的是带有返回值的任务, Runnable描述的是不带返回值的任务. Callable通常需要搭配FutureTask来使用.FutureTask用来保存Callable的返回结果.因Callable往往是在另一个线程中执行的,啥时候执行完并不确定. FutureTask就可以负责这个等待结果出来的工作. 理解FutureTask: 类似于吃饭的号码牌,号码牌就是作为取餐的依据,通过号码牌来确定是否完成
可重入互斥锁.和synchronized定位类似,都是用来实现互斥效果,保证线程安全. ReentrantLock的用法:
实际开发中,大多数情况下使用synchronized即可
ReentrantLock和synchronized的差别
代码举例: count++十万次
import java.util.concurrent.locks.ReentrantLock;
public class Demo4 {
private static int count;
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock(true);
Thread t1 = new Thread(() ->{
try{
lock.lock();
for (int i = 0; i < 50000; i++) {
count++;
}
}finally {
lock.unlock();
}
});
Thread t2 = new Thread(() ->{
try{
lock.lock();
for (int i = 0; i < 50000; i++) {
count++;
}
}finally {
lock.unlock();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}信号量,用来表示"可用资源的个数".本质上就是一个计数器 理解信号量:
可以把信号量想象成是停车场的展示牌:当前有车位100个.表示有100个可用资源. 当有车开进去的时候,就相当于申请一个可用资源,可用车位就-1(这个称为信号量的P操作)
当有车开出来的时候,就相当于释放一个可用资源,可用车位就+1(这个称为信号量的V操作)
如果计数器的值已经为0了,还尝试申请资源,就会阻塞等待,直到有其他线程释放资源. acquire对应的就是P操作
release对应的就是V操作
操作系统本身提供了信号量实现,JVM把操作系统的信号量封装了一下 通过Semaphore申请一个可用资源,我们就可以达到类似开锁解锁的状态
public class Demo2 {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
ReentrantLock reentrantLock = new ReentrantLock();
Semaphore semaphore = new Semaphore(1);
Thread t1 = new Thread(() ->{
for (int i = 0; i < 50000; i++) {
try {
semaphore.acquire();
count++;
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() ->{
for (int i = 0; i < 50000; i++) {
try {
semaphore.acquire();
count++;
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}同时等待N个任务执行结束 实际的使用场景: 多线程下载(搭配线程池使用)
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo5 {
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(4);//四个线程
CountDownLatch countDownLatch = new CountDownLatch(20);//拆分出来任务的个数
for (int i = 0; i < 20; i++) {
int id = i;
executorService.submit(()->{
System.out.println("下载任务" + id + "开始执行");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("下载任务" + id + "结束执行");
countDownLatch.countDown();
});
}
countDownLatch.await();
System.out.println("所有任务完成");
}
}
线程同步的方式有哪些? synchronized,ReentrantLock,Semaphore等都可以用于线程同步
为什么有了synchronized还需要juc下的lock? 以juc的ReentrantLock为例, ①synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活 ②synchronized 在申请锁失败时,会死等,ReentrantLock可以通过 trylock 的方式等待一段时间就放弃. ③synchronized 是非公平锁, ReentrantLock 默认是非公平锁,可以通过构造方法传入一个 true 开启公平锁模式 ④synchronized 是通过 Object 的 wait/notify 实现等待-唤醒,每次唤醒的是一个随机等待的线程 ReentrantLock搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程
AtomicInteger的实现原理是什么? 基于CAS机制的伪代码
class AtomicInteger {
private int value;
public int getAndIncrement() {
int oldValue = value;
while ( CAS(value, oldValue, oldValue+1) != true) {
oldValue = value;
}
return oldValue;
}
}信号量是什么?用在过那些场景? 信号量用来表示“可用资源的个数”,本质上就是一个计数器 使用信号量可以实现“共享锁”,比如某个资源允许3个线程同时使用,那么就可以使用P操作作为加锁,V操作作为解锁,前三个线程的P操作都能顺利返回,后续线程在进行P操作就会阻塞等待,直到前面的线程执行了V操作
解释一下 ThreadPoolExecutor构造方法的参数的含义

原来的集合类,大部分都不是线程安全的 Vector,Stack,HashTable,是线程安全的(不建议用),其他的集合类不是线程安全的
Hash本身不是线程安全的 在多线程环境下使用哈希表可以使用:
Hashtable 只是简单的把关键方法加上了synchronized关键字
public sychronized V put(K key,V value)
public sychronized V get(Object key)相当于直接针对Hashtable对象本身进行加锁 ①如果多线程访问同一个 Hashtable 就会直接造成锁冲突 ②size 属性也是通过 synchronized 来控制同步,也是比较慢的, ③一旦触发扩容,就由该线程完成整个扩容过程,这个过程会涉及到大量的元素拷贝,效率会非常低

ConcurrentHashMap ①读操作没有加锁(但是使用了 volatile 保证从内存读取结果),只对写操作进行加锁,加锁的方式仍然是用 synchronized,但是不是锁整个对象,而是"锁桶"(用每个链表的头结点作为锁对象),大大降低了锁冲突的概率 ②充分利用 CAS 特性.比如 size 属性通过 CAS 来更新.避免出现重量级锁的情况 优化了扩容方式: 化整为零 发现需要扩容的线程,只需要创建一个新的数组,同时只搬几个元素过去 扩容期间,新老数组同时存在 后续每个来操作 ConcurrentHashMap 的线程,都会参与搬家的过程,每个操作负责搬 运一小部分元素. 搬完最后一个元素再把老数组删掉这个期间,插入只往新数组加这个期间,查找需要同 时查新数组和老数组

