2021-08-04笔记

3.生产者消费者模式

⽣产者消费者问题是研究多线程程序经典问题之⼀,它描述是有⼀块缓冲区作为仓 库,⽣产者可以将产品放⼊仓库,消费者则可以从仓库中取⾛产品。在Java中⼀共有 四种⽅法⽀持同步,其中前三个同步⽅法⼀个管道方法

(1)Object的wait() / notify()⽅法 (2)LockCondition的await() / signal()⽅法

(3) BlockingQueue 阻塞队列⽅法 (4)PipedInputStream / PipedOutputStream

3.1多生产者多消费者

总结:将单⽣产者单消费者代码不做更改,直接再添加⼀个⽣产线程,⼀个消费线程,就形 成了多⽣产者多消费者多消费者.

  • 出现的错误1

    • 错误描述:当有两个⽣产线程,两个消费线程同时存在的时候,有可能出现⽣产⼀次,消费多次或者⽣产多次消费⼀次的情况.

    • 原因:当线程被重新唤醒之后,没有判断标记,直接执⾏了下⾯的代码

    • 解决办法:将标记处的if改成while

  • 出现的错误2

    • 问题描述:继续运⾏程序,会出现死锁的情况(4个线程同时处于等待状态)

    • 原因:唤醒的是本⽅的线程,最后导致所有的线程都处于等待状态.

    • 解决办法:将notify改成notifyAll.保证将对⽅的线程唤醒

3.2 Lock锁

  • 为什么使⽤Lock锁?

    在我们使⽤synchronized进⾏同步的时候,锁对象是Object类的对象,使⽤的 wait,notify⽅法都来⾃Object类,但是咱们知道并不是所有的对象都会⽤到同步, 所以这样⽤法不太合理,⽽且锁相关的功能很多,Lock就是将锁⾯向对象的结果.不 光是将锁⾯向对象了,同时将wait,notify等⽅法也做了⾯向对象处理.形成了 Condition接⼝.当我们想实现多⽣产者多消费者模式时,可以使⽤Lock实现同步, 同时配合Condition接⼝实现唤醒等待.

 //创建锁对象
 Lock lock = new ReentrantLock();
 //⽤于⽣产任务的Condition
 Condition proCon = lock.newCondition();
 //⽤于消费任务的Condition
 Condition conCon = lock.newCondition();
  • 比较synchronized和Lock

    1. synchronized:从jdk1.0就开始使⽤的同步⽅法-称为隐式同步

    synchronized(锁对象){//获取锁 我们将锁还可以称为锁旗舰或者监听器

    同步的代码

    }//释放锁

    Lock:从jdk1.5开始使⽤的同步⽅法-称为显示同步

  • 原理:Lock本身是接⼝,要通过他的⼦类创建对象⼲活⼉

  • 常⽤⼦类: ReentrantLock

  • 使⽤过程:

    ⾸先调⽤lock()⽅法获取锁

    进⾏同步的代码块⼉

    使⽤unlock()⽅法释放锁

  • 使⽤的场景:

    当进⾏多⽣产者多消费者的功能时,使⽤Lock,其他的都使⽤synchronized

  • 使⽤效率:Lock⾼于synchronized

  • ⽐较Object多wait,notify和Condition的await,signal

3.3唤醒等待机制

3.3.1方法简介

Object类中⼏个⽅法如下:

  • wait()

    • 等待,让当前的线程,释放⾃⼰持有的指定的锁标记,进⼊到等待队列。

    • 等待队列中的线程,不参与CPU时间⽚的争抢,也不参与锁标记的争抢。

  • notify()

    • 通知、唤醒。唤醒等待队列中,⼀个等待这个锁标记的随机的线程。

    • 被唤醒的线程,进⼊到锁池,开始争抢锁标记。

  • notifyAll()

    • 通知、唤醒。唤醒等待队列中,所有的等待这个锁标记的线程。

    • 被唤醒的线程,进⼊到锁池,开始争抢锁标记。

3.3.2 wait和sleep的区别

  • sleep()⽅法,在休眠时间结束后,会⾃动的被唤醒。⽽wait()进⼊到的阻塞态,需要被notify/notifyAll⼿动唤醒。

  • wait()会释放⾃⼰持有的指定的锁标记,进⼊到阻塞态。sleep()进⼊到阻塞态的时候,不会释放⾃⼰持有的锁标记。

3.3.3注意事项

  • ⽆论是wait()⽅法,还是notity()/notifyAll()⽅法,在使⽤的时候要注意,⼀定要 是⾃⼰持有的锁标记,才可以做这个操作。

    否则会出现 IllegalMonitorStateException 异常。

  • 为什么wait,notify⽅法要使⽤锁调⽤?

3.4死锁

出现的情况有两种

  • 所有的线程处于等待状态

    ⼤家都处于等待状态,没有⼈获取cpu使⽤

  • 锁之间进行嵌套调⽤

    多个线程,同时持有对⽅需要的锁标记,等待对⽅释放⾃⼰需要的锁标记。此时就是出现死锁。线程之间彼此持有对⽅需要的锁标记,而不进行释放,都在等待。

3.5线程的停止

线程的停⽌:3种

1.通过⼀个标识结束线程

2.调⽤stop⽅法---因为有固有的安全问题,所以系统不建议使⽤.

3.调⽤interrupt⽅法----如果⽬标线程等待很⻓时间(例如基于⼀个条件变量),则应使⽤ interrupt ⽅法来中断该等待。

3.6线程的休眠(sleep())

线程休眠,就是让当前的线程休眠指定的时间。休眠的线程进⼊到阻塞状态,直到 休眠结束。阻塞的线程,不参与CPU时间⽚的争抢。

注: 线程休眠的时间单位是毫秒。

3.7线程的合并

将⼀个线程中的任务, 合并⼊到另外⼀个线程中执⾏, 此时, 合并进来的线程有限 执⾏。 类似于: 插队。

注意:优先级只⽐main线程的⾼.对其他的线程没有影响.

3.8线程的优先级设置

设置线程的优先级,可以决定这个线程能够抢到CPU时间⽚的概率。线程的优先级范围在 [1, 10], 默认的优先级是5。数值越⾼,优先级越⾼。但是要注意,并不是 优先级⾼的线程⼀定能抢到CPU时间⽚, 也不是优先级的线程⼀定抢不到CPU时间 ⽚。 线程的优先级只是决定了这个线程能够抢到CPU时间⽚的概率。 即便是优先级最低的线程,依然可以抢到CPU时间⽚。

3.9守护线程

守护线程, ⼜叫后台线程。 是⼀个运⾏在后台, 并且会和前台线程争抢CPU时间⽚ 的线程。

  • 守护线程依然会和前台线程争抢CPU时间⽚,实现并发的任务。

  • 在⼀个进程中,如果所有的前台线程都结束了,后台线程即便任务没有执⾏结束,也会⾃动结束。

3.10线程池

3.10.1线程池简介

线程池,其实就是⼀个容器,⾥⾯存储了若⼲个线程。

使⽤线程池,最主要是解决线程复⽤的问题。之前使⽤线程的时候,当我们需要使⽤⼀个线程时,实例化了⼀个新的线程。当这个线程使⽤结束后,对这个线程进⾏销毁。对于需求实现来说是没有问题的,但是如果频繁的进⾏线程的开辟和销毁,其实对于CPU来说,是⼀种负荷,所以要尽量的优化这⼀点。

可以使⽤复⽤机制解决这个问题。当我们需要使⽤到⼀个线程的时候,不是直接实例化,⽽是先去线程池中查找是否有闲置的线程可以使⽤。如果有,直接拿来使⽤;如果没有,再实例化⼀个新的线程。并且,当这个线程使⽤结束后,并不是 ⻢上销毁,⽽是将其放⼊到线程池中,以便下次继续使⽤。

3.10.2线程池的开辟

在Java中, 使⽤ThreadPoolExecutor类来描述线程池, 在这个类的对象实例化的时 候, 有⼏个常⻅的参数:

参数描述
int corePoolSize核心线程的数量
int maximunPoolSize线程池最大容量(包含了核心线程和临时线程)
long keepAliveTime临时线程可以空闲的时间
TimeUnit unit临时线程保持存活的时间单位
BlockingQueue workQueue任务等待队列
RejectedExecutionHandler handler拒绝访问策略
  • BlockingQueue

    • ArrayBlockingQueue

    • LinkedBlockingQueue

    • SynchronouseQueue

  • RejectedExecutionHandler

    • ThreadPoolExecutor.AbortPolicy : 丢弃新的任务,并抛出异常

    • RejectedExecutionException ThreadPoolExecutor.DiscardPolicy :丢弃新的任务,但是不会抛出异常

    • ThreadPoolExecutor.DiscardOldestPolicy : 丢弃等待队列中最早的 任务 ThreadPoolExecutor.CallerRunsPolicy : 不会开辟新的线程,由调⽤的 线程来处理

3.10.3线程池的工作原理

线程池中的所有线程, 可以分为两部分: 核心线程临时线程

核心线程

核⼼线程常驻于线程池中, 这些线程, 只要线程池存在, 他们不会被销毁。 只有当 线程池需要被销毁的时候, 他们才会被销毁。

临时线程

就是临时⼯。 当遇到了临时的⾼密度的线程需求时, 就会临时开辟⼀些线程, 处理 ⼀些任务。 这些临时的线程在处理完⾃⼰需要处理的任务后, 如果没有其他的任务 要处理, 就会闲置。 当闲置的时间到达了指定的时间之后, 这个临时线程就会被销 毁

任务分配逻辑:

  1. 当需要处理并发任务的时候,优先分配给核⼼线程处理。

  2. 当核⼼线程都已经分配了任务, ⼜有新的任务出现时,会将这个新的任务存⼊等待队列。

  3. 当等待队列被填满后,再来新的任务时,会从开辟⼀个临时线程,处理这个新的任务。

  4. 当临时线程加核心线程数量已经到达线程池的上限,再来新的任务的时候,就会触发拒绝访问策略。

3.10.4线程池的常用方法

方法描述
execute(Runnable runnable)将任务提交给线程池, 由线程池分配线程来并发处理
shutdown()向线程池发送⼀个停⽌信号,这个操作并不会停⽌线程池 中的线程,⽽是在线程池中所有的任务都执⾏结束后,结 束线程和线程池。
shutdownNow()⽴即停⽌线程池中的所有的线程和线程池。

3.10.5线程池的工具类

线程池的开辟, 除了可以使⽤构造⽅法进⾏实例化, 还可以通过Executors⼯具类进 ⾏获取。 实际应⽤中, ⼤部分的场景下, 可以不⽤前⾯的构造⽅法进⾏线程池的实 例化, ⽽是⽤Executors⼯具类中的⽅法进⾏获取。

方法描述
Executors.newSingleThreadExecutor()核⼼线程1 最⼤线程1 等待队列容量 Integer.MAX_VALUE
Executors.newCachedThreadPool()核⼼线程0 最⼤线程Integer.MAX_VALUE 闲置时间 60
Executors.newFixedThreadPool(int size)核⼼线程size 最⼤线程 size 等待队列容量 Integer.MAX_VALUE
submit()向线程池中添加任务
shutdown()停⽌线程池
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值