本篇文章给大家带来了关于java的相关知识,其中主要介绍了关于多线程的相关问题,一个进程可以并发多个线程,每条线程并行执行不同的任务,线程是进程的基本单位,是一个单一顺序的控制流,下面一起来看一下,希望对大家有帮助。
推荐学习:《java视频教程》
Java 提供了多线程编程的内置支持,让我们可以轻松开发多线程应用。
Java 中我们最为熟悉的线程就是 main 线程——主线程。
一个进程可以并发多个线程,每条线程并行执行不同的任务。线程是进程的基本单位,是一个单一顺序的控制流,一个进程一直运行,直到所有的“非守护线程”都结束运行后才能结束。Java 中常见的守护线程有:垃圾回收线程、
多线程可以帮助我们高效地执行任务,合理利用 CPU 资源,充分地发挥多核 CPU 的性能。但是多线程也并不总是能够让程序高效运行的,多线程切换带来的开销、线程死锁、线程异常等等问题,都会使得多线程开发较单线程开发更麻烦。因此,有必要学习 Java 多线程的相关知识,从而提高开发效率。
1 创建多线程
根据官方文档 Thread (Java Platform SE 8 ) (oracle.com) 中 java.lang.Thread 的说明,可以看到线程的创建方式主要有两种:
可以看到,有两种创建线程的方式:
声明一个类继承 Thread 类,这个子类需要重写 run 方法,随后创建这个子类的实例,这个实例就可以创建并启动一个线程执行任务;
声明一个类实现接口 Runnable 并实现 run 方法。这个类的实例作为参数分配给一个 Thread 实例,随后使用 Thread 实例创建并启动线程即可
除此之外的创建线程的方法,诸如使用 Callable 和 FutureTask、线程池等等,无非是在此基础上的扩展,查看源码可以看到 FutureTask 也实现了 Runnable 接口。
使用继承 Thread 类的方法创建线程的代码:
/**
* 使用继承 Thread 类的方法创建线程
*/
public class CreateOne {
public static void main(String[] args) {
Thread t = new MySubThread();
t.start();
}
}
class MySubThread extends Thread {
@Override
public void run() {
// currentThread() 是 Thread 的静态方法,可以获取正在执行当前代码的线程实例
System.out.println(Thread.currentThread().getName() + "执行任务");
}
}
// ================================== 运行结果
Thread-0执行任务
登录后复制
使用实现 Runnable 接口的方法创建线程的代码:
/**
* 使用实现 Runnable 接口的方法创建线程
*/
public class CreateTwo {
public static void main(String[] args) {
RunnableImpl r = new RunnableImpl();
Thread t = new Thread(r);
t.start();
}
}
class RunnableImpl implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "执行任务");
}
}
// ================================== 运行结果
Thread-0执行任务
登录后复制
1.1 孰优孰劣
创建线程虽然有两种方法,但是在实际开发中,使用实现接口 Runnable 的方法更好,原因如下:
查看 Thread 的 run 方法,可以看到:
// Thread 实例的成员变量 target 是一个 Runnable 实例,可以通过 Thread 的构造方法传入
private Runnable target;
// 如果有传入 Runnable 实例,那么就执行它的 run 方法
// 如果重写,就完全执行我们自己的逻辑
public void run() {
if (target != null) {
target.run();
}
}
登录后复制
查看上面的源码,我们可以知道,Thread 类并不是定义执行任务的主体,而是 Runnable 定义执行任务内容,Thread 调用执行,从而实现线程与任务的解耦。
由于线程与任务解耦,我们可以复用线程,而不是当需要执行任务就去创建线程、执行完毕就销毁线程,这样带来的系统开销太大。这也是线程池的基本思想。
此外,Java 只只支持单继承,如果继承 Thread 使用多线程,那么后续需要通过继承的方式扩展功能,那会相当麻烦。
2 start 和 run 方法
从上面可以得知,有两种创建线程的方式,我们通过 Thread 类或 Runnable 接口的 run 方法定义任务,通过 Thread 的 start 方法创建并启动线程。
我们不能通过 run 方法启动并创建一个线程,它只是一个普通方法,如果直接调用这个方法,其实只是调用这个方法的线程在执行任务罢了。
// 将上面的代码修改一下,查看执行结果
public class CreateOne {
public static void main(String[] args) {
Thread t = new MySubThread();
t.run();
//t.start();
}
}
// ===================== 执行结果
main执行任务
登录后复制
查看 start 方法的源码:
// 线程状态,为 0 表示还未启动
private volatile int threadStatus = 0;
// 同步方法,确保创建、启动线程是线程安全的
public synchronized void start() {
// 如果线程状态不为 0,那么抛出异常——即线程已经创建了
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) {
}
}
}
// 由本地方法实现,只需要知道,该方法调用后会创建一个线程,并且会执行 run 方法
private native void start0();
登录后复制
由上面的源码可以得知:
创建并启动一个线程是线程安全的
start() 方法不能反复调用,否则会抛出异常
3 怎么停止线程
线程并不是无休止地执行下去的,通常情况下,线程停止的条件有:
run 方法执行结束
线程发生异常,但是没有捕获处理
除此之外,我们还需要自定义某些情况下需要通知线程停止,例如:
用户主动取消任务
任务执行时间超时、出错
出现故障,服务需要快速停止
...
为什么不能直接简单粗暴的停止线程呢?通过通知线程停止任务,我们可以更优雅地停止线程,让线程保存问题现场、记录日志、发送警报、友好提示等等,令线程在合适的代码位置停止线程,从而避免一些数据丢失等情况。
令线程停止的方法是让线程捕获中断异常或检测中断标志位,从而优雅地停止线程,这是推荐的做法。而不推荐的做法有,使用被标记为过时的方法:stop,resume,suspend,这些方法可能会造成死锁、线程不安全等情况,由于已经过时了,所以不做过多介绍。
3.1 通知线程中断
我们要使用通知的方式停止目标线程,通过以下方法,希望能够帮助你掌握中断线程的方法:
/**
* 中断线程
*/
public class InterruptThread {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
long i = 0;
// isInterrupted() 检测当前线程是否处于中断状态
while (i < Long.MAX_VALUE && !Thread.currentThread().isInterrupted()) {
i++;
}
System.out.println(i);
});
t.start();
// 主线程睡眠 1 秒,通知线程中断
Thread.sleep(1000);
t.interrupt();
}
}
// 运行结果
1436125519
登录后复制
这是中断线程的方法之一,还有其他方法,当线程处于阻塞状态时,线程并不能运行到检测线程状态的代码位置,然后正确响应中断,这个时候,我们需要通过捕获异常的方式停止线程:
/**
* 通过捕获中断异常停止线程
*/
public class InterruptThreadByException {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
long i = 0;
while (i < Long.MAX_VALUE) {
i++;
try {
// 线程大部分时间处于阻塞状态,sleep 方法会抛出中断异常 InterruptedException
Thread.sleep(100);
} catch (InterruptedException e) {
// 捕获到中断异常,代表线程被通知中断,做出相应处理再停止线程
System.out.println("线程收到中断通知 " + i);
// 如果 try-catch 在 while 代码块之外,可以不用 return 也可以结束代码
// 在 while 代码块之内,如果没有 return / break,那么还是会进入下一次循环,并不能正确停止
return;
}
}
});
t.start();
Thread.sleep(1000);
t.interrupt();
}
}
// 运行结果
线程收到中断通知 10
登录后复制
以上,就是停止线程的正确做法,此外,捕获中断异常后,会清除线程的中断状态,在实际开发中需要特别注意。例如,修改上面的代码:
public class InterruptThreadByException {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
long i = 0;
while (i < Long.MAX_VALUE) {
i++;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println("线程收到中断通知 " + i);
// 添加这行代码,捕获到中断异常后,检测中断状态,中断状态为 false
System.out.println(Thread.currentThread().isInterrupted());
return;
}
}
});
t.start();
Thread.sleep(1000);
t.interrupt();
}
}
登录后复制
所以,在线程中,如果调用了其他方法,如果该方法有异常发生,那么:
将异常抛出,而不是在子方法内部捕获处理,由 run 方法统一处理异常
捕获异常,并重新通知当前线程中断,Thread.currentThread().interrupt()
例如:
public class SubMethodException {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new ExceptionRunnableA());
Thread t2 = new Thread(new ExceptionRunnableB());
t1.start();
t2.start();
Thread.sleep(1000);
t1.interrupt();
t2.interrupt();
}
}
class ExceptionRunnableA implements Runnable {
@Override
public void run() {
try {
while (true) {
method();
}
} catch (InterruptedException e) {
System.out.println("run 方法内部捕获中断异常");
}
}
public void method() throws InterruptedException {
Thread.sleep(100000L);
}
}
class ExceptionRunnableB implements Runnable {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
method();
}
}
public void method() {
try {
Thread.sleep(100000L);
} catch (InterruptedException e) {
System.out.println("子方法内部捕获中断异常");
// 如果不重新设置中断,线程将不能正确响应中断
Thread.currentThread().interrupt();
}
}
}
登录后复制
综上,总结出令线程正确停止的方法为:
使用 interrupt() 方法通知目标线程停止,标记目标线程的中断状态为 true
目标线程通过 isInterrupted() 不时地检测线程的中断状态,根据情况决定是否停止线程
如果线程使用了阻塞方法例如 sleep(),那么需要捕获中断异常并处理中断通知,捕获了中断异常会重置中断标记位
如果 run() 方法调用了其他子方法,那么子方法:
将异常抛出,传递到顶层 run 方法,由 run 方法统一处理
将异常捕获,同时重新通知当前线程中断
下面再说说关于中断的几个相关方法和一些会抛出中断异常的方法,使用的时候需要特别注意。
3.2 线程中断的相关方法
interrupt() 实例方法,通知目标线程中断。
static interrupted() 静态方法,获取当前线程是否处于中断状态,会重置中断状态,即如果中断状态为 true,那么调用后中断状态为 false。方法内部通过 Thread.currentThread() 获取执行线程实例。
isInterrupted() 实例方法,获取线程的中断状态,不会清除中断状态。
3.3 阻塞并能响应中断的方法
Object.wait()
Thread.sleep()
Thread.join()
BlockingQueue.take() / put()
Lock.lockInterruptibly()
CountDownLatch.await()
CyclicBarrier.await()
Exchanger.exchange()
4 线程的生命周期
线程的生命周期状态由六部分组成:
可以用一张图总结线程的生命周期,以及各个过程之间是如何转换的:
5 Thread 和 Object 中的线程方法
现在,我们已经知道了线程的创建、启动、停止以及线程的生命周期了,那么,再来看看线程相关的方法有哪些。
首先,看看 Thread 中的一些方法:
再看看 Object 中的相关方法:
运行以下代码,查看 wait() 和 sleep() 是否会释放同步锁
/**
* 证明 sleep 不会释放锁,wait 会释放锁
*/
public class SleepAndWait {
private static Object lock = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(()->{
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "获得同步锁,调用 wait() 方法");
try {
lock.wait(2000);
System.out.println(Thread.currentThread().getName() + "重新获得同步锁");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(()->{
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "获得同步锁,唤醒另一个线程,调用 sleep()");
lock.notify();
try {
// 如果 sleep() 会释放锁,那么在此期间,上面的线程将会继续运行,即 sleep 不会释放同步锁
Thread.sleep(2000);
// 如果执行 wait 方法,那么上面的线程将会继续执行,证明 wait 方法会释放锁
//lock.wait(2000);
System.out.println(Thread.currentThread().getName() + "sleep 结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
}
}
登录后复制
上面的代码已经证明了 sleep() 不会释放同步锁,此外,sleep() 也不会释放 Lock 的锁,运行以下代码查看结果:
/**
* sleep 不会释放 Lock 锁
*/
public class SleepDontReleaseLock implements Runnable {
private static Lock lock = new ReentrantLock();
@Override
public void run() {
// 调用 lock 方法,线程会尝试持有该锁对象,如果已经被其他线程锁住,那么当前线程会进入阻塞状态
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "获得 lock 锁");
// 如果 sleep 会释放 Lock 锁,那么另一个线程会马上打印上面的语句
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "释放 lock 锁");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 当前线程释放锁,让其他线程可以占有锁
lock.unlock();
}
}
public static void main(String[] args) {
SleepDontReleaseLock task = new SleepDontReleaseLock();
new Thread(task).start();
new Thread(task).start();
}
}
登录后复制
5.1 wait 和 sleep 的异同
接下来总结 Object.wait() 和 Thread.sleep() 方法的异同点。
相同点:
都会使线程进入阻塞状态
都可以响应中断
不同点:
wait() 是 Object 的实例方法,sleep() 是 Thread 的静态方法
sleep() 需要指定时间
wait() 会释放锁,sleep() 不会释放锁,包括同步锁和 Lock 锁
wait() 必须配合 synchronized 使用
6 线程的相关属性
现在我们已经对 Java 中的多线程有一定的了解了,我们再看看 Java 中线程 Thread 的一些相关属性,即它的成员变量。
运行以下代码,了解线程的相关属性
public class ThreadFields {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
// 自定义线程的 ID 并不是从 2 开始
System.out.println("线程 " + Thread.currentThread().getName()
+ " 的线程 ID " + Thread.currentThread().getId());
while (true) {
// 守护线程一直运行,但是 用户线程即这里的主线程结束后,也会随着虚拟机一起停止
}
});
// 自定义线程名字
t.setName("自定义线程");
// 将其设置为守护线程
t.setDaemon(true);
// 设置优先级 Thread.MIN_PRIORITY = 1 Thread.MAX_PRIORITY = 10
t.setPriority(Thread.MIN_PRIORITY);
t.start();
// 主线程的 ID 为 1
System.out.println("线程 " + Thread.currentThread().getName() + " 的线程 ID " + Thread.currentThread().getId());
Thread.sleep(3000);
}
}
登录后复制
7 全局异常处理
在子线程中,如果发生了异常我们能够及时捕获并处理,那么对程序运行并不会有什么恶劣影响。
但是,如果发生了一些未捕获的异常,在多线程情况下,这些异常打印出来的堆栈信息,很容易淹没在庞大的日志中,我们可能很难察觉到,并且不好排查问题。
如果对这些异常都做捕获处理,那么就会造成代码的冗余,编写起来也不方便。
因此,我们可以编写一个全局异常处理器来处理子线程中抛出的异常,统一地处理,解耦代码。
7.1 源码查看
在讲解如何处理子线程的异常问题前,我们先看看 JVM 默认情况下,是如何处理未捕获的异常的。
查看 Thread 的源码:
public class Thread implements Runnable {
【1】当发生未捕获的异常时,JVM 会调用该方法,并传递异常信息给异常处理器
可以在这里打下断点,在线程中抛出异常不捕获,IDEA 会跳转到这里
// 向处理程序发送未捕获的异常。此方法仅由JVM调用。
private void dispatchUncaughtException(Throwable e) {
【2】查看第 9 行代码,可以看到如果没有指定异常处理器,默认是线程组作为异常处理器
【3】调用这个异常处理器的处理方法,处理异常,查看第 15 行
getUncaughtExceptionHandler().uncaughtException(this, e);
}
public UncaughtExceptionHandler getUncaughtExceptionHandler() {
return uncaughtExceptionHandler != null ?
uncaughtExceptionHandler : group;
}
【4】UncaughtExceptionHandler 是 Thread 的内部接口,线程组也是该接口的实现,
只有一个方法处理异常,接下来查看第 25 行,看看 Group 是如何实现的
@FunctionalInterface
public interface UncaughtExceptionHandler {
void uncaughtException(Thread t, Throwable e);
}
}
public class ThreadGroup implements Thread.UncaughtExceptionHandler {
【5】默认异常处理器的实现
public void uncaughtException(Thread t, Throwable e) {
// 如果有父线程组,交给它处理
if (parent != null) {
parent.uncaughtException(t, e);
} else {
// 获取默认的异常处理器,如果没有指定,那么为 null
Thread.UncaughtExceptionHandler ueh =
Thread.getDefaultUncaughtExceptionHandler();
if (ueh != null) {
ueh.uncaughtException(t, e);
}
// 没有指定异常处理器,打印堆栈信息
else if (!(e instanceof ThreadDeath)) {
System.err.print("Exception in thread ""
+ t.getName() + "" ");
e.printStackTrace(System.err);
}
}
}
}
登录后复制
7.2 自定义全局异常处理器
通过上面的源码讲解,已经可以知道 JVM 是如何处理未捕获的异常的了,即只打印堆栈信息。那么,要如何自定义异常处理器呢?
具体方法为:
实现接口 Thread.UncaughtExceptionHandler 并实现方法 uncaughtException()
为创建的线程指定异常处理器
示例代码:
public class MyExceptionHandler implements Thread.UncaughtExceptionHandler{
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("发生了未捕获的异常,进行日志处理、报警处理、友好提示、数据备份等等......");
e.printStackTrace();
}
public static void main(String[] args) {
Thread t = new Thread(() -> {
throw new RuntimeException();
});
t.setUncaughtExceptionHandler(new MyExceptionHandler());
t.start();
}
}
登录后复制
8 多线程带来的问题
合理地利用多线程能够带来性能上的提升,但是如果因为一些疏漏,多线程反而会成为程序员的噩梦。
例如,多线程开发,我们需要考虑线程安全问题、性能问题。
首先,讲讲线程安全问题。
什么是线程安全?所谓线程安全,即
因此,在编写多线程程序时,就需要考虑某个数据是否是线程安全的,如果这个对象满足:
被多个线程共享
操作具有时序要求,先读后写
这个对象的类有他人编写,并且没有声明是线程安全的
那么我们就需要考虑使用同步锁、Lock、并发工具类(java.util.concurrent)来保证这个对象是在多线程下是安全的。
再看看多线程带来的性能问题。
多个线程的调度需要上下文切换,这需要耗费 CPU 资源。
所谓上下文,即处理器中寄存器、程序计数器内的信息。
上下文切换,即 CPU 挂起一个线程,将其上下文保存到内存中,从内存中获取另一个运行线程的上下文,恢复到寄存器中,根据程序计数器中的指令恢复线程运行。
一个线程被挂起,另一个线程恢复运行,这个时候,被挂起的线程的数据缓存对于运行线程来说是无效的,减缓了线程的运行速度,新的线程需要重新缓存数据提升运行速度。
通常情况下,密集的 IO 操作、抢锁操作都会带来密集的上下文切换。
以上,是上下文切换带来的性能问题,Java 的内存模型也会带来性能问题,为了保证数据的可见性,JVM 会强制令数据缓存失效,保证数据是实时最新的,这也牺牲了缓存带来的性能提升。
9 总结
这里总结下上面的内容。
创建线程有两种方式,继承 Thread 和实现 Runnable
start 方法才能正确创建和启动线程,run 方法只是一个普通方法
start 方法不能反复调用,反复调用会抛出异常
正确停止线程的方法是通过 interrupt() 通知线程
线程不时地检查中断状态并判断是否停止线程,使用方法 isInterrupt()
如果线程阻塞,捕获中断异常,判断是否停止线程
线程调用的子方法最好将异常抛出,由 run 方法统一捕获处理
线程调用的子方法如果捕获异常,需要重新通知线程中断
线程的生命周期为
NEW
RUNNABLE
BLOCKED
WAITING
TIMED WAITING
TERMINATED
wait()/notify()/notifyAll() 必须配合同步锁使用
wait() 会释放锁,sleep() 不会释放锁,包括同步锁和 Lock 锁
线程的一些属性
线程ID,无法修改
线程名 name,可以自定义
守护线程 daemon,线程类型会继承自父线程,通常不指定线程为守护线程
优先级 priority,通常使用默认优先级,不改变优先级
可以自定义全局异常处理器,处理非主线程中的未捕获的异常,如备份数据、日志处理、报警等等
多线程开发会带来线程安全问题、性能问题,开发过程需要特别注意
推荐学习:《java视频教程》
以上就是简单总结Java多线程知识点的详细内容,更多请关注悠悠之家其它相关文章!
发表评论 取消回复