并发基础(三):线程

「尺有所短,寸有所长;不忘初心,方得始终。」

线程是一个Java开发者必备的基础知识,整个并发编程离不来线程,那么线程有些基本概念呢?本文通过以下七点对线程的基本概念做一个简单的认识。

并发基础(三):线程

一、线程与进程的区别和关系

1.1 进程

  • 进程是指在系统中正在运行的一个应用程序

  • 每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存

1.2 线程

  • 线程是进程的基本执行单元,一个进程的所有任务都在线程中执行

  • 进程要想执行任务,必须得有线程,进程至少要有一条线程

  • 程序启动会默认开启一条线程,这条线程被称为主线程或 UI 线程

1.3 进程与线程的区别

  • 【地址空间】

    「同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间」

  • 【资源拥有】

    「同一进程内的线程共享本进程的资源(如内存、I/O、cpu等),但是进程之间的资源是独立的」

  • 【执行过程】

    每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。

    线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

  • 【崩溃影响】

    一个进程崩溃后,在保护模式下不会对其他进程产生影响,而一个线程崩溃整个进程都死掉。因此多进程要比多线程健壮。

  • 【资源切换】

    进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。而对于同时进行又要共享某些变量的并发操作,只能用线程不能用进程

1.4 总结

  • 进程是资源分配的最小单位,线程是CPU处理器调度的最小单位
  • 进程有独立的地址空间,且进程之间互不影响,线程没有独立的地址空间,属于同一进程的多个线程共享同一块地址空间
  • 进程切换的开销比线程切换大

二、线程的特点

  • 原子性
  • 可见性
  • 有序性

针对这三个特性的的描述在《并发基础(一):并发理论》中有解释,这里不在赘述。

三、线程的状态

话不多说先上源码,在Java的「Thread类中有一个内部枚举类State,State的枚举就是表示的线程的六种状态」

public 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
* {@link Object#wait() Object.wait}.
*/

BLOCKED,

/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called <tt>Object.wait()</tt>
* on an object is waiting for another thread to call
* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
* that object. A thread that has called <tt>Thread.join()</tt>
* is waiting for a specified thread to terminate.
*/

WAITING,

/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/

TIMED_WAITING,

/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/

TERMINATED;
}

3.1 New 新建状态

「线程刚刚创建,还未启动时的状态」,此时【「没有调用start()方法」】。

并发基础(三):线程

3.2 Runnable 运行状态

操作系统中的【就绪】和【运行】两种状态,在Java中统称为RUNNABLE」

3.2.1 就绪状态(READY)

「线程对象调用了start()方法之后」,线程处于「就绪状态」,就绪表示着该线程「可以执行」,但具体啥时候执行将取决于JVM里线程调度器的调度。

并发基础(三):线程

「其他状态 —>就绪状态」

  • 「线程调用start()」,新建状态转化为就绪状态。
  • 「线程sleep(long)时间到)」,等待状态转化为就绪状态。
  • 「阻塞式IO操作结果返回)」,线程变为就绪状态。
  • 「其他线程调用join()方法)」,结束之后转化为就绪状态。
  • 「线程对象拿到对象锁之后)」,进入就绪状态。

3.2.2 运行状态(RUNNING)

JVM调度器调用就绪状态的线程时,该「线程就获得了CPU,开始真正执行run()方法的线程执行体,该线程转换为运行状态」

对于单处理器,同一个时刻只能有一个线程处于运行状态。对于抢占式策略的系统来说,系统会给每个线程一小段时间(CPU时间片)处理各自的任务。时间用完之后,系统负责夺回线程占用的资源。下一段时间里,系统会根据一定规则,再次进行调度。

「运行状态 —> 就绪状态」

「当线程未执行完就失去了CPU处理器资源(CPU时间片到了,资源被其他线程抢占),CPU时间片到了之后会线程会调用yield()静态方法,向调度器提出释放CPU时间片的请求,不会释放锁。线程进入就绪状态」

所有线程再次竞争CPU资源,此时这个线程完全有可能再次获得CPU资源,再次运行。

  • 「yield方法」

    yield()由线程自己调用,其作用官方描述如下:

    A hint to the scheduler that the current thread is willing to yield its current use of a processor. The scheduler is free to ignore this hint.

    提示调度程序,当前线程愿意放弃当前对处理器的使用。此时当前线程将会被置为就绪状态,和其他线程一样等待调度,这时候根据不同优先级决定的概率,当前线程完全有可能再次抢到处理器资源。

  • 「sleep和yield的不同之处」

    • sleep(long)方法会「使线程转入超时等待状态」,时间到了之后才会转入就绪状态。

    • yield()方法不会将线程转入等待,而是「强制线程进入就绪状态」

    • 使用sleep(long)方法「需要处理异常」,而yield()不用。

3.3 Blocked 阻塞状态

「线程被阻塞等待监视器锁定的状态」。线程进入阻塞状态一般有三种方式:

  • 「线程休眠」

    调用**【sleep(),sleep(long millis),sleep(long millis, int nanos)】**等方法的时候,线程会进入休眠状态,当前线程被阻塞。

  • 「线程阻塞」

    代码中出现耗时比较长的逻辑,比如:慢查询,读取文件、接受用户输入都会导致其他线程阻塞

  • 「线程死锁」

    两个线程都在等待对方先执行完,而导致程序死锁在那里。

「线程取得锁,就会从阻塞状态转变为就绪状态」。阻塞状态类型也有三种:

  • 「等待阻塞」

    通过调用线程的wait()方法,让线程等待某工作的完成。

  • 「同步阻塞」

    线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。

  • 「其他阻塞」

    通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。

    当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

3.4 Waiting 等待状态

「线程无限期等待另一个线程执行特定操作(通知或中断)的状态」

「运行状态->等待状态」

  • 当前线程运行过程中,其他线程调用join方法,当前线程将会进入等待状态。

  • 当前线程对象调用wait()方法

  • 调用LockSupport.park():出于线程调度的目的「禁用当前线程」

    并发基础(三):线程

「等待状态->就绪状态」

  • 等待的线程「被其他线程对象唤醒」,notify()和notifyAll()。

  • LockSupport.unpark(Thread),解除线程等待状态

    LockSupport.park()方法对应

    并发基础(三):线程

3.5 Time_Waiting 超时等待状态

「线程正在等待另一个线程执行【特定时间】的操作的状态」。区别于WAITING,它可以在「指定的时间」自行返回。

  • 「运行状态->超时等待状态」

    • 调用静态方法Thread.sleep(long)
    • 线程对象调用wait(long)方法
    • 其他线程调用指定时间的join(long)。
    • LockSupport.parkNanos()。
    • LockSupport.parkUntil()。
  • 「超时等待状态->就绪状态」

    • 超时时间到了自动进入就绪状态
    • 等待的线程被其他线程对象唤醒,即其他线程调用notify()和notifyAll()。
    • LockSupport.unpark(Thread)。

3.6 Terminated 终止状态

「线程执行完了或者因异常退出了run()方法,该线程结束生命周期」。有两个原因会导致线程死亡:

  • run()和call()线程执行体中顺利执行完毕,「线程正常终止」

  • 线程抛出一个没有捕获的Exception或Error。

主线成和子线程互不影响,子线程并不会因为主线程结束就结束。

可以使用使用isAlive方法方法确定当前线程是否存活(可运行状态,阻塞状态),如果是如果是可运行或被阻塞状态,该方法返回true,如果当前线程是new状态且不是可运行的, 或者线程死亡了,则返回false

并发基础(三):线程

3.7 总结

「线程的在同一时刻只会处于一种状态,这些状态【属于是虚拟机状态】,不反映任何操作系统线程的状态」

一个线程从创建到终止都是在这六种状态中流转,流转示意图如下:

并发基础(三):线程

四、线程优先级

  • 「线程的优先级是什么」

    「在操作系统中,线程可以划分优先级,线程优先级越高,获得CPU时间片的概率就越大」,但线程优先级的高低与线程的执行顺序并没有必然联系,优先级低的线程也有可能比优先级高的线程先执行。

  • 「设置线程优先级」

    在Java的Thread类中提供了一个setPriority(int newPriority)方法来设置线程的优先级,一般默认为5

    Thread.currentThread().setPriority(int newPriority)
  • 「线程优先级的等级」

    在Java的Thread源码中,提供了 3 个常量值可以用来定义优先级

    并发基础(三):线程

    从Thread中的setPriority方法可知:「线程的优先级分为 1~10 一共 10 个等级,如果优先级小于 1 或大于 10则会抛出 java.lang.IllegalArgumentException 异常」

    并发基础(三):线程
  • 「线程优先级的继承」

    「在 Java 中,线程的优先级具有继承性,如果主线程启动了子线程,则子线程的优先级与主线程的优先级是一样的」。例如

    并发基础(三):线程
    并发基础(三):线程
    • 「调整主线程优先级之后」
    • 「调整主线程优先级之前」
  • 「总结」

    • Java提供一个「线程调度器来监控程序中启动后进入就绪状态的所有线程」,线程调度器按照优先级决定应该调度哪个线程来执行

    • 「优先级低只是表示获取调度的概率低,并不一定会在后面执行,主要cpu的调度」

    • 在Java中,「main线程的优先级默认为5,因此在Java应用中由main线程衍生的线程优先级都默认为5」

五、线程的实现

线程的实现方式耳熟能详,在Java中有四种方式可以创建一个线程,分别是:

  • 「继承Thread类,重写run方法」
  • 「实现Runnable接口,实现run方法」
  • 「实现Callable接口,实现call方法」
  • 「通过线程池的创建线程」

5.1 继承Thread类,重写run方法

public class ThreadTest extends Thread{

@Override
public void run() {
System.out.println("子线程执行 : "+ Thread.currentThread().getName());
}
public static void main(String[] args) {
System.out.println("main线程开始执行 : "+ Thread.currentThread().getName());
ThreadTest t1=new ThreadTest();
ThreadTest t2=new ThreadTest();
t1.start();
t2.start();
System.out.println("main线程结束执行 : "+ Thread.currentThread().getName());
}
}
并发基础(三):线程

5.2 实现Runnable接口,实现run方法

「实现Runnable接口之后,由于Runnable是接口,没有启动线程的start的方法,因此我们需要用Thread类进行封装」

public class RunnableTest implements Runnable{
@Override
public void run() {
System.out.println("Runnable测试----->>>>>子线程执行 : "+ Thread.currentThread().getName());
}
public static void main(String[] args) {
System.out.println("Runnable测试----->>>>>main线程开始执行 : "+ Thread.currentThread().getName());
RunnableTest runnableTest1=new RunnableTest();
Thread t1=new Thread(runnableTest1);
Thread t2=new Thread(runnableTest1);
t1.start();
t2.start();
System.out.println("Runnable测试----->>>>>main线程结束执行 : "+ Thread.currentThread().getName());
}
}
并发基础(三):线程

5.3 实现Callable接口,实现call方法

在Java中,Callable接口中有声明了一个方法call方法,

并发基础(三):线程

从源码可知,「该方法的无参且返回类型是Callable接口的类泛型」

  • 「实现伪代码」
public class CallableTest implements Callable<Integer> {
private Integer anInt;
public CallableTest(int anInt) {
this.anInt = anInt;
}

@Override
public Integer call() {
System.out.println("Callable 测试----->>>>>子线程执行 : "+ Thread.currentThread().getName());
return anInt + 1;
}

public static void main(String[] args) {
System.out.println("Callable 测试----->>>>>main线程开始执行 : "+ Thread.currentThread().getName());
Callable callable = new CallableTest(2);
FutureTask<Integer> future =new FutureTask<Integer>(callable);
Thread t =new Thread(future);
t.start();
Integer integer = null;//获取到线程执行体的返回值
try {
integer = future.get();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("Callable测试----->>>>> 执行结果 :" + integer);
System.out.println("Callable 测试----->>>>>main线程结束执行 : "+ Thread.currentThread().getName());
}
}
并发基础(三):线程

上述实现过程使用到了FutureTask与Thread两个类,「通过FutureTask获取返回值,通过Thread执行线程」

  • 「Runnable与Callable的区别」
    • Runnable没有返回值,Callable可以有返回值
    • Callable接口实现类中的run方法允许异常向上抛出,可以在内部try catch,但是Runnable接口实现类中run方法的异常必须在内部处理,不能抛出

5.4 通过线程池的创建线程

jdk1.5之后就有了线程池的概念,利用ExecutorService一次性创建很多个线程,需要的时候直接充线程池中获取线程

public class ExecutorServiceTest implements Runnable{
public static void main(String[] args) {
System.out.println("ExecutorService 测试----->>>>>main线程开始执行 : "+ Thread.currentThread().getName());
ExecutorService executorService= Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("ExecutorService测试----->>>>>子线程执行 : "+ Thread.currentThread().getName());
}
});
}
System.out.println("ExecutorService 测试----->>>>>main线程结束执行 : "+ Thread.currentThread().getName());
}
}
并发基础(三):线程
  • 这里我用的线程池是newFixedThreadPool,线程池有很多种,这里不做展开讲。
  • 「线程池是一个池子,池里面可以通过Runnable、Callable、Thread中任意一种方式创建的线程」。这里我用的是Runnable的方式

六、线程调度

「在线程池中,多个处于就绪状态的线程在等待CPU,JAVA虚拟机会负责线程的调度,线程调度是指按照特定机制为多个线程分配CPU的使用权」

在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行指令。多线程的并发运行本质上各个线程轮流获得CPU的使用权,分别执行各自的任务。

线程调度「分时调度和抢占式调度」有两种。

  • 「分时调度」

    「所有线程轮流拥有cpu的使用权,平均分配每个线程占用cpu的时间」 (前面说的CPU时间片)。

  • 「抢占式调度」

    「抢占式优先让优先级高的线程使用cpu,优先级相同,则会随机选择一个」。Java为抢占式调度

    优先级越高,抢夺cpu的几率就越大,从而优先级高的占用cpu的时间会更长。

七、守护线程和用户线程

在 Java 中有两种线程:守护线程(Daemon Thread)和用户线程(User Thread)

  • 「守护线程」

    「是一种特殊的线程,在后台默默地完成一些系统性的服务」

    比如垃圾回收线程、JIT 线程都是守护线程

  • 「用户线程」

    「可以理解为是系统的工作线程,它会完成这个程序需要完成的业务操作」

    如 Thread 创建的线程在默认情况下都属于用户线程

    「Java守护线程一般可开发一些为其它用户线程服务的功能。比如说心跳检测,事件监听等。Java 中最有名的守护进程当属 GC 垃圾回收」

  • 「设置线程成为用户线程与守护线程」

    • 通过 Thread.setDaemon(false) 设置为用户线程,默认
    • 通过 Thread.setDaemon(true) 设置为守护线程
  • 「用户线程与守护线程的区别」

    • 主线程结束后,用户线程会继续运行的,此时 JVM 是存活的。
    • 如果没有用户线程,只有守护线程,当 JVM 结束的时候,所有的线程都会结束


原文始发于微信公众号(星河之码):并发基础(三):线程

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由半码博客整理,本文链接:https://www.bmabk.com/index.php/post/61012.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
半码博客——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!