Java线程的理解(上)


前言

什么是线程? 线程和进程的区别?

线程:是进程的一个实体,是 cpu 调度和分派的基本单位,是比进程更小的
可以独立运行的基本单位。
进程:具有一定独立功能的程序关于某个数据集合上的一次运行活动,是操作
系统进行资源分配和调度的一个独立单位。


线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。
多进程是指操作系统能同时运行多个任务(程序)。
多线程是指在同一程序中有多个顺序流在执行。

特点:线程的划分尺度小于进程,这使多线程程序拥有高并发性,进程在运行
时各自内存单元相互独立,线程之间 内存共享,这使多线程编程可以拥有更好
的性能和用户体验。


一、创建线程有几种方式?

1、扩展java.lang.Thread类

继承 Thread 类并重写 run 方法创建线程,实现简单但不可以继承其他类,继承Thread类的方法是比较常用的一种,如果说你只是想起一条线程。没有什么其它特殊的要求,那么可以使用Thread。

package com.thread;
/**
 * @author YangPeng
 * @version 1.0
 * @date 2023/4/11 13:44
 */
class Thread1 extends Thread{
	private String name;
    public Thread1(String name) {
       this.name=name;
    }
	public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(name + "运行  :  " + i);
            try {
                sleep((int) Math.random() * 10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
       
	}
}
public class Main {
 
	public static void main(String[] args) {
		Thread1 mTh1=new Thread1("A");
		Thread1 mTh2=new Thread1("B");
		mTh1.start();
		mTh2.start();
	}
}

运行结果:
B运行 : 0
A运行 : 0
A运行 : 1
A运行 : 2
B运行 : 1
A运行 : 3
B运行 : 2
A运行 : 4
B运行 : 3
B运行 : 4

再次运行:
A运行 : 0
B运行 : 0
A运行 : 1
A运行 : 2
B运行 : 1
B运行 : 2
B运行 : 3
B运行 : 4
A运行 : 3
A运行 : 4

说明:
程序启动运行main时候,java虚拟机启动一个进程,主线程main在main()调用时候被创建。随着调用MitiSay的两个对象的start方法,另外两个线程也启动了,这样,整个应用就在多线程下运行。

注意:
start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行是由操作系统决定的。
从程序运行的结果可以发现,多线程程序是乱序执行。因此,只有乱序执行的代码才有必要设计为多线程。
Thread.sleep()方法调用目的是不让当前线程独自霸占该进程所获取的CPU资源,以留出一定时间给其他线程执行的机会。
实际上所有的多线程代码执行顺序都是不确定的,每次执行的结果都是随机的。
但是start方法重复调用的话,会出现java.lang.IllegalThreadStateException异常

		Thread1 mTh1=new Thread1("A");
		Thread1 mTh2=mTh1;
		mTh1.start();
		mTh2.start();

运行结果:
A运行  :  0
A运行  :  1
A运行  :  2
A运行  :  3
A运行  :  4
Exception in thread "main" java.lang.IllegalThreadStateException
	at java.lang.Thread.start(Thread.java:708)
	at com.thread.Main.main(Main.java:38)

2、实现java.lang.Runnable接口

实现 Runnable 接口并重写 run 方法。避免了单继承局限性,编程更加灵活,实
现解耦。

package com.thread;

/**
 * @author YangPeng
 * @version 1.0
 * @date 2023/4/11 14:22
 */

class Thread2 implements Runnable{
    private String name;

    public Thread2(String name) {
        this.name=name;
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(name + "运行  :  " + i);
            try {
                Thread.sleep((int) Math.random() * 10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Main1 {
    public static void main(String[] args) {
        new Thread(new Thread2("C")).start();
        new Thread(new Thread2("D")).start();
    }
}

运行结果:
D运行 : 0
C运行 : 0
C运行 : 1
C运行 : 2
C运行 : 3
D运行 : 1
C运行 : 4
D运行 : 2
D运行 : 3
D运行 : 4

说明:
Thread2类通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个约定。所有的多线程代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。
在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。
实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是扩展Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。

3、使用Callable和FutureTask创建线程

前面已经介绍了继承Thread类或者实现Runnable接口这两种方式来创建线程类,但是这两种方式有一个共同的缺陷:不能获取异步执行的结果。这是一个比较大的问题,很多场景都需要获取异步执行的结果,通过Runnable无法实现,是因为它的run()方法不支持返回值。
为了解决异步执行的结果问题,Java语言在1.5版本之后提供了一种新的多线程创建方法:通过Callable接口和FutureTask类相结合创建线程。
1、Callable接口
Callable接口位于java.util.concurrent包中,查看它的Java源代码,如下:

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

Callable接口是一个泛型接口,也是一个“函数式接口”。其唯一的抽象方法call()有返回值,返回值的类型为Callable接口的泛型形参类型。call()抽象方法还有一个Exception的异常声明,容许方法的实现版本的内部异常直接抛出,并且可以不予捕获。

2、RunnableFuture接口
这个重要的中间搭桥接口就是RunnableFuture接口,该接口与Runnable接口、Thread类紧密相关。与Callable接口一样,RunnableFuture接口也位于java.util.concurrent包中,使用的时候需要用import导入。
RunnableFuture是如何在Callable与Thread之间实现搭桥功能的呢?RunnableFuture接口实现了两个目标:一是可以作为Thread线程实例的target实例,二是可以获取异步执行的结果。它是如何做到一箭双雕的呢?请看RunnableFuture接口的代码:

public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

RunnableFuture继承了Runnable接口,从而保证了其实例可以作为Thread线程实例的target目标;同时,RunnableFuture通过继承Future接口,保证了可以获取未来的异步执行结果。

3、Future接口
Future接口至少提供了三大功能:

  • 能够取消异步执行中的任务;
  • 判断异步任务是否执行完成;
  • 获取异步任务完成后的执行结果;

Future接口的源代码如下:

public interface Future<V> {
	
    // 取消异步执行中的任务
    boolean cancel(boolean mayInterruptIfRunning);

    boolean isCancelled();

    // 判断异步任务是否执行成功
    boolean isDone();

    // 获取异步任务完成后的结果
    V get() throws InterruptedException, ExecutionException;

    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}
  • get():获取异步任务执行的结果。注意,这个方法的调用是阻塞性的。如果异步任务没有执行完成,异步结果获取线程(调用线程)会一直被阻塞,一直阻塞到异步任务执行完成,其异步结果返回给调用线程。
  • get(Long timeout,TimeUnit unit):设置时限,(调用线程)阻塞性地获取异步任务执行的结果。该方法的调用也是阻塞性的,但是结果获取线程(调用线程)会有一个阻塞时长限制,不会无限制地阻塞和等待,如果其阻塞时间超过设定的timeout时间,该方法将抛出异常,调用线程可捕获此异常。
  • boolean isDone():获取异步任务的执行状态。如果任务执行结束,就返回true。
  • boolean isCancelled():获取异步任务的取消状态。如果任务完成前被取消,就返回true。
  • boolean cancel(boolean mayInterruptRunning):取消异步任务的执行。

总体来说,Future是一个对异步任务进行交互、操作的接口。但是Future仅仅是一个接口,通过它没有办法直接完成对异步任务的操作,JDK提供了一个默认的实现类——FutureTask。

4、FutureTask类
FutureTask类是Future接口的实现类,提供了对异步任务的操作的具体实现。但是,FutureTask类不仅实现了Future接口,还实现了Runnable接口,或者更加准确地说,FutureTask类实现了RunnableFuture接口。

public class FutureTask<V> implements RunnableFuture<V> {
    
}

前面讲到RunnableFuture接口很关键,既可以作为Thread线程实例的target目标,又可以获取并发任务执行的结果,是Thread与Callable之间一个非常重要的搭桥角色。但是,RunnableFuture只是一个接口,无法直接创建对象,如果需要创建对象,就需用到它的实现类——FutureTask。所以说,FutureTask类才是真正的在Thread与Callable之间搭桥的类
FutureTask
从FutureTask类的UML关系图可以看到:FutureTask实现了RunnableFuture接口,而RunnableFuture接口继承了Runnable接口和Future接口,所以FutureTask既能作为一个Runnable类型的target执行目标直接被Thread执行,又能作为Future异步任务来获取Callable的计算结果。

FutureTask如何完成多线程的并发执行、任务结果的异步获取呢?FutureTask内部有一个Callable类型的成员——callable实例属性,具体如下:

private Callable<V> callable;

callable实例属性用来保存并发执行的Callable类型的任务,并且callable实例属性需要在FutureTask实例构造时进行初始化。FutureTask类实现了Runnable接口,在其run()方法的实现版本中会执行callable成员的call()方法。
FutureTask
此外,FutureTask内部还有另一个非常重要的Object类型的成员——outcome实例属性:

private Object outcome;

FutureTask的outcome实例属性用于保存callable成员call()方法的异步执行结果。在FutureTask类的run()方法完成callable成员的call()方法的执行之后,其结果将被保存在outcome实例属性中,供FutureTask类的get()方法获取。

5、使用Callable和FutureTask创建线程的具体步骤

通过FutureTask类和Callable接口的联合使用可以创建能够获取异步执行结果的线程,具体步骤如下:
(1) 创建一个Callable接口的实现类,并实现其call()方法,编写好异步执行的具体逻辑,可以有返回值。
(2) 使用Callable实现类的实例构造一个FutureTask实例。
(3) 使用FutureTask实例作为Thread构造器的target入参,构造新的Thread线程实例。
(4) 调用Thread实例的start()方法启动新线程,启动新线程的run()方法并发执行。其内部的执行过程为:启动Thread实例的run()方法并发执行后,会执行FutureTask实例的run()方法,最终会并发执行Callable实现类的call()方法。
(5) 调用FutureTask对象的get()方法阻塞性地获得并发线程的执行结果。

按照以上步骤,通过Callable接口和Future接口相结合创建多线程,实例如下:

创建一个Callable接口的实现类:

public class CallableTaskDemo implements Callable {
    // 编写好异步执行的具体逻辑,可以有返回值。
    // (Runnable接口中的run()方法是没有返回值得,Callable接口的call()方法有返回值)
    @Override
    public Long call() throws Exception {
        Long startTime = System.currentTimeMillis();
        System.out.println(Thread.currentThread().getName()+" 线程开始运行");
        Thread.sleep(1000);

        for(int i=0;i<100000000;i++){
            int j = i*10000;
        }

        Long endTime = System.currentTimeMillis();
        Long used = endTime-startTime;
        System.out.println(Thread.currentThread().getName()+" 线程结束运行");
        return used;
    }
}

在这个例子中有两个线程:一个是执行main()方法的主线程,叫作main;另一个是main线程通过thread.start()方法启动的业务线程,叫作callableTaskThread。该线程是一个包含FutureTask任务作为target的Thread线程。

public class CreateDemo {
    public static void main(String[] args) throws InterruptedException {
        CallableTaskDemo callableTaskDemo = new CallableTaskDemo();
        FutureTask<Long> futureTask = new FutureTask<Long>(callableTaskDemo);
        Thread thread = new Thread(futureTask,"callableTaskThread");
        thread.start();

        Thread.sleep(500);

        System.out.println("main线程执行一会");
        for(int i=0;i<100000000/2;i++){
            int j = i*10000;
        }
        // 获取并发任务的执行结果
        try {
            System.out.println(thread.getName()+" 线程占用时间:"+futureTask.get());
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

main线程通过thread.start()启动callableTaskThread线程之后,会继续自己的事情,callableTaskThread线程开始并发执行。

callableTaskThread线程首先执行的是thread.run()方法,然后在其中会执行到其target(futureTask任务)的run()方法;接着在这个futureTask.run()方法中会执行futureTask的callable成员的call()方法,这里的callable成员(ReturnableTask实例)是通过FutureTask构造器在初始化时传递进来的、自定义的Callable实现类的实例。

main线程和callableTaskThread线程的执行流程大致如图:
线程
执行时长返回之后,将被作为结果保存在FutureTask内部的outcome实例属性中。至此,异步的returnableThread线程执行完毕。在main线程处理完自己的事情后(以上实例中是一个消磨时间的循环),通过futureTask的get实例方法获取异步执行的结果。这里有两种情况:
1. futureTask的结果outcome不为空,callable.call()执行完成。在这种情况下,futureTast.get会直接取回outcome结果,返回给main线程(结果获取线程)。
2.futureTask的结果outcome为空,callable.call()还没有执行完。在这种情况下,main线程作为结果获取线程会被阻塞住,一直阻塞到callable.call()执行完成。当执行完后,最终结果会保存到outcome中,futureTask会唤醒main线程,去提取callable.call()执行结果。

4、通过线程池创建线程(使用 java.util.concurrent.Executor 接口)

前面的示例中,所创建的Thread实例在执行完成之后都销毁了,这些线程实例都是不可复用的。实际上创建一个线程实例在时间成本、资源耗费上都很高,在高并发的场景中,断然不能频繁进行线程实例的创建与销毁,而是需要对已经创建好的线程实例进行复用,这就涉及线程池的技术。Java中提供了一个静态工厂来创建不同的线程池,该静态工厂为Executors工厂类

1、线程池的创建与执行目标提交
通过Executors工厂类创建一个线程池,一个简单的示例如下:

public class CreateDemo {
    // 创建一个包含3个线程的线程池
    private static ExecutorService threadpool = Executors.newFixedThreadPool(3);
}

以上示例通过工厂类Executors的newFixedThreadPool(int threads)方法创建了一个线程池,所创建的线程池的类型为ExecutorService。工厂类的newFixedThreadPool(int threads)方法用于创建包含固定数目的线程池,示例中的线程数量为3。

ExecutorService是Java提供的一个线程池接口,每次我们在异步执行target目标任务的时候,可以通过ExecutorService线程池实例去提交或者执行。ExecutorService实例负责对池中的线程进行管理和调度,并且可以有效控制最大并发线程数,提高系统资源的使用率,同时提供定时执行、定频执行、单线程、并发数控制等功能。

向ExecutorService线程池提交异步执行target目标任务的常用方法有:

// 执行一个Runnbale类型的target执行目标实例,无返回值
void execute(Runnable command);

// 执行一个Callable类型的target执行目标实例,返回一个Future异步任务实例
<T> Future<T> submit(Callable<T> task);

// 执行一个Runnbale类型的target执行目标实例,返回一个Future异步任务实例
Future<?> submit(Runnable task);

2、线程池的使用实战
Runnbale类型的target执行目标实例:

public class RunnableTaskDemo implements Runnable {
    @Override
    public void run() {
        for(int i=0;i<3;i++){
            System.out.println(Thread.currentThread().getName()+" 轮次:"+i);

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Callable类型的target执行目标实例:

public class CallableTaskDemo implements Callable {
    @Override
    public Long call() throws Exception {
        Long startTime = System.currentTimeMillis();
        System.out.println(Thread.currentThread().getName()+" 线程开始运行");
        for(int i=0;i<3;i++){
            System.out.println(Thread.currentThread().getName()+" 轮次:"+i);
            Thread.sleep(1000);
        }
        Long used = System.currentTimeMillis()-startTime;
        System.out.println(Thread.currentThread().getName()+" 线程结束运行");
        return used;
    }
}

创建3个线程,执行3个目标任务:

public class CreateDemo {
    private static ExecutorService threadpool = Executors.newFixedThreadPool(3);

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 1、执行Runnbale类型的target目标实例,无返回
        threadpool.execute(new RunnableTaskDemo());

        //2、 执行Runnbale类型的target目标实例,无返回,内部类形式
        threadpool.execute(new Runnable() {
            @Override
            public void run() {
                for(int i=0;i<3;i++){
                    System.out.println(Thread.currentThread().getName()+" 轮次:"+i);

                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }       
        });

        // 3、提交Callable执行目标实例,有返回
        Future future = threadpool.submit(new CallableTaskDemo());
        System.out.println("异步执行的结果为:" + future.get());
    }
}

执行结果为:

pool-1-thread-1 轮次:0
pool-1-thread-2 轮次:0
pool-1-thread-3 线程开始运行
pool-1-thread-3 轮次:0
pool-1-thread-1 轮次:1
pool-1-thread-2 轮次:1
pool-1-thread-3 轮次:1
pool-1-thread-1 轮次:2
pool-1-thread-3 轮次:2
pool-1-thread-2 轮次:2
pool-1-thread-3 线程结束运行
异步执行的结果为:3029

ExecutorService线程池的execute(…)与submit(…)方法的区别如下:
(1) 接收的参数不一样:
submit()可以接收两种入参:无返回值的Runnable类型的target执行目标实例和有返回值的Callable类型的target执行目标实例。而execute()仅仅接收无返回值的target执行目标实例,或者无返回值的Thread实例。

(2) submit()有返回值,而execute()没有:
submit()方法在提交异步target执行目标之后会返回Future异步任务实例,以便对target的异步执行过程进行控制,比如取消执行、获取结果等。execute()没有任何返回,target执行目标实例在执行之后没有办法对其异步执行过程进行控制,只能任其执行,直到其执行结束。

二、Thread和Runnable的区别

如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。

总结:
实现Runnable接口比继承Thread类所具有的优势:
1):适合多个相同的程序代码的线程去处理同一个资源
2):可以避免java中的单继承的限制
3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
4):线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类

三、Runnable 和 Callable 的区别?

主要区别
Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,支持泛型
Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方
法允许抛出异常,可以获取异常信息

提醒一下大家:main方法其实也是一个线程。在java中所以的线程都是同时启动的,至于什么时候,哪个先执行,完全看谁先得到CPU的资源。
在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM实际在就是在操作系统中启动了一个进程。

四、线程状态转换

线程状态

  1. 第一是 new->新建状态。在生成线程对象,并没有调用该对象的 start 方法,这是线程处于 创建状态。
  2. 第二是 Runnable->就绪状态。当调用了线程对象的 start 方法之后,该线程就进入了就绪 状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。
  3. 第三是 Running->运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线 程就进入了运行状态,开始运行 run 函数当中的代码。
  4. 第四是阻塞状态。阻塞状态是线程因为某种原因放弃 CPU 使用权,暂时停止运行。直到线程 进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
    (1)等待 – 通过调用线程的 wait() 方法,让线程等待某工作的完成。
    (2)超时等待 – 通过调用线程的 sleep() 或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。 当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状 态。
    (3)同步阻塞 – 线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),它会进入同 步阻塞状态。
  5. 第五是 dead->死亡状态: 线程执行完了或者因异常退出了 run()方法,该线程结束生命周期.

五、如何启动一个新线程、调用 start 和 run 方法的区别

启动线程线程对象调用 run 方法不开启线程。仅是对象调用方法。
线程对象调用 start 开启线程,并让 jvm 调用 run 方法在开启的线程中执行
调用 start 方法可以启动线程,并且使得线程进入就绪状态,而 run 方法只是 thread 的一 个普通方法,还是在主线程中执行。

六、线程相关的基本方法

  1. 调整线程优先级:Java线程有优先级,优先级高的线程会获得较多的运行机会。
    Java线程的优先级用整数表示,取值范围是1~10,Thread类有以下三个静态常量:
在这里插入代码片static int MAX_PRIORITY
          线程可以具有的最高优先级,取值为10static int MIN_PRIORITY
          线程可以具有的最低优先级,取值为1static int NORM_PRIORITY
          分配给线程的默认优先级,取值为5

Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。
每个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY。
线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。
JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。

  1. 线程等待(wait)
    调用该方法的线程进入WAITING 状态,只有等待另外线程的通知或被中断才会返回,需要注意的是调用 wait()方法后,会释放对象的锁。因此,wait 方法一般用在同步方法或同步代码块中。
  2. 线程睡眠(sleep)
    sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占有的锁,sleep(long)会导致线程进入 TIMED-WATING 状态,而 wait()方法会导致当前线程进入 WATING状态.
  3. 线程让步(yield)
    yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片。一般情况下,优先级高的线程有更大的可能性成功竞争得到 CPU 时间片,但这又不是绝对的,有的操作系统对线程优先级并不敏感。
  4. 线程中断(interrupt)
    中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)
  5. Join 等待其他线程终止
    join()方法,等待其他线程终止,在当前线程中调用一个线程的 join()方法,则当前线程转为阻塞状态,回到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu的宠幸.
  6. 线程唤醒(notify)
    Object 类中的 notify()方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调用其中一个 wait()方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类似的方法还有 notifyAll(),唤醒再次监视器上等待的所有线程。

七、线程常用的基本方法说明

①sleep(long millis): 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
②join():指等待t线程终止。
使用方式:
join是Thread类的一个方法,启动线程后直接调用,即join()的作用是:“等待该线程终止”,这里需要理解的就是该线程是指的主线程等待子线程的终止。也就是在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行

Thread t = new AThread();  	t.start();	 t.join();

为什么要用join()方法,(join的应用场景)
在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。

不加join。

/**
 *@functon 多线程学习,join
 *@author yp
 *@time 2023.4.12
 */
package com.multithread.join;
class Thread1 extends Thread{
	private String name;
    public Thread1(String name) {
    	super(name);
       this.name=name;
    }
	public void run() {
		System.out.println(Thread.currentThread().getName() + " 线程运行开始!");
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程"+name + "运行 : " + i);
            try {
                sleep((int) Math.random() * 10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + " 线程运行结束!");
	}
}
 
public class Main {
 
	public static void main(String[] args) {
		System.out.println(Thread.currentThread().getName()+"主线程运行开始!");
		Thread1 mTh1=new Thread1("A");
		Thread1 mTh2=new Thread1("B");
		mTh1.start();
		mTh2.start();
		System.out.println(Thread.currentThread().getName()+ "主线程运行结束!");
	}
}
 
运行结果:
main主线程运行开始!
main主线程运行结束!
A 线程运行开始!
B 线程运行开始!
子线程A运行 : 0
子线程B运行 : 0
子线程A运行 : 1
子线程B运行 : 1
子线程A运行 : 2
子线程B运行 : 2
子线程A运行 : 3
子线程A运行 : 4
子线程B运行 : 3
A 线程运行结束!
子线程B运行 : 4
B 线程运行结束!
//发现主线程比子线程早结束

加join。

public class Main {
	public static void main(String[] args) {
		System.out.println(Thread.currentThread().getName()+"主线程运行开始!");
		Thread1 mTh1=new Thread1("A");
		Thread1 mTh2=new Thread1("B");
		mTh1.start();
		mTh2.start();
		try {
			mTh1.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		try {
			mTh2.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread().getName()+ "主线程运行结束!");
	}
}

运行结果:
main主线程运行开始!
A 线程运行开始!
B 线程运行开始!
子线程B运行 : 0
子线程A运行 : 0
子线程B运行 : 1
子线程B运行 : 2
子线程B运行 : 3
子线程A运行 : 1
子线程B运行 : 4
子线程A运行 : 2
B 线程运行结束!
子线程A运行 : 3
子线程A运行 : 4
A 线程运行结束!
main主线程运行结束!
//主线程一定会等子线程都结束了才结束

③yield():暂停当前正在执行的线程对象,并执行其他线程。
Thread.yield()方法作用是:暂停当前正在执行的线程对象,并执行其他线程。
yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
结论:yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。

/**
 *@functon 多线程学习 yield
 *@author yp
 *@time 2023.4.13
 */
package com.thread_yield;
class ThreadYield extends Thread{
    public ThreadYield(String name) {
        super(name);
    }
    @Override
    public void run() {
        for (int i = 1; i <= 50; i++) {
            System.out.println("" + this.getName() + "-----" + i);
            // 当i为30时,该线程就会把CPU时间让掉,让其他或者自己的线程执行(也就是谁先抢到谁执行)
            if (i ==30) {
                this.yield();
            }
        }
	
}
}
public class Main {
	public static void main(String[] args) {
		ThreadYield yt1 = new ThreadYield("张三");
    	ThreadYield yt2 = new ThreadYield("李四");
        yt1.start();
        yt2.start();
	}
 
}
运行结果:
第一种情况:李四(线程)当执行到30时会CPU时间让掉,这时张三(线程)抢到CPU时间并执行。

第二种情况:李四(线程)当执行到30时会CPU时间让掉,这时李四(线程)抢到CPU时间并执行。

sleep()和yield()的区别
sleep()和yield()的区别):sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。
sleep 方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程
另外,sleep 方法允许较低优先级的线程获得运行机会,但 yield() 方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程些时获得 CPU 占有权。在一个运行系统中,如果较高优先级的线程没有调用 sleep 方法,又没有受到 I\O 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。

④setPriority(): 更改线程的优先级。
MIN_PRIORITY = 1
NORM_PRIORITY = 5
MAX_PRIORITY = 10

用法:
Thread4 t1 = new Thread4("t1");
Thread4 t2 = new Thread4("t2");
t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(Thread.MIN_PRIORITY);

⑤interrupt():不要以为它是中断某个线程!它只是线线程发送一个中断信号,让线程在无限等待时(如死锁时)能抛出抛出,从而结束线程,但是如果你吃掉了这个异常,那么这个线程还是不会中断的!

⑥wait()导致当前线程等待,直到另一个线程调用该对象的notify()方法或notifyAll()方法。
Obj.wait(),与Obj.notify()必须要与synchronized(Obj)一起使用,也就是wait,与notify是针对已经获取了Obj锁进行操作,从语法角度来说就是Obj.wait(),Obj.notify必须在synchronized(Obj){…}语句块内。从功能上来说wait就是说线程在获取对象锁后,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的notify()唤醒该线程,才能继续获取对象锁,并继续执行。相应的notify()就是对对象锁的唤醒操作。但有一点需要注意的是notify()调用后,并不是马上就释放对象锁的,而是在相应的synchronized(){}语句块执行结束,自动释放锁后,JVM会在wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。这样就提供了在线程间同步、唤醒的操作。Thread.sleep()与Object.wait()二者都可以暂停当前线程,释放CPU控制权,主要的区别在于Object.wait()在释放CPU同时,释放了对象锁的控制。

建立三个线程,A线程打印10次A,B线程打印10次B,C线程打印10次C,要求线程同时运行,交替打印10次ABC。这个问题用Object的wait(),notify()就可以很方便的解决。代码如下:

/**
 * wait用法
 * @author yp
 * @time 2023.4.13
 */
package com.thread_wait;
public class MyThreadPrinter2 implements Runnable {   
	  
    private String name;   
    private Object prev;   
    private Object self;   
  
    private MyThreadPrinter2(String name, Object prev, Object self) {   
        this.name = name;   
        this.prev = prev;   
        this.self = self;   
    }   
  
    @Override  
    public void run() {   
        int count = 10;   
        while (count > 0) {   
            synchronized (prev) {   
                synchronized (self) {   
                    System.out.print(name);   
                    count--;  
                    
                    self.notify();   
                }   
                try {   
                	if(count!=0)
                    prev.wait();   
                } catch (InterruptedException e) {   
                    e.printStackTrace();   
                }   
            }   
        }   
    }   
  
    public static void main(String[] args) throws Exception {   
        Object a = new Object();   
        Object b = new Object();   
        Object c = new Object();   
        MyThreadPrinter2 pa = new MyThreadPrinter2("A", c, a);   
        MyThreadPrinter2 pb = new MyThreadPrinter2("B", a, b);   
        MyThreadPrinter2 pc = new MyThreadPrinter2("C", b, c);   
        new Thread(pa).start();
        Thread.sleep(100);  //确保按顺序A、B、C执行
        new Thread(pb).start();
        Thread.sleep(100);  
        new Thread(pc).start();   
        Thread.sleep(100);  
        }   
}  
运行结果:
ABCABCABCABCABCABCABCABCABCABC
 先来解释一下其整体思路,从大的方向上来讲,该问题为三线程间的同步唤醒操作,主要的目的就是ThreadA->ThreadB->ThreadC->ThreadA循环执行三个线程。为了控制线程执行的顺序,那么就必须要确定唤醒、等待的顺序,所以每一个线程必须同时持有两个对象锁,才能继续执行。一个对象锁是prev,就是前一个线程所持有的对象锁。还有一个就是自身对象锁。主要的思想就是,为了控制执行的顺序,必须要先持有prev锁,也就前一个线程要释放自身对象锁,再去申请自身对象锁,两者兼备时打印,之后首先调用self.notify()释放自身对象锁,唤醒下一个等待线程,再调用prev.wait()释放prev对象锁,终止当前线程,等待循环结束后再次被唤醒。运行上述代码,可以发现三个线程循环打印ABC,共10次。程序运行的主要过程就是A线程最先运行,持有C,A对象锁,后释放A,C锁,唤醒B。线程B等待A锁,再申请B锁,后打印B,再释放B,A锁,唤醒C,线程C等待B锁,再申请C锁,后打印C,再释放C,B锁,唤醒A。看起来似乎没什么问题,但如果你仔细想一下,就会发现有问题,就是初始条件,三个线程按照A,B,C的顺序来启动,按照前面的思考,A唤醒B,B唤醒C,C再唤醒A。但是这种假设依赖于JVM中线程调度、执行的顺序。

wait和sleep区别:
共同点:

  1. 他们都是在多线程的环境下,都可以在程序的调用处阻塞指定的毫秒数,并返回。
  2. wait()和sleep()都可以通过interrupt()方法 打断线程的暂停状态 ,从而使线程立刻抛出InterruptedException。
    如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B正在wait/sleep /join,则线程B会立刻抛出InterruptedException,在catch() {} 中直接return即可安全地结束线程。
    需要注意的是,InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛出的。对某一线程调用 interrupt()时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。但是,一旦该线程进入到 wait()/sleep()/join()后,就会立刻抛出InterruptedException 。

不同点:

  1. Thread类的方法:sleep(),yield()等
    Object的方法:wait()和notify()等
  2. 每个对象都有一个锁来控制同步访问。Synchronized关键字可以和对象的锁交互,来实现线程的同步。
    sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
  3. wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
    所以sleep()和wait()方法的最大区别是:
      sleep()睡眠时,保持对象锁,仍然占有该锁;   wait()睡眠时,释放对象锁。
    但是wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException(但不建议使用该方法)。

sleep()方法
sleep()使当前线程进入停滞状态(阻塞当前线程),让出CUP的使用、目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会;
   sleep()是Thread类的Static(静态)的方法;因此他不能改变对象的机锁,所以当在一个Synchronized块中调用Sleep()方法是,线程虽然休眠了,但是对象的机锁并木有被释放,其他线程无法访问这个对象(即使睡着也持有对象锁)。
  在sleep()休眠时间期满后,该线程不一定会立即执行,这是因为其它线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级。

wait()方法
wait()方法是Object类里的方法;当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时失去(释放)了对象的机锁(暂时失去机锁,wait(long timeout)超时时间到后还需要返还对象锁);其他线程可以访问;
  wait()使用notify或者notifyAlll或者指定睡眠时间来唤醒当前等待池中的线程。
  wiat()必须放在synchronized block中,否则会在program runtime时扔出”java.lang.IllegalMonitorStateException“异常。

八、常见线程名词解释

主线程:JVM调用程序main()所产生的线程。
当前线程:这个是容易混淆的概念。一般指通过Thread.currentThread()来获取的进程。
后台线程:指为其他线程提供服务的线程,也称为守护线程。JVM的垃圾回收线程就是一个后台线程。 用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束
前台线程:是指接受后台线程服务的线程,其实前台后台线程是联系在一起,就像傀儡和幕后操纵者一样的关系。傀儡是前台线程、幕后操纵者是后台线程。由前台线程创建的线程默认也是前台线程。可以通过isDaemon()和setDaemon()方法来判断和设置一个线程是否为后台线程。

线程类的一些常用方法:

  • sleep(): 强迫一个线程睡眠N毫秒。
  • isAlive(): 判断一个线程是否存活。
  • join(): 等待线程终止。
  • activeCount(): 程序中活跃的线程数。
  • enumerate(): 枚举程序中的线程。
  • currentThread(): 得到当前线程。
  • isDaemon(): 一个线程是否为守护线程。
  • setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)
  • setName(): 为线程设置一个名称。
  • wait(): 强迫一个线程等待。
  • notify(): 通知一个线程继续运行。
  • setPriority(): 设置一个线程的优先级。

总结

以上就是对于线程基础知识的理解,后续会再更新一篇线程锁,和线程同步的文章。【用于日常学习记录,有问题还请各位大佬指出】

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

是鹏鹏哦

你的鼓励将是我创作最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值