java多线程基础

为什么使用多线程

可以最大限度地利用CPU的空闲时间来处理其它任务。异步处理不同的任务,提高任务处理效率。

线程的五种状态

1、新建状态(New):线程对象创建后,就进入新建状态。
2、就绪状态(Runnable):就绪状态又称可执行状态,线程被创建后通过调用start()方法,从而启动线程。就绪状态的线程,随时有可能被CPU调度运行。
3、运行状态(Running):线程获取CPU权限进行执行。只有就绪状态的线程才能进入运行状态。
4、阻塞状态(Blocked):线程因为某种原因放弃CPU使用权,停止运行。直到线程进入就绪状态,才可以再到运行状态。
阻塞状态三种情况:
(1)、等待阻塞:通过调用线程的wait()方法,让线程等待某工作完成
(2)、同步阻塞:线程获取同步锁synchronized同步锁失败(因为锁正在被其它线程使用),进入同步阻塞。
(3)、其它阻塞:通过调用线程的sleep()、join()或发出I/O请求,线程进入阻塞状态。当sleep()状态超时、join()等待终止或超时、或者I/O处理完毕时,线程重新进入就休状态。
5、死亡状态(Dead):线程正常直行完成或者因为异常原因退出run()方法,该线程生命周期结束。

Paste_Image.png

通过Thread和Runnable创建线程

Java的JDK开发包中,已经自带了对多线程技术的支持,我们可以很方便的进行多线程编程。
实现多线程编程的方式主要有两种,一种是继承Thread类,另一种就是实现Runnable接口。而Thread和Runnable的关系就是Thread类实现了Runnable接口:

public class Thread implements Runnable {}

1、Runnable实现

java.lang.Runnable是一个接口,里面只定义了run()抽象方法。如果要实现多线程,可以实现Runnable接口,然后通过Thread thread = new Thread(new Xxx()),其中Xxx是实现Runnable接口的类。

public interface Runnable {
  public abstract void run();
}

Runnable方式实现多线程

public class RunnableTest implements Runnable{
    int num = 10;
    @Override
    public void run() {
        for(int i=0;i<20;i++){
            if(this.num > 0){
                System.out.println(Thread.currentThread().getName()+" num:" +this.num-- );
            }
        }
    }
}
class Test{
    public static void main(String[] args){
        RunnableTest runnable = new RunnableTest();
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        Thread thread3 = new Thread(runnable);
        thread1.start();//只有start()后才进入就绪状态
        thread2.start();
        thread3.start();
    }
}

运行结果

  • Thread-0 num:10
  • Thread-0 num:7
  • Thread-2 num:8
  • Thread-1 num:10
  • Thread-1 num:9
  • Thread-1 num:4
  • Thread-1 num:3
  • Thread-1 num:2
  • Thread-1 num:1
  • Thread-2 num:5
  • Thread-0 num:6

结论:三个线程共享num变量,共同对num相减十次。这种情况也是出现“非线程安全的”原因,两个线程可能同时获取了同一变量值,同时进行操作,比如上面的线程0和线程1都打印了10。

2、Thread实现

java.lang.Thread是一个类,实现了Runnable接口。如果要实现多线程,需要继承Thread类,然后通过创建实现类对象来启动线程。
Thread方式实现多线程

class ThreadTest extends Thread{
    int num = 10;
    @Override
    public void run() {
        for(int i=0;i<20;i++){
            if(this.num > 0){
                System.out.println(this.getName() + " num:" + this.num--);
            }
        }
    }
}
class Test{
    public static void main(String[] args){
        ThreadTest threadTest1 = new ThreadTest();
        ThreadTest threadTest2 = new ThreadTest();
        ThreadTest threadTest3 = new ThreadTest();
        threadTest1.start();
        threadTest2.start();
        threadTest3.start();
    }
}

运行结果

  • Thread-0 num:10
  • Thread-0 num:9
  • Thread-0 num:8
  • Thread-0 num:7
  • Thread-0 num:6
  • Thread-2 num:10
  • Thread-2 num:9
  • Thread-1 num:10
  • Thread-1 num:9
  • Thread-0 num:1
  • ....

结论:通过继承Thread类创建的线程,每个线程直接不会共享变量。每个线程都会各自对num进行10次相减。
实际上因为Thread实现了Runnable接口,所以我们也可以使用new Thead(theadTest)形式来启动线程,这样得到结果和实现Runnable接口结果时一样的,因为所有线程都共享了同一个theadTest的num变量。

还有一个需要注意的点,就是如果多次调用start()方法,则会抛出异常:Exception in thread "main" java.lang.IllegalThreadStateException。

Thread对象交由其它线程执行

Thread的构造方法可以接收实现了Runnable接口的线程,而Thread类是Runnable接口的实现类,所以我们可以将一个Thread对象交由另一个线程执行,也就是另一个线程指向这个Thread实现类的run方法。
在说明这个问题前我们先介绍下Thread类的介个方法:

  • currentThread:返回当前代码块被哪个线程执行。
  • getName:返回当前线程名称。
  • isAlive():判断当前线程是否处于活动状态,活动状态是指线程已经启动并且还没有执行完成(就绪状态、运行状态和阻塞状态)。

下面是将线程1直接交由线程2来执行:

public class ThreadDemo {
    public static void main(String[] args) {
        //创建线程1
        ThreadTest thread1 = new ThreadTest();
        thread1.setName("rename thread1");
        //创建线程2,并将线程1传递给线程2
        Thread thread2 = new Thread(thread1);
        thread2.setName("rename thread2");
        thread2.start();
    }
}

class ThreadTest extends Thread {

    public ThreadTest() {
        System.out.println("====1:" + Thread.currentThread().getName());
        System.out.println("====1:" + Thread.currentThread().isAlive());
        System.out.println("====2:" + this.getName());
        System.out.println("====2:" + this.isAlive());
    }

    @Override
    public void run() {
        System.out.println("====3:" + Thread.currentThread().getName());
        System.out.println("====3:" + Thread.currentThread().isAlive());
        System.out.println("====4:" + this.getName());
        System.out.println("====4:" + this.isAlive());
    }
}

上面程序执行结果:

====1:main
====1:true
====2:Thread-0
====2:false
====3:rename thread2
====3:true
====4:rename thread1
====4:false

以====1开头的打印内容是在ThreadTest构造方法中打印的,而调用ThreadTest构造方法是在main进程中的main方法中调用的,所以Thread.currentThread()表示的是main进程(因为ThreadTest代码块当前被main进程执行)。那====2的打印结果又说明什么呢,我们先看一下this.getName()好Thread.currentThread().getName()的区别。

Thread.currentThread()是指执行当前代码块的线程,所以Thread.currentThread().getName()给出的是执行当前代码块的线程名称。而this是指当前线程对象,上面的例子也就是ThreadTest的实例,所以this.getName()给出的是当前对象的线程名称。

通过上面的说明我们就知道了====2实际打印的是ThreadTest的一个实例,Thread-0是当我们创建一个Thread实例时,Java为我们提供的默认线程名称,并且Thread-0此时并没有运行(当前是main进程在执行)。
====3和=====4是在ThreadTest的run方法定义的,当启动thread2线程时候会执行该run方法。====3同样指的是运行当前代码块的线程,也就是"rename thread2"(因为我们在启动thread2时为其重新定义名称),并且thread2处于运行状态(在执行当前代码块)。而====4的this此时还是值得ThreadTest的实例,也就是thread1线程,thread1也进行了重命名"rename thread1"(调用构造方法时还没有进行重命名,所以当时的名称是默认名称Thread-0),并且当前thread1线程并没有执行,thread1中的run方法是交由thread2线程执行的。

注意当前执行线程(Thread.currentThread())和当前线程对象(this)的区别。但是要清楚this.currentThread()和Thread.currentThread()是一样。

上面的实例就说明:线程被创建后可以不执行,而是交由其它线程执行。其它线程启动后,就会调用这个被创建的线程的run方法。

3、Runnable和Thread区别

  • 通过实现Runnable接口能够解决单继承问题,如果实现Thread类则不能继承其它类了。
  • 通过Runnable接口方式实现多线程,能够起到资源共享的目的,因为多个线程共用一个Runnable实现类,而Thread是继承Runnable接口,每个Thread都会实现各自的Runnable接口。这种说法也不绝对,因为我们也可以将同一个Thread的实现类交由new Thread(Runnable target)来达到共享变量的目的。

4、Thread中start()和run()方法

start()和run()都是Thread中的方法,start()会启动一个新线程,新线程会去执行相应的run()方法,start()不能重复调用。run()和普通成员方法一样,单独调用run()方法,不会启动新线程,而是使用当前的线程执行run()方法(哪个线程调用的run方法,就由哪个线程来执行),这个run()和普通方法一样,可以被重复调用。
run()源代码:

@Override
public void run() {
    if (target != null) {
        //交由指定的线程来执行定义的run方法
        target.run();
    }
}

target是一个Runnable对象,run()方法直接调用Thread线程的Runnable的run()方法,并不会新建一个线程。

start()源代码:

public synchronized void start() {
    
    if (threadStatus != 0)//如果线程不是处于就绪状态,抛出异常
        throw new IllegalThreadStateException();
    group.add(this);//将当前线程添加到线程组里

    boolean started = false;
    try {
        start0();//启动线程
        started = true;//启动标志位
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);//启动失败,添加到失败队列里
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
              it will be passed up the call stack */
        }
    }
}

private native void start0(); //启动线程的本地方法

start()通过调用本地start0()方法,启动一个线程,新线程会调用run()方法。

线程常用方法

sleep()

Thread.sleep()的作用是“当前正在执行的线程”休眠指定时间,这个当前正在执行的线程指的是Thread.currentThread()。

getId()

this.getId()用于获取线程的唯一标识。

yield()方法

yield()方法的作用是让当前线程放弃CPU的使用权,让其它任务执行。但是放弃的时间不固定,有可能马上又获取到CPU时间片。
测试下面代码,查看注释Thread.yield()前后的执行耗时。

class YieldThread extends Thread {
    @Override
    public void run() {
        int count = 0;
        long beginTime = System.currentTimeMillis();
        for(int i= 0; i < 1000000; i++){
            //Thread.yield();
            count = count + i + 1;
        }
        long endTime = System.currentTimeMillis();
        System.out.println("耗时" + (endTime - beginTime) + "毫秒!");
    }
}
//注释Thread.yield()打印结果:
耗时6毫秒!
//打开注释
耗时820毫秒!

停止线程

Java中有以下三种方式来停止正在运行的线程。

  1. 使用退出标志,让线程正常退出,也就是执行完了run方法后自动停止。
  2. 使用Thread类提供的stop()、suspend()或resume(),但是这三种方法是不建议使用的,因为它们可能产生不可预期的结果(可能一些清理工作得不到完成;对一些锁定对象进行了“解锁”,导致得不到同步处理,从而出现数据不一致的问题),并且这三个方法已经被标记为废弃了。
  3. 使用interrupt方法来中断线程,进而手动实现停止当前线程。

对于第一点没什么说的,通过判断退出标志位来退出run方法。第二点中Thread中自带的stop()和suspend()由于不安全(从JDK2,开始不建议使用),所以已经过时不在建议使用了。线程中断是目前停止线程的通用方式,也是我们这里要说的方式。

线程中断方法

使用interrupt()方法并不会像使用for-break那样立即退出循环,调用interrupt()方法只是将当前线程打了一个标记,也就是中断标记。我们还需要加入一个判断逻辑,来手动停止当线程。
Thread类提供了两个方法来判断当前线程中断标记:

  • Thread.interrupted():判断当前线程是否已经中断,当前线程指的是当前正在运行的线程(如果使用threadInstance.intrrupted()方式则指的是当前线程对象,有可能不是正在运行的线程)。Thread.interrupted()除了具有判断线程是否中断的功能,它还会清除线程中断标记。也就是说如果当前线程中断状态为true,那么调用过interrupted后线程中断状态重新恢复到false。
  • isInterrupted():判断线程对象(有可能不是正在运行的线程)是否已经中断,该方法不是静态方法,所以不能通过Thread.currentThread调用。

注意isInterrupted和interrupted的区别:所以只能被线程对象调用,这时候线程对象有可能并没有运行;另一个重要的区别在于isInterrupted方法不会清楚中断状态。

停止运行状态的线程

停止运行中的线程,我们可以通过:判断线程中断标记+手动抛出异常的方式来停止线程。

class ThreadDemo extends Thread {
    @Override
    public void run()  {
        try {
            while (true){
                if(this.isInterrupted())
                    throw new InterruptedException();
                
                //执行业务逻辑
            }
        }catch (InterruptedException e) {
            System.out.println("进入Catch代码块,run方法执行完成,退出异常");
        }
    }
}
public class ThreadTest {
    public static void main(String[] args) {
        ThreadDemo  threadDemo = new ThreadDemo();
        threadDemo.start();
        //中断线程标记
        threadDemo.interrupt();
    }
}

注意try-catch需要在while循环外部,否则不会退出run方法。

停止阻塞状态的线程

停止阻塞状态的线程比较容易,因为它不需要我们判断中断标记。当对阻塞状态中的线程调用interrupt()方法时,会自动抛出InterrupterdException异常,并且会清除中断标记状态值,也就是变为false。

public class ThreadTest {
    public static void main(String[] args) {
        ThreadDemo  threadDemo = new ThreadDemo();
        threadDemo.start();
        threadDemo.interrupt();
    }
}

class ThreadDemo extends Thread {
    @Override
    public void run()  {
        try {
            while (true){
                //业务执行逻辑
                
                Thread.sleep(10000);
            }
        }catch (InterruptedException e) {
            System.out.println("进入Catch代码块,run方法执行完成,退出异常");
        }
    }
}

需要注意try-catch在代码中的维值,如果将异常处理放在while()中,这样while(true)不会被停止。

暂停线程

暂停线程是指此线程能够暂时停止,之后还能恢复运行。我们可以使用Thread类中的suspend()方法来暂停线程,使用resume()方法来恢复线程执行。

需要注意:suspend()、resume()方法和 stop()方法都已经废弃了,因为它们有可能产生不可预知的后果

suspend()和resume()使用实例:

class MyThread extends Thread {
    private long i = 0;

    public long getI() {
        return i;
    }
    @Override
    public void run() {
        while (true)
            i++;
    }
}
public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();
        myThread.start();
        Thread.sleep(3000);
        myThread.suspend();
        System.out.println("i:" + myThread.getI());
        Thread.sleep(3000);
        System.out.println("i:" + myThread.getI());
        myThread.resume();
        Thread.sleep(3000);
        myThread.suspend();
        System.out.println("i:" + myThread.getI());
        Thread.sleep(3000);
        System.out.println("i:" + myThread.getI());
    }
}
打印结果:
i:1629776063
i:1629776063
i:3296163993
i:3296163993

从上面的打印结果可以看到,线程确实暂停了,之后又重新恢复了。之所以将它们废弃,主要是使用suspend()和resume()极易造成公共同步对象独占,导致其它线程无法访问公共对象。

synchronized void printString(String str) {
  if(Thread.currentThread().getName().equals("TheadA")) {
    System.out.println("被线程A独占");
    Thread.currentThread().suspend();
  }
}

除了上面问题,suspend()和resume()方法如果使用不当,也会造成数据不同步,所以我们应该避免使用这两个方法。

synchronized同步锁

原理:每个对象都有且只有一个同步锁,同步锁依赖于对象存在。当调用某个对象的synchronized方法时,就获取该对象的同步锁。例如synchronized(obj)就是获取了obj的同步锁,不同线程对同步锁访问是互斥的。就是说一个时间点,对象的同步锁只能被一个线程调用,其它线程如果要使用,需要等待正在使用同步锁的线程释放掉后才能使用。

synchronized规则:

  • 当一个线程访问某对象的synchronized方法或synchronized代码块时,其它线程对该对象的该synchronized方法或者synchronized代码块访问将被受阻;
  • 当一个线程访问某个对象的synchronized方法或synchronized代码块时,其它线程可以访问该对象的非synchronized方法或synchronized代码块;
  • 当一个线程访问某个对象的synchronized方法或synchronized代码块时,当其它对象访问该对象的synchronized方法或synchronized代码块时,其它对象的线程将被受阻。
public class RunnableTest implements Runnable{
    @Override
    public void run() {
        synchronized(this){
            for(int i=0;i<3;i++){
                System.out.println(Thread.currentThread().getName()+" num:" + i );
            }
        }
    }
}

class Test{
    public static void main(String[] args){
        RunnableTest runnable = new RunnableTest();
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        Thread thread3 = new Thread(runnable);
        thread1.start();//只有start()后才进入就绪状态
        thread2.start();
        thread3.start();
    }
}

运行结果

  • Thread-0 num:0
  • Thread-0 num:1
  • Thread-0 num:2
  • Thread-2 num:0
  • Thread-2 num:1
  • Thread-2 num:2
  • Thread-1 num:0
  • Thread-1 num:1
  • Thread-1 num:2

结论:
当一个线程访问对象的synchronized方法或者代码块,其它线程将会被阻塞。Thread0、Thread1、Thread2共用RunnableTest实现Runnable接口的同步锁,当一个线程运行synchronized()代码块时候,其它线程需要等待正在运行的线程释放同步锁后才能运行。

再来看下使用Thread方式实现多线程的获取同步锁的执行流程

class ThreadTest extends Thread{
    int num = 10;
    @Override
    public void run() {
        synchronized(this){
            for(int i=0;i<3;i++){
                System.out.println(Thread.currentThread().getName()+" num:" + i );
            }
        }

    }
}

class Test{
    public static void main(String[] args){
        ThreadTest threadTest1 = new ThreadTest();
        ThreadTest threadTest2 = new ThreadTest();
        ThreadTest threadTest3 = new ThreadTest();
        threadTest1.start();
        threadTest2.start();
        threadTest3.start();
    }
}

运行结果

  • Thread-0 num:0
  • Thread-0 num:1
  • Thread-0 num:2
  • Thread-2 num:0
  • Thread-1 num:0
  • Thread-2 num:1
  • Thread-1 num:1
  • Thread-2 num:2
  • Thread-1 num:2

结论:
发现并没有我们之前说的Thread0、Thread1、Thread2阻塞顺序执行,这个主要是和Thread形式创建多线程有关,trhreadTest1、trhreadTest2、trhreadTest3是三个不同的对象,它们是通过new ThreadTest()创建的三个对象,这里synchronized(this)是指的ThreadTest对象,所以threadTest1、threadTest2、threadTest3是获取的三个不同的同步锁。而上面使用RunnableTest方式实现的多线程,this是指的RunnableTest,这样三个线程使用的是同一个对象的同步锁。

当一个进程访问对象的同步锁时,其它线程可以访问这个对象的非synchronize代码块

class ThreadTest2{
    public void synMethod(){
        synchronized (this){
            for(int i=0;i<3;i++){
                System.out.println(Thread.currentThread().getName() + " num: " + i);
            }
        }
    }

    public void nonSynMethod(){
        for(int i=0;i<3;i++){
            System.out.println(Thread.currentThread().getName() + " num:" + i);
        }
    }
}
class Test{
    public static void main(String[] args){
        final ThreadTest2 threadTest = new ThreadTest2();
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                threadTest.synMethod();
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                threadTest.nonSynMethod();
            }
        });
        thread1.start();
        thread2.start();
    }
}

返回结果

  • Thread-0 num:0
  • Thread-0 num:1
  • Thread-1 num:0
  • Thread-0 num:2
  • Thread-1 num:1
  • Thread-1 num:2

结论:
thread1访问对象的synchronize代码块,thread2访问非synchronized代码块。thread2并没有因为thread1受阻。

当一个线程访问一个对象的synchronized方法或代码块,其它线程访问这个对象的其它synchronized也是受阻的。

class ThreadTest2{
    public void synMethod1(){
        synchronized (this){
            for(int i=0;i<3;i++){
                System.out.println(Thread.currentThread().getName() + " num: " + i);
            }
        }
    }

    public void synMethod2(){
        synchronized (this){
            for(int i=0;i<3;i++){
                System.out.println(Thread.currentThread().getName() + " num:" + i);
            }
        }
    }
}
class Test{
    public static void main(String[] args){
        final ThreadTest2 threadTest = new ThreadTest2();
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                threadTest.synMethod1();
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                threadTest.synMethod2();
            }
        });
        thread1.start();
        thread2.start();
    }
}

返回结果

  • Thread-0 num:0
  • Thread-0 num:1
  • Thread-0 num:2
  • Thread-1 num:0
  • Thread-1 num:1
  • Thread-1 num:2

结论:thread1、thread2都会调用ThreadTest2的synchronized(this)代码块,而这个this都是ThreadTest2,所以线程2需要等到线程1执行完synchronized才能执行。

synchronized方法和synchronized代码块

synchronized方法是用synchronized修饰类方法,synchronized代码块是用synchronized修饰代码块的。synchronized代码块可以更精准的控制限制区域,有时效率也是比synchronized方法高的。

class ThreadTest2{
    public synchronized void synMethod1(){
            for(int i=0;i<3;i++){
                System.out.println(Thread.currentThread().getName() + " num: " + i);
            }
    }

    public void synMethod2(){
        //this获取当前对象的同步锁,如果修改成xxx,则获取xxx的同步锁
        synchronized (this){
            for(int i=0;i<3;i++){
                System.out.println(Thread.currentThread().getName() + " num:" + i);
            }
        }
    }
}

实例锁和全局锁

  • 实例锁:如果锁在某一个实例上面,那么该锁就是实例锁。如果这个类是单例,那么这个锁也具有全局锁的概念。实例锁使用synchronized关键字。
  • 全局锁:如果锁针对的是一个类上面,无论多少个实例共享这个锁。全局锁使用static synchronized,或者锁在该类的class或classloader上面。
class SomeLock{
    public synchronized void intanceLockA(){}
    public synchronized void instanceLockB(){};
    public static synchronized void globalLockA(){};
    public static synchronized void globalLockB(){};
}
  1. x.instaceLockA()和x.instanceLockB(),二者不能同时被访问,因为二者都是访问的都是x的实例锁。
  2. x.instaceLockA()和y.instaceLockA(),二者可以同时被访问,因为二者访问的不是同一个对象的锁。
  3. x.globalLocckA()和y.globalLockB(),二者不能同时访问,因为y.globalLockB()相当于SomeLock.globalLockB(),x.globalLockA()相当于SomeLock.globalLockA(),二者使用的是同一个同步锁,所以不能同时被访问。
  4. x.instaceLocakA()和b.globalLockA(),二者可以同时被访问,因为一个是示例的锁,一个是类的锁。

线程的等待与唤醒

线程的等待与唤醒使用了Object类中的wait()、wait(long timeout)、wait(long timeout,int nanos)、notify()、notifyAll()

  • wait():使线程进入等待状态(等待阻塞),直到其它线程调用该对象的 notify()或notifyAll(),当前线程会被唤醒(进入就绪状态)。
  • wait(long timeout):使线程进入等待状态(等待阻塞),直到其它线程调用该对象的notify()或notifyAll()或超过了指定时间,当前线程会被唤醒(进入就绪状态)。
  • wait(long timeout,int nanos):使线程进入等待状态(等待阻塞),直到其它线程调用该对象的notify()或notifyAll()或超过了指定时间或被其它线程中断,当前线程会被唤醒(进入就绪状态)。
  • notify():唤醒在此对象监视器(同步锁的实现原理)上等待的单个线程。
  • notifyAll():唤醒在此对象监视器上等待的多个线程。

注意:
wait()的作用是让当前线程等待,当前线程指的是正在cpu运行的线程,而不是调用wait()方法的线程。wait()、notify()、notifyAll()都是属于Object类下边的方法,之所以在Object下面而没有在Thread类下面,主要原因就是同步锁。

wait()和notify()都是对对象的同步锁进行操作,同步锁是对象持有的,并且每个对象有且仅有一个。

线程让步yield()

yield()的作用是让步。让当前线程由运行状态进入就绪状态,从而让其他具有高优先级的线程获取cpu执行。但是并不会保证当前线程调用yield()后,其它同等级线程一定获取到cpu执行权。也有可能当前线程又进入到运行状态。

yield()与wait()的区别

  • wait()会由运行状态进入等待状态(阻塞状态),而yield()会从运行状态进入就绪状态。
  • wait()会释放对象的同步锁,而yield()是不会释放对象的同步锁的。

线程休眠sleep()

sleep()在Thread.class类中定义,让当前线程由运行状态进入休眠状态(阻塞状态)。sleep()需要指定休眠时间,线程休眠时间会大于等于该休眠时间;线程被重新唤醒时会由阻塞状体进入就绪状态。

sleep()与wait()区别

sleep()和wait()都会让线程由运行状态进入阻塞状态,但是wait()会释放对象同步锁,而sleep()不会释放同步锁。

线程join()

join()在Thread.class类中定义,让主线程等待子线程结束后才能继续运行。

// 主线程
public class Father extends Thread {
    public void run() {
        Son s = new Son();
        s.start();
        s.join();
        ...
    }
}
// 子线程
public class Son extends Thread {
    public void run() {
        ...
    }
}

Son线程是在Father线程中创建的,并且调用了s.join(),这样Father线程要等到Son线程执行完成后,才会执行。可以查看下join()的源代码:

public final void join() throws InterruptedException {
    join(0);
}
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()进行等待,所以即使是子线程调用的join(),而真实等待的是正在执行的父进程。

线程优先级

在操作系统中,线程可以划分优先级,优先级较高的线程被CPU优先执行。设置线程优先级就是帮助“线程规划器”确定下次选哪一个线程来优先执行。
java中线程优先级从1~10,默认是5。Thead类提供了3个常量预定义优先级的值:

    public final static int MIN_PRIORITY = 1;
    public final static int NORM_PRIORITY = 5;
    public final static int MAX_PRIORITY = 10;

线程优先级具有继承性,这里的继承性是指比如在A线程中启动B线程,那么B线程就有与A线程同样的优先级。
需要注意的是,高优先级的线程总是大部分都会先执行完成,但是并不代表高优先级的线程执行完成后,再去执行低优先级的线程。高优先级,只说明CPU尽量将执行资源给优先级比较高的线程。

守护线程

在Java线程有两种线程,非守护线程(又称为用户线程)和守护线程(Daemon)。
守护线程是一种特殊的线程,它的特性就是陪伴,当进程中不存在非守护线程了,则守护线程自动销毁。比如GC线程就是典型的守护线程,当进程中没有非守护线程了,则垃圾回收线程自动销毁。
比如下面测试用例:

class DaemonThread extends Thread {
    @Override
    public void run() {
        int i =0;
        try {
            while (true) {
                System.out.println(i++);
                Thread.sleep(1000);
            }
        }catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        DaemonThread daemonThread = new DaemonThread();
        daemonThread.setDaemon(true);
        daemonThread.start();
        Thread.sleep(5000);
        System.out.println("main线程退出,daemonThread也不会执行了");
    }
}
打印结果:
0
1
2
3
4
main线程退出,daemonThread也不会执行了

因为DaemonThread是守护线程,而main线程为非守护线程,当main线程退出后,daemonThread也会退出。

关注我

欢迎关注我的公众号,会定期推送优质技术文章,让我们一起进步、一起成长!
公众号搜索:data_tc
或直接扫码:🔽


欢迎关注我
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • 写在前面的话: 这篇博客是我从这里“转载”的,为什么转载两个字加“”呢?因为这绝不是简单的复制粘贴,我花了五六个小...
    SmartSean阅读 4,709评论 12 45
  • 前言 多线程并发编程是Java编程中重要的一块内容,也是面试重点覆盖区域,所以学好多线程并发编程对我们来说极其重要...
    嘟爷MD阅读 7,303评论 21 272
  • 进程:正在执行中的程序,其实是应用程序在内存中运行的那片空间。 线程:进程中的一个执行单元,负责进程中程序的执行。...
    七弦桐语阅读 459评论 2 7
  • 简介 本次主要介绍java多线程中的同步,也就是如何在java语言中写出线程安全的程序。如何在java语言中解决非...
    小人物灌篮阅读 468评论 0 1
  • 吃完pizza之后,发过朋友圈之后,一阵阵的大笑之后,又剩下了什么呢,六个可爱的室友,在一起笑,在一起嗨,哈哈大笑...
    Janice1阅读 203评论 0 0