当前位置: 首页 > news >正文

JavaEE多线程进阶

多线程进阶

  • 常见的锁策略
    • 乐观锁和悲观锁
    • 重量级锁和轻量级锁
    • 挂起等待锁和自旋锁(Spin Lock)
    • 公平锁和非公平锁
    • 可重入锁和不可以重入锁
    • 读写锁和普通互斥锁
  • synchronized
    • 锁升级
    • 锁消除
    • 锁粗化
  • CAS
    • 原子类
    • CAS自旋锁
    • ABA问题
  • JUC(java.util.concurrent)的常⻅类
    • Callable
    • ReentrantLock
    • Semaphore(信号量)
    • CountDownLatch
  • 线程安全的集合类
    • Hashtable
    • ConcurrentHashMap

常见的锁策略

乐观锁和悲观锁

乐观锁:认为多个线程访问同一个共享变量发生冲突概率小,并且并不是真正的加锁,而是直接访问数据,访问时候要识别当前的数据是否出现冲突
悲观锁:认为多个线程访问同一共享变量发生冲突概率大,每次访问变量会真的加锁

重量级锁和轻量级锁

重量级锁:加锁操作开销较大
轻量级锁:加锁操作开销较小
并且重量级锁较依赖mutex,其会有大量内核态和用户态之间切换,并且容易发生线程调度
轻量级锁,尽量不使用mutex,尽量让代码在用户态完成,不太容易发生线程调度

挂起等待锁和自旋锁(Spin Lock)

挂起等待锁:遇到锁冲突,就把线程阻塞,等待未来某个时间唤醒
(会涉及系统内部线程调度,比较复杂,开销较大)
自旋锁:遇到锁冲突,先不把线程阻塞,重试几下
(用户态操作,不涉及内核态和线程调度,开销较小)
自旋锁会会消耗CPU资源,当锁释放了,起就会第一时间获取,但是可能会浪费CPU资源,反之挂起等待锁不会浪费CPU资源,但是会获取锁不及时

公平锁和非公平锁

公平锁:遵循"先来后到"
非公平锁:不遵循先来后到

可重入锁和不可以重入锁

可重入锁:允许一个线程多次获取同一把锁,不会出现死锁问题
不可冲入锁:如果一个线程多次获取同一把锁,会出现死锁问题

读写锁和普通互斥锁

普通互斥锁:就涉及到加锁和解锁
读写锁:加读锁、加写锁和解锁
读锁和读锁之间不互斥
写锁和写锁之间互斥
读锁和写锁之间互斥
遇到读就加读锁,遇到写就加写锁,读写锁适用于"读多写少"

synchronized

锁升级

在这里插入图片描述

无锁:还没有进入加锁代码块
偏向锁:没有真正加锁,只是通过一个标记,如果有竞争才进行加锁
自旋锁:真正加锁,轻量级锁
重量级锁:当竞争激烈时候,变成重量级锁
synchronized锁会根据竞争情况,自动升级,但是一旦升级无法回到过去

锁消除

在synchronized中一个编译器优化,有的地方并不需要加锁,但是我们进行了加锁,其编译器就会将这个锁给自动消除,毕竟加锁和解锁比较消耗时间和空间

锁粗化

锁的粒度
如果加锁和解锁中间有逻辑比较多,锁的粒度较粗
反之如果逻辑比较少,锁的粒度较细

CAS

cas的全程是 Compare and swap就是比较和交换的意思
在这里插入图片描述
上面这个CAS的伪代码,看着像一个函数,但是其是一条"指令",其是线程安全的
CPU提供了CAS的指令
操作系统对其封装,并且提供了API使用CAS的机制
JVM封装调用操作系统的API
java代码就可以使用JVM的API
但是这并不安全,因为只要涉及底层的都不是特别安全

原子类

java.util.concurrent.atomic,java的这个包中里面实现了这样AtomicInteger类,并且这个类中有方法可以进行i++操作,并且是线程安全的
在这里插入图片描述

//创建一个原子类对象,并且初始化为0
private static AtomicInteger count = new AtomicInteger(0);
public class demo29 {//使用原子类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();//count++
//                count.getAndDecrement();//count--
//                count.incrementAndGet();//++count
//                count.decrementAndGet();//--count}});Thread t2 = new Thread(() ->{for (int i = 0; i < 50000; i++) {count.getAndIncrement();//count++}});t1.start();t2.start();t1.join();t2.join();//获取值System.out.println(count.get());}
}

此时使用这个类定义的成员,并且使用对应方法,这样其就是线程安全的
在这里插入图片描述

CAS自旋锁

synchronized中的自旋锁就是依赖CAS实现的
在这里插入图片描述

ABA问题

有一个共享数据num,初始值为A,t1线程在执行更新操作时候将其修改成Z,中间有t2线程插入将其数据修改成B,这时候执行t1线程发现不相同又将B修改成了A,在进行下面操作,这样就不符合我们的要求了

例如:
1.初始值100,线程1获取当前存款为100,期望更新为50,线程2获取当前存款值为100,期望更新为50
2.线程1执行扣款成功,存款变成50,此时线程2还在等待
3.但是在线程2执行之前,有人存入50,存款又变成了100
4.线程2执行,发现是100和之前一样,这样就再次执行扣款,这样存款变成了50

解决方案:使用一个version来表示版本号,版本号只可以加,通过版本号判断是否有插队执行的任务

JUC(java.util.concurrent)的常⻅类

Callable

1.创建一个匿名内部类,实现Callable接口,有泛型参数,并且有返回类型
2.需要重写call方法,使用FutureTask接收Callable对象,Thread接收不了

//要重写call方法
Callable<Integer> callable = new Callable<Integer>() {@Overridepublic Integer call() throws Exception {int sum = 0;for (int i = 0; i <= 100; i++) {sum+=i;}return sum;}};
public class demo30 {public static void main(String[] args) throws ExecutionException, InterruptedException {Callable<Integer> callable = new Callable<Integer>() {@Overridepublic Integer call() throws Exception {int sum = 0;for (int i = 0; i <= 100; i++) {sum+=i;}return sum;}};//Thread无法接收callable对象//使用FutureTaskFutureTask<Integer> futureTask = new FutureTask<>(callable);Thread t = new Thread(futureTask);t.start();//get获取call的结果System.out.println(futureTask.get());}
}

Callable和Runnable都是描述一个任务,而Callable描述的任务有返回值,而Runnable没有返回值
Callable需要搭配FutureTask来接收Callable返回结果,并且要等待结果执行出来

ReentrantLock

 ReentrantLock locker = new ReentrantLock();locker.lock();//加锁locker.unlock();//解锁
public class demo31 {private static int count = 0;public static void main(String[] args) throws InterruptedException {ReentrantLock locker = new ReentrantLock();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {locker.lock();count++;locker.unlock();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {locker.lock();count++;locker.unlock();}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
}

在这里插入图片描述

ReentrantLock和synchronized区别

  1. ReentrantLock,支持tryLock,如果加锁不成,可以直接返回(放弃),并且也支持超时时间,但是lock加锁不成就进行阻塞等待
    在这里插入图片描述
    2.并且这个支持公平锁,这里设置为true就是公平锁,此处就会按照时间来执行线程
    3.等待通知的不同
    synchronized搭配 wait 和notify,并且这里的notify只可以随机唤醒一个
    ReentrantLock搭配 Condition 类,可以指定唤醒

RenntrantLock是使用lock加锁,unlock解锁,要手动解锁,但是这样容易出现忘记解锁问题

Semaphore(信号量)

信号量本表示的是可使用资源个数,本质是一个计数器

Semaphore semaphore = new Semaphore(3);//只有3个可用资源
semaphore.acquire();//获取资源 P操作
semaphore.release();//释放资源 V操作
public class demo32 {public static void main(String[] args) throws InterruptedException {Semaphore semaphore = new Semaphore(3);//p操作semaphore.acquire();System.out.println("P操作");semaphore.acquire();System.out.println("P操作");semaphore.acquire();System.out.println("P操作");semaphore.acquire();System.out.println("P操作");//V操作semaphore.release();System.out.println("V操作");}
}

此时只有三个资源,但是我们要获取4个资源,因此第四次获取就会一直等资源释放
在这里插入图片描述

public class demo32 {public static void main(String[] args) throws InterruptedException {Semaphore semaphore = new Semaphore(3);//p操作semaphore.acquire();System.out.println("P操作");semaphore.acquire();System.out.println("P操作");semaphore.acquire();System.out.println("P操作");//V操作semaphore.release();System.out.println("V操作");semaphore.acquire();System.out.println("P操作");}
}

此时释放一个,就有资源释放了
在这里插入图片描述

CountDownLatch

CountDownLatch latch = new CountDownLatch(8);//创建一个对象
latch.countDown();//数量-1
 latch.await();//等待结束所有,也就是当计数为0
public class demo33{public static void main(String[] args) throws InterruptedException {CountDownLatch latch = new CountDownLatch(8);for (int i = 0; i < 8  ; i++) {final  int id = i;Thread t = new Thread(() ->{System.out.println("运动员" + id + "出发");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("运动员" + id + "到达终点");latch.countDown();//结束就-1});t.start();}//通过awit进行等待,等待都执行完latch.await();System.out.println("比赛结束");}
}

在这里插入图片描述

线程安全的集合类

1.Vector,Stack,HashTable都是线程安全的,但是不建议使用,因为内部实现了锁,有时候是不需要锁,这样加了锁会增大开销
2.CopyOnWriteArrayList,当添加新元素时候,不直接向容器里面添加,而是先将起拷贝一份,将新元素添加到新容器中,添加完以后,将原容器的引用指向新对象,这样的确可以避免我们读到修改一半的数据
优点:读多写少的场景下,性能高
缺点:因为每次都要拷贝占用内存多,并且新添加的元素不能立即读取
3.阻塞队列
ArrayBlockingQueue基于数组实现
LinkedBlockingQueue//基于链表实现
PriorityBlockingQueue//基于堆实现
TransferQueue//最多包含一个元素的阻塞队列

Hashtable

HashMap是线程不安全的,在多线程下使用哈希表可以使用Hashtable和ConcurrentHashMap

Hashtable中队put和get进行了加锁
在这里插入图片描述
在这里插入图片描述
相当于给this对象加锁
在这里插入图片描述

存在问题
1.如果多线程访问同一个Hashtable就会直接造成锁阻塞
2.size属性同步比较满,因为sunchronized
3.当需要扩容时候,该线程完成整个扩容过程,会涉及大量拷贝,效率低下

ConcurrentHashMap

在这里插入图片描述
哈希表中每一个链表的头节点都有一个一把锁,将头节点作为锁对象
1.多线程竞争同一把锁,出现锁冲突,竞争不同的锁,不会出现锁冲突
并且链表的个数比较多,元素分布在不同链表上,这样出现锁冲突概率低
2.使用CAS的方式进行size更新,避免加锁
3.扩容,采用"化整为零",不是一次性将所有元素进行搬运,而是分成多次,这时候会一个ConcurrentHashMap维护新数组和老数组

http://www.dtcms.com/a/561393.html

相关文章:

  • 网站建设结课总结如何在亚马逊开店流程及费用
  • 学习网页制作的网站如何修改网站源文件
  • 停车场管理|停车预约管理|基于Springboot的停车场管理系统设计与实现(源码+数据库+文档)
  • 计算机网络---ICMP协议(Internet Control Message Protocol,互联网控制消息协议)
  • 网站如何做淘宝客网站做要钱
  • 做公司网站需要什么资料开源手机网站系统
  • 成都网站优化公司哪家好南京哪家网络公司做网站优化好
  • Java 通配符
  • java-learn(9):常见算法,collection框架
  • 海口网站建设维护网校 039 网站建设多少钱
  • 网站建设的频道是什么济南企业网站制作费用
  • 外卖餐饮小程序带商城系统餐桌预定点餐寄存排队等待在线点单程序
  • 广州市公司网站建设价格wordpress播放音乐
  • Onnxruntime源码解析
  • Typescript - type 类型别名(通俗易懂教程)
  • 专业建站lhznkj挂机宝做网站
  • 单位网站建设 管理制度wordpress中文视频插件下载
  • 【ComfyUI】混元3D 2.0 Turbo 多视图生成模型
  • 【SAM】eval_coco.py说明
  • 阜宁网站制作具体报价手机端网页设计尺寸规范
  • 青岛做网站和小程序的公司大连长建个人主页
  • [MySQL] JDBC
  • 从零开始学习Redis(六):Redis最佳实践(使用经验总结)
  • 秦皇岛建设网站西安百度seo代理
  • 备案 几个网站职业生涯规划
  • Ruby CGI Cookie 使用指南
  • 网站建设重要意义西部数码做跳转网站
  • X-plore安卓版(安卓手机文件管理器)
  • 【自然语言处理】基于生成式语言模型GPT
  • 广州网站建设方案案例用ps做网站画布一般建多大