「尺有所短,寸有所长;不忘初心,方得始终。」
线程是一个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