之前两篇文章我们解释了应该怎么理解线程,这次开始我们对如何使用线程进行一个全面的了解,包括认识线程类,常见的方法以及使用场景,线程创建的几种方式,认识线程的状态,线程池的执行原理等等。
一.认识Thread类 以及它的常用属性和行为
我们理解了什么是线程之后,对它的概念就没有那么陌生了,接下里就是如何使用线程。
在面向对象型语言中,一切皆对象,而类是对象的模版,通过类我们可以创建对象,类封装了对象拥有的属性和行为,因此我们明确一点就是,线程也会被抽象成一个类,Thread,然后我们需要通过不同的方式创建这个类的对象,通过这个对象的行为,开启线程,来驱动操作系统创建线程帮我们完成任务。首先我们先开始认识这个Thread类。
Thread 拥有20多个属性,和几十个方法,听起来就很多,让人头大,显然让我们都了解一遍还是很困难的,因此我们重点了解可能使用到的一些属性以及一些方法。
首先从属性开始。
/**
线程的名称
**/
private volatile String name;
通过下面的操作 我们可以看到 每个创建的线程对象都可以拿到名字
// 创建 计数器
private static CountDownLatch countDownLatch = new CountDownLatch(1);
public static void main(String[] args) throws InterruptedException {
String tName = Thread.currentThread().getName();
System.out.println("主线程名称: "+tName);
new Thread(new Runnable() {
@Override
public void run() {
String tName = Thread.currentThread().getName();
System.out.println("手动开启线程名称: "+tName);
// 计数器减一
countDownLatch.countDown();
}
}).start();
// 计数器减到0前 主线程阻塞等待 子线程执行完毕
countDownLatch.await();
}
----------------------------------------------------------------------------
主线程名称: main
手动开启线程名称: Thread-0
----------------------------------------------------------------------------
通过我们十年的增删改查经验来说,能取一般就能设置,因此我们可以给手动创建的线程
设置线程名称
new Thread(new Runnable() {
@Override
public void run() {
String tName = Thread.currentThread().getName();
System.out.println("手动开启线程名称: "+tName);
countDownLatch.countDown();
}
}, "线程一").start();
----------------------------------------------------------------------------
主线程名称: main
手动开启线程名称: 线程一
----------------------------------------------------------------------------
这简直简单死了
线程名称在多线程编程中具有几个重要的作用和用途:
识别和调试:
线程名称是在识别和调试程序中非常有用的标识。通过为每个线程分配一个描述性的名称
,可以轻松地区分和识别线程。这对于在日志中追踪线程活动、诊断死锁或其他并发问题
,以及分析程序行为时都非常有帮助。
监控和管理:
在某些情况下,线程名称是管理和监控线程活动的一种方式。例如,可以通过线程名称
来组织和过滤线程信息,或者在用户界面上显示线程状态。在一些开发工具和监控系统中
,线程名称常被用来标记和统计线程的使用情况和性能特征。
上下文传递:
在某些多线程环境下,线程名称可能被用来传递上下文信息。虽然不是主要的上下文传递
方式,但在某些情况下,线程名称可能被用作线程本地存储(ThreadLocal)的一种简单
替代方法,用来存储和传递线程相关的数据。
可读性和维护性:
合理的线程命名能够提升代码的可读性和维护性。通过为线程分配有意义的名称,能够更
清晰地表达线程的作用和目的,使代码更易于理解和修改。
总之,线程名称在多线程编程中是一种良好的实践,它不仅帮助诊断和调试程序,还可以
提高程序的可管理性和可维护性。因此,尽可能为每个线程分配一个明确且描述性的名
称是推荐的做法。
下面看点难得东西 (看不懂可以跳过 这不是基础知识)
volatile 我们发现Thread的名字 通过了volatile 关键字修饰。
学习过JUC的都知道这个关键字的作用,volatile 保证了数据的可见性和有序性,
它禁止了指令重排,同时
强制要求数据存放在主存当中,修改数据不需要通知其他线程同步更新,直接修改主存,
即可保证多线程环境下,同时可见该数据。
但是volatile 不能保证原子性,即操作它修饰的数据的行为,必须不可拆分,说人话就是
我们通常的操作,不并一定是原子的,我们执行了一行命令之后,在真正执行时,可能发生
的是复合操作,最直观的例子就是i++。 它只有一行命令,我们直观理解是 它可以给i这
个变量自增,但是我们通过javap反编译后可以发现,i++ 实际执行了4个指令,也就是说
i++ 这个过程可以被拆分,获取i的地址 得到i的值给i++ 将加的结果赋值回去,
这就有些麻烦了,因为线程的上下文切换时,可能这些命令值执行了两行,而本该加一之后其他
线程继续操作,导致其他线程在操作时没有拿到加一后的结果,最终发生线程安全问题,
因此volatile 需要结合同步锁,或原子类(底层不安全类)来保证原子性。
这里的name 为啥要加这个关键字呢 就是这个name在多线程环境下,可能被不同的线程获取名称
或重新设置名称,因此要保证多线程的可见性,主线程修改了名字,其他线程能够立刻通知到。
/**
线程的优先级 1-10 优先级逐步提高
**/
private int priority;
// 设置优先级
thread.setPriority(10);
/** 最小优先级
* The minimum priority that a thread can have.
*/
public static final int MIN_PRIORITY = 1;
/** 推荐优先级
* The default priority that is assigned to a thread.
*/
public static final int NORM_PRIORITY = 5;
/** 最大先级
* The maximum priority that a thread can have.
*/
public static final int MAX_PRIORITY = 10;
这个值的作用是通知操作系统,当前线程的优先度,该值越大在线程竞争cpu时间片
执行时越优先,但是这只是推荐并不能保证一定按照优先级执行,具体调度还是有
操作系统来保证。
通常情况下,我们一般不会设置该属性。
具体测试的话,可以通过 Thread.yield(); 让出cpu的时间片,然后重新竞争上岗
,来模拟效果,认识这个方法的时候在做具体测试。
/* Whether or not the thread is a daemon thread. */
private boolean daemon = false;
该线程是否是守护线程
守护线程概念:
守护线程是一种在后台提供服务的线程,它的作用是为其他非守护线程(用户线程)提供服务和
支持。当JVM中只剩下守护线程时,JVM会自动退出,这是守护线程的一个特性。
设置线程为守护线程:
在Java中,通过设置线程的 daemon 属性来决定线程是否为守护线程。
例如,如果设置 thread.setDaemon(true),则表示将 thread 标记为守护线程。
默认情况下,线程是非守护线程(daemon 属性为 false)。
作用和用途:
守护线程通常用于提供服务和支持,比如垃圾回收器线程就是典型的守护线程。它们在
后台运行,不会阻止JVM的退出。相比之下,非守护线程(用户线程)的存在会阻止JVM
的正常退出,除非所有非守护线程都已经结束或者被中断。
注意事项:
设置线程为守护线程必须在启动线程之前完成,一旦线程开始执行,守护属性就不能再改变。
守护线程在运行过程中,如果所有的非守护线程结束了,它们会被强制结束,因此不应该用
于执行需要完整性和稳定性的任务。
综上所述,通过 private boolean daemon = false; 可以指示一个线程对象的
daemon 属性,决定其是否为守护线程。这一属性的设置对于控制线程的行为和程序的退出
具有重要的影响。
/* What will be run.
什么会被执行 ,也就是执行的任务是什么,可以到这是一个Runnable接口
*/
private Runnable target;
Runnable接口 是一个函数是接口 它只定义了一个run方法
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
我们通过实现run方法内部具体的任务,交由Thread类来执行,通过上面可知,
我们手动调用run方法是没有任何意义的,需要使用start()方法
/** 构造器 里面包含了 target 通过构造器来设置 target的值
*/
private Thread(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
......
// 设置target的值
this.target = target;
setPriority(priority);
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* Set thread ID */
this.tid = nextThreadID();
}
执行方法 内部调用start0()
public synchronized void start()
本地方法 执行时会调用c++ 实现的线程启动方法 执行 target中的run方法内定义的任务
private native void start0();
除此之外常接触到的还有 线程状态、线程组、线程本地变量(ThreadLocal)、可继承线程本地变量(inheritableThreadLocal)之后单独开篇
下面介绍常用的方法
// 该方法是线程类上的一个静态方法 此方法的作用是获取当前代码执行的线程
Thread thread = Thread.currentThread();
// 该方法经常使用
// 通过获取当前线程我们能进行一下几种常见的行为
thread.getName(); // 获取当前线程名称
thread.getPriority(); // 获取当前线程的优先级
thread.getThreadGroup(); //获取线程组
thread.getState(); // 获取线程状态
获取名称和优先级 比较简单,获取线程组单独开篇讲,我们先结合一些其他线程方法,理解一下线程的状态相关的概念,首先我们先认识一个枚举类。
/**
线程状态。线程可以处于以下状态之一:
* A thread state. A thread can be in one of the following states:
* <ul>
* <li>{@link #NEW}<br> 新 尚未启动的线程处于此状态。
* A thread that has not yet started is in this state.
* </li>
* <li>{@link #RUNNABLE}<br> RUNNABLE 在 Java 虚拟机中执行的线程处于此状态。
* A thread executing in the Java virtual machine is in this state.
* </li>
* <li>{@link #BLOCKED}<br> 已阻止 等待监视器锁定而被阻塞的线程处于此状态。
* A thread that is blocked waiting for a monitor lock
* is in this state.
* </li>
* <li>{@link #WAITING}<br> 无限期等待另一个线程执行特定操作的线程处于此状态。
* A thread that is waiting indefinitely for another thread to
* perform a particular action is in this state.
* </li>
* <li>{@link #TIMED_WAITING}<br> 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.
* </li>
* <li>{@link #TERMINATED}<br> TERMINATED 已退出的线程处于此状态。
* A thread that has exited is in this state.
* </li>
* </ul>
*在给定时间点,线程只能处于一种状态。这些状态是不反映任何操作系统线程状态的虚拟机状态。
* <p>
* 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.
*
* @since 1.5
* @see #getState
*/
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 {@code Object.wait()}
* on an object is waiting for another thread to call
* {@code Object.notify()} or {@code Object.notifyAll()} on
* that object. A thread that has called {@code Thread.join()}
* 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;
}
上面的枚举类 定义了 一个线程在jvm中的生命周期状态,通过这句话,在给定时间点,线程只能处于一种状态。这些状态是不反映任何操作系统线程状态的虚拟机状态。我们可以察觉一点,这个状态是属于JVM内部的线程对象的,并影响操作系统的线程状态。因此我们需要先了解下,操作系统中的状态都包括什么,来横向和JVM线程状态对比。
上图比较清晰的 表示了 操作系统线程状态都包括哪些?
new 新建状态 表示线程已经被创建,但是此时并没有运行
runnable 就绪状态 此时通知线程开始执行,但是此时线程还没有分到cpu时间片,也就是调用了start方法 但是还没真正执行的就绪状态
running 运行状态,此时线程已经分得了时间片开始真正执行了
block 阻塞状态 线程由于某种原因陷入了等待状态,停止执行等待恢复
dead 死亡 线程正常退出,死亡后的线程无法恢复
下面看下java线程状态与操作系统线程状态有什么区别
new 新建状态
runnable 就绪 或 运行状态
Blocked 阻塞状态 此时获得了 同步锁
Waiting 等在状态 在获取锁之后 手动进入等待状态 等待被唤醒
TimedWaiting 超时等待 阻塞一定时间后 恢复执行
Teminated 终止状态 线程结束 资源回收
可以看出 java的线程 把 就绪和运行状态合并到了一起
并且将阻塞状态 细分成 为 阻塞 等待 和 超时等待。下面我们结合代码,更加详细的理解线程状态。
// new 线程创建状态
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
}
}, "线程一");
}
// 此时我们创建了一个线程对象 ,并没有使用start方法启动,因此线程处于new 创建状态
// 我们调用了start方法 线程开始进入就绪状态 如果获得了cpu时间片之后
将进入运行状态 执行 run方法内部任务逻辑
thread.start(); // 此时处于Runnable状态
// 我们现在调用sleep(10000)方法 让线程睡眠 十秒钟
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "线程一");
// 同时开启线程
thread.start();
// 主线程 睡2s等下 线程一创建并执行 然后睡眠
Thread.sleep(2000);
// 获取线程状态 我们可以看到结果是 TIMED_WAITING 超时等待状态
// 此时证明了 线程睡眠 线程状态将会进入 超时等待状态
System.out.println(thread.getState());
}
-----------------------------------------------------------------------------------------
TIMED_WAITING
-----------------------------------------------------------------------------------------
现在我们来 找到什么场景下 线程会进入 BLOCKED状态 ,先描述一下思路
我们创建两个线程 然后定义一个同步方法 将方法的代码放入 同步代码块中,
同步代码块中让线程睡眠100s,然后让两个线程去争抢对象锁,谁拿到谁将会睡眠,
那么它的状态将是超时等待,而另一个线程将是BLOCKED状态 下面看代码以及结果
public class TestThread {
public static Object object = new Object();
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
syn();
}
}, "线程一");
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
syn();
}
}, "线程二");
thread.start();
thread1.start();
Thread.sleep(2000);
System.out.println(thread.getName()+thread.getState());
System.out.println(thread1.getName()+thread1.getState());
}
public static void syn() {
synchronized (object) {
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
-----------------------------------------------------------------------------------------
线程一TIMED_WAITING
线程二BLOCKED
-----------------------------------------------------------------------------------------
以上印证了 在线程竞争的情况下,未得到锁的线程将等待锁释放,进入BLOCKED 状态
下面我们想办法找到 waiting的情况
先说下思路
依旧是两个线程 ,我们需要再获得锁的情况下 ,让 线程一进入等待状态,然后释放锁
3s后线程二进行唤醒线程一的操作,同时主线程等待2s 此时 线程一没有被唤醒
线程二还在睡眠 因此线程一 线程一WAITING
线程二TIMED_WAITING ,然后 主线程睡眠十秒 此时 线程二将在3秒后 唤醒
线程一 线程一打印醒了,十秒后主线程醒来 程序结束。
public class TestThread {
public static Object object = new Object();
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
syn();
}
}, "线程一");
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
syn();
}
}, "线程二");
thread.start();
thread1.start();
Thread.sleep(2000);
System.out.println(thread.getName()+thread.getState());
System.out.println(thread1.getName()+thread1.getState());
Thread.sleep(10000);
}
public static void syn() {
synchronized (object) {
if (Thread.currentThread().getName().equals("线程一")){
try {
object.wait();
System.out.println(Thread.currentThread().getName()+"醒了");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else {
object.notifyAll();
}
}
}
}
-----------------------------------------------------------------------------------------
线程一WAITING
线程二TIMED_WAITING
线程一醒了
-----------------------------------------------------------------------------------------
下面展示 线程结束 很简单 线程执行完毕 即结束 未执行完毕即运行
public class TestThread {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
}
}, "线程一");
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
while (true){
}
}
}, "线程二");
thread.start();
thread1.start();
Thread.sleep(2000);
System.out.println(thread.getName()+thread.getState());
System.out.println(thread1.getName()+thread1.getState());
}
}
-----------------------------------------------------------------------------------------
线程一TERMINATED
线程二RUNNABLE
-----------------------------------------------------------------------------------------
上面简单聊了一下 线程的状态 ,其实这些状态如果能理解锁的结构是怎样的,更容易立即 我这里简单阐述一下,后面会单独开篇,12000字了,键盘要敲冒烟了。
简单解释一下上图,现在先忽略java 对象头的内容,当一个线程抢到锁之后,锁对象的owner属性将是当前获得锁的线程,此时如果其他线程来要锁,需要判断 owner 是不是为空 如果是 则修改owner 拥有者,如果不为空 说明锁已经被人拿走了,此时需要等待 即 等待线程加入EntryList 阻塞队列,他就是个链表,可以简单把它想象成 头加 尾取 这样就是个队列了 ,但实际上 线程时候获取锁的概率应该是平等的,并不是先进先取 ,实际的调度算法更加综合,这里简单理解,后来的就要排队等owner 自己释放锁。
如果owner 在获得锁的情况下,调用了 wait方法 那么owner 将进入等待池中,此时释放锁,owner将为null,并通知EntryList 你们来抢锁啦 我不要啦,这时等待队列中的线程竞争锁。
随着wait方法 调用越来越多 等待池中的线程也会越来越多,此时如果持有锁的线程 调用了 notify 将随机唤醒一个 线程从等待池中 ,加入等待队列重新竞争锁,而调用notifyAll 将唤醒全部的等待池内的线程。大致的流程就是这样的希望能帮助你们理解锁到底是怎么个结构,怎么实现的。
接下来我们接着说 线程的方法。这次也写不少了 我们下篇接着说把。求点赞求收藏啊 纯手打。