Java基础之线程(Threads)与并发
文章目录
零、课上内容
线程的生命周期
- 调用start前不参与调度
- 只有在run结束后,该线程才会结束(不会被强制关闭)
- yield()有让的意思,指正在运行的程序主动让出CPU。
线程操作
其中sleep和yield是静态成员函数,这意味着sleep和yield只能对当前正在运行的线程起作用
函数的synchronized
当函数所有的操作都需要被同步,并且它的key是this时,可以为函数加synchronized关键字。
解决死锁的方案是放弃synchronized??
一、线程基础
1. 基础信息
线程是任务执行和CPU使用的最小单元,是各自运行的任务体,线程之间可以共享变量。
线程类型(Thread)实现了Runnable接口(含有无参的run方法)。
线程可以接受Runnable实现实例化后的runnable对象,进而获取它的runnable方法。
对于线程对象:Thread t = new Thread(r);
,有上图所示操作:
- 它可以调用
t.start()
启动线程,这意味着该线程将被放入ready queue中。当它被分配到时间片中时,就会执行其run方法。(从此看出,应当避免直接调用run方法,而应当在Thread中使用start方法,从而获得多线程的效果) - Thread.sleep()可以让当前进程挂起,等待一定时间(传入和millis参数)后再放入ready queue中,它挂起的同时其他线程也可以执行。
- stop、suspend和resume已经不再使用。
- run方法执行完成后,线程就会结束。stop弃用之后,线程不能被强制结束。
- interrupt()方法是成员方法,尝试调用的线程中断。线程自己对自己的中断是始终被允许的,其他线程令某线程中断时就需要检查(可能会报错),如果一个线程处于阻塞状态,无法中断并返回InterruptedException。
2. 线程的优先级
Thread.setPriority(int n)
:可以看到这是一个静态方法,用与设置当前线程的优先级,1~10,默认5。
3. 线程的状态与生命周期
Java线程的状态有以下几种:
- New:新创建的线程,尚未执行,这时线程不参与调度
- Ready:执行了start方法后的线程
- Runnable:运行中的线程,正在执行run()方法的Java代码;
- Blocked:运行中的线程,因为某些操作被阻塞而挂起;
- Waiting:运行中的线程,因为某些操作在等待中;
- Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待;
- Finished:线程已终止,因为run()方法执行完毕。
线程终止的原因: - run执行结束或遇到return语句
- run方法抛出异常(这里指的是未处理的,应当避免的Exception,如下标越界等)
- 不再使用stop
join()的用法
一个线程可以等待另一个线程直到其运行结束。A线程可以调用b.join(),进而等待t线程结束后再继续运行。
对结束后的线程调用join会立刻返回
也可以使用join(long millis)
,从而让调用的线程等待目标线程等待指定上限时间,之后则不再等待。
4. 线程的中断
如果线程需要执行一个长时间任务,它可能需要被中断,如中断速度过慢的下载。这个中断信号应当由其他线程给予,并且该线程能够自行判断情况并处理,Java使用interrupt()
和相关调用解决问题。
其他线程可以调用目标线程的interrupt()
方法,从而在j检查通过的情况下使得目标函数的中断状态置位。而可能会被中断的线程应当反复检测自身的中断状态(调用isInterrupted()
方法)。
目标线程有2种方法察觉到自己被调用中断方法:
- 在目标线程正在运行的时候,通过检测检测到了
isInterrupted()
为真。此时它应当做出决策,尽快结束自己(但其实并不是强制的)。 - 当目标线程正在被**阻塞(Blocked)**时(三个使线程进入阻塞状态的操作:sleep、wait和join都声明抛出
InterruptedException
,所以理论上它们都应当在try-catch块中),如果阻塞动作抛出InterruptedException
,这说明他们被调用中断方法,此时它们要做的也是尽快结束自己。
进程对自身调用中断函数是始终被允许的
volatile关键字
线程间的共享变量应当使用volatile
关键字声明。
在Java虚拟机中,变量的值保存在主内存中,但是,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中。如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的!这会导致如果一个线程更新了某个变量,另一个线程读取的值可能还是更新前的。
volatile
关键字的目的是告诉虚拟机:
- 每次访问变量时,总是获取主内存的最新值;
- 每次修改变量后,立刻回写到主内存。
5. 守护线程(Deamon Thread)
Java程序入口是由JVM启动main线程,main线程可启动其他线程,所有线程结束后程序结束。
但确实需要某种无限循环(比如一直定时触发任务)的线程,而在其它线程结束时,程序也应当结束。此时就应当使用守护线程:
Thread t = new MyThread();
t.setDaemon(true);
t.start();
将线程t设置为守护线程后,JVM退出时就不会管理守护线程是否结束。
显然守护线程不应该持有任何需要关闭的资源,否则就会在JVM退出时可能产生错误。
二、简单的线程同步
1. synchronized关键字
多线程对共享变量的操作可能会导致数据的不一致性,这需要保证操作代码的原子性。java最简单的原子性操作通过synchronized
关键字实现原子操作。synchronized
关键字的实现见此链接。
多个线程在进入synchronized
语句块时,必须先获得锁(即传入的参数),否则就会阻塞。执行结束后,在synchronized
语句块结束后,对应的进程就会释放锁。可以看到它确保了安全性,但性能下降。显然它是一种排他锁。
一些注意事项:
- 无论是否有异常,都会在
synchronized
结束处正确释放锁 - 当某个成员方法的函数体内所有语句都需要同步,并且这个语句的锁是当前对象(即this),可以对方法使用
synchronized
关键字 - 对类的静态方法使用
synchronized
关键字时,没有对象可供锁,因此锁住的是类对应的Class对象 - Java的
synchronized
获取的锁是可重入的(已获取锁的进程再次请求获取锁是合法的),也即:
synchronized(lock){
...
synchronized(lock){
...
}
}
是被允许的
不需要synchronized的操作
- 单行基本类型(long和double除外)赋值,例如:
int n = m;
(long和double是64位数据,JVM没有明确规定64位赋值操作是不是一个原子操作,不过在x64平台的JVM是把long和double的赋值作为原子操作实现的。) - 多行引用类型赋值,例如:
List<String> list = anotherList
。
类的线程安全问题
没有特殊说明时,类一般默认是非线程安全的
以下是一些例外:
- String,Integer,LocalDate等所有成员变量都是final,多线程访问时不可写的不变类是线程安全的
- StringBuffer是线程安全的
- 类似Math等只提供静态方法,没有成员变量的类是线程安全的
2. 死锁
简而言之就是循环等待
3. wait和notify
本文地址:https://blog.csdn.net/weixin_44662670/article/details/109628876