首页 > 基础资料 博客日记
Java之多线程进阶
2023-07-24 14:13:28基础资料围观303次
目录
7.ReentrantLock和synchronized的区别
一.上节内容复习
内容指路:Java之线程池
1.线程池的实现
1.阻塞队列保存要执行的任务
2.构造方法初始化线程的数量,不断扫描阻塞队列中的任务并执行
2.自定义一个线程池,构造方法的参数及含义
不推荐使用Executors工厂方法构建ExecutorService线程池对象,可能会存在浪费系统的资源的现象.可以自行new出来一个ExecutorService线程池对象
int corePoolSize 核心线程数,创建线程是包含的最小线程数
int maximumPoolSize 最大线程数,也叫临时线程数(核心线程数不够时,允许创建最大的线程数)
long keepAliveTime 临时空闲时长,超过这个时间自动释放
TimeUnit unit 空闲的时间单位,和keepAliveTime一起使用
BlockingQueue<Runnable> workQueue 用来保存任务的阻塞队列
ThreadFactory threadFactory 线程工厂,如何去创建线程
RejectedExecutionHandler handler 拒绝策略,触发的时机,当线程池处理不了过多的任务
3.线程池的工作原理
- 当任务添加到线程池中时,先判断任务数是否大于核心线程数,如果不大于,直接执行任务
- 任务数大于核心线程数,则加入阻塞队列
- 当阻塞队列满了之后,会创建临时线程,会按照最大线程数,一次性创建到最大线程数
- 当阻塞队列满了并且临时线程也创建完成,再提交任务,就会执行拒绝策略.
- 当任务减少,临时线程达到空闲时长时,会被回收.
4.拒绝策略
AbortPolicy:直接拒绝任务的加入,并且抛出RejectedExecutionException异常
CallerRunsPolicy:返回给提交任务的线程执行
DiscardOldestPolicy:舍弃最老的任务
DiscardPolicy:舍弃最新的任务
5.为什么不推荐系统提供的线程池
1.无界队列
2.最大线程数使用了Integer.MAX_VALUE
二.常见的锁策略
1.乐观锁和悲观锁
乐观锁:对运行环境处乐观态度,刚开始不加锁,当有竞争的时候才加锁
悲观锁:对运行环境处悲观态度,刚开始就直接加锁
2.轻量级锁和重量级锁
判断依据:消耗资源的多少.描述的实现锁的过程
轻量级锁:可以是纯用户态的锁,消耗的资源比较少
重量级锁:可能会调用到系统的内核态,消耗的资源比较多
3.读写锁和普通互斥锁
现实中并不是所有的锁都是互斥锁,互斥会消耗很多的系统资源,所以优化出读写锁
读锁:共享锁,读与读操作都能同时拿到锁资源
写锁:排它锁,读写,写读,写写不能同时拿到锁资源
普通互斥锁:synchronized,只要其中一个线程拿到锁资源,其他的线程就要堵塞等待.
4.自旋锁和挂起等待锁
自旋锁:不停的询问资源是否被释放,如果释放了可以第一时间获得锁资源
挂起等待锁:等待通知之后再去竞争锁,并不会第一时间获得锁资源
5.可重入锁和不可重入锁
可重入锁:对同一个锁资源可以加多次锁
不可重入锁:不可以对同一个锁资源加多次锁
6.公平锁和非公平锁
公平锁:先堵塞等待锁资源的线程先拿到锁资源
非公平锁:先争抢到锁资源的线程先拿到锁,没有先后顺序之说
所有关于争抢的事情,大多是都是非公平的,这样可以提高系统效率.
三.synchronized实现的锁策略
- 既是乐观锁,又是悲观锁
- 既是轻量级锁,又是重量级锁 轻量级锁是基于自旋锁实现的,重量级锁是基于挂起等待锁实现的
- 是普通互斥锁
- 既是自旋锁又是挂起等待锁
- 是可重入锁
- 是非公平锁
四.CAS自旋锁
1.CAS
CAS:compare and swap,比较并交换
boolean CAS(address, expectValue, swapValue) {
if (&address == expectedValue) {
&address = swapValue;
return true;
}
return false;
}
address:指的是内存地址
expectValue:期望值
swapValue:要交换的值
具体实现:用期望值(expectValue)和内存中的值(&address)进行比较,如果内存中的值和期望值相等,用要交换的值(swapValue)覆盖内存中的值(&address).如果不等,什么都不做
2.用户态自旋实现自增操作
CAS用户态实现原子性
public class Demo01_CAS {
public static void main(String[] args) throws InterruptedException {
//原子整型
AtomicInteger atomicInteger = new AtomicInteger();
Thread thread = new Thread(() -> {
//五万次自增操作
for (int i = 0; i < 50000; i++) {
atomicInteger.getAndIncrement();
}
});
Thread thread2 = new Thread(() -> {
//五万次自增操作
for (int i = 0; i < 50000; i++) {
atomicInteger.getAndIncrement();
}
});
//启动线程
thread.start();
thread2.start();
//等待两个线程执行完成
thread.join();
thread2.join();
System.out.println(atomicInteger);
}
}
3.CAS工作原理
线程1将主内存的value值加载到工作内存1中,工作内存1中var5=0(expectValue),然后线程1调离CPU,线程2调入CPU,此时主内存的value值加载到工作内存1中,工作内存2中var5=0(expectValue),然后线程2调离CPU,线程1调入CPU,此时进入到while循环判断,var5==&(var1+var2=1)(主内存中value的值),将主内存中的值赋值为swapValue(var5+1)返回true,线程1操作结束.
此时线程1调离CPU,线程2调入CPU,此时线程2工作内存中var5=0(expectValue),进入到CAS操作中,此时将&(var1+var2)=1与var5=0(expectValue)进行对比,发现不相同,返回false,之后重新将主内存中的value值加载到工作内存中,此时var5=1,进入到CAS操作中,此时将&(var1+var2)=1与var5=0(expectValue)进行对比,发现相同,将主内存中的值赋值为swapValue(var5+1=2)返回true,线程2操作结束.
两个线程的操作结束,没有发生线程不安全的现象,因此我们可以总结出:CAS操作通过不停的自旋检查预期值来保证了线程安全,while循环是在用户态(应用层)的层面上支持了原子性,所以比内核态的锁效率要高很多.
4.CAS实现自旋锁
轻量级锁,自旋锁的实现
public class SpinLock {
private Thread owner = null;
public void lock(){
// 通过 CAS 看当前锁是否被某个线程持有.
// 如果这个锁已经被别的线程持有, 那么就自旋等待.
// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
while(!CAS(this.owner, null, Thread.currentThread())){
}
}
public void unlock (){
this.owner = null;
}
}
owner:标记了哪一个线程竞争到了锁.
还是拿线程1和线程2举例,当线程1加锁,线程1执行lock()方法,while循环的CAS操作,此时owner=null,符合预期值,将owner赋值为线程1的信息,CAS方法返回true,while循环结束,加锁方法结束,此时线程2也执行lock()方法,进入到while循环CAS操作,owner此时保存线程1的信息,返回false,然后线程2一直执行while循环,直到线程1的内容执行完毕之后,执行unlock()方法,此时owner置为null,此时线程2的CAS操作owner符合预期值null,将owner赋值为线程2的信息,返回ture,结束while循环,线程2执行相应的操作,直到完毕.
5.CAS的ABA问题
ABA分别代表预期值的三种状态.
CAS的ABA状态可能会带来的问题:接下来我们看一个具体的场景
我的账户里面有2000块钱(状态A),我委托张三说:如果我忘给李四转1000块钱,下午帮我转一下,我在中午给李四转了1000块钱(状态B),但是随后公司发奖金1000到我的账户,此时我账户有1000块钱(状态A),张三下午检查我账户,发现我有2000块钱,于是又给李四转了1000块钱,此时就出现问题了,李四收到了两次1000元,不符合我们的需求了.
解决ABA问题:
给预期值加一个版本号.
在做CAS操作时,同时要更新预期值的版本号,版本号只增不减
在进行CAS比较的时候,不仅预期值要相同,版本号也要相同,这个时候才会返回true.
五.synchronized原理
通过以上锁策略学习可以知道,synchronized在不同的时期可能会用到不同的锁策略
1.锁升级
随着线程间对锁竞争的激烈程度不断增加,锁的状态不断升级.
查看锁对象的对象头信息
在pom.xml中导入依赖
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
</dependency>
public class Demo02_Synchronized {
// 定义一些变量
private int count;
private long count1 = 200;
private String hello = "";
// 定义一个对象变量
private TestLayout test001 = new TestLayout();
public static void main(String[] args) throws InterruptedException {
// 创建一个对象的实例
Object obj = new Object();
// 打印实例布局
System.out.println("=== 任意Object对象布局,起初为无锁状态");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
System.out.println("=== 延时4S开启偏向锁");
// 延时4S开启偏向锁
Thread.sleep(5000);
// 创建本类的实例
Demo02_Synchronized monitor = new Demo02_Synchronized();
// 打印实例布局,注意查看锁状态为偏向锁
System.out.println("=== 打印实例布局,注意查看锁状态为偏向锁");
System.out.println(ClassLayout.parseInstance(monitor).toPrintable());
System.out.println("==== synchronized加锁");
// 加锁后观察加锁信息
synchronized (monitor) {
System.out.println("==== 第一层synchronized加锁后");
System.out.println(ClassLayout.parseInstance(monitor).toPrintable());
// 锁重入,查看锁信息
synchronized (monitor) {
System.out.println("==== 第二层synchronized加锁后,锁重入");
System.out.println(ClassLayout.parseInstance(monitor).toPrintable());
}
// 释放里层的锁
System.out.println("==== 释放内层锁后");
System.out.println(ClassLayout.parseInstance(monitor).toPrintable());
}
// 释放所有锁之后
System.out.println("==== 释放 所有锁");
System.out.println(ClassLayout.parseInstance(monitor).toPrintable());
System.out.println("==== 多个线程参与锁竞争,观察锁状态");
Thread thread1 = new Thread(() -> {
synchronized (monitor) {
System.out.println("=== 在线程A 中获取锁,参与锁竞争,当前只有线程A 竞争锁,轻度锁竞争");
System.out.println(ClassLayout.parseInstance(monitor).toPrintable());
}
});
thread1.start();
// 休眠一会,不与线程A 激烈竞争
Thread.sleep(100);
Thread thread2 = new Thread(() -> {
synchronized (monitor) {
System.out.println("=== 在线程B 中获取锁,与其他线程进行锁竞争");
System.out.println(ClassLayout.parseInstance(monitor).toPrintable());
}
});
thread2.start();
// 不休眠直接竞争锁,产生激烈竞争
System.out.println("==== 不休眠直接竞争锁,产生激烈竞争");
synchronized (monitor) {
// 加锁后的类对象
System.out.println("==== 与线程B 产生激烈的锁竞争,观察锁状态为fat lock");
System.out.println(ClassLayout.parseInstance(monitor).toPrintable());
}
// 休眠一会释放锁后
Thread.sleep(100);
System.out.println("==== 释放锁后");
System.out.println(ClassLayout.parseInstance(monitor).toPrintable());
System.out.println("===========================================================================================");
System.out.println("===========================================================================================");
System.out.println("===========================================================================================");
System.out.println("===========================================================================================");
System.out.println("===========================================================================================");
System.out.println("===========================================================================================");
// 调用hashCode后才保存hashCode的值
monitor.hashCode();
// 调用hashCode后观察现象
System.out.println("==== 调用hashCode后查看hashCode的值");
System.out.println(ClassLayout.parseInstance(monitor).toPrintable());
// 强制执行垃圾回收
System.gc();
// 观察GC计数
System.out.println("==== 调用GC后查看age的值");
System.out.println(ClassLayout.parseInstance(monitor).toPrintable());
// 打印类布局,注意调用的方法不同
System.out.println("==== 查看类布局");
System.out.println(ClassLayout.parseClass(Demo02_Synchronized.class).toPrintable());
// 打印类对象布局
System.out.println("==== 查看类对象布局");
System.out.println(ClassLayout.parseInstance(Demo02_Synchronized.class).toPrintable());
}
}
class TestLayout {
}
打印的信息:
无锁的状态(non-biasable)
可偏向锁状态(biasable)
已偏向锁状态(biased)
当有一个线程参与竞争之后,就会升级成为轻量级锁(thin lock)
继续创建线程参与锁竞争,那么就会升级为重量级锁
2.锁消除
在写代码的时候,程序员自己加synchronized来保证线程安全
如果加了synchronized的代码块只有读操作没有写操作,JVM就认为这个代码块没必要加锁,JVM运行的时候就会被优化掉,这个现象就叫做锁消除
简单来说就是过滤掉了无效的synchronized,从而提高了效率.JVM只有100%的把握才会优化
3.锁粗化
四.JUC
java.util.concurrent 包的简称,JDK1.5之后对多线程的一种实现,这个包下的类都与多线程有关,提供了许多工具类.
1.Callable接口
也是描述线程任务的接口
Callable接口接口使用说明
public class Demo03_Callable {
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 < 5; i++) {
sum += i;
TimeUnit.SECONDS.sleep(1);
}
return sum;
}
};
//通过FutureTask来创建一个对象,这个对象持有Callable
FutureTask<Integer> futureTask = new FutureTask<>(callable);
//让线程执行好定义的任务
Thread thread = new Thread(futureTask);
thread.start();
Integer result = futureTask.get();
System.out.println("最终的结果为:" + result);
}
}
打印结果:
Thread类没有关于Callable的构造方法,因此我们要借助FutureTask让Thread执行Callable接口定义的任务,FutureTask是Runnable的一个实现类,所以可以传入Thread的构造方法中.
Callable接口抛出异常演示:
public class Demo04_CallableException {
public static void main(String[] args){
// 先定义一个线程的任务
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 5; i++) {
sum += i;
TimeUnit.SECONDS.sleep(1);
throw new Exception("业务出现异常");
}
// 返回结果
return sum;
}
};
// 通过FutureTask类来创建一个对象,这个对象持有callable
FutureTask<Integer> futureTask = new FutureTask<>(callable);
// 创建线程并指定任务
Thread thread = new Thread(futureTask);
// 让线程执行定义好的任务
thread.start();
// 获取线程执行的结果
System.out.println("等待结果...");
Integer result = null;
// 捕获异常
try {
result = futureTask.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
System.out.println("处理异常" + e.getMessage());
e.printStackTrace();
}
// 打印结果
System.out.println(result);
}
}
打印结果:
Runnable和Callable接口的区别
1.Callable实现的是call()方法,Runnable实现的是run()方法
2.Callable可以返回一个结果,Runnable没有返回值
3.Callable要配合FutureTask一起使用.
4.Callable可以抛出异常,Runnabe不可以
2.ReentrantLock
本身就是一个锁,是基于CAS实现的纯用户态的锁
1.常用方法
lock() tryLock() unlock()
2.ReentrantLock类的使用
public class Demo05_ReentrantLock {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
//加锁
lock.lock();
//尝试加锁,死等
lock.tryLock();
//尝试加锁,有超时时间
lock.tryLock(1, TimeUnit.SECONDS);
//释放锁
lock.unlock();
}
}
3.ReentrantLock模拟出现异常的处理
//模拟出现异常的处理
public static void exception() {
ReentrantLock lock = new ReentrantLock();
try {
//加锁
lock.lock();
//需要实现的业务
throw new Exception("出现异常");
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
4.ReentrantLock的公平和非公平锁
//公平锁 ReentrantLock lock = new ReentrantLock(true);//非公平锁
ReentrantLock lock = new ReentrantLock(false);
5.ReentrantLock的读写锁
public class Demo06_ReadWriteLock {
public static void main(String[] args) {
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
//读锁,共享锁,读与读可以共享
lock.writeLock();
//写锁,排它锁,读写,读读,写读不能共存
lock.readLock();
}
}
6.根据不同condition休眠和唤醒操作
/**
* ReentrantLock可以根据不同的Condition去休眠或唤醒线程
* 同一把锁可以分为不同的休眠或唤醒条件
*/
private static ReentrantLock reentrantLock = new ReentrantLock();
// 定义不同的条件
private static Condition boyCondition = reentrantLock.newCondition();
private static Condition girlCondition = reentrantLock.newCondition();
public static void demo05_Condition () throws InterruptedException {
Thread threadBoy = new Thread(() -> {
// 让处理男生任务的线程去休眠
try {
boyCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 唤醒处理女生任务的线程
girlCondition.signalAll();
});
Thread threadGirl = new Thread(() -> {
// 让处理女生任务的线程去休眠
try {
girlCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 唤醒处理男生任务的线程
boyCondition.signalAll();
});
}
7.ReentrantLock和synchronized的区别
1. synchronized 使用时不需要手动释放锁.ReentrantLock使用时需要手动释放.使用起来更灵活,但是也容易遗漏unlock.
2. synchronized在申请锁失败时,会一直等待锁资源.ReentrantLock可以通过trylock的方式等待一段时间就放弃.
3. synchronized是非公平锁, ReentrantLock 默认是非公平锁.可以通过构造方法传入一个true开启公平锁模式.
4. synchronized是一个关键字,是JVM内部实现的(可能涉及到内核态).ReentrantLock是标准库的一个类,基于Java JUC实现(用户态实现)
3.原子类
JUC包下常见的原子类
- AtomicBoolean
- AtomicInteger
- AtomicIntegerArray
- AtomicLong
- AtomicReference
- AtomicStampedReference
原子类是基于CAS操作实现的,因此比加锁的方式保证线程安全要高效的很多.
下面以AtomicInteger为例看一些方法
public class Demo07_Atomic {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger();
//相等于i++
atomicInteger.getAndIncrement();
System.out.println(atomicInteger);
//相当于++i
atomicInteger.incrementAndGet();
System.out.println(atomicInteger);
//相当于i--
atomicInteger.getAndDecrement();
System.out.println(atomicInteger);
//相当于--i
atomicInteger.decrementAndGet();
System.out.println(atomicInteger);
}
}
4.JUC四大并发工具类的使用
1.Semaphore 信号量
信号量:表示可用资源的数量,本质上就是一个计数器
时间案例:停车场展示牌,一共有100个车位,表示一共最多有100个车位可以使用
当有车开进去的时候,表示P操作(申请资源),可用车位就-1;当有车开出去的时候,表示V操作(释放资源),可用车位就+1,如果计数器已经为0了,这个时候还有车想进来,仅需要阻塞等待.
操作系统有PV操作
P操作表示申请资源,可用资源-1;
V操作表示释放资源,可用资源+1;
当没有可用资源的时候,其他线程就阻塞等待
Semaphore信号量的使用案例
public class Demo08_Semaphore {
private static Semaphore semaphore = new Semaphore(3);
public static void main(String[] args) {
//定义一个任务
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":申请资源");
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + ":获取到了资源");
//等待一秒,模仿处理业务
Thread.sleep(1000);
semaphore.release();
System.out.println(Thread.currentThread().getName() + ":释放了资源");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
};
//创建10个线程
for (int i = 0; i < 10; ++i) {
Thread thread = new Thread(runnable);
thread.start();
}
}
}
打印结果:
由结果可以看出,几乎所有的线程都在申请资源,但是信号量控制了最多三个可以申请到资源,因此最多有三个线程同时工作,其他的线程都处在阻塞等待的状态,等到申请到的资源的线程释放资源之后,其他阻塞等待的线程才可能申请到资源.
应用场景:需要指定有效资源个数(比如同时最多支持多少个并发执行),可以考虑使用Semaphore
2.CountDownLatch-闭锁
同时等待 N 个任务执行结束.
现实案例:相当于10个人赛跑,需要等待10个人都跑完之后,才能公布所有人的成绩.
public class Demo09_CountDownLatch {
private static CountDownLatch countDownLatch = new CountDownLatch(10);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; ++i) {
Thread thread = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + ":出发");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//执行完毕,计数减一
countDownLatch.countDown();
//走到这里说明这个线程已经结束
}, "线程"+i);
thread.start();
}
//等待所有的线程执行完成之后再继续执行下面的内容
countDownLatch.await();
System.out.println("所有的线程执行完毕");
}
}
打印结果:
应用场景:把一个大任务分为几个小任务,或是等待一些前置资源,可以考虑使用CountDownLatch
3.CyclicBarrier-循环栅栏
通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。我们暂且把这个状态就叫做barrier,当调用await()方法之后,线程就处于barrier了。
4.Exchanger-交换器
Exchanger一般用于两个工作线程之间交换数据。
5.线程安全的集合类
1.集合类线程不安全的现象
public class Demo10_List {
public static void main(String[] args) throws InterruptedException {
ArrayList<Integer> list = new ArrayList<>();
for (int i = 0; i < 10; ++i) {
int num = i;
Thread thread = new Thread(() -> {
list.add(num);
System.out.println(list);
});
thread.start();
}
Thread.sleep(1000);
System.out.println("=============");
System.out.println(list);
}
}
打印结果:
我们可以看出报了并发修改异常的错误,而且每一次执行,报异常的位置不一样,也可能没有报异常.
如果我们在开发中遇到这个问题,我们要先考虑是不是集合类使用不恰当.也就是说我们使用了线程不安全的集合类,如何使用线程安全的集合类
2.使用线程安全的集合类
1.前面我们也学习过了Vector,HashTable,但是不推荐使用.不推荐使用,效率太低
只是方法加了synchronized
2.自己加synchronized和ReentrantLock进行加锁,也不推荐,效率还是很低.
3.通过工具类,创建一个线程安全的集合类 ---不推荐
List<Object> list = Collections.synchronizedList(new ArrayList<>());
源码分析:本质上和Vector一样,不过是将所有代码加上synchronized
4.CopyOnWriteArrayList
它是JUC包下的一个类,使用的是一种叫写时复制技术来实现的
1.当要修改一个集合时,先复制这个集合的复本
2.修改复本的数据,修改完成后,用复本覆盖原始集合
优点:
在读多写少的场景下,性能很高,不需要加锁竞争.
缺点:
1.占用内存较多.是因为复制了一份新的数据进行修改
2.新写的数据不能被第一时间读取到.
在多线程环境下,如果使用集合类,优先推荐使用CopyOnWriteArrayList
6.线程安全的队列
7.多线程下使用哈希表
1.HashTable
线程安全的哈希表,但只是将方法简单的加上了synchronized修饰,效率很低,不推荐使用
2.HashMap
线程不安全的哈希表,单线程下使用不会报错,但是多线程环境下使用会产生线程不安全的问题
3.ConcurrentHashMap
多线程环境下推荐使用这个哈希表,并不是通过synchronized简单的加锁,与HashTable不同,而是通过JUC包下的ReentrantLock实现的加锁(基于CAS,纯用户态实现).
1.更小的锁粒度
HashTable的加锁方式:一个HashTable对象只有一把锁,当一个线程修改一个哈希桶的时候,其他线程无法修改任何一个桶的数据
ConcurrentHashMap的加锁方式:加锁的方式是synchronized,但是不是锁整个对象, 而是 "锁桶" (用每个链表的头结点作为锁对象)
2.只给写加锁,不给读加锁
3.共享变量用volatile修饰
4.充分利用CAS机制
比如size属性通过CAS来更新.避免出现重量级锁的情况.
5.对扩容进行了特殊优化
对于需要扩容的操作,新建一个新的Hash桶,随后的每次操作都搬运一些元素去新的Hash桶
在扩容没有完成时,两个Hash桶同时存在每次写入时只写入新的Hash桶.每次读取需要新旧的Hash桶同时读取.所有的数据搬运完成后,把老的Hash桶删除
8.死锁
死锁就是一个线程加上锁之后不运行也不释放僵住了,死锁会导致程序无法继续运行,是一个最严重的BUG之一.
死锁出现的场景:
1.一个线程一把锁
一个线程对一把锁加锁两次,如果是不可重入锁,就会产生死锁的现象,如果是可重入锁,就不会产生死锁的现象.
2.两个线程两把锁
public class Demo11_DeadLock {
public static void main(String[] args) {
//定义两个锁对象
Object locker1 = new Object();
Object locker2 = new Object();
//线程1先获取locker1,再获取locker2
Thread thread1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + ":申请locker1");
synchronized (locker1) {
System.out.println(Thread.currentThread().getName() + ":获取到了locker1");
//模拟业务处理
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//获取locker2
System.out.println(Thread.currentThread().getName() + ":申请locker2");
synchronized (locker2) {
System.out.println(Thread.currentThread().getName() + ":获取到了locker2");
}
}
});
thread1.start();
//线程1先获取locker2,再获取locker1
Thread thread2 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + ":申请locker2");
synchronized (locker2) {
System.out.println(Thread.currentThread().getName() + ":获取到了locker2");
//模拟业务处理
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//获取locker1
System.out.println(Thread.currentThread().getName() + ":申请locker1");
synchronized (locker1) {
System.out.println(Thread.currentThread().getName() + ":获取到了locker1");
}
}
});
thread2.start();
}
}
打印结果:
线程1获取到了locker1,线程2获取到了locker2,线程1又想获取到了locker2,而线程2又想获取locker1,但是这两个锁已经被占用,而且不会被释放,所以两个线程互相拿到了对象想要获取的锁,就产生了死锁的现象.
2.发生死锁的原因
1.互斥使用:锁A被线程1占用了,线程2就不能用了
2.不可抢占:锁A被线程1占用了,线程2不能主动把锁A抢过来,除非线程1主动释放
3.请求保持:有多把锁,线程1拿到了锁A之后,不释放还要继续再拿锁B
4.循环等待:线程1等待线程2释放锁,线程2要释放锁得等待线程3先释放锁,线程3释放锁得等待线程1释放锁...形成了循环关系
3.避免死锁解决方案
以上四条是形成死锁的必要条件,打破上面四条中的任何一条就可以,逐条分析一下
1.互斥使用:这个不能打破,这个是锁的基本特性
2.不可抢占:这个也不能打破,这个也是锁的基本特性
3.请求保持:这个有可能打破,取决于代码怎么写
4.循环等待:约定好加锁顺序就可以把破循环等待,t1.locker1 ->locker2,t2.locker2 -> locker1这个顺序造成了循环等待,如调整加锁顺序,就可以避免循环等待
现在举一个哲学家吃面的案例:一共有五个哲学家,一共5个筷子.
哲学家只做两件事情:吃面或者思考人生(阻塞等待).
如果所有的哲学家都先拿右手的筷子,然后再尝试拿左手的筷子的时候,发现左手的筷子都被占用了,全部都会阻塞得不到筷子,所以就会产生死锁.
现在我们重新安排一下,就可以避免死锁的问题.
让每个哲学家先拿身边编号小的筷子,然后再拿身边编号相对大的筷子.
这样哲学家1和哲学家5就会先拿筷子1,比如哲学家1争抢到了筷子1(此时哲学家5处在阻塞等待状态),哲学家2拿到筷子2,哲学家3拿到筷子3,哲学家4可以拿到筷子4和筷子5,哲学家4吃完之后释放筷子,哲学家3,2,1依次吃面,最后哲学家5拿到筷子吃饭,就可以避免死锁的问题.
9.ThreadLocal
场景:多个班级根据各班的人数订制校服
public class Demo12_ThreadLocal {
// 初始化一个ThreadLocal
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
// 多个线程分别去统计人数
Thread thread1 = new Thread(() -> {
// 统计人数
int count = 35;
threadLocal.set(count);
// Integer value = threadLocal.get();
// System.out.println(value);
// 订制校服
print();
}, "threadNameClass1");
Thread thread2 = new Thread(() -> {
// 统计人数
int count = 40;
threadLocal.set(count);
// Integer value = threadLocal.get();
// System.out.println(value);
// 订制校服
print();
}, "threadNameClass2");
thread1.start();
thread2.start();
}
// 订制校服
public static void print() {
// 从threadLocal中获取值
Integer value = threadLocal.get();
System.out.println(Thread.currentThread().getName() + " : 需要订制 " + value + "套校服.");
}
}
相当于一个map,以当前线程作为key,值为value保存
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!
标签: