运行一个 Java 程序就是跑一个 Java 进程,该进程至少有一个线程即主线程,而 main 方法就是主线程的入口;
一、常见多线程名词解释
并发:一个 CPU 核心上,通过快速轮转调度的方式,执行多个进程,微观上 "有先有后";
并行:两个进程同时分别在两个 CPU 核心上执行,微观上 "同时进行";
进程:进程是资源分配的最小单位;
线程:⼀个线程就是⼀个 "执⾏流",每个线程之间都可以按照顺序执行自己的代码,线程是调度执行的最小单位;
终止线程:想要终止线程本质就是让 run 方法执行结束;
线程等待:让一个线程等待另一个线程执行结束,再继续执行;
线程安全:让多个线程同时执行同样的代码,出现的 bug;
进程和线程的区别:
1. 进程包含线程,一个进程可以有一个或者多个线程;
2. 进程和线程都是实现并发编程的,但线程更轻量,更高效;
3. 同一个进程的线程之间,共用同一份资源,如内存,硬盘等,即线程省去了申请资源的开销;
4. 进程之间是有独立性的,同一个进程内的线程之间可能会相互影响(存在线程安全,线程异常等问题);
5. 进程是分配资源的最小单位,线程是调度执行的最小单位;
二、创建线程的几种方式
1. 继承 Thread(java.long包下) 类,重写 run 方法
(可通过单独定义一个类,或者使用匿名内部类),单独创建一个类不常用;
public static void main(String[] args) {
Thread t1 = new Thread(){
@Override
public void run() {
System.out.println("通过匿名内部类创建线程");;
}
};
t1.start();
}
Thread 类:是 JVM 用来管理线程的⼀个类,每个线程都有⼀个唯⼀的 Thread 对象与之关联;
Thread 类中的 run 方法 是对 Runnable 的重写,run 方法描述了线程的入口,每个线程都是一个独立的执行流,一个线程从入口方法开始执行,即 run 方法,run 方法执行完毕,线程就会结束;
start 方法 用于开启线程,负责调用系统 API,系统创建线程,再调用 run 方法执行线程 ;直接调用 run 方法是不会创建线程的
常用构造方法如下:
常用其他方法如下:
2. 实现 Runnable(java.long包下) 接口,重写 run 方法;
可通过单独创建一个类,或者匿名内部类,或者 lambda 表达式实现,后两者更为常用;
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("通过匿名内部类创建线程");
}
});
Thread t2 = new Thread(() -> System.out.println("通过 lambda 表达式创建线程"));
}
Runnable 接口表示一个 "可执行的任务";由于 Runnable 接口中没有 start 方法,start 方法是 Thread 类的,故只实现 Runnable 接口的类的对象不能通过直接调用 start 方法启动线程,要通过 Thread 类的构造方法实现;
3. 实现 Callable 接口(JUC,java.util.concurrent包下),重写 call 方法
可通过单独创建一个类,或者匿名内部类
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> callable = new Callable<Integer>(){
@Override
public Integer call() throws Exception {
return 1;
}
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
int result = futureTask.get();
System.out.println(result);
}
上述代码的执行流程为:
1. 创建⼀个匿名内部类,实现 Callable 接⼝,Callable 带有泛型参数,泛型参数表示返回值的类型;
2. 重写 Callable 的 call 方法,并返回 1;
3. 把 callable 实例使用 FutureTask 包装⼀下,创建线程,线程的构造方法传入 FutureTask,此时新线程就会执行 FutureTask 内部的 Callable 的 call 方法,完成计算,计算结果会放到 FutureTask 对象中;
4. 在主线程中调用 futureTask.get() 方法能够阻塞等待新线程计算完毕,并获取到 FutureTask 中的 结果;
理解 Callable 接口:
Callable 和 Runnable 相对,都是描述⼀个 "任务",Callable 描述的是带有返回值的任务,Runnable 描述的是不带返回值的任务,Callable 通常需要搭配 FutureTask 来使用,FutureTask用来保存 Callable 的返回结果,因为 Callable 往往是在另⼀个线程中执行的,啥时候执行完并不确定,FutureTask 就可以负责这个等待结果出来的工作;
4. 使用线程池创建线程
使用线程池创建线程的优势:减少每次启动线程,销毁线程的损耗;
1. Executors 创建线程池的几种常见方式(其底层是使用 ThreadPoolExecutor 创建)
1)newFixedThreadPool:创建固定线程数的线程池(也为最大线程数);
public static void main(String[] args) throws ExecutionException, InterruptedException {
AtomicInteger count = new AtomicInteger(0);
ExecutorService service = Executors.newFixedThreadPool(3);
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 执行第 " + count.incrementAndGet() + " 个任务");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
Future<?> submit = service.submit(new Callable<AtomicInteger>() {
@Override
public AtomicInteger call() throws Exception {
System.out.println(Thread.currentThread().getName() + " 执行第 " + count.incrementAndGet() + " 个任务");
int i = 10;
while(i-- > 0) {
count.incrementAndGet();
}
return count;
}
});
System.out.println(submit.get());
}
上述代码中, 创建出固定包含 3 个线程的线程池,并通过 submit 方法,execute 方法分别分别执行了一个任务;那着两个方法有什么区别呢?
execute 方法只能执行 Runnable 任务,返回值为 void,因为 run 方法没有返回值;而 submit 方法即可以执行 Runnable 任务,也可以执行 Callable 任务,返回值为 Future,因为 Callable 时可以有返回值的;
当任务数超过线程池中的固定线程数会怎们样呢? 修改上述代码进行测试:
public static void main(String[] args) {
AtomicInteger count = new AtomicInteger(0);
ExecutorService service = Executors.newFixedThreadPool(2);
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 执行第 " + count.incrementAndGet() + " 个任务");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 执行第 " + count.incrementAndGet() + " 个任务");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 执行第 " + count.incrementAndGet() + " 个任务");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
}
可以看出,固定线程数为 2,只有两个线程执行任务,第三个任务会等某个线程执行完任务空闲了才会执行;
2)newCachedThreadPool:创建线程数目动态增长的线程池;(即根据任务数控制线程数量)
public static void main(String[] args) {
AtomicInteger count = new AtomicInteger(0);
ExecutorService service = Executors.newCachedThreadPool();
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("执行第 " + count.incrementAndGet() + " 个任务");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("执行第 " + count.incrementAndGet() + " 个任务");
}
});
}
但这并不一定意味着,来多少任务就开启多少线程,有可能某个线程的任务执行完毕,当任务来临时,就不会在创建新的线程,直接使用空闲线程执行任务,当所有线程都不空闲时,才会创建新的线程,即线程数是动态变化的;可以自己试试,多执行几个任务,查看线程名称是否有一样的,然后再试试多执行几个任务,并休眠几秒,再查看线程名;
3)newSingleThreadExecutor:创建只包含单个线程的线程池;
public static void main(String[] args) {
AtomicInteger count = new AtomicInteger(0);
ExecutorService service = Executors.newSingleThreadExecutor();
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 执行第 " + count.incrementAndGet() + " 个任务");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 执行第 " + count.incrementAndGet() + " 个任务");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 执行第 " + count.incrementAndGet() + " 个任务");
}
});
}
可以看到线程名都是一样的,说明是同一个线程在执行不同的任务;
2. 通过 ThreadPoolExecutor 创建线程池
ThreadPoolExecutor 构造方法:
参数说明:
int corePoolSize:核心线程数,线程池的最少线程数;
int maximumPoolSize:最大线程数,线程池的最多线程数;
long keepAliveTime:没有任务是的最长存活时间;
TimeUnit unit:long keepAliveTime 的时间单位;
BlockingQueue<Runnable> workQueue:阻塞队列,用于存放线程池中的任务;
ThreadFactory threadFactory:线程工厂类,负责创建线程,并对线程的属性进行设置;
RejectedExecutionHandler handler:线程池拒绝策略,让任务数量上限之后,并且阻塞队列也满了,此时对新任务的处理方式;(关于线程池拒绝策略的详细讲解:点击这里)
如果将线程池比作一个公司的话,那么 corePoolSize 就指的是公司里的正式员工,maximumPoolSize 就是公司里所有员工(正式员工 + 实习生),线程的总数目会在 [corePoolSize, maximumPoolSize] 内变化,即员工最少的情况下公司都是正式工,员工最多的情况下公司的实习生都招满了;keepAliveTime 表示非核心线程,允许的最大空闲时间,unit 表示keepAliveTime 的时间单位,可以理解为公司业务空闲时实习生的最长摸鱼时间,(过了这个时间,实习生就会被裁掉,对应非核心线程就会自动销毁)
public class Demo2 {
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 3, 10, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1), new ThreadPoolExecutor.AbortPolicy());
MyRunnable runnable1 = new MyRunnable();
MyRunnable runnable2 = new MyRunnable();
MyRunnable runnable3 = new MyRunnable();
MyRunnable runnable4 = new MyRunnable();
MyRunnable runnable5 = new MyRunnable();
executor.execute(runnable1);
executor.execute(runnable2);
executor.execute(runnable3);
executor.execute(runnable4);
executor.execute(runnable5);
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 正在执行 " + System.currentTimeMillis());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
上述代码创建了一个 核心线程数为 2,最大线程数为 3,非核心线程最长空闲时间为 8s,阻塞队列容量为 1,拒绝策略为直接抛出异常 的线程池;
然后创建了五个任务,前两个被执行的任务会使用核心线程来执行,第三个被执行的任务会使用非核心线程来执行,由于最大核心线程数为 3,此时已经达到上限,故第四个任务会被放入阻塞队列中,而阻塞队列的最大容量为 1,故此时阻塞队列也满了,第五个任务来临时,会执行拒绝策略,此处的拒绝策略为直接抛出异常;在某个线程执行完任务之后,就会继续执行阻塞队列中的任务;