-
javaSE高级篇1 — 异常与多线程基础
1、异常的体系结构
- 注:Throwable是一个类,不是一个接口,这个类里面是描述的一些Error和Exception的共性,如图所示:
-
异常 / 错误是什么意思?
-
定义:指的是程序运行过程中,可能会发生一些不被期望的效果,它会阻止我们的程序按照指令去执行
- 而这种不被预期出现的结果,肯定需要抛出来告诉我们
- 而在java中,有一个定义好的规则——就是前面所提到的:Throwable( 意思是:可抛出的 )
-
定义:指的是程序运行过程中,可能会发生一些不被期望的效果,它会阻止我们的程序按照指令去执行
-
Error又是什么意思?
- 错误:指的是物理上的问题,如:JVM虚拟机本身的问题
-
Exception又是什么意思?
-
异常:通常是一些人为的错误
-
这种情况一般都是漏网之鱼的一些程序猿搞出来的,也就是那些应该卷铺盖走人的人搞出来的代码性问题,因此编写程序的时候:切记别出现这种情况( 我说的不是一开始写代码写出异常的人啊,写编程一次性就成功的,我这么久还没见过。我是说的:项目上线的时候,出现了异常 )
- 因此:在编写程序的时候,一定要考虑到会出现的异常问题,可以采用在编写程序的时候测试一下看会不会出现问题
-
这种情况一般都是漏网之鱼的一些程序猿搞出来的,也就是那些应该卷铺盖走人的人搞出来的代码性问题,因此编写程序的时候:切记别出现这种情况( 我说的不是一开始写代码写出异常的人啊,写编程一次性就成功的,我这么久还没见过。我是说的:项目上线的时候,出现了异常 )
-
异常:通常是一些人为的错误
-
Exception的分支体系
-
1、运行时异常( 又叫非检查型异常 )
- Error和Exception都是属于运行时异常,这种情况在编译时是不会报错的,也不是必须添加处理手段(自己想要处理也可以),但是需要知道:出现了这种问题怎么解决
-
1、运行时异常( 又叫非检查型异常 )
-
-
最常见的一个异常,也是出现之后必须知道的——因为:别人看到这个情况就不会帮忙解决,默认自己会解决,所以不会帮忙
-
空指针异常——也叫空引用异常
-
nullPointerException 举个例子:
-
package cn.xieGongZi.testException; public class Demo { public static void main(String[] args) { Person p = null; System.out.println( p.name ); // 在这里调用Person类里面的name属性,最后就会产生一个nullPointerException } } class Person{ String name; Integer age; }
分析一下:产生这个nullPointerException的原因
-
因为Person这个对象都没有创建出来,但是我用这个对象去调了Person类里面的属性(当然这时调方法也会产生这个异常)
-
为什么可以调到Person类中的属性 / 方法的原因:
- 因为我创建了这个对象,也赋值了,但是没有创建成功,所以可以调到Person里面的属性 / 方法,而这种在编译的时候不会识别出来,只有运行时才会发现
-
为什么可以调到Person类中的属性 / 方法的原因:
-
因为Person这个对象都没有创建出来,但是我用这个对象去调了Person类里面的属性(当然这时调方法也会产生这个异常)
-
-
nullPointerException 举个例子:
-
空指针异常——也叫空引用异常
-
最常见的一个异常,也是出现之后必须知道的——因为:别人看到这个情况就不会帮忙解决,默认自己会解决,所以不会帮忙
-
-
另外的一些异常——这些也需要了解它们产生的原因
-
1、InputMisMatchException————输入不匹配异常
-
int value = input.nextInt(); // 输入的时候:输入abc,造成输入类型不匹配
-
-
2、NumberFormatException————数字格式化异常
-
int value = Integer.parseInt("12.45"); // 要的是int类型的,但是我这里传的是:double类型的
-
-
3、ArrayIndexOutOfBound————数组索引越界
-
int[] array = {1,4,2,5,7}; System.out.println( array[7] ); // 在这里会导致ArrayIndexOutOfBound
-
-
4、NegativeArraySizeException————数组长度为负数
-
int[] array = {1,4,2,5,7}; System.out.println( array[-1] ); // 在这里会导致NegativeArraySizeException
-
-
5、ArithmeticException————数字异常
- 1 / 0 整型不允许除以0
-
还有一种:infinily 无穷
- 小数 / 0 会产生无穷
-
1、InputMisMatchException————输入不匹配异常
-
另外的一些异常——这些也需要了解它们产生的原因
-
-
-
6、classCastException————造型异常
-
Person p = new Teacher(); Student s = (Student)p; // 在这里会产生classCastException // 这里是假设Person和Teacher都继承了Animal // 但是它们两个之间没有关系
-
-
7、StringIndexOutOfBoundException——-——字符串索引越界
-
String s = “abcd”; System.out.println( s.CharAt( 5 ) ); // 这个方法是:返回给定index位置的字符 // 所以在这里会造成StringIndexOutOfBound异常
-
-
8、IndexOutOfBound————索引越界( list家族中会出现 )
-
ArrayList arrayList = new ArrayList(); arrayList.add(3); arrayList.add(6); arrayList.add(9); System.out.println( arrayList.get( 8 ) ); // 在这里会造成IndexOutOfBound异常
-
-
9、IllegalArgumentException————非法参数异常
-
ArrayList arrayList = new ArrayList( -1 ); // 这里会导致IllegalArgumentException异常
-
-
6、classCastException————造型异常
-
2、编译时异常(也叫检查性异常)
-
除了Error和Exception以外的异常就是编译时异常,这种异常在我们编写程序的时候,必须把这一部分会造成问题的代码利用处理手段进行处理
- 因为:这种问题如果不处理,会直接导致我们程序后面代码的运行(即:直接让后续的所有执行都停止了)
-
除了Error和Exception以外的异常就是编译时异常,这种异常在我们编写程序的时候,必须把这一部分会造成问题的代码利用处理手段进行处理
-
-
异常处理手段有哪些?
- 注意:添加异常处理手段处理异常,并不是说异常消失了,而是指的是:处理此异常后,后续的代码不会因为此异常而导致停止
-
-
第一种手段:通过try{ }.....catch(){ }.......finally{ }进行捕获
-
try { System.out.println( Integer.parseInt("12.345") ); }catch ( NumberFormatException e){ e.printStackTrace(); // 这是指打印栈信息————也可以自己定义提示语句 }finally { System.out.println("鬼迷日眼嘞,你看清楚是什么类型的值好伐?"); }
-
-
第一种手段:通过try{ }.....catch(){ }.......finally{ }进行捕获
-
-
- try不能单独出现,后面必须有catch 或 finally
- catch后面有一个 () 小括号,目的:捕获某一种类型的异常
-
catch可以有多个存在
- 捕获的异常之间没有任何的继承关系
-
catch捕获的异常,需要从小到大进行捕获
-
try { System.out.println( Integer.parseInt("12.345") ); }catch ( NumberFormatException e){ // 这是小异常类型 e.printStackTrace(); }catch (Exception e){ // 这是一个大类型异常 e.printStackTrace(); }finally { System.out.println("鬼迷日眼嘞,你看清楚是什么类型的值好伐?"); }
-
-
finally不是必须有的,但是一旦存在finally结构,则:finally里面的内容最后必须执行
-
System.out.println("程序开始运行"); try { System.out.println("try开始执行了"); int[] array = {1, 4, 6, 2, 7}; System.out.println(array[6]); // 这里会导致一个ArrayIndexOutOfBoundException异常 System.out.println("try执行完毕"); // 如果有异常,这里其实是不会执行的,这里只是为了后续的推导 } catch (NullPointerException e) { System.out.println("空指针异常了"); e.printStackTrace(); // 这个是打印栈信息 } catch (NumberFormatException e) { System.out.println("数字格式化异常了"); e.printStackTrace(); } catch (Exception e) { System.out.println("你整的问题是个什么异常,我也不晓得"); e.printStackTrace(); } finally { System.out.println("异常执行完毕"); }
-
从这个结果图可以看出try....catch...finally的执行原理
-
执行try
-
如果try中有异常,则:直接去找相应的catch。try中产生异常后,则:后面的内容就不会再执行了(即:实例中的:那句try执行完毕)
- 如果有相应的catch,则:执行里面的内容
- 如果没有相应的catch,则:抛出异常的原因( 这里会抛出是因为我打印了栈信息,如果没有的话是不会显示的 )
- 如果有finally结构,则:最后执行finally结构中的内容
-
如果try中有异常,则:直接去找相应的catch。try中产生异常后,则:后面的内容就不会再执行了(即:实例中的:那句try执行完毕)
-
执行try
-
-
-
-
-
插入一个小问题:
-
finally、final、finalize有什么区别?
- finally是处理异常手段try....catch....finally中的一个结构,只要有这个结构,则:是最后必须执行的程序内容
-
final是一个特征修饰符,它可以修饰类本身、属性、一般方法
- 修饰类:表明这个类是不可以被其他类继承的类————即:太监类
-
修饰属性:
- 若修饰的是基本数据类型,则:表明这个属性就是一个常量
- 若修饰的是引用数据类型,则:表明这个属性的地址不可以更改
- 修饰一般方法:表明这个方法在子类继承的时候不可以重写
- finalize是Object类里面的一个protected修饰的方法,它的作用就是垃圾回收,系统回收垃圾的时候,默认会调用它
-
finally、final、finalize有什么区别?
-
插入一个小问题:
-
-
-
对于try.....catch.....finally的一个小问题,含return返回值的情况,看如下代码:
-
public static void main(String[] args) { System.out.println( new Demo().tryProblem() ); } public String tryProblem() { System.out.println("程序开始运行"); try { String s = "abcde"; System.out.println(s.charAt(7)); return "这是没有异常时的返回值"; // 没有异常的话,那么返回值结果就是这个 } catch ( StringIndexOutOfBoundsException e ) { // 有这个异常就要执行这里面的内容。注:catch里面必须有相关的异常 // 就像这里:如果这里没有这个StringIndexOutOfBoundsException异常捕获 // 那么系统就会根据前面产生的问题,直接打印出相关异常问题,之后直接终止程序了 // 这样的话,那最后的那个return直接不会执行了 System.out.println("字符串越界异常了"); } finally { // try中最后执行的结构块 System.out.println("finally执行完毕了"); } return "最终的返回值"; // 产生了异常的话,那么最终的返回值就是这个 }
-
-
对于try.....catch.....finally的一个小问题,含return返回值的情况,看如下代码:
-
-
第二种处理手段:利用throws抛出某一类型的异常
- 只能在方法上抛出
- 在方法上可以抛出多个异常,通过 , 隔开,要求和catch一样,要么没关系,要么就是异常的大小问题(小在前,大在后)
- 可以在一般方法、构造方法上抛出(这种一般不会出现,因:构造方法作用是创建对象)
-
这种手段是将异常进行甩锅,也就是谁调用了方法,那么就把异常甩给谁处理(示例中是抛出了指定的异常类型)
- 但是:如果示例中没有抛出具体的异常,那么调用这个方法的对象,就必须对这个异常进行处理
-
第二种处理手段:利用throws抛出某一类型的异常
-
-
-
举个例子:
-
public static void main(String[] args) { new Demo().test(); } public void test() throws ArrayIndexOutOfBoundsException{ // 这里使用throws抛出了异常 int[] arr = {1, 4, 5, 2, 7}; System.out.println( arr[8] ); }
-
-
举个例子:
-
-
-
第三种处理手段:利用throw抛出某一种指定的异常。注:这里没有s————这种方式一般都不会用
- 这种处理手段是放在方法内部的
- 这种手段是抛出的某一种异常,而不是像第二种手段一样:是抛出某一类型的异常
- 这种处理手段需要套娃,也就是要么利用throws抛出,要么利用try....catch..处理掉
-
第三种处理手段:利用throw抛出某一种指定的异常。注:这里没有s————这种方式一般都不会用
-
-
-
示例(本例中用tyr...catch....进行套娃):
-
public class ByoneselfThrow { String name; String sex; int age; public void byoneself(){ Scanner input=new Scanner(System.in); System.out.println("请输入你的姓名:"); name=input.next(); System.out.println("请输入你的年龄:"); age=input.nextInt(); System.out.println("请输入你的性别:"); sex=input.next(); try{ if( "男".equals( sex ) || "女".equals( sex ) ){ System.out.println("我的名字叫"+name+",年龄为"+age+",性别为"+sex); }else{ throw new Exception("性别只能是男 / 女!"); } }catch (Exception e){ e.printStackTrace(); } } } class Test{ public static void main(String[] args) { ByoneselfThrow center=new ByoneselfThrow(); center.byoneself(); } }
-
-
示例(本例中用tyr...catch....进行套娃):
-
-
自定义异常
-
为什么需要自定义异常?
- 当我们在设计方法时,没有什么异常能够描述我们方法中的这种异常,则:就需要我们自定义异常
-
为什么需要自定义异常?
-
-
自定义异常怎么实现?
- 自己描述一个类
-
让这个类进行继承
- 1、若:继承的是RuntimeException,则:说明我们定义的这种异常属于运行时异常( 不是必须添加处理手段 )
- 2、若:继承的是Exception,则:说明我们定义的这种异常属于编译时异常————这种情况:我们设计的方法中必须添加处理手段
- 3、通过throw关键抛出自定义的异常
-
自定义异常怎么实现?
-
来个实例:
-
public class TestMyException { public static void main( String[] args ) { transMoney(10000); } public static void transMoney( int money ) { // 余额--- 从数据库查 int total = 1000; if( money > total ){ throw new MoneyTooLessException2("余额不足");// 手动抛出异常 }else{ System.out.println("转账成功!"); } } }
- 另外:自定义异常其实在实际开发中没什么卵用,因为直接放提示就行了,如:浏览器中登录窗口,输错了直接在当前窗口就弹出提示信息了,谁还看控制台这些啊
-
2、多线程 —— 面试贼喜欢问
- 1、多线程基础
-
先来了解几个名词
-
程序
- 可以理解为:是一组静态的代码
-
程序
-
-
进程
- 就是正在执行的程序( 即:把静态的代码运行起来 )
-
进程
-
-
线程:
- 就是正在执行程序中的一个小单元( 小单元指的是一个操作项,如:视频播放器中有一个暂停,这个暂停就是一个小单元 、甚至调大音量、拉动进度条......这些都是 )
-
线程:
-
线程的级别
- 主线程:也叫系统线程。指的是执行主方法的线程( 即:执行main方法的线程——这个方法的执行顺序是从上到下执行 ),线程执行main()方法,main就会进入到栈内存中,而这时JVM就会找操作系统开辟一条main()方法通往CPU的执行路线
-
- 用户线程:粗暴的理解就是一堆用户数据,这个线程是独立存在的,默认情况下,系统启动的就是用户线程
-
- 守护线程:也叫精灵线程、后台线程,指的是:程序运行的时候,后台提供的一种通用服务线程
-
线程的状态是那些?
-
看源码————Thread.state state就是状态的意思
-
public enum State { // 新建线程 NEW, // 运行 RUNNABLE, // 阻塞 BLOCKED, // 等待———— 就是死等,一直等 WAITING, // 超时等待 TIMED_WAITING, // 消亡 TERMINATED; }
-
-
看源码————Thread.state state就是状态的意思
-
怎么实现线程?————即:怎么创建线程
-
第一种方式:
- 自己描述一个类
- 让这个类继承Thread父类
- 重写run()方法
- new一个线程对象,调用start()方法,让线程进入就绪状态————然后CPU会根据资源碎片自动分配执行顺序( 指的是多个线程谁先谁后的事 )
-
第一种方式:
package cn.xieGongZi.test;
public class Demo {
public static void main(String[] args) {
// 创建线程对象
sum sum = new sum();
// 让线程进入就绪状态,CPU会给这个线程分配资源碎片(简单理解:就是CPU会分配这个线程什么时候执行)
sum.start();
}
}
// 这个线程是为了:计算1————100之和
class sum extends Thread{ // 继承Thread类
@Override // 重写run()方法
public void run() {
int sum = 0;
for (int i = 0; i <= 100; i++) {
sum += i;
}
System.out.println( "100以内之和为:" + sum );
}
}
-
-
第二种方式
- 自己描述一个类
- 让这个类去实现Runnable
- 重写run()方法
- 创建线程对象,利用这个对象去调用start(),让线程进入就绪状态
-
第二种方式
package cn.xieGongZi.test;
public class RunnableMethod {
public static void main(String[] args) {
/* 这里需要多绕一步
因为:通过implements的方式实现的线程 没有start()这些方法
所以:这里就相当于是 造型————即:把implements的类造型成Thread类型( 只是相当于啊,是为了好理解而已 )
*/
Thread thread = new Thread( new EvenSum() );
// 让线程进入就绪状态
thread.start();
}
}
// 这个线程是为了计算:100以内的偶数之和
class EvenSum implements Runnable{
@Override
public void run() {
int evevSum = 0;
for (int i = 0; i <= 100; i += 2) {
evevSum += i;
}
System.out.println( "100以内的偶数之和为:" + evevSum );
}
}
- Thread和Runnalbe能够调用的方法对比(前为Thread,后为Runnable)
-
综上的这两种实现线程的方式的区别是什么?
- 第一种方式:是利用一个类继承Thread类———这么一看也就知道:这种方式的线程(new出来的那个线程)和任务(做求和 或 偶数之和的那部分)是耦合的,同时会牺牲这个类的继承能力,也就是说:这个类不能再继承其他的类了,但是这种方式能够调取到的方法更多,也是严格来讲:真正的线程实现方式
- 第二种方式:是利用一个类实现Runnable接口,所以这种方式,即让这个类保留了继承能力,同时还可以多实现其他类———也就可以得出:这种方式的线程和任务是分离的,所以说:要是一个类它继承了另外的类,但是还想实现线程,那么就可以采用这第二种方式。但是:这种方式能调取到的方法不多,甚至Runnable底层接口中就一个run()方法,所以想要调取更多就需要多走一步( 利用Thread包裹 )
-
线程的API————有个别的方法先在这里知道,后续会慢慢的展示出来是怎么玩的
- 1. start()————让一个线程进入就绪状态————已经在前面玩过了
- 2. stop()————让一个线程停止(不推荐使用,所以也就不玩了,知道有这么个东西就行)
- 3. Thread.sleep( long ms [, int nanos] )————————让执行这条语句的线程休眠指定时间————其中:【】表示可有可无,重载嘛。前面用了一个Thread,表示这个方法是Thread中的,同时是一个静态方法,所以需要通过类名调用
- 4. setName() / getName()——————设置 / 获取线程的名字
- 5. Thread.currentThread()——————获得当前执行的线程对象
- 6. join()——————让一个线程加入到当前线程中来
- 7. Thread.yield()——————线程礼让
- 8. setDeamon()——————设置为守护线程
- 9. setPriority()————设置线程的优先级
来吧,展示!
-
package cn.xieGongZi.build.API; public class Demo { public static void main(String[] args) { // 创建线程————Foo这个类继承了Thread嘛,所以Foo类就是一个线程涩 Foo foo = new Foo(); // 设置线程的名字 foo.setName("紫邪情"); // 获取线程的名字 System.out.println( foo.getName() ); System.out.println(); // 获得当前执行的线程对象 System.out.println( Thread.currentThread() ); System.out.println(); // 让线程进入就绪状态 foo.start(); } } class Foo extends Thread { // 我也不知道这个线程是用来干嘛的^ _ ^,所以直接就叫Foo了 @Override public void run() { // 那就输出一句话:输出100次嘛 for (int i = 0; i < 100; i++) { System.out.println("邪公子好骚气" + i ); // 让线程休息一会儿 try { Thread.sleep(100); // 这是一个毫秒值,这个方法要捕获 / 抛出异常哦 } catch (InterruptedException e) { e.printStackTrace(); } } } }
-
理解线程同步和异步
- 同步:指的是一个线程结束之后,另一个线程才开始执行,即:一个线程的开始总是接着另一个线程的结束
-
异步:指的是线程交替执行,即:一个线程执行一会,另一个线程执行一会,这种看起来就是几个线程一起执行的,但是:宏观上来看:却是有先后顺序的
- 线程默认是异步的,想要线程同步需要自己控制
-
多线程的好处
- 可以充分利用CPU,尤其是在处理阻塞问题的时候( 即:一个线程卡住了,但是可以去执行另外的线程涩 ),使用多线程就可以提高效率了嘛
-
体验一下多线程、以及看一下它的安全问题
-
package cn.xieGongZi.threasExpound; // 卖票————引申线程安全问题 public class Demo { public static void main(String[] args) { // 多个线程 并发 访问同一个资源 SalesTicket st1 = new SalesTicket(); st1.setName("紫邪情"); st1.start(); SalesTicket st2 = new SalesTicket(); st2.setName("邪公子"); st2.start(); SalesTicket st3 = new SalesTicket(); st3.setName("小紫"); st3.start(); } } // 票的信息类 class Ticket{ // 假设票有20张 static int ticket = 20; public static void sales() { if ( ticket > 0 ){ ticket --; System.out.println( Thread.currentThread().getName() + "卖了1张票:还剩" + ticket ); } } public static int ticketCount() { return ticket; } } // 卖票的线程 class SalesTicket extends Thread{ @Override public void run() { while ( Ticket.ticketCount() > 0 ){ Ticket.sales(); } } }
-
从以上的程序:可以看出——多个线程并发去访问同一个资源的时候,出现了很大问题
- 1、一个人已经卖完了一张票,但是另一个收到的票总数没变
- 2、明明票已经卖完了,但是另外的人还可以拿到票
- 当然,扯犊子的,这不是老衲把代码写错了啊,这没错!要是写错了那还延伸个锤子了啊
所以这个问题怎么解决?
这就引申出一个东西:线程同步
-
线程同步
-
为什么需要线程同步?
-
就是为了保证数据一致,保证数据的安全性
-
就是为了保证数据一致,保证数据的安全性
-
为什么需要线程同步?
-
-
实现线程同步的方式
- 首先说一个词Synchronized —— 即:同步的意思 —— 在面向对象编程思想的特征修饰符中提到过这个修饰符,它的用途就在这里,在这里是线程同步,也可以叫线程安全锁
-
实现线程同步的方式
-
-
-
- 即:一个时间点只能有一个线程去访问资源,其他线程被锁在资源地之外,不能进行对资源的访问
-
-
-
-
-
Synchronized有两种写法
-
第一种:写在方法的结构上——这种是将这个方法进行锁定,即:这个方法在一个时间点只能有一个线程访问,写法如下:
-
public Synchronized void get(){ }
-
-
第一种:写在方法的结构上——这种是将这个方法进行锁定,即:这个方法在一个时间点只能有一个线程访问,写法如下:
-
Synchronized有两种写法
-
-
-
-
-
来个实例(如:刚刚的卖票,来做优化 —— 把票的信息类哪里进行锁定):
-
// 其他部分代码省略 // 票的信息类 class Ticket{ // 假设票有20张 static int ticket = 20; public Synchronized static void sales() { // 这里就利用Synchronized修饰符来对这个方法进行锁定了 if ( ticket > 0 ){ ticket --; System.out.println( Thread.currentThread().getName() + "卖了1张票:还剩" + ticket ); } } public static int ticketCount() { return ticket; } }
-
-
来个实例(如:刚刚的卖票,来做优化 —— 把票的信息类哪里进行锁定):
-
-
-
-
-
-
这种写法在java底层中什么地方用到了呢?
-
集合体系结构的时候,不是说了:vector、hashtable是线程安全的吗 —— 这些就用到了这种 将Synchronized放在方法结构上的写法
-
-
集合体系结构的时候,不是说了:vector、hashtable是线程安全的吗 —— 这些就用到了这种 将Synchronized放在方法结构上的写法
-
这种写法在java底层中什么地方用到了呢?
-
-
-
-
-
-
第二种:将Synchronized放在方法( 构造、块 )内部,这种更灵活 —— 这种也叫Synchronized块。
- 同时这种方式:是把“可能会让另外线程抢夺资源的地方进行锁定“ —— 即:让其他线程等着,这就把当前的这个线程做的这件事给锁定住了
- 另外:也把不重要的事情、不影响其他线程、或是不会和其他线程出现抢夺的事给排除出来,让其继续执行,不影响整个线程的效率
-
第二种:将Synchronized放在方法( 构造、块 )内部,这种更灵活 —— 这种也叫Synchronized块。
-
-
-
-
结构写法如下:
-
public void get(){ 代码1; Synchronized( 传一个对象,而且是必须传 ){ 代码2; } 代码3; }
- 1、可以是执行当前方法的那个线程对象
- 2、可以是某一个线程让另外线程来执行这个方法的对象( 有点绕:这里指的是 这句话中的另外线程 的那个对象 )
- 3、这个对象必须满足:不同线程进来时都是同一个对象,这样才保证互斥性,不然不同线程是不同对象,那不得都可以进入到Synchronized块中了,还怎么保证安全啊
-
-
-
-
-
-
来个示例:还是用前面的那个卖票
-
// 其他的代码省略 class Ticket{ // 假设票有20张 static int ticket = 20; public void sales() { synchronized ( this ){ // 这里传this 这是因为:要调用这个方法,需要当前类的一个对象涩, // 那么这个this不就是当前类的对象了吗,然后在线程中只传递当前类的一个对象就可以了 if ( ticket > 0 ){ ticket --; System.out.println( Thread.currentThread().getName() + "卖了1张票:还剩" + ticket ); } } } public static int ticketCount() { return ticket; } }
-
-
来个示例:还是用前面的那个卖票
-
-
-
-
-
-
-
-
另外:那这种写法又在java的什么地方用到了?
-
在这里就要扩充一个知识了:Collections工具类
- 这个类的作用:就是为了把线程不安全的集合 转化成 线程安全的集合
-
在这里就要扩充一个知识了:Collections工具类
-
另外:那这种写法又在java的什么地方用到了?
-
-
-
-
-
-
-
-
-
这个类里面有三个常用方法
-
static list< Object > = synchronizedList( list< Object > list )————这个方法是:将一个List接口下的子集合 转化为 一个线程安全的List集合,注意:支持泛型啊。
- 另外:这个方法是静态的,所以是通过类名调用
-
List<Integer> arrayList = new ArrayList<>(); Collections collections = (Collections) Collections.synchronizedList( arrayList ); // 注意:需要进行造型
-
static list< Object > = synchronizedList( list< Object > list )————这个方法是:将一个List接口下的子集合 转化为 一个线程安全的List集合,注意:支持泛型啊。
-
这个类里面有三个常用方法
-
-
-
-
-
-
-
-
-
-
- static Map< key , value > = synchronizedMap( Map< key ,value > m )————这个方法是:将一个Map接口下的子集合 转化为 一个线程安全的Map集合,也支持泛型————用法就不展示了,和上面一样
-
-
-
-
-
-
-
-
-
-
-
- static Set< Object > = synchronizedSet( Set< Object > s )————这个方法是:将一个Set接口下的子集合 转化为 一个线程安全的Set集合,同样支持泛型,用法也还是一样的
-
-
-
-
-
-
-
-
-
-
有了上面这个工具类,就可以看到java中应用synchronize块的写法 —— 即:Collections里面的这三个方法实现原理就是采用了Synchronized块
-
有了上面这个工具类,就可以看到java中应用synchronize块的写法 —— 即:Collections里面的这三个方法实现原理就是采用了Synchronized块
-
-
-
经过以上的Synchronized锁定之后,代码和效果如下:
-
package cn.xieGongZi.threasExpound; // 卖票————引申线程安全问题 public class Demo { public static void main(String[] args) { SalesTicket st1 = new SalesTicket(); st1.setName("紫邪情"); st1.start(); SalesTicket st2 = new SalesTicket(); st2.setName("邪公子"); st2.start(); } } // 票的信息类 class Ticket{ // 假设票有20张 static int ticket = 20; public static synchronized void sales() { // 把这件事情进行锁定————即:保证一个时间点只能有一个线程进入到这个方法里 if ( ticket > 0 ){ ticket --; System.out.println( Thread.currentThread().getName() + "卖了1张票:还剩" + ticket ); } } public static int ticketCount() { return ticket; } } // 卖票的线程 class SalesTicket extends Thread{ @Override public void run() { while ( Ticket.ticketCount() > 0 ){ Ticket.sales(); } } }
这样问题是解决了,但是好像不符合实际吧!
- 因为这是第一个线程访问的时候,另外的一个线程就被锁在外面了,就像上面例子中:先是紫邪情这个线程访问了资源,然后再是邪公子这一个线程去访问,这就成单线程了,根本不是什么多线程嘛
- 所以这里引申一个模型:生产消费者模型,来用这个模型模拟一下多线程
-
来玩一下多线程、然后推导出它的安全问题、以及分析出:怎么实现一个安全的多线程
-
先来了解一个模型 —— 生产消费者模型
- 一个仓库( 即:存放资源的一个资源地 )
- 生产者 —— 负责向仓库中放东西( 可以是一个,也可以是多个,但这里是为了模拟多线程嘛,所以是一个 )
- 消费者 —— 负责向仓库中取东西( 一样的可以是一个,也可以是多个,为了模拟多线程,所以是多个 )
- 别搞蒙了啊。一个生产多个消费,这样才好搞多线程效果嘛,多个生产多个消费也可以整,只是看效果时不太好看而已
-
先来了解一个模型 —— 生产消费者模型
-
-
package cn.xieGongZi.playProducerAndConsumer; // 仓库 public class WareHouse { // 搞一个容器来当做仓库————假设这个仓库的大小为10————同时保证这个仓库不可以自动扩容,不然模仿个锤子啊 // 这里就选用数组 private static Object[] wareHouse = new Object[10]; // 计数器————默认指向仓库的第一个位置( 看代码看逻辑 )————这不是一开始就知道需要定义这个东西 private static int index = 0; // 这个仓库需要给人提供一些方法涩 // 1、给生产者(线程)提供添加方法————但是需要保证多个线程只能有一个线程进行操作涩————所以:synchronize public synchronized void add( Object element ){ // 添加元素得知道仓库中原本是添加到哪里来了涩,不然无法知道添加到什么位置啊 // 因此:需要一个计数器————用来记录上一次添加到什么位置来了——————即:全局变量 // 好了,有了一个全局的计数器 // 那就需要判断一手儿计数器是不是指向的仓库最后一个位置,不然都已经满了,那就添加不了了 if ( index == wareHouse.length ){ // 这就说明:仓库满了,添加不了元素 // 那怎么办?————也就是让生产者不用生产了嘛 return; // 这个关键字在这里就相当于是break,但是不是终止程序 // 而是:让操作这个方法的线程进入等待状态,然后等没有资源了再继续进行操作 }else { // 为了看到效果————所以加一句 System.out.println( Thread.currentThread().getName() + "这个线程添加了:" + element); // 否则:就是仓库没有满,还可以放东西进去嘛 // 怎么添加?把传进来的元素 放到 计数器指向的那个位置嘛 // 另外也要保证添加完了本次操作之后,让计数器指向仓库的下一个位置涩,不然下一次添加不就还是添加到当前位置了吗 // 因此这里一步搞定————即先存入东西,又重新给计数器赋值:让其指向下一个位置 wareHouse[ index ++ ] = element; } } // 2、给消费者(线程)提供取东西的方法————同样的保证多个线程只有一个能够进行操作 public synchronized void get() { // 得先看一下仓库中有没有东西可以取涩 if ( index == 0 ){ // 这表明仓库没有东西————那就小眯一会儿 return; }else { // 否则:说明仓库中有东西可以取 // 那就把计数器指向位置的上一个元素拿走 // 同样为了看效果 // 另外:把东西取了 得让计数器指向上一个位置涩————因为:生产者中是添加了元素,然后把计数器指向后一个位置的嘛 // 因此:在这里需要把计数器指向还没取的那一个元素位置涩 System.out.println(Thread.currentThread().getName() + "这个线程拿走了:" + wareHouse[ --index ] ); } } }
-
package cn.xieGongZi.playProducerAndConsumer; // 生产者 public class Producer extends Thread{ // 得保证这个生产者是一个线程涩————继承Thread 或 实现Runnable接口 // 得去一个地方放东西涩 ———— 保证和消费者去的是同一个地方涩 // 即:让生产者 和 消费者传一个对象进来( 这样就保证它们到时用的是同一个对象了 ) // 所以这里采用:类关系中的 组合方式 ———— 即:一个类中 使用到了 另一个类的对象 private WareHouse wareHouse; public Producer(WareHouse wareHouse) { this.wareHouse = wareHouse; } // 重写run()方法 @Override public void run() { // 这便为 生产者需要干的事情 ———— 它添加了什么东西 for (int i = 1; i < 10; i++) { wareHouse.add( "冈本零点零一:" + i ); } } }
-
package cn.xieGongZi.playProducerAndConsumer; // 消费者 public class Consumer extends Thread{ // 同样的原理:保证对象唯一 private WareHouse wareHouse; public Consumer(WareHouse wareHouse) { this.wareHouse = wareHouse; } @Override public void run() { for (int i = 1; i < 10; i++) { wareHouse.get(); } } }
-
package cn.xieGongZi.playProducerAndConsumer; // 测试 public class Main { public static void main(String[] args) { // 不是要保证仓库对象唯一吗————创一个 WareHouse wareHouse = new WareHouse(); // 创一个生产者线程————需要一个仓库对象嘛 Producer producer = new Producer( wareHouse ); // 给这个线程起个名字涩 producer.setName("唐三"); // 让这个生产者线程进入就绪状态 producer.start(); // 创建一个消费者线程 Consumer consumer = new Consumer(wareHouse); consumer.setName("邪公子"); consumer.start(); } }
-
效果如下:
-
这好像也不是多线程嘛,这不就是一个生产,一个消费的一对一单线程吗,那就多来几个消费者嘛————即:测试方法中多整几个消费者对象涩
-
package cn.xieGongZi.playProducerAndConsumer; // 测试 public class Main { public static void main(String[] args) { // 不是要保证仓库对象唯一吗————创一个 WareHouse wareHouse = new WareHouse(); // 创一个生产者线程————需要一个仓库对象嘛 Producer producer = new Producer(wareHouse); // 给这个线程起个名字涩 producer.setName("唐三"); // 让这个生产者线程进入就绪状态 producer.start(); // 创建一个消费者线程 Consumer consumer = new Consumer(wareHouse); consumer.setName("邪公子"); consumer.start(); // 多来几个消费者线程 Consumer consumer2 = new Consumer(wareHouse); consumer2.setName("紫邪情"); consumer2.start(); Consumer consumer3 = new Consumer(wareHouse); consumer3.setName("小紫"); consumer3.start(); } }
-
效果如下(看起来好像成功了):
-
-
-
-
-
但是:
- 在这里,以及后面给消费者线程提供的方法那里,都是采用的return,即:是让当前线程等一会儿。这不符合实际啊。因为这种是进入了一个“假死”状态,什么意思?也就是当前线程进来之后,发现没有东西 / 东西已经满了,那当前这个线程就在仓库(资源地)中等着,等到有东西了 / 可以放东西了再干活儿,那这样的话,资源地之外的线程也就要跟着一起等了
-
正常的流程应该是:进到资源地之后,发现没有东西了 / 东西已经满了,那就要通知其他线程:放东西 / 取东西,不能让其他线程干等着,同时自己也在资源地中干等着,陷入“假死”状态
- 举个例子:就像现实中偷奸耍滑那种,让他去做一件事A,发现A那件事有人做了 / 要做的A那件事还没准备开始弄,结果呢?这个吊毛就啥都不说,然后偷偷摸摸地去偷懒了,而老板以为他去做A那件事了,A那件事那边的人,又以为他回到原来岗位去了,最后的结果却是:他跑去偷懒 —— 装死去了!!!
-
但是:
-
-
-
所以:认为return不好,需要做调整,应该让线程之间来回切换
-
这就需要用到前面说的几个方法了 —— 这些方法也叫“线程通信“:就是协调多个线程有序访问资源
- wait() —— 让调用当前方法的线程进入等待状态
-
这就需要用到前面说的几个方法了 —— 这些方法也叫“线程通信“:就是协调多个线程有序访问资源
-
-
notify() —— 唤醒单个等待线程( 这个是:只有一个等待线程被唤醒,不确定是哪一个,得看线程调度器自己分配 )
- 如:前面例子中多个消费者进去访问资源,如果第一个消费者进去的时候,发现资源地中没有资源了,那么这个线程就需要等待涩,相应的:其他的消费者线程也要等待,同时生产者线程也是等待着的( 因为都没资源了嘛,要是不是等待状态的话,那仓库中就有资源了 )
- 因此:当第一个进去的线程调用notify()方法的时候,线程调度器就会从这些处于等待状态的线程中随机唤醒一个线程,从而进行工作( 即:从等待线程 ——变成——> 同步线程 )—— 这种情况的话:一般都是唤醒生产者线程( 联系实际嘛,不扯远了,不然要解释一大堆 )
-
但是:这种情况也会导致一个问题
- 即:进到资源地中的那个线程( 就前面举的例子,假设进入的是第一个消费者线程 ),它虽然执行了notify()方法,随机唤醒了一个线程,可是:资源地中那把锁还是在它的手里,并没有释放( 即:它掌握的那把消费者线程的锁:用synchronize修饰了嘛 ) ,所以这就会出现一种情况了啊 —— 其他同类型的消费者线程进不来了,换句话说:只有当前进去的这第一个消费者线程才可以执行消费者可以干的事情,其他消费者线程想做事,得让它做完了,把锁释放了才可以
- 因此:最后结论就是 —— notify()方法也不好
-
notify() —— 唤醒单个等待线程( 这个是:只有一个等待线程被唤醒,不确定是哪一个,得看线程调度器自己分配 )
-
-
notifyAll() —— 唤醒所有的等待线程
- 这个方法就很好了,它唤醒了所有的等待线程( 例子中的:生产者和消费者 ),同时还让当前进入的线程把安全锁交出来,然后其他的线程就可以自由争夺这把锁了
-
notifyAll() —— 唤醒所有的等待线程
-
-
-
-
由上述的方法讲解之后,那么就来对刚刚的生产消费者模型做一下优化
-
// 1、给生产者(线程)提供添加方法————但是需要保证多个线程只能有一个线程进行操作涩,所以:synchronize public synchronized void add( Object element ){ // 添加元素得知道仓库中原本是添加到哪里来了涩,不然无法知道添加到什么位置啊 // 因此:需要一个计数器————用来记录上一次添加到什么位置来了——————全局变量 // 好了,有了一个全局的计数器 // 那就需要判断一手儿计数器是不是指向的仓库最后一个位置,不然都已经满了,那就添加不了了 if ( index == wareHouse.length ){ // 这就说明:仓库满了,添加不了元素 // 那怎么办?————也就是让生产者不用生产了嘛 // return; // 这个关键字在这里就相当于是break,但是不是终止程序 // 而是:让操作这个方法的线程进入等待状态,然后等没有资源了再继续进行操作 // 发现return这个逼玩意儿不好————来做优化 // 发现资源地中满了,那就让当前线程进入等待状态 try { this.wait(); // 注意啊:这里是this,所以别理解错了,虽然这里是当前这个类调用了wait()方法 // 但是,这不是指:让当前这个类的对象进入等待啊 // 而是调用这个方法的那个线程对象进入等待,别忘了联想synchronize(传对象)块中的那个对象// 对于给消费者提供的操作方法那里也是一样的优化方案 // 同时唤醒其他的线程————可以来做事了,另外:让当前进来的这个线程把安全锁交出来 this.notifyAll(); } catch (InterruptedException e) { e.printStackTrace(); } }else { // 为了看到效果————所以加一句 System.out.println( Thread.currentThread().getName() + "这个线程添加了:" + element); // 否则:就是仓库没有满,还可以放东西进去嘛 // 怎么添加?把传进来的元素 放到 计数器指向的那个位置嘛 // 另外也要保证添加完了本次操作之后,让计数器指向仓库的下一个位置涩,不然下一次添加不就还是添加到当前位置了吗 // 因此这里一步搞定————即先存入东西,又重新给计数器赋值:让其指向下一个位置 wareHouse[ index ++ ] = element; } }
-
-
由上述的方法讲解之后,那么就来对刚刚的生产消费者模型做一下优化
-
-
-
-
通过这个生产消费者模型之后,可以得出做一个完整且安全的多线程需通过以下方法实现
-
1、利用synchronize实现线程安全锁
- 有两种写法 —— 放方法结构上、放在方法里面
-
1、利用synchronize实现线程安全锁
-
通过这个生产消费者模型之后,可以得出做一个完整且安全的多线程需通过以下方法实现
-
-
-
2、使用下列方法控制线程状态的来回切换
- wait()
- notify()——唤醒线程。这个一般不会用
- notifyAll() —— 最常用的是这一个( 但是事情无绝对 ,还是要看实际情况)
-
2、使用下列方法控制线程状态的来回切换
-
-
-
多线程中会产生的异常
-
ArrayIndexOutOfBoundsException —— 数组索引越界( 或是集合索引越界 )—— 这个异常看是用了什么来存储,数组就抛数组的,集合就抛集合的
-
出现这个异常的情况:
- 多个线程同时访问一个资源, 这个资源不够了,但其中一个线程已经把剩下的资源拿走了,同时:在前一个线程拿走资源的那一瞬间,后一个资源进来判定出有资源可以拿,但是:这后一个资源刚进来,资源就被前一个线程拿走了,所以就产生了这么一个异常
-
出现这个异常的情况:
-
ArrayIndexOutOfBoundsException —— 数组索引越界( 或是集合索引越界 )—— 这个异常看是用了什么来存储,数组就抛数组的,集合就抛集合的
-
多线程中会产生的异常
-
-
-
ILLegedMonitorStateException ——— 线程不是其拥有者
-
即:一个线程去资源地进行相应操作时,资源地中的那个操作方法调用了wait()、notify()、notifyAll()中的一个
-
这表示:调用资源地中操作方法的那个线程不是wait()、notify()、notifyAll()方法的拥有者,换句话说:那个线程不是唯一可以调到这些方法的线程,其他的线程在这一瞬间也可以调用这些方法
-
解决办法:
-
把资源地中的对应操作方法 / 可能会出现资源抢夺的那部分代码进行安全锁定,即:使用synchronized
- 这就表明:调用资源地中对应操作方法的那个线程被锁在这个方法里了,其他线程进不来。而此时被锁在资源地中的那个线程不就成“唯一访问资源的线程”了吗( 即:多线程并发去访问同一资源时,保证只有一个线程在访问这个资源 ),这样那个线程也就是wait()、notify()、notifyAll()方法的拥有者了
-
把资源地中的对应操作方法 / 可能会出现资源抢夺的那部分代码进行安全锁定,即:使用synchronized
-
解决办法:
-
这表示:调用资源地中操作方法的那个线程不是wait()、notify()、notifyAll()方法的拥有者,换句话说:那个线程不是唯一可以调到这些方法的线程,其他的线程在这一瞬间也可以调用这些方法
-
即:一个线程去资源地进行相应操作时,资源地中的那个操作方法调用了wait()、notify()、notifyAll()中的一个
-
ILLegedMonitorStateException ——— 线程不是其拥有者
-
-
线程的生命周期(高频面试题)—— 要根据实际情况来,这里不扯远了,知道这是个什么东西,然后基本上就可以根据实际情况分析出来了
-
指的是:线程对象从创建 到 死亡 的一系列状态
-
指的是:线程对象从创建 到 死亡 的一系列状态
-
线程死锁
-
首先:来了解一个方法
- Thread.join() —— 指的是:把一个线程加入到另一个线程中来,然后两个线程同时执行( 这只是看起来是这样的啊,但是实质还是有先后顺序的 )——Thread是表明这个方法是Thread中的
-
首先:来了解一个方法
-
-
然后:来玩一下这个join()方法
-
package cn.xieGongZi.deadLock; // 第一个线程 public class OneThread extends Thread{ // 要让这个线程搞件事情涩 @Override public void run() { // 搞个啥子事情诶 ———— 来嘛,算100以内之和,即:1 + 2 + 3......+ 100 int sum = 0; for (int i = 0; i <= 100; i++) { sum += i; } System.out.println( Thread.currentThread().getName() + "龟儿线程算出了100以内之和为:" + sum + ",好聪明啊"); } }
-
package cn.xieGongZi.deadLock; // 第二个线程 public class TwoThread extends Thread{ // 这个线程干个啥子事情好嘞 ———— 那就算100以内的偶数之和嘛 @Override public void run() { int evenSum = 0; for (int i = 0; i <= 100; i += 2) { evenSum += i; } System.out.println( Thread.currentThread().getName() + "杂皮线程算出了100以内的偶数之和:" + evenSum +",居然可以算出来,牛逼啊"); // 龟儿嘞:为了好看效果,换个行嘛 System.out.println(); // 诶嘿 ~ 老衲还要搞点事情 ———— 在这第二个线程中把第一个线程加进来 try { OneThread oneThread = new OneThread(); oneThread.setName("小日子过得不错的"); oneThread.join(); // 让谁进来就用谁调用这个方法 oneThread.start(); } catch (InterruptedException e) { e.printStackTrace(); } } }
-
package cn.xieGongZi.deadLock; // 玩玩儿 public class Play { public static void main(String[] args) { // 让2线程跑起来 TwoThread twoThread = new TwoThread(); twoThread.setName("吴亦凡"); twoThread.start(); } }
-
效果如下:
-
-
然后:来玩一下这个join()方法
-
最后:真正开始搞事情 —— 由join()方法研究Synchronized锁引申出死锁问题
-
把上面的例子变一下
- 在2线程中不是加入了1线程吗。那万一在2线程让1线程执行的这个时间点,1线程被抢走了呢?—— 来玩儿一下
-
把上面的例子变一下
-
-
-
package cn.xieGongZi.deadLock; // 第一个线程 public class OneThread extends Thread{ // 要让这个线程稿件事情涩 @Override public void run() { synchronized (this){ // 给线程1加把锁 ———— 这样保证这个线程只能被一个线程来访问 System.out.println("我线程1需要15秒钟才可以执行完我自己的线程"); try { Thread.sleep(15000); // 先小睡15秒之后再做事情 // 线程1开始做事情 int sum = 0; for (int i = 0; i <= 100; i++) { sum += i; } System.out.println( Thread.currentThread().getName() + "龟儿线程算出了100以内之和为:" + sum + ",好聪明啊"); } catch (InterruptedException e) { e.printStackTrace(); } } } }
-
package cn.xieGongZi.deadLock; // 第二个线程 public class TwoThread extends Thread{ // 这个线程干个啥子事情好嘞————那就算100以内的偶数之和嘛 @Override public void run() { // 2线程本身需要做的事情 int evenSum = 0; for (int i = 0; i <= 100; i += 2) { evenSum += i; } System.out.println( Thread.currentThread().getName() + "杂皮2线程算出了100以内的偶数之和:" + evenSum +",居然可以算出来,牛逼啊"); // 至此 2线程自己本身需要做的事情做完了 // 但是我要搞点事情 ———— 把1线程加进来 System.out.println("我2线程只给你1线程两秒钟的执行时间,两秒之后,1线程你记得给我回来。你开始执行吧"); try { System.out.println(); // 为了好看效果 OneThread oneThread = new OneThread(); oneThread.join(2000); // 在这里2线程只给1线程2000毫秒的时间做事情————即:2秒的执行时间 oneThread.setName("2线程中的小日子过得不错的"); oneThread.start(); } catch (InterruptedException e) { e.printStackTrace(); } } }
-
package cn.xieGongZi.deadLock; // 玩玩儿 public class Play { public static void main(String[] args) { // 让2线程跑起来 TwoThread twoThread = new TwoThread(); twoThread.setName("吴亦凡"); twoThread.start(); // 拐了,这时候来了一个土匪 ThreeThread threeThread = new ThreeThread(); threeThread.start(); } }
-
package cn.xieGongZi.deadLock; // 完了,2线程让1线程执行2秒时间,但是这个时候来了个土匪————3线程 public class ThreeThread extends Thread{ // 这个线程要搞点事情 @Override public void run() { System.out.println("土匪线程3:来搞事情了"); System.out.println("贫僧把1线程借过来玩玩啊,2线程你别跟老衲哔哔"); synchronized (this){ // 也上把锁,这样就是直接锁定住1线程了 // 搞啥子事情诶?————把1线程从2线程手里抢过来 new OneThread(); try { // 然后睡一觉 Thread.sleep(15000); } catch (InterruptedException e) { e.printStackTrace(); } // 睡完之后就放别人走 System.out.println("老衲3线程睡完了。2线程,我把1线程还给你"); System.out.println("1线程,贫僧3线程可以让你走了"); System.out.println(); } } }
-
效果如下:
-
-
-
-
由上面的这个例子得出了一个结果:
- Synchronized锁一旦把对象锁定,不释放的话,其他的对象都需要等待,有可能产生这例子中的这种“死锁”问题
-
由上面的这个例子得出了一个结果:
-
-
死锁是个啥子样子嘞?
-
经典的哲学家就餐问题
-
三个哲学家坐在一起吃饭,但是只有3支筷子,即:一个哲学家1支筷子,然后拿筷子吃饭 —— 需要两支筷子才可以吃饭,然后就抢筷子( 彼此不谦让,一起抢筷子 )
- 从上图就可以看出:死锁就是 —— 多个线程彼此抢夺资源,从而彼此都陷入无法恢复的等待状态
-
三个哲学家坐在一起吃饭,但是只有3支筷子,即:一个哲学家1支筷子,然后拿筷子吃饭 —— 需要两支筷子才可以吃饭,然后就抢筷子( 彼此不谦让,一起抢筷子 )
-
经典的哲学家就餐问题
-
死锁是个啥子样子嘞?
-
-
- 死锁发生的必要条件
-
-
-
-
- 互斥条件:即在一段时间内某资源仅为一进程所占用。
- 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
- 环路等待条件:在发生死锁时,必然存在一个 进程——与——资源 的环形链。
-
-
-
-
-
解决死锁的办法
- 1、让线程之间产生时间差——这个看实际情况来设计,总之就是:让线程之间岔开就行
- 2、线程之间彼此谦让,利用yield()方法————谦让线程嘛,也可以根据实际情况自己判断,从而达到谦让的目的
-
解决死锁的办法
-
至此,异常和多线程基础篇已经结束
作者:紫邪情
.
出 处:https://www.cnblogs.com/xiegongzi/p/15120253.html