深入理解Java多线程(1.5)- Java线程相关方法(1)

2020年9月9日14:21:29 评论 34

线程相关方法

线程的方法主要是Thread类的方法和Object类的方法。本文介绍Thread类的run & start, sleep & yield, join, suspend & resume (已过时), stop (已过时), 以及Object类的wait & notify/notifyAll。

public class Thread{
    // 线程的启动
    public void start(); 
    // 线程体
    public void run(); 
    // 已废弃
    public void stop(); 
    // 已废弃
    public void resume(); 
    // 已废弃
    public void suspend(); 
    // 在指定的毫秒数内让当前正在执行的线程休眠
    public static void sleep(long millis); 
    // 同上,增加了纳秒参数
    public static void sleep(long millis, int nanos); 
    // 测试线程是否处于活动状态
    public boolean isAlive(); 
    // 中断线程
    public void interrupt(); 
    // 测试线程是否已经中断
    public boolean isInterrupted(); 
    // 测试当前线程是否已经中断
    public static boolean interrupted(); 
    // 等待该线程终止
    public void join() throws InterruptedException; 
    // 等待该线程终止的时间最长为 millis 毫秒
    public void join(long millis) throws InterruptedException; 
    // 等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒
    public void join(long millis, int nanos) throws InterruptedException; 
}
深入理解Java多线程(1.5)- Java线程相关方法(1)
线程方法状态转换

线程就绪、运行和死亡状态转换

  1. 就绪状态转换为运行状态:此线程得到CPU资源;
  2. 运行状态转换为就绪状态:此线程主动调用yield()方法或在运行过程中失去CPU资源。
  3. 运行状态转换为死亡状态:此线程执行执行完毕或者发生了异常;

注意:

当调用线程中的yield()方法时,线程从运行状态转换为就绪状态,但接下来CPU调度就绪状态中的那个线程具有一定的随机性,因此,可能会出现A线程调用了yield()方法后,接下来CPU仍然调度了A线程的情况。

run & start

通过调用start启动线程,线程执行时会执行run方法中的代码。

  1. start():线程的启动;
  2. run():线程的执行体;

sleep & yield

sleep():通过sleep(millis)使线程进入休眠一段时间,该方法在指定的时间内无法被唤醒,同时也不会释放对象锁

比如,我们想要使主线程每休眠100毫秒,然后再打印出数字:

/**
 * 可以明显看到打印的数字在时间上有些许的间隔
 */
public class Test1 {  
    public static void main(String[] args) throws InterruptedException {  
        for(int i=0;i<100;i++){  
            System.out.println("main"+i);  
            Thread.sleep(100);  
        }  
    }  
}

注意如下几点问题:

  1. sleep是静态方法,最好不要用Thread的实例对象调用它因为它睡眠的始终是当前正在运行的线程,而不是调用它的线程对象它只对正在运行状态的线程对象有效。看下面的例子:
    public class Test1 {  
        public static void main(String[] args) throws InterruptedException {  
            System.out.println(Thread.currentThread().getName());  
            MyThread myThread=new MyThread();  
            myThread.start();  
            // 这里sleep的就是main线程,而非myThread线程 
            myThread.sleep(1000); 
            Thread.sleep(10);  
            for(int i=0;i<100;i++){  
                System.out.println("main"+i);  
            }  
        }  
    }
  2. Java线程调度是Java多线程的核心,只有良好的调度,才能充分发挥系统的性能,提高程序的执行效率。但是不管程序员怎么编写调度,只能最大限度的影响线程执行的次序,而不能做到精准控制。因为使用sleep方法之后,线程是进入阻塞状态的,只有当睡眠的时间结束,才会重新进入到就绪状态,而就绪状态进入到运行状态,是由系统控制的,我们不可能精准的去干涉它,所以如果调用Thread.sleep(1000)使得线程睡眠1秒,可能结果会大于1秒。
    public class Test1 {  
        public static void main(String[] args) throws InterruptedException {  
            new MyThread().start();  
            new MyThread().start();  
        }  
    }  
      
    class MyThread extends Thread {  
        @Override  
        public void run() {  
            for (int i = 0; i < 3; i++) {  
                System.out.println(this.getName()+"线程" + i + "次执行!");  
                try {  
                    Thread.sleep(50);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
        }  
    }

    看某一次的运行结果:可以发现,线程0首先执行,然后线程1执行一次,又了执行一次。发现并不是按照sleep的顺序执行的。

    Thread-0线程0次执行!  
    Thread-1线程0次执行!  
    Thread-1线程1次执行!  
    Thread-0线程1次执行!  
    Thread-0线程2次执行!  
    Thread-1线程2次执行!

yield():与sleep类似,也是Thread类提供的一个静态的方法,它也可以让当前正在执行的线程暂停,让出CPU资源给其他的线程。但是和sleep()方法不同的是,它不会进入到阻塞状态,而是进入到就绪状态。yield()方法只是让当前线程暂停一下,重新进入就绪线程池中,让系统的线程调度器重新调度器重新调度一次,完全可能出现这样的情况:当某个线程调用yield()方法之后,线程调度器又将其调度出来重新进入到运行状态执行

实际上,当某个线程调用了yield()方法暂停之后,优先级与当前线程相同,或者优先级比当前线程更高的就绪状态的线程更有可能获得执行的机会,当然,只是有可能,因为我们不可能精确的干涉cpu调度线程。

public class Test1 {  
    public static void main(String[] args) throws InterruptedException {  
        new MyThread("低级", 1).start();  
        new MyThread("中级", 5).start();  
        new MyThread("高级", 10).start();  
    }  
}  
  
class MyThread extends Thread {  
    public MyThread(String name, int pro) {  
        super(name);// 设置线程的名称  
        this.setPriority(pro);// 设置优先级  
    }  
  
    @Override  
    public void run() {  
        for (int i = 0; i < 30; i++) {  
            System.out.println(this.getName() + "线程第" + i + "次执行!");  
            if (i % 5 == 0)  
                Thread.yield();  
        }  
    }  
}

关于sleep()方法和yield()方的区别如下

  1. sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield方法调用后 ,是直接进入就绪状态,所以有可能刚进入就绪状态,又被调度到运行状态;
  2. sleep方法声明抛出了InterruptedException,所以调用sleep方法的时候要捕获该异常,或者显示声明抛出该异常。而yield方法则没有声明抛出任务异常
  3. sleep方法比yield方法有更好的可移植性,通常不要依靠yield方法来控制并发线程的执行

join

线程的合并的含义就是 将几个并行线程的线程合并为一个单线程执行,应用场景是 当一个线程必须等待另一个线程执行完毕才能执行时,Thread类提供了join方法来完成这个功能,注意,它不是静态方法

join有3个重载的方法:

void join()    
    当前线程等该加入该线程后面,等待该线程终止。    
void join(long millis)    
    当前线程等待该线程终止的时间最长为 millis 毫秒。 如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度   
void join(long millis,int nanos)    
    等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度

例子代码,如下

/**
 * 在主线程中调用thread.join(); 就是将主线程加入到thread子线程后面等待执行。不过有时间限制,为1毫秒。
 */
public class Test1 {  
    public static void main(String[] args) throws InterruptedException {  
        MyThread t=new MyThread();  
        t.start();  
        t.join(1);//将主线程加入到子线程后面,不过如果子线程在1毫秒时间内没执行完,则主线程便不再等待它执行完,进入就绪状态,等待cpu调度  
        for(int i=0;i<30;i++){  
            System.out.println(Thread.currentThread().getName() + "线程第" + i + "次执行!");  
        }  
    }  
}  
  
class MyThread extends Thread {  
    @Override  
    public void run() {  
        for (int i = 0; i < 1000; i++) {  
            System.out.println(this.getName() + "线程第" + i + "次执行!");  
        }  
    }  
}

在JDK中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方法实现是通过调用wait方法实现。当main线程调用t.join时候,main线程会获得线程对象t的锁(wait 意味着拿到该对象的锁),调用该对象的wait(等待时间),直到该对象唤醒main线程,比如退出后。这就意味着main 线程调用t.join时,必须能够拿到线程t对象的锁

suspend & resume (已过时)

suspend-线程进入阻塞状态,但不会释放锁。此方法已不推荐使用,因为同步时不会释放锁,会造成死锁的问题

resume-使线程重新进入可执行状态

为什么 Thread.suspend 和 Thread.resume 被废弃了?

Thread.suspend 天生容易引起死锁。如果目标线程挂起时在保护系统关键资源的监视器上持有锁,那么其他线程在目标线程恢复之前都无法访问这个资源。如果要恢复目标线程的线程在调用 resume 之前试图锁定这个监视器,死锁就发生了。这种死锁一般自身表现为“冻结( frozen )”进程。

其他相关资料:

  1. https://blog.csdn.net/dlite/article/details/4212915

stop(已过时)

不推荐使用,且以后可能去除,因为它不安全。为什么 Thread.stop 被废弃了?

因为其天生是不安全的。停止一个线程会导致其解锁其上被锁定的所有监视器(监视器以在栈顶产生ThreadDeath异常的方式被解锁)。如果之前被这些监视器保护的任何对象处于不一致状态,其它线程看到的这些对象就会处于不一致状态。这种对象被称为受损的 (damaged)。当线程在受损的对象上进行操作时,会导致任意行为。这种行为可能微妙且难以检测,也可能会比较明显。

不像其他未受检的(unchecked)异常, ThreadDeath 悄无声息的杀死及其他线程。因此,用户得不到程序可能会崩溃的警告。崩溃会在真正破坏发生后的任意时刻显现,甚至在数小时或数天之后。

其他相关资料:

  1. https://blog.csdn.net/dlite/article/details/4212915

wait & notify/notifyAll

wait & notify/notifyAll这三个都是Object类的方法。使用 wait ,notify 和 notifyAll 前提是先获得调用对象的锁

  1. 调用 wait 方法后,释放持有的对象锁线程状态有 Running 变为 Waiting,并将当前线程放置到对象的 等待队列
  2. 调用notify 或者 notifyAll 方法后,等待线程依旧不会从 wait 返回,需要调用 noitfy 的线程释放锁之后,等待线程才有机会从 wait 返回
  3. notify 方法:将等待队列的一个等待线程从等待队列种移到同步队列中 ,而 notifyAll 方法:将等待队列种所有的线程全部移到同步队列,被移动的线程状态由 Waiting 变为 Blocked

前面一直提到两个概念,等待队列(等待池),同步队列(锁池),这两者是不一样的。具体如下:

同步队列(锁池):假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的同步队列(锁池)中,这些线程状态为Blocked

等待队列(等待池):假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时 线程A就进入到了该对象的等待队列(等待池)中,此时线程A状态为Waiting。如果另外的一个线程调用了相同对象的notifyAll()方法,那么 处于该对象的等待池中的线程就会全部进入该对象的同步队列(锁池)中,准备争夺锁的拥有权。如果另外的一个线程调用了相同对象的notify()方法,那么 仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的同步队列(锁池)

被notify或notifyAll唤起的线程是有规律的,具体如下:

  1. 如果是通过notify来唤起的线程,那 先进入wait的线程会先被唤起来
  2. 如果是通过nootifyAll唤起的线程,默认情况是 最后进入的会先被唤起来,即LIFO的策略;

继续阅读:1. Java多线程之线程基础

深入理解Java多线程(1.1)- 进程和线程的区别
深入理解Java多线程(1.2)- 创建线程之Runnable和Thread区别
深入理解Java多线程(1.3)- Java线程的生命周期
深入理解Java多线程(1.4)- Java线程之start和run区别
深入理解Java多线程(1.5)- Java线程相关方法(1)
深入理解Java多线程(1.6)- Java线程相关方法(2)

回到目录:深入理解Java多线程 - 教程系列详解

(链接:https://www.jianshu.com/p/468c660d02da)

素课网
  • 本文由 发表于 2020年9月9日14:21:29
  • 转载请注明:https://www.suketech.com/9909.html
匿名

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: