多线程理解(一)

多线程理解二
线程安全、锁
并发工具类

线程、进程、并发、并行

1、线程与进程

  • 线程也被称作轻量级进程,线程是进程的执行单元;
  • 线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程;

2、并发与并行

  • 并发:指在同一时刻只能有一条指令执行,但多个指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果;
  • 并行:指在同一时刻,有多条指令在多个处理器上同时执行;
    在这里插入图片描述

3、为什么要使用多线程?

现在大多数计算机处理器都是多核,而一个线程在同一时刻只能运行在一个处理器核心上。试想一下,一个单线程程序在运行时只能使用一个处理器核心,那么再多的处理器核心加入也无法显著提升该程序的效率。

相反,使用多线程,将计算逻辑分配到多个处理器核心上,就会显著减少程序的处理时间!

4、多线程调度方式

现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完了就会发生线程调度,并等待着下次分配。

5、上下文切换(有开销的,并不是越多线程越好)

即使是单线程处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒;

CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换;

创建线程的两种方式

1、继承Thread类创建线程类

通过继承Thread类来创建并启动多线程的步骤如下:

  1. 定义Thread类的子类,并重写该类的run()方法。把run()方法称为线程执行体;
  2. 创建Thread子类的实例,即创建线程对象;
  3. 调用线程对象的start()方法来启动该线程;
public class Test7 extends Thread{
    @Override
    public void run() {
        //当线程类继承Thread类时,可以直接调用getName()方法来返回当前线程的名。
        System.out.println(getName());
    }

    public static void main(String[] args) {
        //调用Thread的currentThread方法获取当前线程
        System.out.println(Thread.currentThread().getName());
        new Thread(new Test7()).start();
    }
}
//控制台打印
main
Thread-0

在这里插入图片描述
另外,Thread.java类也实现了Runnable接口:

public
class Thread implements Runnable {
    /* Make sure registerNatives is the first thing <clinit> does. */
    private static native void registerNatives();
    static {
        registerNatives();
    }

这意味着构造函数Thread(Runnable target)不仅可以传入Runnable接口的对象,还可以传入一个Thread对象,这样做完全可以将一个Thread对象中的run()方法交由其他线程执行:

class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("MyThread");
    }
}
public class Test7{
    public static void main(String[] args) {
        new Thread(new MyThread()).start();
    }
}
//控制台打印MyThread

2、实现Runnable接口创建线程类

通过实现Runnable接口来创建并启动多线程的步骤如下:

  1. 定义Runnable接口的实现类,并重写该接口的run()方法;
  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象;
  3. 调用线程对象的start()方法来启动线程;
public class Test7 implements Runnable
{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        //调用Thread的currentThread方法获取当前线程
        System.out.println(Thread.currentThread().getName());
        new Thread(new Test7()).start();
        
        //设定线程名字
        //new Thread(new Test7(),"新线程").start();
    }
}
//控制台打印
main
Thread-0

3、创建线程的两种方式对比

采用实现Runnable接口的方式:

  • 还可以继承其他类;
  • 这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况;
  • 如需访问当前线程,则必须使用Thread.currentThread()方法;

采用继承Thread类的方式:

  • 已经继承了Thread类,所以不能再继承别的类;
  • 如需访问当前线程,直接使用this即可获得当前线程;

推荐使用接口的形式;

4、继承Thread类不共享数据,而实现Runnable接口共享数据的原因

    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

查看Thread.java源码可知,当target不为null时则执行的是target的run()方法。所以当我们使用实现Runnable接口的方式时,因为传入的是同一个的target对象,所以都是执行的同一个方法,数据是共享的。

而继承Thread类的方式,由于我们重写了父级的run()方法,所以线程会执行当前类的run()方法,并且每个线程的变量都是独立的(new 创建出来的对象都是独立的)!

4、对同一线程多次调用start()方法会怎样?

public class Test implements Runnable {

    @Override
    public void run() {
        System.out.println("Test");
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new Test());
        thread.start();
        thread.start();
    }
}

运行控制台会报错:
在这里插入图片描述
这是因为start()方法会在调用开始前检查当前线程的状态。线程创建时默认状态为0,当调用start()方法后,线程状态被修改,所以再次调用start()方法会报错;

    /* Java thread status for tools,
     * initialized to indicate thread 'not yet started'
     */
    private volatile int threadStatus = 0;
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

5、容易混淆的创建线程的几种方式?
在这里插入图片描述
网上对于创建线程的几种方式各有不同,有说两种、四种、三种的?那么到底是几种呢?

    /* What will be run. */
    private Runnable target;

    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

我们查看Thread源代码可知,其实本质上就两种,一种是继承Thread,重写run方法的话,那么Thread原本的run方法则不再存在,我们调用的就是我们重写后的方法。

还有一种就是实现Runnable并重写run方法,此种方式的话,我们传入了Runnable对象,如上代码进行判断,target不为空,则执行了target的run方法;

查看网上其它的创建方式,其实本质上都是调用的我们最基本的两种创建方式,只是对他们进行了封装!

6、如果同时使用两种创建方式运行多线程会出现什么结果?

public class Test{
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("我来自Runnable");
            }
        }){
            @Override
            public void run() {
                System.out.println("我来自Thread");
            }
        }.start();
    }
}

控制台打印:我来自Thread;

这是为什么呢?这是因为我们重写了Thread的run()方法,所以即使我们传入了Runnable对象,但是下面的代码已经不存在了。最终是直接执行我们所重写的run()方法

    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

详细分析 Java 中实现多线程的方法有几种?(从本质上出发)

7、执行start()的顺序不代表执行run()的顺序

Thread.java类中的start()方法通知“线程规划器”此线程已经准备就绪,准备调用线程对象的run()方法。这个过程其实就是让系统安排一个时间来调用Thread中的run()方法,即让线程执行具体的任务,具有随机顺序执行的效果!

多线程随机输出的原因是CPU将时间片(时间片即CPU分配给各个程序的时间)分给不同的线程,线程获得时间片后就执行任务,所以这些线程在交替地执行并输出,导致输出结果呈乱序的效果!

线程的生命周期

java.lang.Thread定义了一个内部枚举State,如下:

public enum State {
    NEW,

    RUNNABLE,

    BLOCKED,

    WAITING,

    TIMED_WAITING,

    TERMINATED;
}

可以知道Java线程有六个状态:NEW(新建)、RUNNABLE(可运行)、BLOCKED(阻塞)、WAITING(等待)、TIMED_WAITING(计时等待)、TERMINATED(终止);

线程状态之间的转换关系如下:
在这里插入图片描述

当程序使用new关键字创建了一个线程之后,该线程就处于NEW(新建)状态,此时它和其他的Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体;

当线程对象调用了start()方法后,该线程处于RUNNABLE(可运行)状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度;
在这里插入图片描述
调用线程对象的start()方法之后,该线程立即进入RUNNABLE状态-----RUNNABLE状态相当于等待执行,但该线程并未真正进入运行状态;
在这里插入图片描述

1、NEW、RUNNABLE、TERMINATED状态演示

public class Test implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(i);
        }

    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Test());
        System.out.println(thread.getState());

        //调用start()方法转为RUNNABLE状态,此状态并不会立马执行,至于该线程何时开始运行,取决于JVM里线程调度器的调度
        thread.start();
        System.out.println(thread.getState());

        //休眠10毫秒让程序正常走完,线程执行完为TERMINATED状态
        Thread.sleep(10);
        System.out.println(thread.getState());
    }
}

//控制台打印
NEW
RUNNABLE
0
1
...
99
TERMINATED

2、WAITING、TIMED_WAITING、BLOCKED状态演示

public class Test implements Runnable {

    @Override
    public void run() {
        sync();
    }

    private synchronized void sync(){
        try {
            Thread.sleep(1000);
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Test runnable = new Test();
        Thread thread1 = new Thread(runnable);
        thread1.start();
        Thread thread2 = new Thread(runnable);
        thread2.start();

        //避免主线程执行太快,休眠50毫秒再打印状态
        Thread.sleep(50);
        System.out.println(thread1.getState());
        System.out.println(thread2.getState());

        //同步方法里子线程休眠1000毫秒,那么这里主线程需要休眠超过1000毫秒,等待子线程代码走到wait()再打印状态
        Thread.sleep(1500);
        System.out.println(thread1.getState());

    }
}
//控制台打印
TIMED_WAITING
BLOCKED
WAITING

一般习惯而言,我们通常会把WAITING、TIMED_WAITING、BLOCKED统称为阻塞状态!

各API对应线程状态切换如图所示:
在这里插入图片描述

interrupt()、interrupted()、isInterrupted()

中断可以理解为线程的一个标识属性,线程通过检查自身是否被中断来进行响应!

1、interrupt()

public void interrupt()

中断线程,将会设置该线程的中断状态位,即设置为true。

如果线程在调用 Object 类的 wait()、wait(long) 或 wait(long, int) 方法,或者该类的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 方法过程中受阻,则其中断状态将被清除,它还将收到一个 InterruptedException。

换句话说就是:线程处于被阻塞状态(例如,sleep、wait、join)中断,那么线程将立即退出阻塞状态并抛出一个interruptedException异常!

2、interrupted()

public static boolean interrupted()

测试当前线程是否已经中断。线程的中断状态由该方法清除。

换句话说,如果连续两次调用该方法,则第二次调用将返回 false(在第一次调用已清除了其中断状态之后,且第二次调用检验完中断状态前,当前线程再次中断的情况除外)。

3、isInterrupted()

public boolean isInterrupted()

测试线程是否已经中断。线程的 中断状态 不受该方法的影响。

4、中断退出示例

public class MyThread800 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            if (Thread.currentThread().isInterrupted()){
                System.out.println("线程中断退出");
               break;
            }
            System.out.println("i:"+ i);
        }
        //中断后这一行仍会输出,因为并没有根据中断状态判断退出执行。
        System.out.println("--------");
        /*if (!Thread.currentThread().isInterrupted()){
            System.out.println("--------");
        }*/
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread800 thread800 = new MyThread800();
        thread800.start();
        Thread.sleep(10);
        thread800.interrupt();
    }
}
//控制台打印:
。。。
i:285
i:286
线程中断退出

或者通过抛异常的方式进行处理:

@Override
public void run() {
    try {
        for (int i = 0; i < 10000; i++) {
            if (Thread.currentThread().isInterrupted()){
                System.out.println("线程中断退出");
                throw new Exception();
            }
            System.out.println("i:"+ i);
        }
        System.out.println("--------");
    }catch (Exception e){
    }
}

wait()、notify()、notifyAll()

这三个方法并不属于Thread类,而是属于Object类。

wait():使当前执行wait()方法的线程等待,在wait()所在的代码行处暂停执行,并立即释放锁,直到接到通知或被中断为止!

wait(long):等待某一时间内是否有线程对锁进行notify()通知唤醒,如果超过这个时间则线程自动唤醒(前提是再次持有锁)!

public class Test6{
    private static Object object = new Object();
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            synchronized (object){
                System.out.println(Thread.currentThread().getName()+"获取到了锁");
                try {
                    //释放对象锁,释放cpu资源并进入等待。5s后自动唤醒
                    object.wait(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"被唤醒");
            }
        }).start();
    }
}
//控制台打印:
Thread-0获取到了锁
Thread-0被唤醒

notify():用来通知那些处于等待该锁的其他线程,如果有多个线程等待,则按照执行wait()方法的顺序对处于wait状态的线程发出一次通知,并使该线程重新获取锁!

注意:执行notify()方法后,当前线程不会马上释放锁,要等到执行notify()方法的线程执行完,也就是退出synchronized 同步区域后,当前线程才会释放锁,而呈wait状态的线程才可以获取相应对象锁!

1、使用notify唤醒等待线程

public class Test{

    private static Object object = new Object();

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            synchronized (object){
                System.out.println(Thread.currentThread().getName()+"获取到了锁");
                try {
                	//释放对象锁,释放cpu资源并进入等待
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"被唤醒");
            }
        }).start();
		
		//主线程休眠50毫秒,保证上面的线程先执行
        Thread.sleep(50);

        new Thread(() -> {
            synchronized (object){
                System.out.println(Thread.currentThread().getName()+"获取到了锁");
                object.notify();
                //执行notify()后并不是立即就释放锁,需要等线程后面的代码执行完
                Thread.sleep(4000)
            }
        }).start();

    }
}
//控制台打印----多次运行,唤醒的一定是最先执行wait()方法的线程
Thread-0获取到了锁
Thread-1获取到了锁
//。。。间隔了4S才打印下面的语句
Thread-0被唤醒

每调用一次notify()方法,只通知一个线程进行唤醒,唤醒的顺序与执行wait()方法的顺序一致!

2、notify与notifyAll的区别

public class Test2 implements Runnable{

    private static Object object = new Object();

    @Override
    public void run() {
        synchronized (object){
            System.out.println(Thread.currentThread().getName()+"获取到锁");
            try {
                //释放对象锁,释放cpu资源并进入等待
                object.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"被唤醒");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(new Test2()).start();
        new Thread(new Test2()).start();

        //主线程休眠50毫秒,保证上面的线程先执行
        Thread.sleep(50);

        new Thread(() -> {
            synchronized (object){
                //object.notify();
                object.notifyAll();
            }
        }).start();
    }

}
//控制台打印---notifyAll()唤醒的顺序永远都是执行wait()方法的倒序
Thread-0获取到锁
Thread-1获取到锁
Thread-1被唤醒
Thread-0被唤醒

notify()方法只通知一个线程进行唤醒,唤醒的顺序与执行wait()方法的顺序一致!

notifyAll()方法可以唤醒全部线程,notifyAll()方法会按照执行wait()方法的倒序依次对其他线程进行唤醒。

注意:唤醒的顺序是正序、倒序、随机取决于具体的JVM实现,也不是所有的JVM在执行notify()时都是按调用wait()方法的正序进行唤醒的,也不是所有的JVM在执行notifyAll()时都是按wait()方法的倒序进行唤醒的,具体的唤醒顺序依赖于JVM的具体实现!

3、wait只释放当前锁

public class Test3{
    private static volatile Object objectA = new Object();
    private static volatile Object objectB = new Object();

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            synchronized (objectA){
                System.out.println(Thread.currentThread().getName()+"获取到objectA锁");
                synchronized (objectB){
                    System.out.println(Thread.currentThread().getName()+"获取到objectB锁");
                    try {
                        objectA.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        Thread.sleep(1000);

        new Thread(() -> {
            synchronized (objectA){
                System.out.println(Thread.currentThread().getName()+"获取到objectB锁");
                synchronized (objectB){
                    System.out.println(Thread.currentThread().getName()+"获取到objectA锁");
                }
            }

        }).start();
    }
}
//控制台打印
Thread-0获取到objectA锁
Thread-0获取到objectB锁
Thread-1获取到objectB锁

4、为什么wait()、notify()、notifyAll()必须在同步方法/代码块中调用?

对于对象的同步方法/代码块来说,在任意时刻有且仅有一个拥有该对象独占锁的线程能够调用它们;

wait()方法强制当前线程释放对象锁,这意味着在调用某对象的wait()方法之前,当前线程必须已经获得该对象的锁。因此,线程必须在某个对象的同步方法或同步代码块中才能调用该对象的wait()方法;

当某线程调用某对象的notify()或notifyAll()方法时,任意一个(对于notify())或者所有(对于notifyAll())在该对象的等待队列中的线程,将被转移到该对象的入口队列。接着这些队列(可能只有一个)将竞争该对象的锁,最终获得锁的线程继续执行。如果没有线程在该对象的等待队列中等待获得锁,那么notify()和notifyAll()将不起任何作用。在调用对象的notify()和notifyAll()方法之前,调用线程必须已经得到该对象的锁。因此,必须在某个对象的同步方法或同步代码块中才能调用该对象的notify()或notifyAll()方法。

5、避免先执行notify后执行wait()

如果先执行notify后执行wait()的话,那么程序将永久等待,永远没人来唤醒wait()了!

public class Test6{
    private static Object object = new Object();
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            synchronized (object){
                System.out.println("唤醒了锁");
                object.notify();
            }
        }).start();
        new Thread(() -> {
            synchronized (object){
                System.out.println(Thread.currentThread().getName()+"获取到了锁");
                //释放对象锁,释放cpu资源并进入等待。5s后自动唤醒
                object.wait();
                System.out.println(Thread.currentThread().getName()+"被唤醒");
            }
        }).start();
    }
}
//控制台打印
唤醒了锁
Thread-1获取到了锁

程序将一直陷入等待状态!

join()、sleep()、yield()、isAlive()

1、join()

Thread提供了让一个线程等待另一个线程完成的方法—join()方法。当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的join()线程执行完成为止。

join()方法在内部使用wait()方法进行等待!

public class JoinThread extends Thread
{
    @Override
    public void run()
    {
        for (int i = 0; i < 100 ; i++ )
        {
            System.out.println(getName() + "  " + i);
        }
    }
    public static void main(String[] args) throws Exception
    {
        for (int i = 0; i < 100 ; i++ )
        {
            if (i == 20)
            {
                JoinThread jt = new JoinThread();
                jt.start();
                //main线程调用了jt线程的join方法,main线程
                //必须等jt执行结束才会向下执行
                jt.join();
            }
            System.out.println(Thread.currentThread().getName() + "  " + i);
        }
    }
}

控制台打印:
在这里插入图片描述
在这里插入图片描述
主线程等待子线程的终止。也就是说主线程的代码块中,如果碰到了t.join()方法,此时主线程需要等待,等待子线程结束了,才能继续执行t.join()之后的代码块;

2、sleep()

如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread类的静态sleep()方法来实现:

  • static void sleep(long millis):让当前正在执行的线程暂停millis毫秒,并进入阻塞状态;
  • static void sleep(long millis,int nanos):让当前正在执行的线程暂停millis毫秒加nanos毫微秒;

当当前线程调用sleep()方法进入阻塞状态后,在其睡眠时间段内,该线程不会释放锁,也不会获得执行的机会,即使系统中没有其他可执行的线程,处于sleep()中的线程也不会执行,因此sleep()方法常用来暂停程序执行;

public class TestSleep
{
    public static void main(String[] args) throws Exception
    {
		for (int i = 0; i < 10 ; i++ )
		{
			System.out.println("当前时间: " + new Date());
			//调用sleep方法让当前线程暂停1s。
			Thread.sleep(1000);
		}
    }
}

sleep(long mills):让出CPU资源,但是不会释放锁资源(包括synchronized和lock)!
wait():让出CPU资源和锁资源!

2.1、sleep不释放synchronized锁

public class Test4 implements Runnable{

    @Override
    public void run() {
        sync();
    }

    public synchronized void sync(){
        System.out.println(Thread.currentThread().getName()+"获取锁");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"释放锁");
    }
    public static void main(String[] args) {
        Test4 runnable = new Test4();
        new Thread(runnable).start();
        new Thread(runnable).start();
    }
}
//控制台打印
Thread-1获取锁
Thread-1释放锁
Thread-0获取锁
Thread-0释放锁

2.2、sleep不释放lock锁

public class Test5 implements Runnable{
    private static final Lock LOCK = new ReentrantLock();

    @Override
    public void run() {
        try {
            LOCK.lock();
            System.out.println(Thread.currentThread().getName()+"获取锁");
            Thread.sleep(5000);
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            LOCK.unlock();
            System.out.println(Thread.currentThread().getName()+"释放锁");
        }
    }

    public static void main(String[] args) {
        Test5 runnable = new Test5();
        new Thread(runnable).start();
        new Thread(runnable).start();
    }
}
//控制台打印
Thread-0获取锁
Thread-0释放锁
Thread-1获取锁
Thread-1释放锁

总结:sleep方法可以让线程进入Waiting状态,并且不占用CPU资源,但是不释放锁,直到规定时间后再运行,休眠期间如果被中断,会抛出异常并清除中断状态

3、yield()

yield()方法是一个和sleep()方法有点相似的方法,它也是Thread类提供的一个静态方法,它也可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程转入就绪状态。yield()只是让当前线程暂停一下,让系统的线程调度器重新调度一次,完全可能的情况是:当某个线程调用了yield()方法暂停之后,线程调度器又将其调度出来重新执行;

实际上,当某个线程调用了yield()方法暂停之后,只有优先级与当前线程相同,或者优先级比当前线程更高的处于就绪状态的线程才会获得执行的机会;

public class TestYield extends Thread
{
	public TestYield()
	{
	}
	public TestYield(String name)
	{
		super(name);
	}
	//定义run方法作为线程执行体
	public void run()
	{
		for (int i = 0; i < 50 ; i++ )
		{
			System.out.println(getName() + "  " + i);
			//当i等于20时,使用yield方法让当前线程让步
			if (i == 20)
			{
				Thread.yield();
			}
		}
	}
    public static void main(String[] args) throws Exception
    {
		//启动两条并发线程
		TestYield ty1 = new TestYield("高级");
		//将ty1线程设置成最高优先级
		ty1.setPriority(Thread.MAX_PRIORITY);
		ty1.start();
		TestYield ty2 = new TestYield("低级");
		//将ty1线程设置成最低优先级
		ty2.setPriority(Thread.MIN_PRIORITY);
		ty2.start();	
    }
}

在这里插入图片描述
4、isAlive()

isAlive()方法的功能是判断当前的线程是否存活!

resume()、suspend()、stop()

这三个方法分别是恢复线程、暂停线程、终止线程,这三个方法已经弃用;

suspend()可能出现死锁,stop()是线程不安全的;

1、stop()

调用stop()方法会抛出java.lang.ThreadDeath异常,该异常不需要显式地捕捉!

stop()方法会破坏线程安全,这是因为它会释放锁,并且这种释放是不可控制的,非预期的!

class MyService{
    private String userName = "a";
    private String password = "aa";
    synchronized public void printString(String userName,String password){
        try {
            this.userName = userName;
            Thread.sleep(1000000L);
            this.password = password;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class TestStop {
    public static void main(String[] args) throws InterruptedException {
        MyService service = new MyService();
        Thread threadA = new Thread(() -> {
            service.printString("b", "bb");
        });
        threadA.start();

        Thread threadB = new Thread(() -> {
            System.out.println(service.getUserName());
            System.out.println(service.getPassword());
        });
        threadB.start();
		
        Thread.sleep(3000);
        threadA.stop();
    }
}
//控制台打印
b
aa

printString()本是为了解决线程安全问题而加上的synchronized关键字,但是因为线程A执行了stop()释放锁,线程B能直接获取到锁操作线程A未处理完成的数据,造成数据不一致的结果。

并且线程无法判断stop()具体在哪里被停止,这将会造成业务处理的不确定性!

public class TestStop{
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 1000000; i++) {
                System.out.println(i);
            }
        });
        thread.start();
        //模拟业务运行中
        Thread.sleep(50);
        thread.stop();
    }
}
//多次运行,控制台每次打印的数字都将会不一样。

2、resume()、suspend()

不推荐使用 suspend() 去挂起线程的原因,是因为 suspend() 在导致线程暂停的同时,并不会去释放任何锁资源。其他线程都无法访问被它占用的锁。直到对应的线程执行 resume() 方法后,被挂起的线程才能继续,从而其它被阻塞在这个锁的线程才可以继续执行。

但是,如果 resume() 操作出现在 suspend() 之前执行,那么线程将一直处于挂起状态,同时一直占用锁,这就产生了死锁。而且,对于被挂起的线程,它的线程状态居然还是 Runnable。

public class SuspendResumeTest {
    public static Object object = new Object();
    static TestThread t1 = new TestThread();
    public static class TestThread extends Thread{
        @Override
        public void run() {
            synchronized (object) {
                System.out.println(getName()+" 暂停。。");
                Thread.currentThread().suspend();
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        t1.start();
        //Thread.sleep(200);
        t1.resume();
        System.out.println("resume执行完毕");
        System.out.println(t1.getState());
    }
}
//控制台打印
resume执行完毕
Thread-0 暂停。。
RUNNABLE

线程属性:线程ID、线程名字、守护线程、优先级

1、线程ID

每个线程都有自己的ID,用于标识不同的线程;

public class Test7 implements Runnable
{
    @Override
    public void run() {
        System.out.println("子线程ID为:"+Thread.currentThread().getId());
    }

    public static void main(String[] args) {
        new Thread(new Test7()).start();
        System.out.println("主线程ID为:"+Thread.currentThread().getId());
    }
}
//控制台打印
主线程ID为:1
子线程ID为:12

查看源码可知,线程ID每次都是自增1,并且是从1开始的!

tid = nextThreadID();

private static long threadSeqNumber;

private static synchronized long nextThreadID() {
    return ++threadSeqNumber;
}

public long getId() {
    return tid;
}

那么为什么上述程序打印主线程ID为1,而子线程ID为12了呢?

这是因为JVM启动的时候还有很多后台进程同步启动:
在这里插入图片描述
2、线程名字

让用户或者程序员在开发、调试或运行过程中,更容易区分每个不同的线程、定位问题等;

public class Test7 implements Runnable
{
    @Override
    public void run() {
        System.out.println("子线程名称为:"+Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        new Thread(new Test7(),"新建子线程").start();
        new Thread(new Test7()).start();
        System.out.println("主线程名称为:"+Thread.currentThread().getName());
    }
}
//控制台打印:
主线程名称为:main
子线程名称为:新建子线程
子线程名称为:Thread-0

查看源码可知,当我们没有传入名称时,会自动生成一个默认名字,因为++写在后面,所以是从0开始的:

public Thread() {
    init(null, null, "Thread-" + nextThreadNum(), 0);
}
public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}
private static int threadInitNumber;
private static synchronized int nextThreadNum() {
    return threadInitNumber++;
}

3、守护线程

有一种线程,它是在后台运行的,它的任务是为其他的线程提供服务,这种线程被称为“后台线程”,又称为“守护线程”。JVM的垃圾回收线程就是典型的后台线程;

后台线程有个特征:如果所有的前台线程都死亡,后台线程会自动死亡;调用Thread对象的setDaemon(true)方法可将指定线程设置成后台线程:

public class DaemonThread extends Thread
{
    //定义后台线程的线程执行体与普通线程没有任何区别
    @Override
    public void run()
    {
        System.out.println("守护线程");
    }

    public static void main(String[] args) throws InterruptedException {
        DaemonThread t = new DaemonThread();

        //前台线程创建的子线程默认也是前台线程
        System.out.println(t.isDaemon());

        //将此线程设置成后台线程,要在执行start()方法之前执行此方法,不然会报错java.lang.IllegalThreadStateException
        t.setDaemon(true);
        //启动后台线程
        t.start();

        //因为前台线程结束的话,后台线程也随之结束,所以在这休眠50毫秒以让后台线程可以执行完毕
        Thread.sleep(50);

        //isDaemon()方法用于判断指定线程是否为后台线程
        System.out.println(t.isDaemon());

        System.out.println(Thread.currentThread().getName());
        //------程序执行到此处,前台线程(main线程)结束------
        //后台线程也应该随之结束
    }
}
//控制台打印
false
守护线程
true
main

从以上程序可以看出,主线程默认是前台线程,t线程默认也是前台线程。并不是所有的线程默认都是前台线程,有些线程默认就是后台线程-----前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程。
在这里插入图片描述
4、线程优先级

每个线程执行时都具有一定的优先级,优先级高的线程获得较多的执行机会,而优先级低的线程则获得较少的执行机会(优先级高的线程分配的时间片数量多于优先级底的线程);

每个线程默认的优先级都与创建它的父线程的优先级相同,在默认的情况下,main线程具有普通优先级,由main线程创建的子进程也具有普通优先级(线程的优先级具有继承性);

Thread类提供了setPriority(int newPriority)、getPriority()方法来设置和返回指定线程的优先级。其中setPriority()方法的参数可以是一个整数,范围是1~10之间,也可以是Thread类的如下三个静态常量:

  • MAX_PRIORITY:其值是10;
  • MIN_PRIORITY:其值是1;
  • NORM_PRIORITY:其值是5;

注意:不要试图使用优先级来控制线程的执行顺序,线程优先级的设置,并不能指定线程的运行顺序,这个是由操作系统调度的。线程优先级不能作为程序正确性的依赖,因为操作系统可以完全不用理会JAVA线程对于优先级的设定

getStackTrace()、dumpStack()、getAllStackTraces()获取线程堆栈信息

1、getStackTrace()

StackTraceElement[] getStackTrace()方法的作用是返回一个表示该线程堆栈跟踪元素数组。

如果该线程尚未启动或已终止,则该方法将返回一个零长度的数组。如果返回的数组不是零长度的,则其第一个元素代表堆栈顶表示该数组中最新的方法调用。最后一个元素代表堆栈底,是该数组中最旧的方法调用!

public class Test1 {
    public void a(){
        b();
    }
    public void b(){
        c();
    }
    public void c(){
        StackTraceElement[] array = Thread.currentThread().getStackTrace();
        if (array != null){
            for (int i = 0; i < array.length; i++) {
                StackTraceElement stackTraceElement = array[i];
                System.out.println("className:"+stackTraceElement.getClassName()+",methodName:"+stackTraceElement.getMethodName()
                +",fileName:"+stackTraceElement.getFileName()+",lineNumber:"+stackTraceElement.getLineNumber());
            }
        }
    }
    public static void main(String[] args) {
        Test1 test1 = new Test1();
        test1.a();
    }
}
//控制台打印
className:java.lang.Thread,methodName:getStackTrace,fileName:Thread.java,lineNumber:1559
className:com.example.demo.Test1,methodName:c,fileName:Test1.java,lineNumber:15
className:com.example.demo.Test1,methodName:b,fileName:Test1.java,lineNumber:12
className:com.example.demo.Test1,methodName:a,fileName:Test1.java,lineNumber:9
className:com.example.demo.Test1,methodName:main,fileName:Test1.java,lineNumber:27

2、dumpStack()

dumpStack()方法的作用是将当前线程的堆栈跟踪信息输出至标准错误流:

public class Test1 {
    public void a(){
        b();
    }
    public void b(){
        c();
    }
    public void c(){
        Thread.dumpStack();
    }
    public static void main(String[] args) {
        Test1 test1 = new Test1();
        test1.a();
    }
}
//控制台打印
java.lang.Exception: Stack trace
	at java.lang.Thread.dumpStack(Thread.java:1336)
	at com.example.demo.Test1.c(Test1.java:15)
	at com.example.demo.Test1.b(Test1.java:12)
	at com.example.demo.Test1.a(Test1.java:9)
	at com.example.demo.Test1.main(Test1.java:19)

3、getAllStackTraces()

getAllStackTraces()方法的作用是返回所有活动线程的堆栈跟踪的一个映射。键是线程,值都是一个StackTraceElement数组;

public class Test1 {
    public void a(){
        b();
    }
    public void b(){
        c();
    }
    public void c(){
        Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
        if (map != null && map.size() > 0){
            Iterator<Thread> iterator = map.keySet().iterator();
            while (iterator.hasNext()){
                Thread next = iterator.next();
                System.out.println("线程名称:"+next.getName());
                System.out.println("线程状态:"+next.getState());
                StackTraceElement[] elements = map.get(next);
                System.out.println("线程StackTraceElement[]具体信息:");
                for (int i = 0; i < elements.length; i++) {
                    StackTraceElement stackTraceElement = elements[i];
                    System.out.println("className:"+stackTraceElement.getClassName()+",methodName:"+stackTraceElement.getMethodName()
                            +",fileName:"+stackTraceElement.getFileName()+",lineNumber:"+stackTraceElement.getLineNumber());
                }
            }
        }
    }
    public static void main(String[] args) {
        Test1 test1 = new Test1();
        test1.a();
    }
}
//控制台打印
线程名称:Finalizer
线程状态:WAITING
线程StackTraceElement[]具体信息:
className:java.lang.Object,methodName:wait,fileName:Object.java,lineNumber:-2
className:java.lang.ref.ReferenceQueue,methodName:remove,fileName:ReferenceQueue.java,lineNumber:144
className:java.lang.ref.ReferenceQueue,methodName:remove,fileName:ReferenceQueue.java,lineNumber:165
className:java.lang.ref.Finalizer$FinalizerThread,methodName:run,fileName:Finalizer.java,lineNumber:216
线程名称:main
线程状态:RUNNABLE
线程StackTraceElement[]具体信息:
className:java.lang.Thread,methodName:dumpThreads,fileName:Thread.java,lineNumber:-2
className:java.lang.Thread,methodName:getAllStackTraces,fileName:Thread.java,lineNumber:1610
className:com.example.demo.Test1,methodName:c,fileName:Test1.java,lineNumber:18
className:com.example.demo.Test1,methodName:b,fileName:Test1.java,lineNumber:15
className:com.example.demo.Test1,methodName:a,fileName:Test1.java,lineNumber:12
className:com.example.demo.Test1,methodName:main,fileName:Test1.java,lineNumber:37
。。。省略

线程异常

public class Test7 implements Runnable
{
    @Override
    public void run() {
        throw new RuntimeException();
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(new Test7()).start();
        Thread.sleep(1000);
        System.out.println("执行结束");
    }
}
//控制台打印
Exception in thread "Thread-0" java.lang.RuntimeException
	at com.example.demo.Test7.run(Test7.java:11)
	at java.lang.Thread.run(Thread.java:748)
执行结束

由上代码运行结果可知:子线程的异常不会影响到主线程的执行,即使子线程有异常抛出,并且打印了异常信息,主线程依然能够正常运行!

使用try/catch包住线程语句:

try{
    new Thread(new Test7()).start();
}catch (Exception e){
    System.out.println("捕获异常");
}

运行结果与上述仍然一样,可知:子线程异常无法用传统方法进行捕获!

使用try/catch包住线程执行体内代码:

@Override
public void run() {
    try{
        throw new RuntimeException();
    }catch (RuntimeException e){
        System.out.println("捕获异常");
    }
}

运行程序控制台打印捕获异常,可知:try/catch只能捕获对应线程内的异常;

但是,如果我们手动在每个run方法里面都使用try/catch进行捕获的话,那工作量不仅大、重复性工作还麻烦,代码也很冗余。

所以我们可以使用UncaughtExceptionHandler,这个接口可以检测出由于未捕获异常而终止的情况,并且对此进行处理;

@FunctionalInterface
public interface UncaughtExceptionHandler {
    void uncaughtException(Thread t, Throwable e);
}

编写我们自己的全局异常处理器:

public class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println("线程名称为:"+t.getName()+" 出现异常,异常为:"+e.getMessage());
    }
}

设置子线程的全局异常处理器:

public class Test7 implements Runnable
{
    @Override
    public void run() {
        throw new RuntimeException();
    }

    public static void main(String[] args){
        Thread thread = new Thread(new Test7());
		
		//给指定对象设置异常处理器
        thread.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());

        thread.start();

    }
}

运行程序,控制台打印:线程名称为:Thread-0 出现异常,异常为:null

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值