3.生产者消费者模式
⽣产者消费者问题是研究多线程程序经典问题之⼀,它描述是有⼀块缓冲区作为仓 库,⽣产者可以将产品放⼊仓库,消费者则可以从仓库中取⾛产品。在Java中⼀共有 四种⽅法⽀持同步,其中前三个是同步⽅法,⼀个是管道方法。
(1)Object的wait() / notify()⽅法 (2)Lock和Condition的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
-
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线程池的工作原理
线程池中的所有线程, 可以分为两部分: 核心线程 和 临时线程
核心线程
核⼼线程常驻于线程池中, 这些线程, 只要线程池存在, 他们不会被销毁。 只有当 线程池需要被销毁的时候, 他们才会被销毁。
临时线程
就是临时⼯。 当遇到了临时的⾼密度的线程需求时, 就会临时开辟⼀些线程, 处理 ⼀些任务。 这些临时的线程在处理完⾃⼰需要处理的任务后, 如果没有其他的任务 要处理, 就会闲置。 当闲置的时间到达了指定的时间之后, 这个临时线程就会被销 毁
任务分配逻辑:
-
当需要处理并发任务的时候,优先分配给核⼼线程处理。
-
当核⼼线程都已经分配了任务, ⼜有新的任务出现时,会将这个新的任务存⼊等待队列。
-
当等待队列被填满后,再来新的任务时,会从开辟⼀个临时线程,处理这个新的任务。
-
当临时线程加核心线程数量已经到达线程池的上限,再来新的任务的时候,就会触发拒绝访问策略。
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() | 停⽌线程池 |