JavaのMap接口,多线程,死锁
Map接口
1).Map接口介绍
Map 用于保存具有映射关系的数据,因此 Map 集合里保存着两组值,一组值用于保存 Map 里的 Key,另外一组用于保存 Map 里的 Value
Map 中的 key 和 value 都可以是任何引用类型的数据
Map 中的 Key 不允许重复,即同一个 Map 对象的任何两个 Key 通过 equals 方法比较中返回 false
Key 和 Value 之间存在单向一对一关系,即通过指定的 Key 总能找到唯一的,确定的 Value。
2).HashMap集合
HashMap是 Map 接口使用频率最高的实现类。
HashMap 是基于哈希表的 Map 接口的非同步实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。与HashSet一样,此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 方法返回 true,hashCode 值也相等。
HashMap 判断两个 value相等的标准是:两个 value 通过 equals() 方法返回 true。
3).遍历HashMap
①Map集合遍历键找值方式:即通过元素中的键,获取键所对应的值
操作步骤:
1.获取Map集合中所有的键,由于键是唯一的,所以返回一个Set集合存储所有的键。
2.遍历键的Set集合,得到每一个键
3.根据键,获取键所对应的值
public class Main {
public static void main(String[] args) {
Map<String,String> map=new HashMap<>();
map.put("007","hello");
map.put("002","hello2");
map.put("003","hello3");
map.put("004","hello4");
map.put("005","hello5");
Set<String> keys = map.keySet();
Iterator<String> iterator = keys.iterator();
while (iterator.hasNext()){
String key=iterator.next();
System.out.println(key+":"+map.get(key));
}
}
}
②Map集合遍历键值对方式:即通过集合中每个键值对(Entry)对象,获取键值对对象中的键与值
操作步骤:
1.获取Map集合中,所有的键值对(Entry)对象,以Set集合形式返回。
2.遍历包含键值对(Entry)对象的Set集合,得到每一个键值对(Entry)对象
3.通过键值对(Entry)对象,获取Entry对象中的键与值。
public class Main {
public static void main(String[] args) {
Map<String,String> map=new HashMap<>();
map.put("007","hello");
map.put("002","hello2");
map.put("003","hello3");
map.put("004","hello4");
map.put("005","hello5");
Set<Map.Entry<String, String>> entries = map.entrySet();
Iterator<Map.Entry<String, String>> iterator = entries.iterator();
while(iterator.hasNext()){
Map.Entry<String, String> entry=iterator.next();
System.out.println(entry.getKey()+" :" +entry.getValue());
}
}
}
3).TreeMap集合
- TreeMap是用键来进行升序顺序来排序的。通过Comparable 或 Comparator来排序。
package pm;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
public class Test1 {
public static void main(String[] args) {
//输入一串字符串,然后输出每个字符出现的次数,用TreeMap
String s="aklsdhajhfakjhgdasdgahg中华787人民共45678和国";
Map<Character,Integer> map=new TreeMap<>();
char[] chars = s.toCharArray();
for(int i=0;i<chars.length;i++){
if(map.get(chars[i])==null){
map.put(chars[i],1);
}else{
map.put(chars[i],map.get(chars[i])+1);
}
}
Set<Map.Entry<Character, Integer>> entries = map.entrySet();
Iterator<Map.Entry<Character, Integer>> iterator = entries.iterator();
while (iterator.hasNext()){
Map.Entry<Character,Integer> entry=iterator.next();
System.out.println(entry.getKey()+":"+entry.getValue());
}
}
}
4).Properties集合
- Properties 类是 Hashtable 的子类,该对象用于处理属性文件由于属性文件里的 key、value 都是字符串类型,所以 properties 里的 Key 和 Value 都是字符串类型的
斗地主洗牌发牌
public class Demo4 {
public static void main(String[] args) {
//斗地主发牌
//先把牌弄出来,2个集合,一个集合装花色,一个集合装数字
List<String> type = new ArrayList<>();
type.add("♣");
type.add("♠");
type.add("♦");
type.add("♥");
List<String> dot = new ArrayList<>();
//添加数字牌
for (int i = 3; i <= 10; i++) {
dot.add("" + i);
}
//添加非数字牌
dot.add("J");
dot.add("Q");
dot.add("K");
dot.add("A");
dot.add("2");
Map<Integer,String> poke = new HashMap<>();
int index = 0;
for (String s:dot) {
for (String sc :type) {
poke.put(index,sc+s);
index++;
}
}
poke.put(52,"大王");
poke.put(53,"小王");
Set<Map.Entry<Integer, String>> entries = poke.entrySet();
Iterator<Map.Entry<Integer,String>> iterator = entries.iterator();
while (iterator.hasNext()){
Map.Entry<Integer, String> entry = iterator.next();
System.out.print(entry.getKey()+":" + entry.getValue()+" ");
}
System.out.println();
//分配编号
List<Integer> number = new ArrayList<>();
for (int i = 0; i < 54; i++) {
number.add(i);
}
//打乱牌号
Collections.shuffle(number);
// System.out.println(number.size());
//新建三个人,一个底牌来接收牌号
List<Integer> p1 = new ArrayList<>();
List<Integer> p2 = new ArrayList<>();
List<Integer> p3 = new ArrayList<>();
List<Integer> boom = new ArrayList<>();
//分配牌号
for (int i = 0; i < 51; i++) {
if(i%3==1){
p1.add(number.get(i));
}
if(i%3==2){
p2.add(number.get(i));
}if(i%3==0){
p3.add(number.get(i));
}
}
for(int i = 51;i<=53;i++){
boom.add(number.get(i));
}
//按照编号排序
Collections.sort(p1);
Collections.sort(p2);
Collections.sort(p3);
Collections.sort(boom);
//新建四个集合来装牌
List<String> p11 = new ArrayList<>();
List<String> p21 = new ArrayList<>();
List<String> p31 = new ArrayList<>();
List<String> pboom = new ArrayList<>();
for (Integer i :
p1) {
p11.add(poke.get(i));
}
for (Integer i :
p2) {
p21.add(poke.get(i));
}
for (Integer i :
p3) {
p31.add(poke.get(i));
}
for (Integer i :
boom) {
pboom.add(poke.get(i));
}
System.out.println(p11);
System.out.println(p21);
System.out.println(p31);
System.out.println(pboom);
}
}
输入字符串 统计各个字符的个数
public class Demo3 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("请输入字符串:");
String str = sc.next();
char[] strs = str.toCharArray();
Map<Character,Integer> map = new HashMap<>();
for (Character c:strs) {
if(map.get(c)==null){
map.put(c,1);
}else{
map.put(c,map.get(c)+1);
}
}
Set<Map.Entry<Character, Integer>> entries = map.entrySet();
Iterator<Map.Entry<Character, Integer>> iterator = entries.iterator();
while (iterator.hasNext()){
Map.Entry<Character, Integer> entry = iterator.next();
System.out.println(entry.getKey()+ ":" + entry.getValue());
}
}
}
关于集合的总结:
1)Set集合和List集合的区别?
Set: 不允许元素重复, 集合元素唯一(元素可以为null), 不能保证迭代顺序恒久不变, 无序(存储和取出不一致).
List: 允许元素重复, 并且元素有序(存储和取出一致).
2).Set 集合存储元素时可以保证元素的唯一性, 原因什么?
HashSet 集合的add()方法底层依赖于双列集合HashMap, 它依赖于两个方法 equals()和hashCode(); 先比较元素hashCoede值, 再比较equals().
3).HashSet元素如何添加?
当向HashSet集合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,判断已经存储在集合中的对象的hashCode值是否与添加的对象的hashCode值一致:若不一致:直接添加进去;若一致,再进行equals方法比较,equals方法如果返回true,表明对象已经添加进去了,就不会再添加新的对象了,否则添加进去;如果我们重写了equals方法,也要重写hashCode方法,反之亦然;。
HashSet集合判断两个元素相等的标准是两个对象通过equals方法比较相等,并且两个对象的hashCode方法返回值也相等。
4).ArrayList和LinkedList有什么区别?
ArrayList和LinkedList在性能上各有优缺点,都有各自所适用的地方,总的说来可以描述如下:
1.对ArrayList和LinkedList而言,在列表末尾增加一个元素所花的开销都是固定的。对ArrayList而言,主要是在内部数组中增加一项,指向所添加的元素,偶尔可能会导致对数组重新进行分配;而对LinkedList而言,这个开销是统一的,分配一个内部Entry对象。
2.在ArrayList的中间插入或删除一个元素意味着这个列表中剩余的元素都会被移动;而在LinkedList的中间插入或删除一个元素的开销是固定的。
3.LinkedList不支持高效的随机元素访问。
4.ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间
可以这样说:当操作是在一列数据的后面添加数据而不是在前面或中间,并且需要随机地访问其中的元素时,使用ArrayList会提供比较好的性能;当你的操作是在一列数据的前面或中间添加或删除数据,并且按照顺序访问其中的元素时,就应该使用LinkedList了。
5).HashMap和HashTable有什么区别?
HashMap和Hashtable都实现了Map接口,但决定用哪一个之前先要弄清楚它们之间的分别。主要的区别有:线程安全性,同步(synchronization),以及速度。
- HashMap几乎可以等价于Hashtable,除了HashMap是非synchronized的,并可以接受null(HashMap可以接受为null的键值(key)和值(value),而Hashtable则不行)。
- HashMap是非synchronized,而Hashtable是synchronized,这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。
- 另一个区别是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别。
- 由于Hashtable是线程安全的也是synchronized,所以在单线程环境下它比HashMap要慢。如果你不需要同步,只需要单一线程,那么使用HashMap性能要好过Hashtable。
- HashMap不能保证随着时间的推移Map中的元素次序是不变的。
多线程
1.线程概述
1. 进程
程序(program)是对数据描述与操作的代码的集合,是应用程序执行的脚本。
进程(process)是程序的一次执行过程,是系统运行程序的基本单位。程序是静态的,进程是动态的。系统运行 一个程序即是一个进程从创建、运行到消亡的过程。
多任务(multi task)在一个系统中可以同时运行多个程序,即有多个独立运行的任务,每个任务对应一个进程。
- 线程
线程(thread):比进程更小的运行单位,是程序中单个顺序的流控制。一个进程中可以包含多个线程。
简单来讲,线程是一个独立的执行流,是进程内部的一个独立执行单元,相当于一个子程序。
一个进程中的所有线程都在该进程的虚拟地址空间中,使用该进程的全局变量和系统资源。
操作系统给每个线程分配不同的CPU时间片,在某一时刻,CPU只执行一个时间片内的线程,多个时间片中的相应线程在CPU内轮流执行。
- 多线程应用场景
VNC同时共享屏幕给多个电脑
迅雷开启多条线程一起下载
QQ同时和多个人一起视频
服务器同时处理多个客户端请求
- 并行和并发
并行就是两个任务同时运行,就是甲任务进行的同时,乙任务也在进行。(需要多核CPU)
并发是指两个任务都请求运行,而处理器只能按受一个任务,就把这两个任务安排轮流进行,由于间时间隔较短,使人感觉两个任务都在运行
- Java程序运行原理
Java命令会启动java虚拟机(JVM),等于启动了一个应用程序,也就是启动了一个进程。
该进程会自动启动一个 “主线程” ,然后主线程去调用某个类的 main 方法
一个应用程序有且只有一个主线程,程序员不能New主线程,可以New子线程。
- 思考:JVM启动的是多线程吗?
- JVM启动至少启动了垃圾回收线程和主线程,所以是多线程的
2. 线程的创建
每个Java程序启动后,虚拟机将自动创建一个主线程,可以通过以下两种(其实有三种)方式自定义线程类:
方式一:继承Thread类
实现步骤:
1.定义类继承Thread类
2.重写run方法
3.把新线程要做的事写在run方法中
4.创建线程对象
5.开启新线程, 内部会自动执行run方法
class MyThread extends Thread{
@Override
public void run() {
System.out.println("子线程任务");
}
}
public class Demo01 {
public static void main(String[] args) {
/*主线程,程序员不能创建,程序员只能创建子线程*/
//1.创建子线程对象
MyThread t1 = new MyThread();
//2.启动子线程
t1.start();
}
}
方式二:实现Runnable接口
实现步骤:
1.定义类实现Runnable接口
2.实现run方法
3.把新线程要做的事写在run方法中
4.创建自定义的Runnable的子类对象,创建Thread对象传入Runnable
5.调用start()开启新线程, 内部会自动调用Runnable的run()方法
class MyThread implements Runnable{
@Override
public void run() {
System.out.println("子线程任务");
}
}
public class Demo01 {
public static void main(String[] args) {
/* 线程实现的方式 (2) - 定义类实现Runnable接口*/
//1.创建runable对象
MyThread task = new MyThread ();
//2.创建Thread对象
Thread t1 = new Thread(task);
//3.启动线程
t1.start();
}
}
两种实现方式的对比分析
继承Thread : 由于子类重写了Thread类的run(), 当调用start()时直接找子类的run()方法
实现Runnable : 构造函数中传入了Runnable的引用, 有个成员变量记住了它, 调用run()方法时内部判断成员变量Runnable的引用是否为空。
继承Thread
好处是:可以直接使用Thread类中的方法,代码简单
弊端是:如果已经有了父类,就不能用这种方法
实现Runnable接口
好处是:即使自己定义的线程类有了父类也没关系,因为有了父类也可以实现接口,代码更灵活
弊端是:不能直接使用Thread中的方法,需要先获取到线程对象后,才能得到Thread的方法,代码复杂
3.匿名内部类实现线程的两种方式
public static void main(String[] args) {
new Thread(){
public void run() {
System.out.println("任务1...." + Thread.currentThread());
};
}.start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("任务2...." + Thread.currentThread());
}
}).start();
}
4. 线程的常用方法
1. 获取当前线程的对象
* currentThread()方法用于获取当前线程对象
* 在不同的方法中,获取的线程对象名称是有可能不一样的
* 在main中获取的是主线程对象
* 在子线程的run方法中获取的是子线程对象
public class Demo01 {
public static void main(String[] args) {
//获取当前线程的对象(掌握)
Thread mainThread = Thread.currentThread();
mainThread.setName("主线程");
//打印主线程对象
System.out.println(mainThread);
//打印主线程对象类名
System.out.println(mainThread.getClass());
System.out.println("================");
//开启子线程
MyThread mt = new MyThread();
mt.start();
}
}
class MyThread extends Thread{
@Override
public void run() {
System.out.println("任务...");
Thread subThread = Thread.currentThread();
//打印子线程对象
System.out.println(subThread);
//打印子线程对象类名
System.out.println(subThread.getClass().getName());
}
}
2. 获取线程名字和设置名字
- 通过Thread的getName()方法获取线程对象的名字
- 通过setName(String)方法可以设置线程对象的名字
- 通过构造函数可以传入String类型的名字
- 每个线程系统都会默认分配个名字,主线程:main,子线程thread-0 ….
public class Demo01 {
public static void main(String[] args) {
/* 获取线程名字和设置名字(掌握)*/
//1.获取主线程对象
Thread mainThread = Thread.currentThread();
System.out.println(Thread.currentThread());
System.out.println(mainThread);
System.out.println("名称:" + mainThread.getName());
//2.设置线程的名称
mainThread.setName("主线程");
System.out.println(mainThread);
//3.设置子线程的名称
MyThread myThread = new MyThread("子线程1");
myThread.start();
}
}
class MyThread extends Thread{
public MyThread(String name) {
super(name);
}
@Override
public void run() {
System.out.println("银行代发工资任务..." + Thread.currentThread());
}
}
3. 线程休眠
- Thread.sleep(毫秒), 控制当前线程休眠若干毫秒
- 1秒= 1000毫秒
- 1秒 = 1000毫秒* 1000微妙 * 1000纳秒(1000000000 )
public static void test1() {
for(int i=0;i<10;i++){
System.out.println(i);
//休眠【暂停】
try {
Thread.sleep(1000);//主线程休眠
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("AAAAAAAAAAAAAAAAAA");
}
守护线程
setDaemon(), 设置一个线程为守护线程, 该线程不会单独执行, 当其他非守护线程都执行结束后, 自动退出.
特点:男守护女,女的死,男的也不想活了
加入线程
- join(), 当前线程暂停, 等待指定的线程执行结束后, 当前线程再继续
- join(int), 可以等待指定的毫秒之后继续
线程让出
- yield() 让出cpu
线程优先级
setPriority()设置线程的优先级
默认优先级是5,最小优先级1,最高优先级10
可以设置2,3,4
Thread里面有静态常量
5. 线程的生命周期及状态转换
线程的生命周期
指线程从创建到启动,直至运行结束
可以通过调用 Thread 类的相关方法影响线程的运行状态
线程的运行状态
新建(New)
可执行(Runnable)
运行(Running)
阻塞(Blocking)
死亡(Dead)
新建状态(New)当创建了一个Thread对象时,该对象就处于“新建状态”,没有启动,因此无法运行
可执行状态(Runnable)
其他线程调用了处于新建状态线程的start方法,该线程对象将转换到“可执行状态”
线程拥有获得CPU控制权的机会,处在等待调度阶段。
运行状态(Running)
处在“可执行状态”的线程对象一旦获得了 CPU 控制权,就会转换到“执行状态”
在“执行状态”下,线程状态占用 CPU 时间片段,执行run 方法中的代码
处在“执行状态”下的线程可以调用 yield 方法,该方法用于主动出让 CPU 控制权。线程对象出让控制权后回到“可执行状态”,重新等待调度。
阻塞状态(Blocking)
线程在“执行状态”下由于受某种条件的影响会被迫出让CPU控制权,进入“阻塞状态”。
进入阻塞状态的三种情况
调用sleep方法
调用join方法
执行I/O操作
**死亡状态(Dead)
处于“执行状态”的线程一旦从run方法返回(无论是正常退出还是抛出异常),就会进入“死亡状态”。
已经“死亡”的线程不能重新运行,否则会抛出IllegalThreadStateException
可以使用 Thread 类的 isAlive 方法判断线程是否活着
6.线程与同步
- 什么是同步
是多个线程同时访问同一资源时,需等待某个线程对资源访问结束,下个线程才能进行访问,浪费时间,效率低,但是可以保证数据安全.
同步就是加锁,不让其它人访问,synchronized指的就是同步的意思.
- 什么情况下需要同步
- 当多线程并发, 我们希望某一段代码执行的过程中CPU不要切换到其他线程工作. 这时就需要同步,否则会有线程安全问题.
- 同步代码块
使用synchronized关键字加上一个锁对象来定义一段代码, 这就叫同步代码块
多个同步代码块如果使用相同的锁对象, 那么他们就是同步的
使用同步锁时,应该尽是让锁的范围小点,才能提高性能
- 同步方法
使用synchronized关键字修饰一个方法, 该方法中所有的代码都是同步的
非静态同步方法的锁是:this
静态同步方法的锁是:字节码对象(xx.class)
案例:卖火车票
需求,有A、B、C、D 4个窗口同时间买票,只有100张票可以买
public class Ticket implements Runnable {
private int count = 100;
@Override
public void run() {
while (true) {
if (count <= 0) {
System.out.println("不好意思票卖完了");
break;
}
sell();
}
}
private synchronized void sell() {
if(count>0){
System.out.println(Thread.currentThread().getName() + "恭喜你买到票,票号:" + count);
count--;
}
}
}
锁的总结:
同步代码块使用的锁可以是任意对象的。因为synchronized中的对象可以我们自己指定。
同步函数使用的锁是固定的this。当线程任务只需要一个同步时完全可以使用同步函数。
同步代码块使用的锁可以是任意对象。当线程任务中需要多个同步时,必须通过锁来区分,这时必须使用同步代码块。同步代码块较为常用。
静态方法是随着类的加载而加载,静态同步函数使用的锁是字节码class文件对象。
回顾线程安全的类:
Vector,StringBuffer,Hashtable
Vector是线程安全的,ArrayList是线程不安全的
StringBuffer是线程安全的,StringBuilder是线程不安全的
Hashtable是线程安全的,HashMap是线程不安全的
7.单例模式
单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的一个类只有一个实例。即一个类只有一个对象实例。
实现步骤:
第一步:将类的构造函数申明为私有;
第二步:在类中初始化一个类;
第三步:对外提供访问该类的公开方法,并返回第二步中实例化的类;
- 饿汉式
class Single
{
private static final Single s = new Single();
private Single(){}
public static Single getInstance()
{
return s;
}
}
- 懒汉式
class Single{
private static Single s = null;
private Single(){}
public static Single getInstance() {
if(s==null) {
s = new Single();
}
return s;
}
}
- 解决单例懒汉式的并发问题
class Single
{
private static Single s = null;
private Single(){}
/*
并发访问会有安全隐患,所以加入同步机制解决安全问题。
但是,同步的出现降低了效率。
可以通过双重判断的方式,解决效率问题,减少判断锁的次数。
*/
public static Single getInstance()
{
if(s==null)
{
synchronized(Single.class)
//注意这里synchronized关键字之后的静态同步函数使用的锁是字节码class文件对象。
{
if(s==null)
s = new Single();
}
}
return s;
}
}
死锁
1).死锁的概念
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
2).死锁的理解
在一条河上有一座桥,桥面较窄,只能容纳一辆汽车通过,无法让两辆汽车并行。如果有两辆汽车A和B分别由桥的两端驶上该桥,则对于A车来说,它走过桥面左面的一段路(即占有了桥的一部分资源),要想过桥还须等待B车让出右边的桥面,此时A车不能前进;对于B车来说,它走过桥面右边的一段路(即占有了桥的一部分资源),要想过桥还须等待A车让出左边的桥面,此时B车也不能前进。两边的车都不倒车,结果造成互相等待对方让出桥面,但是谁也不让路,就会无休止地等下去。这种现象就是死锁。如果把汽车比做进程,桥面作为资源,那麽上述问题就描述为:进程A占有资源R1,等待进程B占有的资源Rr;进程B占有资源Rr,等待进程A占有的资源R1。而且资源R1和Rr只允许一个进程占用,即:不允许两个进程同时占用。结果,两个进程都不能继续执行,若不采取其它措施,这种循环等待状况会无限期持续下去,就发生了进程死锁。
3).产生死锁的必要条件
〈1〉互斥条件。即某个资源在一段时间内只能由一个进程占有,不能同时被两个或两个以上的进程占有。这种独占资源如CD-ROM驱动器,打印机等等,必须在占有该资源的进程主动释放它之后,其它进程才能占有该资源。这是由资源本身的属性所决定的。如独木桥就是一种独占资源,两方的人不能同时过桥。
〈2〉不可抢占条件。进程所获得的资源在未使用完毕之前,资源申请者不能强行地从资源占有者手中夺取资源,而只能由该资源的占有者进程自行释放。如过独木桥的人不能强迫对方后退,也不能非法地将对方推下桥,必须是桥上的人自己过桥后空出桥面(即主动释放占有资源),对方的人才能过桥。
〈3〉占有且申请条件。进程至少已经占有一个资源,但又申请新的资源;由于该资源已被另外进程占有,此时该进程阻塞;但是,它在等待新资源之时,仍继续占用已占有的资源。还以过独木桥为例,甲乙两人在桥上相遇。甲走过一段桥面(即占有了一些资源),还需要走其余的桥面(申请新的资源),但那部分桥面被乙占有(乙走过一段桥面)。甲过不去,前进不能,又不后退;乙也处于同样的状况。
〈4〉循环等待条件。存在一个进程等待序列{P1,P2,…,Pn},其中P1等待P2所占有的某一资源,P2等待P3所占有的某一源,……,而Pn等待P1所占有的的某一资源,形成一个进程循环等待环。就像前面的过独木桥问题,甲等待乙占有的桥面,而乙又等待甲占有的桥面,从而彼此循环等待。
4).死锁演示
public class Test {
public static String lockA = "A";
public static String lockB = "B";
//死锁
//当第一个线程拿到lockB,第二个线程拿到lockA,两个线程都不能继续执行 导致死锁
public static void main(String[] args) {
new Thread() {
@Override
public void run() {
while (true) {
synchronized (lockA) {
System.out.println("A-------A-------A");
synchronized (lockB) {
System.out.println("B--------A------------B");
}
}
}
}
}.start();
new Thread() {
@Override
public void run() {
while (true) {
synchronized (lockB) {
System.out.println("B---------B-----------B");
synchronized (lockA) {
System.out.println("A--------B------A");
}
}
}
}
}.start();
}
}