多线程详解-02线程周期与同步

27 篇文章 0 订阅
3 篇文章 0 订阅
本文详细介绍了Java线程的生命周期,包括新建、就绪、运行、阻塞和死亡状态。讲解了如何使用join、后台线程、sleep、yield和改变线程优先级来控制线程。同时,深入探讨了线程同步的重要性,通过同步代码块、同步方法和Lock实现线程安全。此外,还讨论了死锁的概念和避免方法。
摘要由CSDN通过智能技术生成

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),等待被join的线程的时间。
    如果millis为0,等价于join();
    join()内部使用wait()方法,所以调用该方法会释放锁
    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;
                }
            }
        }
    

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(1);
            }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;}
    //账户余额不允许随便修改,所以不提供set方法

    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和synchronized区别

关于实例:https://baijiahao.baidu.com/s?id=1648624077736116382&wfr=spider&for=pc

  • 方便灵活:synchronized加锁解锁自动进行,易于操作,不够灵活;ReentrantLock加锁解锁手动进行,不易操作,但灵活。
  • 响应中断:synchronized不可响应中断,一个线程获取不到锁就一直等着;ReentrantLock可以相应中断。
  • 公平锁机制:ReentrantLock还可以实现公平锁机制,谁等待时间长,谁先获取到锁

5.6 死锁

死锁条件

  • 互斥条件:一个资源每次只能被一个进程使用
  • 请求与保持条件:一个进程因请求资源而被阻塞时,对已获得的资源保持不放
  • 不剥夺条件:进程已经获得的资源,在未使用完之前,不能抢行剥夺。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
    当两个线程相互等待对方释放资源时就会发生死锁。Java虚拟机没有监测和处理死锁的机制。
    出现死锁后,程序不报异常,没有提示,只是所有线程处于阻塞状态,无法继续。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值