3. 线程的生命周期
线程的生命周期存在五个状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)
运行和阻塞状态
4. 控制线程
状态 | 说明 |
---|---|
NEW | 刚创建, java虚拟机分配内存,初始化成员变量,但还没调用start()启动方法 |
RUNNABLE | 就绪/运行 执行start创建方法调用栈和程序计数器;获得CPU |
BLOCKED | 阻塞状态,等待获取资源(系统资源、锁)或者调用Object.wait()、join()方法时处于该状态 |
WAITING | 等待,调用sleep, |
TIMED_WAITING | 计时等待,执行方法:Thread#sleep(times).Object#wait(times)、Thread#join(long millis)、LockSupport#parkNanos(times)、ockSupport#parkUntil(times) |
TERMINATED | 终止,线程被终止(抛出未捕获异常,调用stop())或自然结束,isAlive()可以测试是否死亡 |
4.1 join线程
可以让一个线程等待另一个线程。
例如A和B两个线程,当A、B都在运行时,在A中调用了B.join()
那么,A就会处于等待状态,直到B执行完成后,A才会重新开始执行。
- join():等待被join的线程执行完
- join(long millis),最多等待的时间,如果millis为0,等价于join();
- 等待期间不会释放任何锁
join方法代码:public final synchronized void join(long millis) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0; if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (millis == 0) { while (isAlive()) { wait(0); } } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } } }
join() 的等待机制:
当线程A调用 threadB.join() 时,线程A会进入等待状态,直到threadB执行完毕
这个等待是通过调用 Object.wait() 实现的(在Thread类的join方法内部实现)
但是与普通的 wait() 不同,join() 的等待不会释放线程A已经持有的任何锁
锁保持:
如果线程A在调用 join() 前已经获取了某些对象的锁,这些锁在等待期间仍然被线程A持有
其他尝试获取这些锁的线程会被阻塞,即使线程A正在等待另一个线程完成
潜在死锁风险:
synchronized(lock) {
Thread t = new Thread(() -> {
synchronized(lock) { // 这里会死锁
// 一些操作
}
});
t.start();
t.join(); // 持有lock的同时等待t完成
}
上面代码会导致死锁,因为主线程持有lock并等待t完成,而t需要获取lock才能执行
与 wait() 的区别:
Object.wait() 会释放当前对象锁
Thread.join() 不会释放任何锁
4.2 后台线程(守护线程)Daemon Thread
该线程是在后台运行,任务是为其他线程提供服务。如果前台所有线程都死亡,那守护线程会自动死亡。
使用Thread的setDaemon(true)
方法可将指定线程设置为后台线程。
另外:后台线程创建的子线程默认是后台线程
注意:
- 前台线程死亡后,JVM会通知后台线程死亡,但从接受到响应需要一定时间
- setDaemon(true)方法需要在start()方法前执行。
当前台线程全部死亡后,后台线程自动结束生命
通常来说,守护线程经常被用来执行一些后台任务,但是呢,你又希望在程序退出时,或者说 JVM 退出时,线程能够自动关闭,此时,守护线程是你的首选。
4.3 sleep
睡眠线程,调用者会进入阻塞状态。不会释放已经获得的资源
4.4 yield 线程让步
与sleep类似,但又有不同,yield会让调用者线程暂停,进入就绪状态。而sleep是进入阻塞状态。
所以,在yield暂停后,多个线程重新按照优先级获得执行的机会。
4.5 改变线程优先级
线程优先级越大,则获得越多的CPU时间片。
每个线程默认的优先级都与创建它的父线程优先级相同。默认情况下,main线程具有普通优先级。
Thread提供两个方法
- setPriority(int newPriority)
- getPriority()
newPriority取值在1~10的整数 - MAX_PRIORITY 10
- MIN_PRIORITY 1
- NORM_PRIORITY 5
4.6 线程调度
调度策略:
- 时间片:线程的调度采用时间片轮转的方式
- 抢占式:高优先级的线程抢占CPU
Java的调度方法:
1.对于同优先级的线程组成先进先出队列(先到先服务),使用时间片策略
2.对高优先级,使用优先调度的抢占式策略
5. 线程同步
5.1 线程安全问题
这是一段银行取钱的代码,无需细看,只需理解main和DrawThread类即可。
public class Accout{
//账号
private String accountNo;
//余额
private double balance;
public Accout() {};
//构造器
public Accout(String accountNo,double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
public String getAccountNo() {
return accountNo;
}
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
//根据account重写hashCode()和equals
public int hashCode() {
return accountNo.hashCode();
}
public boolean equals(Object obj) {
if(this==obj){
return true;
}
if(obj != null && obj.getClass() == Accout.class) {
Accout target = (Accout)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
public class DrawThread extends Thread{
//模拟用户账号
private Accout accout;
//当前取钱希望取到钱数
private double drawAmount;
public DrawThread (String name,Accout account,double drawAmount){
super(name);
this.accout = account;
this.drawAmount= drawAmount;
}
//当多个线程修改同一个数据,涉及到数据安全问题
@Override
public void run() {
if(accout.getBalance()>=drawAmount){
System.out.println(getName()+" 取钱成功"+drawAmount);
try{
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
accout.setBalance(accout.getBalance()-drawAmount);
System.out.println("余额为"+ accout.getBalance());
}
else{
System.out.println(getName()+"取钱失败");
}
}
}
public class DrawTest{
public static void main(String[] args) {
Accout accout = new Accout("123456",1000);
new DrawThread("甲",accout,800).start();
new DrawThread("乙",accout,800).start();
}
}
代码很长,
请看我们这里DrawThread
类中的 一段代码:
try{
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
这里是利用线程睡眠模拟线程切换,当前一个线程取钱成功但未修改账户余额便被迫暂停后,切换另一线程仍可继续取钱,并可修改余额,然后第……最后,打印结果就可能会是这样的:
乙 取钱成功800.0
甲 取钱成功800.0
余额为200.0
余额为-600.0
余额竟然会有-600,莫非是透支消费?
由于多线程调度的不确定性,当有多个线程并发修改同一个数据,就有可能会遇到的。
之所以会出现结果,因为main()方法的方法体不具有同步安全性
那么便要设法解决掉他。
5.2 同步代码块
Java多线程支持引入同步监听器,使用同步监听器的通用方法是同步代码块:
synchronized(obj){……}
其中obj是选中的同步监听器。执行上面代码块中的代码前,必须获得对obj的控制。
下面修改上面的代码
@Override
public void run() {
//使用accont做同步监听器,任何线程进入下面同步代码块之前
//必须获得对accout的控制权
synchronized(accout) {
if(accout.getBalance()>=drawAmount){
System.out.println(getName()+" 取钱成功!取出金额:"+drawAmount);
try{
Thread.sleep(1);
}catch (InterruptedException e){
e.printStackTrace();
}
accout.setBalance(accout.getBalance()-drawAmount);
System.out.println("余额为"+ accout.getBalance());
}
else{
System.out.println(getName()+"取钱失败!余额不足!");
}
}
}
当再次执行程序,发现:
甲 取钱成功!取出金额:800.0
余额为200.0
乙取钱失败!余额不足!
这里使用synchronized将run()方法的方法体修改为同步代码块,同步监听器是accout对象。多个线程共用accout必须符合 :加锁-》修改-》释放锁,即这些代码块同一时刻只能被一个线程获取并执行。
5.3 同步方法
同步方法是使用synchronized关键字修饰某个方法,this(调用者)作为监听器。
通过同步方法可以实现线程安全的类,线程安全的类具有特征:
- 该类对象可被多个线程安全地访问
- 每个线程调用该对象的任意方法之后都将得到正确结果
- 每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态。
修改Accout类之后
class Accout{
private String accountNo;
private double balance;
public Accout() {};
//构造器
public Accout(String accountNo,double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
public String getAccountNo() {return accountNo;}
public void setAccountNo(String accountNo) {this.accountNo = accountNo;}
public double getBalance() {return balance;}
public synchronized void draw(double drawAmount) {
if(balance>=drawAmount){
System.out.println(Thread.currentThread().getName()+" 取钱成功!取出金额:"+drawAmount);
try{
Thread.sleep(1);
}catch (InterruptedException e){
e.printStackTrace();
}
balance-=drawAmount;
System.out.println("余额为"+ balance);
}
else{
System.out.println(Thread.currentThread().getName()+"取钱失败!余额不足!");
}
}
在DrawThread类中只需要调用accout.draw(drawAmount)
即可,accout作为监听器,所以Accout类编程线程安全类,多线程并发修改同一个accout前,必须先对该对象加锁。
5.4 释放同步监听器的锁定
释放条件:
-
当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器。
-
当前线程在同步代码块、同步方法中遇到 break、return 终止了该代码块/方法的继续执行,当前线程将会释放同步监视器。
-
当前线程在同步代码块/同步方法中出现了未处理的 Error或Exception,导致了该代码块、该方法异常结束时,当前线程将会释放同步监视器。.
-
当前线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法则当前线程哲停,并释放同步监视器。
线程不会释放同步监视器的情况
调用 Thread.sleep()、Thread.yield(),当前线程不会释放同步监视器。
线程执行同步代码块时,其他线程调用了该线程的 suspend()方法将该线程挂起,该线程不会释放同步监视器。
5.5 同步锁Lock
同步锁Lock对象,一般锁提供对资源的独占访问,也有些锁允许对共享资源并发访问,如ReadWriteLock(读写锁)。
Lock和ReadWriteLock是两个根接口。Lock提供了ReentrantLock(可重入锁)实现类,为ReadWriteLock提供ReentrantReadWriteLock实现类。
ReentrantLock
介绍比较常用的ReentrantLock,可重入锁,是一种排它锁。可以显式地加锁、释放锁。
class A{
//定义锁对象
private final ReentrantLock lock = new ReentrantLock();
//需要保证线程安全的方法
public void sagety() {
//加锁
lock.lock();
try{……}
//使用finally保证释放锁
finally{
lock.unlock();
}
}
}
- 独占性:ReentrantLock 实现了 Lock 接口,默认是非公平的排它锁(也可通过构造函数设置为公平锁)。
- 可重入性:同一个线程可以重复获取同一把锁(锁计数器递增),但其他线程仍然会被阻塞
底层依赖 AQS:ReentrantLock 通过 AbstractQueuedSynchronizer(AQS) 的 state 变量实现锁状态管理:
state = 0:锁未被占用。
state > 0:锁被占用,且记录重入次数(排它性体现在只有一个线程能修改 state)。
独占模式:AQS 的 tryAcquire() 方法确保只有一个线程能成功获取锁。
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
ReentrantLock和synchronized区别
关于实例:https://baijiahao.baidu.com/s?id=1648624077736116382&wfr=spider&for=pc
- 方便灵活:synchronized加锁解锁自动进行,易于操作;ReentrantLock加锁解锁手动进行。
- 响应中断:synchronized不可响应中断,一个线程获取不到锁就一直等着;ReentrantLock可以中断(ReentrantLock.lockInterruptibly())。
- 公平锁机制:ReentrantLock还可以实现公平锁机制,谁等待时间长,谁先获取到锁
5.6 ReadWriteLock
5.7 死锁
死锁条件
- 互斥条件:一个资源每次只能被一个进程使用
- 请求与保持条件:一个进程因请求资源而被阻塞时,对已获得的资源保持不放
- 不剥夺条件:进程已经获得的资源,在未使用完之前,不能抢行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
当两个线程相互等待对方释放资源时就会发生死锁。Java虚拟机没有监测和处理死锁的机制。
出现死锁后,程序不报异常,没有提示,只是所有线程处于阻塞状态,无法继续。