多线程之Thread类

线程

一、进程

进程是正在运行的程序,是系统进行资源分配和调度的基本单位;每个进程都有自己的内存空间和系统资源;进程和进程之间的内存空间是相互隔离开来的,互不干扰;

二、线程

线程是进程中执行运算的最小单位,是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。也就是说线程之间共享的是一个进程之间的资源。

三、并发和并行

从以下三个角度来进行分析:

一:并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生;

二:并行是在不同实体上的多个事件,并发是在同一实体上的多个事件;

三:并行是在多台处理器上同时处理多个任务

看一下对应的示意图

并行

并发

四、线程创建

java中本质上只有一种方式创建线程。也就是通过new Thread()方式来创建一个线程。但是因为java屏蔽了底层,所以对外提供了三个API,如下所示:

  • 继承Thread类,重写run方法;
  • 实现Runnable接口,重写run方法;
  • 实现Callable接口,重写run方法;

其实本质上还是调用Thread类中的start方法。

4.1、继承Thread类重写run方法

4.2、实现Runnable接口

4.3、实现Callable接口

4.4、小结

4.4.1、线程的本质

本质上都是一种方式!!!!都是start方法之后调用run方法而已,那么来看下Callable方法中的调用

4.4.2、为什么需要多线程

看一段代码:

/**
 * @author lg
 * @Description
 * @date 2022/10/25 14:27
 */
public class ThreadExtentTest extends Thread{
  private static Logger logger = LoggerFactory.getLogger(ThreadExtentTest.class);

  @Override
  public void run() {
    for (int i = 0; i < 2; i++) {
      logger.info("当前线程名称是:{}",Thread.currentThread().getName());
    }
  }

  public static void main(String[] args) {
    ThreadExtentTest thread = new ThreadExtentTest();
    Thread t1 = new Thread(thread, "t1");
    Thread t2 = new Thread(thread, "t2");
    Thread t3 = new Thread(thread, "t3");
    t1.start();
    t2.start();
    t3.start();
  }
}

结果如下所示:

2023-05-30 15:38:19.368 [t3] INFO  com.guang.thread.ThreadExtentTest - 当前线程名称是:t3
2023-05-30 15:38:19.368 [t1] INFO  com.guang.thread.ThreadExtentTest - 当前线程名称是:t1
2023-05-30 15:38:19.368 [t2] INFO  com.guang.thread.ThreadExtentTest - 当前线程名称是:t2
2023-05-30 15:38:19.371 [t3] INFO  com.guang.thread.ThreadExtentTest - 当前线程名称是:t3
2023-05-30 15:38:19.371 [t2] INFO  com.guang.thread.ThreadExtentTest - 当前线程名称是:t2
2023-05-30 15:38:19.371 [t1] INFO  com.guang.thread.ThreadExtentTest - 当前线程名称是:t1

从程序运行的结果可以发现,多线程程序是乱序执行。因此,只有乱序执行的代码才有必要设计为多线程

那么进一步就是乱序的代码中是否涉及到对共享资源的操作,这就涉及到了共享资源的安全问题

4.4.3、Thread和Runnable的区别

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

总结:

实现Runnable接口比继承Thread类所具有的优势:

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

五、线程启动

5.1、start方法和run方法的区别

首先看下Thread类中关于start方法和run方法的描述

start方法:

Causes this thread to begin execution; the Java Virtual Machine calls the run method of this thread.
The result is that two threads are running concurrently: the current thread (which returns from the call to the start method) and the other thread (which executes its run method).
It is never legal to start a thread more than once. In particular, a thread may not be restarted once it has completed execution.
Throws:
IllegalThreadStateException – if the thread was already started.

线程对象调用start方法将会致使线程开始运行,JVM将会调用当前线程的run方法。

最终的结果是两个线程并发执行,当前线程(从对start方法的调用返回)和另一个线程(执行其run方法)。

多次调用线程的start方法是非法的。而且,一个线程在它运行完成之后,可能并不会重新启动。

如果一个线程已经启动了,还来调用start方法,将会抛出异常。

run方法

If this thread was constructed using a separate Runnable run object, then that Runnable object's run method is called; otherwise, this method does nothing and returns.
Subclasses of Thread should override this method.

如果Thread构造函数中是Runnable对象,然后它的run方法将会被调用,否则,调用之后不做任何事情,也没有返回。

Thread作为Runnable的子类应该重写这个方法。

根据上面的描述,在线程中调用了线程的start方法之后,JVM调用来调用线程的run方法

画个图表示一下:

对应的底层逻辑如下所示:

通过最简单的方式来创建一个线程并启动线程,这里来看下对应的原理。

    public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                System.out.println("thread run.........");
            }
        };
        thread.start();
    }

首先需要说明两点:

1、线程是操作系统的资源。java创建线程,本质上就是利用通过JVM来调用操作系统对外提供的函数而已。

在java中,将线程资源封装成一个线程,这个时候并不是真正的线程,只有通过JVM来调用操作系统提供的创建线程的函数之后,才会在操作系统中来创建一个线程。所以java中的线程和操作系统中的线程映射关系是:1:1

2、操作系统对外提供创建线程的API中,要求传入创建线程需要执行的任务,所以JVM在创建线程的时候就需要将任务传给API。而这个任务就是java线程对象中的run方法;

那么首先通过源码来看一下java是不是调用了JVM,直接看一下run方法:

    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) {              
            }
        }
    }

java线程对象调用了start0方法来让JVM,而JVM拿到java线程的任务之后调用操作系统提供的API创建线程。

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

这里相当于是操作系统使用对象调用run方法了!!!

5.2、线程的启动

对于java中的多线程来说,java调用了run方法之后,只是通过JVM告知了操作系统,当前的线程可以被调用。也就是说,当前的线程处于就绪状态。

但是对于一个就绪状态的线程来说,什么时候会被调用?这是不知道的,因为操作系统是会被随机调度的。

举个例子说明:

        Thread thread1 = new Thread(() -> {
            System.out.println("执行业务方法");
        },"t1");

        Thread thread2 = new Thread(() -> {
            System.out.println("执行业务方法");
        },"t2");
        
        thread1.start();
        thread2.start();

这里的线程映射到操作系统中去了之后,执行顺序是不一定的!!

操作系统可能会先调用线程t1先执行,线程t2后执行;也有可能是线程t2先执行,线程t1后执行。

只是在java层面上,按照顺序执行优先级,认为是线程t1先执行,线程t2后执行而已。

但是实际上在底层并非如此。完全取决于CPU随机调用哪个线程而已。

六、线程状态

6.1、线程状态说明

这里在Thread类中,进行了详细的说明。下面来看下源码中的叙述:

A thread state. A thread can be in one of the following states:

NEW A thread that has not yet started is in this state.

RUNNABLE A thread executing in the Java virtual machine is in this state.

BLOCKED A thread that is blocked waiting for a monitor lock is in this state.

WAITING A thread that is waiting indefinitely for another thread to perform a particular action is in this state.

TIMED_WAITING A thread that is waiting for another thread to perform an action for up to a specified waiting time is in this state.

TERMINATED A thread that has exited is in this state.

A thread can be in only one state at a given point in time. These states are virtual machine states which do not reflect any operating system thread states.

最终补充了一句:JVM中的线程状态不代表操作系统的线程状态

java线程类中提供了一个枚举来进行说明:

public static enum State {
// Thread state for a thread which has not yet started.  
  NEW,
    
  
//Thread state for a runnable thread. 
//A thread in the runnable state is executing in the Java virtual machine 
//but it may be waiting for other resources from the operating system such as processor.  
  RUNNABLE,
    
    
// Thread state for a thread blocked waiting for a monitor lock. 
// A thread in the blocked state is waiting for a monitor lock to 
// enter a synchronized block/method or reenter a synchronized block/method 
// after calling Object.wait.    
  BLOCKED,
    
//Thread state for a waiting thread. A thread is in the waiting state due to calling one of the following methods:
//    Object.wait with no timeout
//    Thread.join with no timeout
//    LockSupport.park
//    A thread in the waiting state is waiting for another thread to perform a particular action. 
//For example, a thread that has called Object.wait() on an object is waiting for another thread 
//to call Object.notify() or Object.notifyAll() on that object. 
//A thread that has called Thread.join() is waiting for a specified thread to terminate.    
  WAITING,
    
    
  TIMED_WAITING,
    
  // Thread state for a terminated thread. The thread has completed execution.  
  TERMINATED;
}

6.2、线程状态转换说明

看一下从网上找到的一个帖子,线程的转换状态:

New: 刚创建而未启动的线程就是这个状态。由于一个线程只能被启动一次,因此一个线程只可能有一次在这个状态。

Runnable:如上图,这个状态实际是个复合状态,包含两个状态:runalbe 和 running。runalbe 是就绪状态,可以被JVM 线程调度器(Scheduler) 进行调度,如果是单核CPU,同一时刻只有一个线程处于Running 状态,可能有多个线程处于 Ready 状态,Running 表示当前线程正在被CPU 执行,在Java 中就是Thread 对象只有run() 方法正在被执行。当 yield() 方法被调用,或者线程时间片被用完,线程就会从 Running 状态转为 Ready 状态。CPU时间片和主机时钟频率有关系,一般是10 ~ 20 ms。

Blocked:这里又分为两种情况:

  • 一个线程发生一个阻塞式I/0 (文件读写I/O, 网络读写I/O)时;

    线程在操作IO操作时,等到操作完成,CPU会发信号进行通知IO操作完成,线程继续操作;

  • 试图获取其他线程持有的锁时,线程会进入此状态;

    例如:获取别的线程已经持有的 synchronized 修饰的对象锁。在Blocked 状态的线程不会占用CPU 资源,但是程序如果出现大量处于这个状态的线程,需要警惕了,可以考虑优化一下程序性能。

Waiting: 一个线程执行了Object.wait( )、 Thread.join( ) 、LockSupport.park( ) 后会进入这个状态,这个状态是处于无限等待状态,没有指定等待时间,可以和Timed_Waiting 对比,Timed_Waiting是有等待时间的。这个状态的线程如果要恢复到Runnable 状态需要通过别的线程调用Object.notify( )、Object.notifyAll( )、LockSupport.unpark( thread )。

Timed_Waiting: 带时间限制的Waiting,不需要别的线程调用Object.notify( )、Object.notifyAll( )等方法,就能够恢复到可运行状态;

Terminated: 已经执行结束的线程处于此状态。Thread 的 run( ) 方法执行结束,或者由于异常而提前终止都会让线程处于这个状态。

这里需要注意的是Waiting和Blocked状态,也是有一个转换状态的,这里会在syncronized章节来进行描述。

七、线程消亡

JVM不推荐是停止一个线程,最好是让其运行完成之后正常结束生命周期。

那么停止一个线程无非是三种情况:1、正常结束;2、阻塞;3、陷入while(true)中

第一种不需要解决,因为这是非常正确的使用场景。而待解决的无非是后面两个场景。

对于后面两种情况分析

public class DemoOne {

    static boolean flag = true;

    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (flag){
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
        try {
            Thread.sleep(3000);
            flag = false;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

传统方式来进行操作的时候,可以发现这种是存在着延时的,不精确,准确来说是不可见问题。只有等到thread睡眠完成之后,才能够发现flag修改了,但是已经是10S之后的事情了。所以不精确。

那么如何让一个线程快速感知到?

Thread.interupt方法和Thread.isInterupted一级Thread.Interupted方法结合使用。

public class DemoTwo {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // 手动调用让外部感知到这个线程。思路来源于lock锁中的实现
                    Thread.currentThread().interrupt()
                    // 继续执行之后的逻辑,而线程还没有停止
                    System.out.println("因为中断而被唤醒的 .......");
                }

            }
        });
        thread.start();
        try {
            Thread.sleep(3000);
            thread.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

控制台打印输出:

java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.guang.thread.stop.DemoTwo.lambda$main$0(DemoTwo.java:8)
	at java.lang.Thread.run(Thread.java:748)
因为中断而被唤醒的 .......

JVM通过这个变量就已经完成了。interupted优雅停止一个线程,JVM不想让程序员停掉,因为突然停掉正在运行的线程,可能会导致线程当前所使用的资源没有释放,造成意想不到的问题。

posted @ 2021-08-01 14:17  雩娄的木子  阅读(83)  评论(0编辑  收藏  举报