JaveEE基础多线程
多线程与反射
多线程
首先回顾进程的概念
进程是程序执行的实体,每一个进程都是一个应用程序,CPU一个核心同时只能处理一个进程,当出现多个进程需要同时运行时,CPU一般通过时间片轮转调度
算法,来实现多个进程的同时运行
在一个进程中同时执行多个任务需要借助线程
线程是程序执行中一个单一的顺序控制流程, 一个进程可以有多个线程, 线程是程序执行流的最小单元, 各个线程之间共享程序的内存空间, 线程之间切换速度也高于进程
线程的创建和启动
我们可以通过创建一个Thread对象来创建一个新的线程,在Thread构造方法中只需要传入一个Runnable接口的实现,所以这里可以直接使用lambda表达式
1 | public static void main(String[] args) { |
start()
方法: 在新线程里执行
run()
方法: 在当前线程里执行(不会创建新的线程)
sleep()
方法: 让当前线程休眠一段时间
stop()
方法: 强行终止该线程
线程的生命周期
线程的休眠与中断
一个处于运行状态下的线程,其会出现如下情况
- 当CPU分配的时间片结束时,会从运行态回到就绪态,等待下一次获得CPU资源
- 当线程进入休眠 / 阻塞(eg.IO请求) / 手动调用wait()方法时,会使线程进入阻塞态,当阻塞态结束后会回到就绪态
- 当线程出现异常或错误 / 被stop()方法强行停止 / 所有低吗执行结束时, 会让线程运行终止
下面研究一下sleep()
方法
通过调用sleep()方法可以将当前线程进入休眠, 让线程进入一定时间的等待状态; 我们发现该方法了可能会抛出一个InterruptedException异常,这个异常会在何时发生?
-> 线程在sleep()
时,被stop()
方法强行终止, 导致该线程内的方法未执行完毕,所以产生中断异常
解决办法: 使用Interrupt()方法: 该方法会通知线程进行停止操作,让线程自行处理后续资源回收等操作
isInterrupted()
:判断线程是否存在中断标志
resume()
: 让线程从sleep或者中断状态中恢复
suspend()
: 挂起该线程(不推荐使用, 因为会持续占用锁资源,从而造成死锁)
线程的优先级
Java程序中的每个线程并不是平均分配CPU时间的而是采用抢占式调度,优先级越高的线程,优先使用CPU资源!我们希望CPU花费更多的时间去处理更重要的任务,而不太重要的任务,则可以先让出一部分资源。线程的优先级一般分为以下三种:
- MIN_PRIORITY 最低优先级
- MAX_PRIORITY 最高优先级
- NOM_PRIORITY 常规优先级
可以通过setPriority()
来设置优先级
线程的礼让和加入
我们还可以在当前线程的工作不重要时,将CPU资源让位给其他线程,通过使用yield()
方法来将当前资源让位给其他同优先级线程
当我们希望一个线程等待另一个线程执行完成后再继续进行,我们可以使用join()
方法来实现线程的加入
注意,线程的加入只是等待另一个线程的完成,并不是将另一个线程和当前线程合并!
线程锁和线程同步
多线程情况下Java的内存管理:
线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的工作内存(本地内存),工作内存中存储了该线程以读/写共享变量的副本。
高速缓存通过保存内存中数据的副本来提供更加快速的数据访问,但是如果多个处理器的运算任务都涉及同一块内存区域,就可能导致各自的高速缓存数据不一致,在写回主内存时就会发生冲突,这就是引入高速缓存引发的新问题,称之为:缓存一致性。
为了解决上面的问题,我们可以通过synchronized
关键字来创造一个线程锁,首先我们来认识一下synchronized代码块,它需要在括号中填入一个内容,必须是一个对象或是一个类
锁内的代码被称为同步代码块,它的执行过程中,拿到了我们传入对象或类的锁(传入的如果是对象,就是对象锁,不同的对象代表不同的对象锁,如果是类,就是类锁,类锁只有一个,实际上类锁也是对象锁,是Class类实例,但是Class类实例同样的类无论怎么获取都是同一个),但是注意两个线程必须使用同一把锁!
当一个线程进入到同步代码块时,会获取到当前的锁,而这时如果其他使用同样的锁的同步代码块也想执行内容,就必须等待当前同步代码块的内容执行完毕,在执行完毕后会自动释放这把锁,而其他的线程才能拿到这把锁并开始执行同步代码块里面的内容(实际上synchronized是一种悲观锁 )
死锁
死锁指两个线程都相互持有对方需要的锁,但是又不将锁资源释放,导致线程被无限期阻塞,程序卡死
不推荐使用
suspend()
去挂起线程,因为suspend()
在使线程暂停的同时,并不会去释放任何锁资源。其他线程都无法访问被它占用的锁。直到对应的线程执行
resume()
方法后,被挂起的线程才能继续,从而其它被阻塞在这个锁的线程才可以继续执行。但是,如果resume()
操作出现在suspend()
之前执行,那么线程将一直处于挂起状态,同时一直占用锁,这就产生了死锁。
wait()
和notify(), notifyAll()
方法
他们需要配合synchronized来使用的(实际上锁就是依附于对象存在的,每个对象都应该有针对于锁的一些操作,所以说就这样设计了), 只有在同步代码块中才能使用这些方法
对象的wait()
方法会暂时使得此线程进入等待状态,同时会释放当前代码块持有的锁,这时其他线程可以获取到此对象的锁,当其他线程调用对象的notify()
方法后,会唤醒刚才变成等待状态的线程(这时并没有立即释放锁)。
notifyAll其实和notify一样,也是用于唤醒,但是前者是唤醒所有调用wait()
后处于等待的线程,而后者是看运气随机选择一个。
ThreadLocal的使用
通过ThreadLocal类,可以创建工作内存中的变量, 它将我们的变量值存储在内部(只能存储一个变量), 不同线程访问ThreadLocal对象时,只能获取到当前线程所属的对象
在线程中创建的子线程,无法获得父线程工作内存中的变量, 我们可以通过使用InheritableThreadLocal来解决, 在InheritableThreadLocal存放的内容,会自动向子线程传递
定时器
Java为我们提供了一套自己的框架用于处理定时任务:
1 | public static void main(String[] args) { |
我们可以通过创建一个Timer类来让它进行定时任务调度,我们可以通过此对象来创建任意类型的定时任务,包延时任务、循环定时任务等。我们发现,虽然任务执行完成了,但是我们的程序并没有停止,这是因为Timer内存维护了一个任务队列和一个工作线程:
1 | public class Timer { |
TimerThread继承自Thread,是一个新创建的线程,在构造时自动启动, 而它的run方法会循环地读取队列中是否还有任务,如果有任务依次执行,没有的话就暂时处于休眠状态
newTasksMayBeScheduled
实际上就是标记当前定时器是否关闭,当它为false时,表示已经不会再有新的任务到来,也就是关闭,我们可以通过调用cancel()
方法来关闭它的工作线程:
1 | public void cancel() { |
因此,我们可以在使用完成后,调用Timer的cancel()
方法以正常退出我们的程序
守护线程
守护线程 不等于 守护进程
守护进程在后台运行,不需要和用户进行交互; 守护线程则不同, 当其他非守护线程结束以后, 守护线程会自动结束, 所以守护线程不适用于I/O操作
在守护线程中产生的新线程也属于守护线程