目录
8、JUC(java.util.concurrent) 的常见类
1、乐观锁 VS 悲观锁
乐观锁、悲观锁顾名思义就是,形容心态,乐观把事情往好了想,悲观把事情往坏了想
乐观锁概念
乐观锁在操作数据的时候,非常乐观,都认为别人不会同时修改数据,所以乐观锁在更新数据之前,都不会对数据进行加锁,只有当执行更新数据操作时候,再去判断数据是否被修改,如果数据被修改了,就放弃被当前修改操作。
悲观锁概念
悲观锁在操作数据时候,比较悲观,都认为别人会和自己同时修改数据,所以悲观锁操作数据时候,会直接给数据上锁,不让别人操作,只有自己操作完成后,才释放锁。
乐观锁与悲观锁的实现方式(含实例)
在说明实现方式之前,需要明确:乐观锁和悲观锁是两种思想,它们的使用是非常广泛的,不局限于某种编程语言或数据库。
悲观锁的实现方式是加锁,加锁既可以是对代码块加锁(如synchronized关键字),也可以是对数据加锁(如MySQL中的排它锁)。
乐观锁的实现方式主要有两种:CAS机制和版本号机制
(1)CAS(Compare And Swap)
CAS操作包括了3个操作数:
1) 需要读写的内存位置(V) - 内存值
2) 进行比较的预期值(A) - 旧预期值
3) 拟写入的新值(B) - 新值
if(V = A){
V = B
}
因此,如果并发执行自增操作,可能导致计算结果的不准确。
在下面的代码示例中:value1没有进行任何线程安全方面的保护,value2使用了乐观锁(CAS),value3使用了悲观锁(synchronized)。
运行程序,使用1000个线程同时对value1、value2和value3进行自增操作,可以发现:value2和value3的值总是等于1000,而value1的值常常小于1000。
public class Test {
//value1:线程不安全
private static int value1 = 0;
//value2:使用乐观锁
private static AtomicInteger value2 = new AtomicInteger(0);
//value3:使用悲观锁
private static int value3 = 0;
private static synchronized void increaseValue3() {
value3++;
}
public static void main(String[] args) throws Exception {
//开启1000个线程,并执行自增操作
for (int i = 0; i < 1000; ++i) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
value1++;
value2.getAndIncrement();
increaseValue3();
}
}).start();
}
//查看活跃线程
while (Thread.activeCount() > 2) {
//Thread.currentThread().getThreadGroup().list();
Thread.yield();//让出cpu
}
//打印结果
Thread.sleep(1000);
System.out.println("线程不安全:" + value1);
System.out.println("乐观锁(AtomicInteger):" + value2);
System.out.println("悲观锁(synchronized):" + value3);
}
}
1)首先来介绍AtomicInteger。
AtomicInteger是java.util.concurrent.atomic包提供的原子类,利用CPU提供的CAS操作来保证原子性;
除了AtomicInteger外,还有AtomicBoolean、AtomicLong、AtomicReference等众多原子类。
源码分析说明如下:
- getAndIncrement()实现的自增操作是自旋CAS操作:在循环中进行compareAndSet,如果执行成功则退出,否则一直执行。
- 其中compareAndSet是CAS操作的核心,它是利用Unsafe对象实现的。
- Unsafe又是何许人也呢?
Unsafe是用来帮助Java访问操作系统底层资源的类(如可以分配内存、释放内存,在netty中大量用到它,属于C++层面的native方法,我们一般使用反射获取),通过Unsafe,Java具有了底层操作能力,可以提升运行效率;
强大的底层资源操作能力也带来了安全隐患(类的名字Unsafe也在提醒我们这一点),因此正常情况下用户无法使用。
AtomicInteger在这里使用了Unsafe提供的CAS功能。
- valueOffset可以理解为value在内存中的偏移量,对应了CAS三个操作数(V/A/B)中的V;偏移量的获得也是通过Unsafe实现的。
- value域的volatile修饰符:Java并发编程要保证线程安全,需要保证原子性、可视性和有序性;
CAS操作可以保证原子性,而volatile可以保证可视性和一定程度的有序性;
在AtomicInteger中,volatile和CAS一起保证了线程安全性。
2) 说完了AtomicInteger,再说synchronized。
synchronized通过对代码块加锁来保证线程安全:在同一时刻,只能有一个线程可以执行代码块中的代码。
synchronized是一个重量级的操作,不仅是因为加锁需要消耗额外的资源,还因为线程状态的切换会涉及操作系统核心态和用户态的转换;
不过随着JVM对锁进行的一系列优化(如自旋锁、轻量级锁、锁粗化等),synchronized的性能表现已经越来越好。
(2)版本号机制实现乐观锁
版本号机制的基本思路是在数据中增加一个字段version,表示该数据的版本号,每当数据被修改,版本号加1。
- 当某个线程查询数据时,将该数据的版本号一起查出来;
- 当该线程更新数据时,判断当前版本号与之前读取的版本号是否一致,如果一致才进行操作。
需要注意的是,这里使用了版本号作为判断数据变化的标记,实际上可以根据实际情况选用其他能够标记数据版本的字段,如时间戳等。
乐观锁和悲观锁优缺点和适用场景
乐观锁和悲观锁并没有优劣之分,它们有各自适合的场景;下面从两个方面进行说明。
- 功能限制 与悲观锁相比,乐观锁适用的场景受到了更多的限制,无论是CAS还是版本号机制。
例如,CAS只能保证单个变量操作的原子性,当涉及到多个变量时,CAS是无能为力的,而synchronized则可以通过对整个代码块加锁来处理。
再比如版本号机制,如果query的时候是针对表1,而update的时候是针对表2,也很难通过简单的版本号来实现乐观锁。
- 竞争激烈程度 如果悲观锁和乐观锁都可以使用,那么选择就要考虑竞争的激烈程度:
当竞争不激烈 (出现并发冲突的概率小)时,乐观锁更有优势,因为悲观锁会锁住代码块或数据,其他线程无法同时访问,影响并发,而且加锁和释放锁都需要消耗额外的资源。
当竞争激烈(出现并发冲突的概率大)时,悲观锁更有优势,因为乐观锁在执行更新时频繁失败,需要不断重试,浪费CPU资源。
乐观锁加锁吗?
下面是我对这个问题的理解:
- 乐观锁本身是不加锁的,只是在更新时判断一下数据是否被其他线程更新了;AtomicInteger便是一个例子。
- 有时乐观锁可能与加锁操作合作,例如,在前述updateCoins()的例子中,MySQL在执行update时会加排它锁。
但这只是乐观锁与加锁操作合作的例子,不能改变“乐观锁本身不加锁”这一事实。
CAS有哪些缺点?
面试到这里,面试官可能已经中意你了。
不过面试官准备对你发起最后的进攻:你知道CAS这种实现方式有什么缺点吗?
下面是CAS一些不那么完美的地方:
1.ABA问题 假设有两个线程——线程1和线程2,两个线程按照顺序进行以下操作:
- (1)线程1读取内存中数据为A;
- (2)线程2将该数据修改为B;
- (3)线程2将该数据修改为A;
- (4)线程1对数据进行CAS操作
在第(4)步中,由于内存中数据仍然为A,因此CAS操作成功,但实际上该数据已经被线程2修改过了。这就是ABA问题。
在AtomicInteger的例子中,ABA似乎没有什么危害。
但是在某些场景下,ABA却会带来隐患,例如栈顶问题:一个栈的栈顶经过两次(或多次)变化又恢复了原值,但是栈可能已发生了变化。
对于ABA问题,比较有效的方案是引入版本号,内存中的值每发生一次变化,版本号都+1;
在进行CAS操作时,不仅比较内存中的值,也会比较版本号,只有当二者都没有变化时,CAS才能执行成功。
Java中的AtomicStampedReference类便是使用版本号来解决ABA问题的。
2.高竞争下的开销问题 在并发冲突概率大的高竞争环境下,如果CAS一直失败,会一直重试,CPU开销较大。
针对这个问题的一个思路是引入退出机制,如重试次数超过一定阈值后失败退出。
当然,更重要的是避免在高竞争环境下使用乐观锁。
3.功能限制 CAS的功能是比较受限的,例如CAS只能保证单个变量(或者说单个内存值)操作的原子性,这意味着:
- 原子性不一定能保证线程安全,例如在Java中需要与volatile配合来保证线程安全;
- 当涉及到多个变量(内存值)时,CAS也无能为力。
除此之外,CAS的实现需要硬件层面处理器的支持,在Java中普通用户无法直接使用,只能借助atomic包下的原子类使用,灵活性受到限制。
2、普通互斥锁 VS 读写锁
互斥锁
互斥锁是一种简单的加锁的方法来控制对共享资源的访问,互斥锁只有两种状态,即上锁( lock )和解锁(unlock),如果互斥量已经上锁,调用线程会阻塞,直到互斥量被解锁。在完成了对共享资源的访问后,要对互斥量进行解锁
互斥锁特点
- 原子性:把一个互斥量锁定为一个原子操作,操作系统保证了如果一个线程锁定了一个互斥量,没有其他线程在同一时间可以成功锁定这个互斥量;
- 唯一性:如果一个线程锁定了一个互斥量,在它解除锁定之前,没有其他线程可以锁定这个互斥量;
- 非繁忙等待:如果一个线程已经锁定了一个互斥量,第二个线程又试图去锁定这个互斥量,则第二个线程将被挂起(不占用任何cpu资源),直到第一个线程解除对这个互斥量的锁定为止,第二个线程则被唤醒并继续执行,同时锁定这个互斥量
读写锁
读写锁允许更改的并行性,写的串行性,也叫共享互斥锁。 互斥量要么是锁住状态,要么就是不加锁状态,而且一次只有一个线程可以对其加锁。 读写锁可以有3种状态:读模式下加锁状态、写模式加锁状态、不加锁状态。 一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁(允许多个线程读但只允许一个线程写)
读写锁特点
- 如果有其它线程读数据,则允许其它线程执行读操作,但不允许写操作;
- 如果有其它线程写数据,则其它线程都不允许读、写操作
3、轻量级锁 VS 重量级锁
轻量级锁
轻量级锁:是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁
重量级锁: 是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低
4、自旋锁 VS 挂起等待锁
自旋锁
自旋锁与互斥量功能一样,唯一一点不同的就是互斥量阻塞后休眠让出cpu,而自旋锁阻塞后不会让出cpu,会一直忙等待,直到得到锁,原地打转 自旋锁在用户态使用的比较少,在内核使用的比较多!自旋锁的使用场景:锁的持有时间比较短,或者说小于2次上下文切换的时间。
自旋锁特点
- 某个协程持有锁时间长,等待的协程一直在循环等待,消耗CPU资源。
- 不公平,有可能存在有的协程等待时间过程,出现线程饥饿(这里就是协程饥饿)
挂起等待锁
如果线程获取不到锁就会堵塞等待,啥时候结束等待就要看cpu的调度策略,在挂起的时候是不吃cpu的资源的
5、公平锁 VS 非公平锁
公平锁
- 公平锁是指多个线程按照申请锁的顺序来获取锁。
非公平锁
- 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
对于Java ReentrantLock
而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。 对于Synchronized
而言,也是一种非公平锁。由于其并不像ReentrantLock
是通过AQS
的来实现线程调度,所以并没有任何办法使其变成公平锁。
打一个不恰当的例子,公共厕所男女一起排队,当厕所里面是将男女分开的,这时当里面一个男的出来了,而外面排队的前三个人都是女的,队伍中就会直接让第四个人进去,此时的优先级就是男>女的,也就是非公平锁了
注:
- 操作系统内部的线程调度就可以视为是随机的. 如果不做任何额外的限制, 锁就是非公平锁. 如果要想实现公平锁, 就需要依赖额外的数据结构, 来记录线程们的先后顺序.
- 公平锁和非公平锁没有好坏之分, 关键还是看适用场景.
6、可重入锁 VS 不可重入锁
可重入锁
为了解决互斥锁导致的死锁问题(哲学家吃面问题),引入可重入锁又叫递归锁
可重入内部维护着一个锁和一个计数器,计数器记录了获取锁的次数,从而使得资源可以被同一个线程多次获取,直到一个线程所有的获取都被释放,其他的线程才能获得资源
不可重入锁
不可重入锁:与可重入锁相反,不可递归调用,递归调用就发生死锁。
以上六组就是我们常见的锁策略
7、Synchronized 原理
7.1、锁升级(锁膨胀)
(1、偏向锁——无竞争
偏向锁并不是真的“加锁”,而是给对象头中做一个“偏向锁的标记”,记录这个锁属于哪个线程
如果后续没有其他线程来竞争这个锁,那就不用加锁了,可以避免加锁解锁带来的开销
但是如果后续有其他线程来竞争该锁,(因为它已经做了标记,很容易识别 它是否是刚才记录的线程),就会取消偏向锁状态,进入一般的轻量级锁状态
怎么去理解呢?其实就是危机感的问题,比如,那么男女交往过程中,双方确定关系,只是彼此知道而已,但别人并不知道,他们也没有官宣(官宣---加锁,官宣分手---需要开销即需要时间),此时就相当于偏向锁,而此时,突然出现了一个男配或者女配的时候,他们产生了危机感,就会立即官宣(加锁)。再打个比方,同志A去上厕所,他没有习惯关门(关门---加锁,开门---解锁,卡开关门的操作需要ATP,消耗能量),每次听到外面有脚步声来了,才会赶快去关上门。
(2、轻量级锁——有竞争
- 通过 CAS 检查并更新一块内存 (比如 null => 该线程引用)
- 如果更新成功, 则认为加锁成功
- 如果更新失败, 则认为锁被占用, 继续自旋式的等待(并不放弃 CPU).
(3、重量级锁——竞争激烈
- 执行加锁操作, 先进入内核态.
- 在内核态判定当前锁是否已经被占用
- 如果该锁没有占用, 则加锁成功, 并切换回用户态.
- 如果该锁被占用, 则加锁失败. 此时线程进入锁的等待队列, 挂起. 等待被操作系统唤醒.
- 经历了一系列的沧海桑田, 这个锁被其他线程释放了, 操作系统也想起了这个挂起的线程, 于是唤醒这个线程, 尝试重新获取锁.
7.2、锁消除
锁消除其实就是JVM做出的一个优化,JVM自动判定这个地方是不需要锁的,如果你加了Synchronized,就会自动的把锁去掉
这里JVM是在有百分之百的把握下才会进行锁消除
7.3、锁粗化
就是把细粒度的加锁优化为粗粒度的加锁,如图:
8、JUC(java.util.concurrent) 的常见类
这个包中放的都是与多线程相关的
8.1、Callable 接口
Callable 接口创建线程:http://t.csdn.cn/skpC0
8.2、ReentrantLock
ReentrantLock 的用法:
lock(): 加锁 , 如果获取不到锁就死等 .trylock( 超时时间 ): 加锁 , 如果获取不到锁 , 等待一定的时间之后就放弃加锁 .unlock(): 解锁
ReentrantLock 和 synchronized的区别:
- tryLock试试看能不能加上锁,成功就成功了,失败就放弃(可以指定加锁的等待超时时间)。synchronized申请失败会死等
- ReentrantLock可以实现公平锁,默认中是非公平的,构造的时候传入一个简单的参数(true)就成了公平锁
- synchronized是搭配wait/notify实现等待通知机制的,唤醒操作是随机唤醒一个
- ReentrantLock搭配Condition类实现的,唤醒操作是可以指定唤醒那个等待的线程
- synchronized使用使不需要手动释放锁,ReentrantLock使用时需要手动释放,使用起来更灵活,也更容易遗漏unlock
如何选择使用哪个锁?
- 锁竞争不激烈的时候, 使用 synchronized, 效率更高, 自动释放更方便.
- 锁竞争激烈的时候, 使用 ReentrantLock, 搭配 trylock 更灵活控制加锁的行为, 而不是死等.
- 如果需要使用公平锁, 使用 ReentrantLock.
8.3、原子类
原子类的底层是基于CAS实现的,这里使用API文档查看即可
synchronized相比于CAS,虽然没有CAS好用,但是synchronized的打击面广,使用场景更多,更加通用
8.4、线程池
这篇博文已经详细总结了:http://t.csdn.cn/f1a9s
另外利用线程池创建线程:http://t.csdn.cn/skpC0
8.5、信号量Semaphore
信号量,用来表示“可用资源的个数”,本质上就是一个计数器
信号量的两个基本操作:
- P操作,申请一个资源,可用资源数就+1
- V操作,释放一个资源,可用资源数-1
- 当计数为0时,进行P操作,就会产生阻塞
例:
public class Test {
public static void main(String[] args) throws InterruptedException {
//指定计数器的初始值,表示有几个资源
Semaphore semaphore = new Semaphore(4);
//P-操作,申请资源,计数器-1
semaphore.acquire();
System.out.println("P操作");
semaphore.acquire();
System.out.println("P操作");
semaphore.acquire();
System.out.println("P操作");
//V-操作,释放资源,计数器+1
semaphore.release();
System.out.println("V操作");
semaphore.acquire();
System.out.println("P操作");
semaphore.acquire();
System.out.println("P操作");
semaphore.acquire();
System.out.println("P操作");//这个不会输出,阻塞等待了
}
}
8.6、CountDownLatch
CountDownLatch
允许一个或者多个线程去等待其他线程完成操作。
例如,在跑步中,等待所有人都跑完了,才算比赛结束:
import java.util.concurrent.CountDownLatch;
public class Test {
public static void main(String[] args) throws InterruptedException {
// 有 10 个选手参加了比赛
CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
// 创建 10 个线程来执行一批任务.
Thread t = new Thread(() -> {
System.out.println("选手出发! " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("选手到达! " + Thread.currentThread().getName());
// 撞线
countDownLatch.countDown();
});
t.start();
}
// await 会等到所有的选手都撞线
countDownLatch.await();
System.out.println("比赛结束!");
}
}
CountDownLatch
提供的一些方法:
9、线程安全的集合类
9.1、多线程环境使用ArrayList
1)、自己使用同步机制 (synchronized 或者 ReentrantLock)
2)、Collections.synchronizedList(new ArrayList);
- synchronizedList是标准库提供的一个基于synchronized进行线程同步的List
- synchronizedList的关键操作上都带有synchronized
这个方法固然是好的,但是更建议自己手动加锁,因为,有的地方可能就是不需要加锁,而这个方法是给所有都加锁了,难免会降低效率的
3)、使用 CopyOnWriteArrayList
- 在读多写少的场景下,性能高,不需要加锁竞争
- 占用内存较多
- 新写的数据不能第一时间读取到
9.2、多线程环境使用队列
9.3、多线程环境使用哈希表
HashMap是线程不安全的
在多线程环境下,使用哈希表可以使用:
- HashTable【不建议使用】
-
ConcurrentHashMap【推荐使用,重点掌握】
1)、 Hashtable
这里是无脑给各种方法加synchronized
- 如果多线程访问同一个 Hashtable 就会直接造成锁冲突.
- size 属性也是通过 synchronized 来控制同步, 也是比较慢的.
- 一旦触发扩容, 就由该线程完成整个扩容过程. 这个过程会涉及到大量的元素拷贝, 效率会非常低.
2)、 ConcurrentHashMap
ConcurrentHashMap核心优化思路,尽可能降低锁冲突的概率
- ConcurrentHashMap是给每个哈希桶一把锁,只有当两个线程访问同一个哈希桶上的数据才会出现锁冲突(算是对Hashtable 做出的一个优化)【控制锁的粒度】
注:产生锁冲突,对于性能影响是非常大的
- ConcurrentHashMap只是给写操作加锁,读操作不加锁,当两个线程同时修改时,才会产生锁冲突
- 充分利用CAS特性
例如:维护元素个数,都是通过CAS实现,而不是加锁
- ConcurrentHashMap对于扩容操作,进行了化整为零的特殊优化
HashTable扩容:
当添加元素时,发现负载因子已经超过阈值时,触发扩容机制,申请一个更大的数组,将原来的数据搬运至新的数组上
问题:
当需要搬运的元素特别多的时候,该操作的开销会非常大,就可能会导致添加某个元素时,卡了很久才添加上,严重的甚至导致请求添加失败
ConcurrentHashMap扩容优化:
扩容时,不是一次性将所有数据全部搬运过去,而是依次搬运一点,就像愚公移山一样
并且,在扩容过程中,旧的数据和新的数据会同时存在一段时间,直到搬运完成,才会释放旧的数据的空间
在这个过程中,如果要进行查询元素,就会新、旧数据一起查询,插入数据,直接插入,删除数据,如果该数据还在旧的数据空间中,就不用搬运该数据了
补充两点:
- 分段锁
ConcurrentHashMap中旧版本的实现方式,好几个链表分一个锁,锁冲突概率更高,Java8开始就不是这样了,而是一个链表分一个锁
- HashMap key可以为null,HashTable,ConcurrentHashMap key不能为null
10、死锁
10.1、什么是死锁
场景举例:
例1: 一个线程一把锁
当一个线程一把锁,连续加锁两次,导致死锁,但是如果该锁是可重入锁,就不会出现死锁情况(synchronized就是可重入锁)
例2:两个线程两把锁
例如有两个线程:
线程A需要先锁a,再锁b,而线程B需要先锁b再锁a,那么就会出现,A把a锁了,B把b锁了,两个互不相让 ,僵持在这里,这时就构成了一个死锁的状态
换句话来说,就是车钥匙锁在家里,而家钥匙锁在车里了,就是一个死锁的状态
看代码:
死锁情况:将上述两个synchronized进行嵌套,也就是说,第一把锁还没有释放,就得给第二个加锁
public class Test {
public static void main(String[] args) {
Object locker1 = new Object();
Object locker2 = new Object();
Thread t1 = new Thread(() -> {
System.out.println("t1尝试获取locker1");
synchronized (locker1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t1尝试获取locker2");
synchronized (locker2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t1获取两把锁成功");
}
}
});
Thread t2 = new Thread(() -> {
System.out.println("t2尝试获取locker2");
synchronized (locker2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t2尝试获取locker1");
synchronized (locker1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t1获取两把锁成功");
}
}
});
t1.start();
t2.start();
}
}
例3:多个线程多把锁
一个经典的问题就是哲学家就餐问题,该问题是描述有五个哲学家共用一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五个碗和五只筷子,他们的生活方式是交替地进行思考和进餐。平时,一个哲学家进行思考,饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐。进餐完毕,放下筷子继续思考。
五个哲学,都先拿起自己左手边的筷子,导致,都吃不了饭,僵持不下,死锁
10.2、如何避免死锁
死锁产生的四个必要条件:
- 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用(锁的基本特性,无法打破)
- 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。(锁的基本特性,无法打破)
- 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。(取决代码实现,及需求场景)
- 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
破坏循环等待:
10.3、解决哲学家就餐问题
三个方案:
- 所有哲学家先拿起编号小的筷子,1号哲学家就会和2号哲学家竞争0号筷子,从这个入口,就可以解决问题了
- 仅当哲学家的左、右两只筷子均可用时,才允许他拿起筷子进餐。
- 规定奇数号哲学家先拿他左边的筷子,然后再去拿右边的筷子,而偶数号哲学家则相反。按此规定,将是 1、2 号哲学家竞争 1 号筷子;3、4 号哲学家竞争 3 号筷子。即五位哲学家都先竞争奇数号筷子,获得后,再去竞争偶数号筷子,最后总会有一位哲学家能获得两只筷子而进餐。
11、 Java多线程是如何实现数据共享的?
下期见!!!