wait() 方法和 sleep() 方法区别
notify和notifyAll使用那个唤醒线程
建议使用notifyAll,两个虽然都是唤醒线程,但是还是有区别的,notify是随机唤醒等待线程池的一个线程,而notifyAll会唤醒对象等待队列池的所有线程,看上去使用notify更好一下,但事实上他是有风险的,比如下面例子
有四个资源A,B,C,D,线程1申请到了A和B资源,线程2申请到了C和D,此时线程3想要申请A和B,因此进入等待队列,线程4想要申请C和D,然后线程1释放了A和B资源,如果使用notify唤醒了线程4,但是线程4是想申请C和D,不满足条件就继续等待,而真正想获取资源A和B并没有被唤醒,因此没有特殊要求建议使用notifyAll.
什么是数据竞争
多个线程同时共享同一个数据,且至少与一个线程修改这个数据,比如下面代码
public class Test {
private long count = 0;
void add10K() {
int idx = 0;
while(idx++ < 10000) {
count += 1;
}
}
}
在多个线程中调用add10k就会对产生数据竞争,多个线程会修改count这个共性变量,不做安全措施的话,就会因此bug,但是此时如果没有改成下面代码
public class Test {
private long count = 0;
synchronized long get(){
return count;
}
synchronized void set(long v){
count = v;
}
void add10K() {
int idx = 0;
while(idx++ < 10000) {
set(get()+1) //由于get set不具备原子性会导致线程安全问题
//get虽然有锁,只能保证多个线程不能同一时刻执行。
// 但是出现不安全的可能是线程a调用get后线程b调用get,
// 这时两个get返回的值是一样的。然后都加一后再分别set.
// 这样两个线程就出现并发问题了。问题在于同时执行get,
// 而在于get和set是两个方法,这两个方法组合不是原子的,
// 就可能两个方法中间的时间也有其它线程分别调用,出现并发问题
}
}
}
在add10k中,我使用get和set去访问变量count,对count变量添加互斥性,此时就不存在数据竞争,但是add10k依然是线程不安全的
什么是竞争条件
竞争条件就是线程执行的结果是依赖于线程的执行顺序,比如上面代码两个线程同时执行get,获取到的值是0,执行get()+1的时候获取的值就是1,此时把1更新到内存,最终你获取的结果是1,而不是期望的2.
两个线程同时执行就会是1,如果线程先后执行就会是2,在并发的世界里,线程的执行顺序是不确定的,如果存在竞争条件,就会导致结果的不确定性。
我们也可以按照下面代码理解竞争条件,程序的执行以来某个状态
if (状态变量 满足 执行条件) {
执行操作
}
当某个线程发现状态变量满足条件,执行操作,可就在线程执行的时候,其他线程修改了状态变量,就会不在满足条件了,
什么是死锁,活锁,饥饿
死锁就是多个线程互相等待,而且会一直等待下去,也就是指阻塞.
活锁就是虽然线程没有阻塞,但是也不会执行下去,在显示生活中,一个人A右手边进门,一个人B从左手边出门,两个人就会碰到,然后两个人谦让,A从左手边进门,B从右手边进门,然后又碰上了,当然人是会交流的,交流之后,就可以解决,但是编程的世界如何解决呢,当然也是非常简单的,只要每一个线程随机时间后在去执行,这样就可以大大减少碰到的概率。
饥饿是线程无法获取到资源而无法执行下去,如果线程的优先级不均,在CPU繁忙下,优先级低的线程无法获取到资源就会导致饥饿,还有就是持有锁时间过长,也会导致饥饿的发生,饥饿也是有策略解决的,可以均匀分配资源,减少持有锁的时间,还有就是资源充足.
什么是管程
管程是指对管理共享变量以及共享变量的操作,让他们支持并发,管程应为名Monitor,直译就是监视器,意译就是管程。
我们要注意管程和信号量是等价的,等价是指,可以用管程实现信号量,也可以用信号量实现管程。
管程是如何管理的呢?
管程是有三种模式,分别是:Hasen 模型、Hoare 模型和 MESA 模型,而java的管程参考实现使用的MESA
在并发编程中,有两大核心内容,同步和互斥,互斥就是指同一时刻只有一个线程执行,同步是指线程之间的协作和通信,
利用管程可以很简单的实现互斥,就是把共享变量和共享操作封装起来,对外提供安全的操作,
正如上图一样,我们使用管程把共享变量和对应的入队enq和出队dep封装起来,线程A和线程B要进行队列的操作就要使用管程提供的enq和dep实现,enq和dep保持了互斥,同一时刻只有一个线程进入管程.
管程实现同步就比较复杂了,我们先来看一下MEAS管程模型的图如下显示
上图外面的黑框就是实现封装的意思,最上面只有一个入口,同一时刻只会有一个线程进入,当多个线程想进入的时候,其余的线程会进入入口等待队列,而管程内部有条件变量和条件变量等待队列,每一个条件变量只有一个条件等待队列,
而条件变量和条件变量等待队列就是实现同步的,这里我要注意一个问题,就是用管程实现阻塞队列和管程内的等待队列是不一样的东西,我们举个例子,一个线程1对管程实现的阻塞队列进行出队的操作,出队的前提条件就是阻塞队列不能为空,而这个前提条件就是管程里面的条件变量,当从阻塞队列出队的时候,发现阻塞队列为空,怎么办呢,此时就会进入等待,而这个等待就是管程里面的条件变量等待队列,然后又有一个线程2要对管程实现的阻塞队列进行入队操作,如果入队成功之后,此时阻塞队列不为空的条件,对于线程1就已经满足了,线程2就会通知线程1,线程1就会从条件变量等待队列出队,但是并不会直接执行,而是进入管程入口的等待队列,
使用管程写一个线程安全的队列
public class BlockedQueue<T>{
final Lock lock =
new ReentrantLock();
// 条件变量:队列不满
final Condition notFull =
lock.newCondition();
// 条件变量:队列不空
final Condition notEmpty =
lock.newCondition();
// 入队
void enq(T x) {
lock.lock();
try {
while (队列已满){
// 等待队列不满
notFull.await();
}
// 省略入队操作...
//入队后,通知可出队
notEmpty.signal();
}finally {
lock.unlock();
}
}
// 出队
void deq(){
lock.lock();
try {
while (队列已空){
// 等待队列不空
notEmpty.await();
}
// 省略出队操作...
//出队后,通知可入队
notFull.signal();
}finally {
lock.unlock();
}
}
}
notify在什么场景使用
上面我们讲过,尽可能使用notifyAll,但是什么时候使用notify呢,需要满足下面三个条件
如果对您有一丝丝帮助,麻烦点个关注,也欢迎转发,谢谢
扫码关注