JAVA架构师之路
本系列文章均是博主原创,意在记录学习上的知识,同时一起分享学习心得。
第一章
第一节 Java集合总结(架构师之路 )
第二节 Java多线程(架构师之路 )
文章目录
前言
本章主要介绍JAVA多线程,主要分3个方面介绍,多线程是什么、有什么用、如何用。
提示:以下是本篇文章正文内容,下面案例可供参考
一、基础简介
进程
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
线程
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。
多线程
多线程就是多个线程同时运行或交替运行。单核CPU的话是顺序执行,也就是交替运行。多核CPU的话,因为每个CPU有自己的运算器,所以在多个CPU中可以同时运行。为何使用多线程
开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
二、Java多线程
使用Java多线程的3种常用方式:
1.继承Thread
代码如下(示例):
public class MyThread extends Thread{
@Override
public void run() {
super.run();
System.out.println("这是线程名称:" + Thread.currentThread().getName());
}
}
public class ThreadTest {
public static void main(String[] args) {
MyThread m1 = new MyThread();
MyThread m2 = new MyThread();
MyThread m3 = new MyThread();
m1.start();
m2.start();
m3.start();
}
}

2.实现Runnable接口
代码如下(示例):
public class MyRunable implements Runnable {
@Override
public void run() {
System.out.println("这是线程名称:" + Thread.currentThread().getName());
}
}
public class ThreadTest {
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunable());
Thread t2 = new Thread(new MyRunable());
Thread t3 = new Thread(new MyRunable());
t1.start();
t2.start();
t3.start();
}
}

3.使用线程池
Java线程池是对多线程的集合管理,避免不必要的线程创建销毁动作,因为在面向对象编程中,对象的创建和销毁是非常消耗时间和资源的。
比较重要的几个类:
| 类名 | 描述 |
|---|---|
| Executors | Java线程池主要使用到Executors 类来创建线程池,使用 Executors 工具类来得到 ExecutorService 接口的具体对象 |
| ExecutorService | 真正的线程池接口。 |
| ScheduledExecutorService | 能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。 |
| ThreadPoolExecutor | ExecutorService的默认实现。 |
| ScheduledThreadPoolExecutor | 继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。 |
3.1 Executors 类
Java线程池主要使用到Executors 类来创建线程池,使用 Executors 工具类来得到 Executor 接口的具体对象,需要注意的是 Executors 是一个类,不是 Executor 的复数形式。
Executors 提供了以下一些 static 的方法:
- callable(Runnable task):将 Runnable 的任务转化成 Callable 的任务。
- newFixedThreadPool(int poolSize) :产生一个 ExecutorService 对象,这个对象带有一个大小为
poolSize 的线程池,若任务数量大于 poolSize ,任务会被放在一个 queue 里顺序执行。 - newCachedThreadPool( ):产生一个 ExecutorService对象,这个对象带有一个线程池,线程池的大小会根据需要自动调整,线程执行完任务后将会返回线程池,供执行下一次任务使用。
- newSingleThreadExecutor( ):产生一个 ExecutorService对象,这个对象只有一个线程可用来执行任务,若任务多于一个,任务将按先后顺序执行。
- newSingleThreadScheduledExecutor :产生一个 ScheduledExecutorService对象,这个对象的线程池大小为 1 ,若任务多于一个,任务将按照先后顺序执行。
- newScheduledThreadPool(int poolSize):产生一个 ScheduledExecutorService对象,这个对象的线程池大小为 poolSize ,若任务数量大于 poolSize ,任务会在一个 queue 里等待执行。
3.1.1 newFixedThreadPool
创建一个可重用的固定线程数量的线程池,以共享的无界队列方式来运行这些线程。
ExecutorService threadPool = Executors.newFixedThreadPool(3); // 创建可以容纳3个线程的线程池
注意:创建一个固定大小的线程池。每次提交一个任务就会创建一个线程,直到创建的线程数量达到线程池的最大值n。线程池的大小一旦达到最大值就会保持不变。如果在所有线程都处于活动状态时,这时再有其他任务提交,他们将进入等待队列中直到有空闲的线程可用。如果任何线程由于执行过程中的故障而终止,将会有一个新线程将取代这个线程执行后续任务。
代码如下(示例):
private static void newFixedThreadPool() {
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
final int col = i;
executorService.submit(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程名称:"
+ Thread.currentThread().getName() + ", 执行任务序号:" + col);
}
});
}
executorService.shutdown();
}

3.1.2 newCachedThreadPool
创建一个可缓存的线程池,可以根据需要自动创建新线程的线程池,但是,在以前创建好的线程可用时 将重用它们。
注意:在JAVA文档中是这样介绍可回收缓存线程池的,创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可以提高程序性能。如果线程池中有线程可用,那么调用 execute 将重用以前构造的线程;如果线程池中没有可用的线程,则创建一个新线程并添加到线程池中。此线程池不会对线程池的大小做限制,线程池大小完全依赖操作系统或者说JVM所能够创建的最大线程数量大小。此线程池会终止并从缓存中移除那些已有 60 秒钟(默认)未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。(可以通过java.util.concurrent.ThreadPoolExecutor 类的构造方法构造更加具体的类,例如指定时间参数等)。创建线程本身需要很多资源,包括内存,记录线程状态,以及控制阻塞等等。因此,相比另外两种线程池,在需要频繁创建短期异步线程的场景下,newCachedThreadPool能够复用已完成而未关闭的线程来提高程序性能。通俗讲就是:CachedThreadPool会创建一个缓存区,将初始化的线程缓存起来,如果线程有可用的,就使用之前创建好的线程,如果没有可用的,就会新创建线程。另外,终止并且从缓存中移除已有60秒未被使用的线程。
代码如下(示例):
private static void newCachedThreadPool() {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int col = i;
executorService.submit(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程名称:" + Thread.currentThread().getName()
+ ", 执行任务序号:" + col);
}
});
}
executorService.shutdown();
}

3.1.3 newSingleThreadExecutor
创建一个使用单个 工作线程的 Executor,以无界队列方式来运行该线程。
注意:创建一个单线程的线程池。这个线程池中只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么就会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
代码如下(示例):
private static void newSingleThreadExecutor() {
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 7; i++) {
final int col = i;
executorService.submit(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程名称:" + Thread.currentThread().getName()
+ ", 执行任务序号:" + col);
}
});
}
executorService.shutdown();
}

3.1.4 newScheduledThreadPool
创建一个可安排在给定延迟时间后 运行的或者可定期地执行的线程池。
注意:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
代码如下(示例):
private static void newScheduledThreadPool() {
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(1);
// 5秒后执行任务
scheduledThreadPool.schedule(new Runnable() {
@Override
public void run() {
System.out.println("当前线程名称:" + Thread.currentThread().getName() + ",执行。");
}
}, 5, TimeUnit.SECONDS);
// 5秒后执行任务,以后每2秒执行一次
scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("当前线程名称:" + Thread.currentThread().getName() + ",执行。");
}
}, 8, 2, TimeUnit.SECONDS);
}

3.2 ThreadPoolExecutor类
(1)无论创建哪种线程池,必须要调用 ThreadPoolExecutor类。线程池类是 java.util.concurrent.ThreadPoolExecutor,常用构造方法为:
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit
, BlockingQueue workQueue, RejectedExecutionHandler handler)
- corePoolSize: 线程池维护核心线程的最少数量。
- maximumPoolSize:线程池维护线程的最大数量。
- keepAliveTime: 线程池维护线程所允许的空闲时间。
- unit: 线程池维护线程所允许的空闲时间的单位。
- workQueue: 线程池所使用的缓冲队列。
- handler: 线程池对拒绝任务的处理策略。
(2)一个任务通过 execute(Runnable r ) 方法被添加到线程池,任务就是一个 Runnable类型的对象,任务的执行方法就是 Runnable类型对象的run()方法。
(3)当一个任务通过execute(Runnable)方法 欲添加到线程池时:
如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
如果此时线程池中的数量等于 corePoolSize,但是缓冲队列 workQueue未满,那么任务被放入缓冲队列。
如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。
如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过
handler所指定的策略来处理此任务。
处理任务的优先级为:
核心线程池corePoolSize、任务队列workQueue、最大线程maximumPoolSize,若三者都满了,使用handler处理被拒绝的任务。
当线程池中的线程数量大于corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。
(4)unit 可选的参数为java.util.concurrent.TimeUnit中的几个静态属性: NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS。
(5)workQueue 常用的可以是:java.util.concurrent.ArrayBlockingQueue
(6)拒绝任务的处理策略,handler有四个常用选择和自定义策略:
- ThreadPoolExecutor.AbortPolicy():抛出java.util.concurrent.RejectedExecutionException异常。
- ThreadPoolExecutor.CallerRunsPolicy():重试添加当前的任务,他会自动重复调用execute()方法。
- ThreadPoolExecutor.DiscardOldestPolicy():抛弃旧的任务 。
- ThreadPoolExecutor.DiscardPolicy():抛弃当前的任务 。
- 根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。
三、共享内存的问题
3.1 竞态条件
所谓竞态条件是指,当多个线程访问和操作同一个对象时,最终执行结果和执行时序有关,可能正确也可能不正确。
代码实例:
public class CounterThread extends Thread{
private static int counter = 0;
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
counter++;
}
}
public static void main(String[] args) throws InterruptedException {
int num = 1000;
Thread[] threads = new Thread[num];
for (int i = 0; i < num; i++) {
threads[i] = new CounterThread();
threads[i].start();
}
for (int i = 0; i < num; i++) {
threads[i].join();
}
System.out.println(counter);
}
}

期望的结果是1000000,但实际执行结果每次都不一样。因为counter++这个操作不是原子操作,分为三个步骤:
1)取counter的当前值;
2)在当前值基础上加1;
3)将新值重新赋值给counter。
两个线程可能同时执行第一步,取到了相同的counter值,最终结果就可能与预期不一致。
如何解决这个问题?
- 使用synchronize关键字;
- 使用显示锁;
- 使用原子变量;
3.2 内存可见性
多个内存可以共享和访问操作相同的变量,但一个线程对一个共享变量的修改,另一个线程不一定马上能看到,甚至永远也看不到。
代码示例:
public class VisibilityDemo {
private static boolean shutdown = false;
static class HelloThread extends Thread {
@Override
public void run() {
while (!shutdown) {
}
System.out.println("exit hello");
}
}
public static void main(String[] args) throws InterruptedException {
new HelloThread().start();
Thread.sleep(1000L);
shutdown = true;
System.out.println("exit main");
}
}

期望结果是两个线程都退出,但实际执行时,HelloThread永远都不会退出,也就是说shutdown永远为false,即使main线程已经更改为true。
这就是内存可见性问题。在计算机系统中,除了内存,数据还会被缓存在CPU的寄存器以及各级缓存中,当访问一个变量时,可能直接从寄存器或CPU缓存中获取,而不一定到内存中去取,当修改一个变量时,也可能是先写到缓存中,稍后才会同步更新到内存中。在单线程的程序中,这一般不是问题,但在多线程的程序中,尤其是在有多CPU的情况下,这就是严重的问题。一个线程对内存的修改,另一个线程看不到,一是修改没有及时同步到内存,二是两一个线程根本没有从内存读。
如何解决这个问题?
- 使用volatile关键字。
- 使用synchronize关键字或显示锁同步。
总结
本次学习笔记仅仅简单介绍了多线程是什么、为什么用、如何用,但是在实际场景中不单单只是使用,还需要了解同步和异步,多线程之间的协作通信等,接下来会继续学习多线程之间的同步协助等内容。