Java - 深入浅出“多线程”和“异常”

多线程

进程和线程的区别

进程的定义: 是指一个内存中运行的应用程序(程序的一次运行就产生一个进程),每个进程都有自己独立的一块内存空间,比如在Windows的任务管理器中,一个运行的xx.exe就是一个进程。
线程的定义: 是指进程中的一个执行任务(控制单元),一个进程中可以运行多个线程,多个线程可共享进程的数据,在进程中,线程被创建后使用进程申请的内存空间,不重新申请内存。

什么是多线程?

简单的说,在同一个进程中并发运行的多个子任务就是多线程。

一个进程至少有一个线程,为了提高CPU的效率,可以在一个进程中开启多个控制单元,这就是多线程。

为什么是多线程?

线程出现的原因: 线程的出现为了解决实时性问题。

多线程和Java

在Java程序运行,至少有2个线程启动了,它们分别是:Main线程(主线程)垃圾回收GC线程

主线程很特殊,在启动JVM的时候自动启动的(同时启动的还有GC的垃圾回收检测线程)。

19:17:46
老师,教案专门强调主线程是在jvm启动时启动,GC线程是啥时候启动的啊?

19:18:08
gc线程在需要gc的时候启动,程序申请内存空间的时候会触发gc算法,计算可用空间,然后根据gc规则决定要不要进行垃圾回收。

19:18:14
它前面说的维护垃圾回收线程指的是不是GC线程啊?

19:18:22
那是守护线程,gc线程也属于守护线程.

19:18:32
gc线程有两部分工作,一部分是检测是否需要垃圾回收,还有一部分是进行垃圾回收

19:18:38
检测的线程是jvm启动时就运行的


用户线程和守护线程(破)(了解)

在Java中有两类线程:User Threapod(用户线程)、Daemon Thread(守护线程)

用户线程和守护线程的区别

二者其实基本上是一样的。唯一的区别在于JVM何时离开。

用户线程:当存在任何一个用户线程未离开,JVM是不会离开的。

守护线程:如果只剩下守护线程未离开,JVM是可以离开的。

用户线程定义:平时用到的普通线程均是用户线程,当在Java程序中创建一个线程,它就被称为用户线程。

守护线程定义:是个服务线程,准确地来说就是服务其他的线程,这是它的作用——而其他的线程只有一种,那就是用户线程。所以java里线程分2种,

  1. 守护线程,比如垃圾回收线程,就是最典型的守护线程。
  2. 用户线程,就是应用程序里的自定义线程

并行和并发

并发:同一时间多进程轮流使用CPU,多进程形成并发,任意时间只有一个程序在占用CPU。

并行:同一时间多个进程同时使用各自的CPU,需要多个CPU支持,多进程形成并行。

Java中线程的创建和启动

方式

方式一:继承Thread类

  1. ​ 自定义类继承Thread
  2. ​ 覆写run方法
  3. ​ 创建自定义类对象
  4. ​ 自定义类对象调用start方法

方式二:实现Runnable接口

  1. ​ 自定义类实现Runnable接口
  2. ​ 覆写run方法
  3. ​ 创建自定义类对象
  4. ​ 把自定类的对象作为Thread类构造器参数,并调用Thread对象start方法

方式一、二的区别:

  • 继承Thread类的方式使用起来方便,启动一个线程也方便,很多功能都在Thread类中定义好了。
  • 实现Runnable接口的方式启动得依赖于Thread,因为本身Runnable中只有run方法,请看Thread的构造方法。
  • 在Thread的子类中,如果想要多个子类共享同一资源必须使用static修饰此资源,也就说明了继承Thread这种方式本身不是很适合共享资源的。Runnable接口的实现类中共享资源无需被静态修饰,即可实现资源共享,因为它是为Thread构造器传入Runnable实现类对象,多个线程使用的是同一个Runnable实现类,而不是直接New一个线程,故而实现了资源共享。
  • 由于Java不允许多继承,因此实现类实现了Runnable接口可以再继承其他类,但是Thread明显不可以。
Runnable为什么不可以直接run?

Runnable其实相对于一个Task,并不具有线程的概念,如果你直接去调用Runnable的run,其实就是相当于直接在主线程中执行了一个函数而已,并未开启线程去执行。

线程的生命周期

课本上的引用:

  • 新生状态:用 new 关键字建立一个线程后,该线程对象就处于新生状态。
    处于新生状态的线程有自己的内存空间,通过调用 start()方法进入就绪状态。
  • 就绪状态
    处于就绪状态线程具备了运行条件,但还没分配到 CPU,处于线程就绪队列,等待系统为其分配
    CPU。当系统选定一个等待执行的线程后,它就会从就绪状态进入执行状态,该动作称为“CPU 调
    度”。
  • 运行状态
    在运行状态的线程执行自己的run 方法中代码,直到等待某资源而阻塞或完成任务而死亡。如果在
    给定的时间片内没有执行结束,就会被系统给换下来回到等待执行状态(就绪)。
  • 阻塞状态
    处于运行状态的线程在某些情况下,如执行了 sleep(睡眠)方法,或等待 I/O
    设备等资源,将让出 CPU 并暂时停止自己运行,进入阻塞状态。
    在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的
    I/O 设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续执行。
  • 死亡状态
    死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有三个,一个是正常运行的线程完成
    了它的全部工作;二是线程抛出未捕获的Exception或Error,三是线程被强制性地终止,如通过
    stop 方法来终止一个线程【易导致死锁,不推荐】

结论:

  1. 就绪状态:有资源,无资格。
  2. 运行状态:有资源,有资格。
  3. 阻塞状态:无资源,无资格。

提示:

线程的执行时间和CPU为其分配的时间片有关,当线程时间片到期,就由运行状态进入就绪状态中,即进入就绪队列,等待CPU的重新分配调度。

操作线程的方法

join()方法

join方法的主要作用就是同步,它可以使得线程之间的并发执行变为串行执行。

比如在A线程中调用了B线程的join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行。

细节:现有A、B两个线程同时启动,B线程执行一会儿但未执行完毕,此时A线程抢占到了资源,A开始执行,此时A执行了一会儿但未执行结束,此时B线程调用了join方法,此时A线程进入阻塞队列等待,B线程从刚刚被A抢占资源时的进度继续执行知道B线程结束,此时A再继续执行。

sleep(毫秒数)方法

sleep方法让正在执行的线程暂停一段时间,进入阻塞状态,常常用来模拟网络延迟,或者耗时操作等等。
调用sleep()后,在指定时间段之内,该线程不会获得执行的机会

线程的优先级

每个线程都有优先级,优先级的高低只和线程获得执行机会的次数多少有关。并不是说优先级高的就一 定先执行,哪个线程的先运行取决于CPU的调度。

Thread对象的setPriority(int x)和getPriority()用来设置和获得优先级。

线程安全性

什么是线程安全?

当多线程并发访问同一个资源对象(共享资源)的时候,可能出现线程不安全的问题。

什么是synchronized?

同步锁,又称之为同步监听对象/同步锁/同步监听器/互斥锁:

synchronized(同步锁){
	// 需要同步操作的代码
}
为什么是synchronized?

为了保证每个线程都能正常执行原子操作(避免线程原子性操作被中断),Java引入了线程同步机制。

在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就执行,其他的线程只能在代码块外等待。

如何判断synchronized应该锁谁?

根据需要同步的资源来划分:

  1. 当需要同步的资源是非静态的基本数据类型时,锁其上一级,例如User类中的成员private int money = 100;是需要同步的资源,此时money是int类型,故而锁User,即锁thissynchronized(this){}
  2. 非静态的由于数据类型时,锁资源本身
  3. 静态的基本数据类型或引用数据类型时,锁当前类的Class对象,即 Employee.class、User.class、Student.class此类

拓展:八锁现象。

synchronized锁时的细节:现有A、B、C三个线程(均带有同步锁),一个共享资源Q。此时ABC三个线程启动进入就绪队列,此时A线程被CPU选中先执行,此时A对资源Q进行上锁,A开始使用资源Q完成任务,此时B、C线程前来抢夺资源Q,此时Q被A锁定,B、C无法抢到,此时B、C进入阻塞队列,此时A本次执行结束回到就绪队列,此时B、C位于阻塞队列中处于阻塞状态,只有A位于就绪队列中处于就绪状态,CPU只有选择A线程,A再执行,A线程执行结束了,为资源解锁,A回到就绪队列,此时B、C知道资源解锁,从阻塞状态变为就绪状态,B、C也加入就绪队列,此时A、B、C均在就绪队列中,等待CPU的下一次调度,如此循环往复知道共享资源耗尽。

在被synchronized锁的时候,sleep()的时间也计入线程的生命周期中,不会为资源进行解锁,而如果此时线程调用的是wait()方法,则wait()方法会为共享资源进行解锁。

同步方法

使用synchronized修饰的方法,就叫做同步方法。保证A线程执行该方法的时候,其他线程只能在方法外等待。

此时同步锁是谁?

锁的是调用当前同步方法的对象:

  • 对于非static方法,同步锁就是this。
  • 对于static方法,同步锁就是当前方法所在类的字节码对象。
synchronized的优劣(掌握)

​ 好处:保证了多线程并发访问时的同步操作,避免线程的安全性问题。
​ 缺点:使用synchronized的方法/代码块的性能要低一些。
​ 建议:尽量减小synchronized的作用域。

面试题:

  1. StringBuilder和StringBuffer的区别
  2. 说说ArrayList和Vector的区别
  3. HashMap和Hashtable的区别

通过源代码会发现,主要就是方法有没有使用synchronized的区别,比如StringBuilder和
StringBuffer。

我们知道,StringBuilder的效率高,但不安全;StringBuffer效率低,但安全。

因此得出结论:使用synchronized修饰的方法性能较低,但是安全性较高,反之则反。

异常

重载和方法是否抛出异常无关,也就是说,两个方法如果方法签名相同但一个抛出异常、一个不抛出异常,这样是不满足重载的。

运行时异常(了解)

runtime异常,顾名思义在编译时期不被检测,只有在运行时期才会被检查出来。

运行异常可以不使用try…catch处理,但一旦出现异常就将由JVM处理(打印堆栈信息)。
RuntimeException(运行时异常)通常是指因设计或实现方式不当而导致的问题。程序员小心谨慎是
可以避免的异常。

如:事先判断对象是否为null就可以避免NullPointerException异常,事先检查除数
不为0就可以避免ArithmeticException异常。

运行时异常特点:
在编译阶段,Java编译器检查不出来。一般的,程序可以不用使用try-catch和throws处理运行异
常。

编译时异常(了解)

编译被检查异常,顾名思义就是在编译时期就会被检测到的异常。除了RuntimeException以及子
类以外,其他的Exception及其子类都是编译异常,有时候也称之为检查时异常。

特点:
在编译阶段,Java编译器会检查出异常,也就说程序中一旦出现这类异常,要么使用try-catch语句
捕获,要么使用throws语句声明抛出它,否则编译就不会通过。

简而言之:程序要求必须处理编译异常,使用try-catch或throws处理。

编译时异常(检查时异常) : 程序必须做出处理,一般继承于Exception

异常处理

抛出异常

自定义异常

常见异常有哪些

Exception和Error的区别

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

君去何方

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值