基本概念
进程和线程
- 进程(Process):程序运行资源分配的最小单位,进程内部有多个线程,会共享这个进程的资源。
- 线程(Thread):CPU调度的最小单位,必须依赖进程而存在。
并行和并发
- 并行(Paralle):同一时刻,可以同时处理事情的能力。
- 并发(Concurrent):与单位时间相关,在单位时间(如1s)内可以处理事情的能力。
并发的意义与问题
意义:
- 充分利用cpu的资源,加快用户响应的时间。
- 程序模块化,异步化。
问题:
- 线程共享资源,存在冲突,出现线程安全问题。
- 容易导致线程的死锁。
- 启用过多的线程,有可能造成计算机宕机。
线程的实现方式
Java中实现多实现线程的方式到底有几种?大部分人会说有 2 种、3 种或是 4 种,很少有人会说有 1 种。我们接下来看看它们具体指什么?
实现Runnable接口
实现Runnable接口的类无法直接启动,必须借助Thread类才能启动。
package com.morris.concurrent.thread.impl;
/**
* 线程的实现方法之一:实现runnable接口
*/
public class ImplementRunnable implements Runnable {
@Override
public void run() {
System.out.println("This thread is implement Runnable interface.");
}
public static void main(String[] args) {
Thread thread = new Thread(new ImplementRunnable()); // 无法直接启动,需借助Thread才能启动
thread.start();
}
}
继承Thread类
继承Thread类的线程可直接启动。
package com.morris.concurrent.thread.impl;
/**
* 线程的实现方法之一:继承Thread类
*/
public class ExtendThread extends Thread {
@Override
public void run() {
System.out.println("This thread is extend Thread class.");
}
public static void main(String[] args) {
ExtendThread thread = new ExtendThread();
thread.start(); // 可直接启动
}
}
实现Callable接口
继承Thread类和实现Runnable接口这两种方式启动的线程执行完后的结果是无法获取的。
而实现Callable接口可以拿到线程执行完后返回的结果。
package com.morris.concurrent.thread.impl;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* 线程的实现方法之一:实现Callable接口
*/
public class ImplementCallable implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("This thread is implement Runnable interface.");
return "morris";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> futureTask = new FutureTask<>(new ImplementCallable());
Thread thread = new Thread(futureTask);
thread.start();
String result = futureTask.get(); // 阻塞等待结果
System.out.println(result);
}
}
线程池创建线程
可以通过线程池快速的创建多个线程:
package com.morris.concurrent.thread.impl;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 使用线程池创建线程
*/
public class ThreadPoolDemo {
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(()-> System.out.println("This thread is create by thread pool."));
executorService.shutdown();
}
}
接下来,我们深入解析线程池中的源码,来看看线程池是怎么创建线程的?
static class DefaultThreadFactory implements ThreadFactory {
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
namePrefix = "pool-" + poolNumber.getAndIncrement() + "-thread-";
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
对于线程池而言,本质上是通过线程工厂创建线程的,默认采用DefaultThreadFactory ,它会给线程池创建的线程设置一些默认值,比如:线程的名字、是否是守护线程,以及线程的优先级等。但是无论怎么设置这些属性,最终它还是通过new Thread()创建线程的 ,只不过这里的构造函数传入的参数要多一些,由此可以看出通过线程池创建线程的本质上还是通过new Thread()实现的。
总结
- Thread类底层实现了Runnable接口。
- 实现Runnable接口的线程必须借助Thread类才能启动。
- 需要接受返回值可以使用Callable。
- 实现Runnable接口可以避免由于Java的单继承特性而带来的局限,增强程序的健壮性,代码能够被多个线程共享,代码与数据是独立的。
- Thread负责线程的启动执行,Runnable接口负责具体的业务逻辑,符合单一职责原则,使用了策略模式。
- 在实际应用场景中,一般不会单独直接创建线程,而是使用线程池来创建线程。
事实上创建线程只有一种方式,就是构造一个Thread类,这是创建线程的唯一方式。
查看jvm启动的线程
当运行main方法后,jvm底层到底启动了多少个线程?
可以使用java api来查看线程的情况。
package com.morris.concurrent.thread.impl;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.util.concurrent.TimeUnit;
/**
* 查看程序运行时,jvm底层到底产生了多少个线程
*/
public class ThreadViewDemo {
public static void main(String[] args) throws InterruptedException {
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
for (ThreadInfo threadInfo : threadInfos) {
System.out.printf("ThreadName:%s, status:%s \n", threadInfo.getThreadName(), threadInfo.getThreadState());
}
TimeUnit.SECONDS.sleep(30);
}
}
运行结果如下:
ThreadName:Monitor Ctrl-Break, status:RUNNABLE
ThreadName:Attach Listener, status:RUNNABLE
ThreadName:Signal Dispatcher, status:RUNNABLE
ThreadName:Finalizer, status:WAITING
ThreadName:Reference Handler, status:WAITING
ThreadName:main, status:RUNNABLE
从运行结果可以发现,jvm不仅启动一个main线程来运行我们的程序,还启动了一些守护线程,如垃圾回收线程、RMI线程等。
也可借助jdk自带的工具jconsole来查看: