• java基础巩固9


    多线程

    (1)使用ReentranLock:我们知道Java语言直接提供了synchraonized关键字用于加锁,但这种锁一是很重,而是获取时必须等待,没有额外的尝试机制。因为sunchronized是java语言层面提供的语法,所以我们不需要考虑异常,而ReentranLock是java代码实现的锁,我们必须先获取锁,然后在finally中正确释放锁。
    (2)使用ReentranLock比直接使用synchronized更安全,可以代替synchronized进行线程同步。但是,synchronized可以配合wait和notify实现线程在条件不满足时等等待,条件满足时唤醒。而使用Condition对象可以来实现wait和notify的功能。使用Condition时,应用的Condition对象必须从Lock实例的newCondition()返回,这样才能获得绑定了Lock实例的Condition实例。Condition提供的await()/signal()/signalAll()原理和synchronized锁对象的wait()/notify()/notifyAll()是一致的,并且行为也是一样的。
    在这里插入图片描述
    (3)使用ReadWriteLock锁可以实现:
    只允许一个线程写入,其他线程不能写入也不能读取
    没有写入时,多个线程允许同时读,提高新能
    因此,把读写操作分别用读锁和写锁来加锁,在读取时,多个线程可以同时获得读锁,这样就大大提高了并发读的执行效率。使用ReadWriteLock时,使用条件是同一个数据,有大量线程读取,但仅有少线程修改。例如,一个论坛的帖子,回复可以看做是写入操作,它是不频繁的,但是浏览是读取操作,是非常频繁的,这种情况就是可以使用ReadWriteLock。
    (4)深入分析ReadWriteLock,会发现它存在一个潜在的问题:如果有线程正在读,写线程需要等待读线程释放锁之后才能获取写锁,这是一种悲观的读锁。要进一步提升并发执行效率,java8引入了新的读写锁:StamptedLock。
    乐观锁的意思就是乐观地估计读的过程中大概率不会写入,因此被称为乐观锁。反过来,悲观锁则是读的过程中拒绝有写入,也就是写入必须等待。显然,乐观锁的并发效率更高,但一旦有小概率的写入导致读取的数据不一致,需要能检测出来,再读一遍。
    和ReadWriteLock相比,写入的加锁是完全一样的,不同的是读取。注意到我们首先通过tryOptimisticRead()获取一个乐观读锁,并返回版本号。接着进行读取,读取完成后,我们通过validate()去验证版本号,如果在读取过程中没有写入,版本号不变,验证成功,我们就可以放心地继续后续工作。如果在读取过程中有修改,版本号就会发生变化,验证将失败。在失败的时候,我们再次通过获取悲观锁再次读取。由于写入的概率不高,程序在绝大部分情况下可以通过获取乐观锁获取数据,极少数情况下使用悲观锁获取数据。
    可见,StampedLock把读锁细分为乐观锁和悲观锁,能进一步提升并发效率。但这也是有代价的,一是代码更加复杂,而是StampedLock是不可重入锁,不能再一个线程中反复获取同一个锁。StampedLock还提供了更复杂的将悲观锁升级为写锁的功能,它主要使用在if-then-update的场景:即先读,如果读的数据满足条件,就返回,如果读的数据不满足条件,在尝试写。
    (5)BlockingQueue的意思是指,当一个线程调用这个TaskQueue的getTask()方法时,该方法内部可能会让线程编程等待状态,知道队列条件满足不为空,线程被唤醒后,getTask()方法才会返回。因为BlockingQueue非常有用,所以我们不必自己编写,可以直接使用Java标准库的java.util.concurrent包提供的线程安全的集合:ArrayBlockingQueue.多线程同时读写并发集合是安全的,尽量使用java标准库提供的并发集合避免自己编写同步代码。

    使用线程池

    (1)java语言虽然内置了很多多线程支持,启动一个新线程非常方便,但是创建线程会需要操作系统资源(线程资源、栈空间等),频繁创建和销毁大量线程需要消耗大量时间。如果可以复用一组线程,那么就可以把很多小任务让一组线程来执行,而不是一个任务对应一个新线程。这种能接收大量小任务并进行并发处理的就是线程池。
    (2)简单的说,线程池内部维护了若干个线程,没有任务的时候,这些线程都属于等待状态。如果有新任务,就分配一个空闲线程执行。如果所有线程都处于忙碌状态,新任务要么放入队列等待,要么增加一个新线程进行处理。

    使用Future

    (1)在执行多个任务的时候,使用Java标准库提供的线程池是非常方便地,我们提交的任务只需要实现Runnable接口,就可以让线程池去执行。Runnable接口有个问题,它的方法没有返回值。如果任务需要一个返回结果,那么只能保存到变量,还要提供额外的方法读取,非常不方便。所以,java标准库还提供了一个Callable接口,和Runnable接口比,它多了一个返回值。并且Callable接口是一个泛型接口,可以返回执行类型的结果。
    (2)当我们提交一个Callable任务后,我们会同时获得一个Future对象,然后我们在主线程某个时刻调用Future对象的get()方法,就可以获得异步执行的结果。在调用get()时,如果异步任务已经完成,我们就直接获得结果,如果异步任务还没有完成,那么get会阻塞,只要任务完成后才返回结果。

    使用ThreadLocal

    (1)多线程是Java实现多任务的基础,Thread对象代表一个线程,我们可以在代码中调用Thread.currentThread()获取当前线程。
    (2)对于多任务,java标准库提供的线程池可以方便地执行这些任务,同时复用线程。Web应用程序就是典型的多任务应用,每个用户请求页面时,我们都会创建一个任务。然后通过线程池去执行这些任务。
    (3)在一个线程中,横跨若干方法调用,需要传递的对象,我们通常称为上下文(Context),它是一种状态,可以是用户身份、任务信息等。给每一个方法增加一个context参数非常麻烦,而且有些时候,如果调用链有无法修改源码的第三方库,User对象就传不进去了。Java标准库提供了一个特殊的ThreadLocal,它可以在一个线程中传递同一个对象。
    (4)ThreadLocal实例通常总是静态字段初始化如下:
    static ThreadLocal threadLocalUser = new ThreadLocal<>();
    通过设置一个User实例关联到ThreadLocal中,在移除之前,所有方法都可以随时获取到该User实例。
    (5)注意到普通的方法调用一定是同一个线程的。实际上,可以把ThreadLocal看成一个全局Map<\Thread, Object>:每个线程获取ThreadLocal变量时,总是使用Thread自身作为key。因此,ThreadLocal相当于给每个线程都开辟了一个独立的存储空间,各个线程的ThreadLocal关联的实例互不干扰。最后,注意ThreadLocal一定要在finally中清除。这是因为当前线程执行完相关代码后,很可能被重新放入线程池中,如果ThreadLocal没有被清除,该线程执行其他代码时,会把上一次的状态带进去。
    (6)为了保证能释放ThreadLocal关联的实例,我们可以通过AutoCloseable接口配合try(resource){…}结构,让编译器自动为我们关闭。

  • 相关阅读:
    windows远程linux主机
    带头双向循环链表
    时间滑动窗口限制请求次数
    对微信支付和支付宝支付SDK的封装
    实时渲染路径追踪概述
    【实例分享】访问后端服务超时,银河麒麟服务器操作系统分析及处理建议
    Vue Mock.js介绍和使用与首页导航栏左侧菜单搭建
    计算机竞赛 深度学习 机器视觉 人脸识别系统 - opencv python
    Thinking in JAVA:深入理解Java编程之道
    Python:实现random forest regressor随机森林回归器算法(附完整源码)
  • 原文地址:https://blog.csdn.net/weixin_49131718/article/details/126471783