多线程的创建方式
多线程概念:从软硬件上实现多条执行流程的技术
继承Tread类
java.lang.Thread
继承该类,重写run()方法
调用该类 xx.start() 来启动线程
缺点
继承了Thread类,无法继承其他类不利于扩展
// 为啥调用start方法 实际执行的是 run 方法
调用的是start来启动的原因
如果调用重写的run,run方法会被当成普通方法来执行,此时还是相当于是单线程执行,调用start方法才会启动一个新的线程
创建的线程会被加到线程列表中 group的类型是 ThreadGroup
实现Runnable接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("thread:"+i);
}
}
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
for (int i = 0; i < 5; i++) {
System.out.println("main:"+i);
}
}
}
1
2
3
4
5
6
7
8
9
10
main:0
thread:0
main:1
thread:1
main:2
thread:2
thread:3
thread:4
main:3
main:4
优点
可以继承其他类,实现其他接口,扩展性更强
缺点
多一层对象包装,如果线程有执行结果是不可以直接返回的
Runnable 匿名对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyRunnable2{
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("thread:"+i);
}
}
});
thread.start();
for (int i = 0; i < 5; i++) {
System.out.println("main:"+i);
}
}
}
Runnbale 是函数式接口
函数式接口:有且仅有一个抽象方法
可以用lambda表达式简化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyRunnable2{
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("thread:"+i);
}
});
thread.start();
for (int i = 0; i < 5; i++) {
System.out.println("main:"+i);
}
}
}
实现Callable接口
主要解决前两种方法无法取得返回结果的问题
流程
-
得到任务类对象
-
定义类实现
Callable
接口,重写call方法 -
用
FutureTask
把Callable
对象封装成线程任务对象
-
-
线程任务对象交由
Thread
处理 -
start()
启动 -
通过
FutureTask
的get方法去获取任务执行的结果
简单示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class MyCallable implements Callable<String> {
private String name;
public MyCallable(String name) {
this.name = name;
}
@Override
public String call() throws Exception {
Thread.sleep(2000);
return "name is "+name;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable myCallable = new MyCallable("haha");
FutureTask<String> stringFutureTask = new FutureTask<>(myCallable);
Thread t = new Thread(stringFutureTask);
t.start();
System.out.println(stringFutureTask.get());
// 上述的get如果没有返回结果 会被阻塞
MyCallable myCallable2 = new MyCallable("xixi");
FutureTask<String> stringFutureTask2 = new FutureTask<>(myCallable2);
Thread t2 = new Thread(stringFutureTask2);
t2.start();
System.out.println(stringFutureTask2.get());
}
}
Callable无法直接交给Thread类 FutureTask 实现了Runnable的接口,所以可以直接交给Thread类
常用API
-
获取线程名称
getName()
-
设置名称
setName()
-
获取当前线程对象
currentThread()
主线程默认名称为 main 线程执行过程 通过 Thread.currentThread().getName()
来获取
- 静态方法
Thread.sleep()
进行休眠
线程安全问题
多个线程操作共享资源
解决思路:加锁
同步代码块
1
2
3
synchronized("锁对象"){
...
}
这里的锁对象 是多个线程处理的一个同一个对象即可 即任意唯一的对象,字符串常量 就是一个唯一对象。
但可能会影响其他无关线程的执行,规范要求是 使用 共享资源作为锁对象,比如银行账户取钱,应该用账户作为锁对象
-
实例方法”锁对象”改为 this
-
静态方法 使用 类名.class
同步方法
方法的修饰符后面,返回类型前 加上 synchronized
即可
底层原理:
本质上也是有 隐式锁对象的,同样 实例方法 用 this作为锁对象 静态方法使用 类名.class
// 同步方法 可以跨方法 ??? 多个方法使用的是同一个锁对象
理论上来说 代码块 效率更高,但是 同步方法更简单 更直观
Lock 锁
Lock是一个接口 可以使用 ReentrantLock
主要使用 lock()
和 unlock()
private final Lock myLock = new ReentrantLock()
final修饰 唯一和不可替换的
1
2
3
4
5
6
myLock.lock();
try{
}finally{
myLock.unlock();
}
解锁 写finally里面 防止死锁之类的问题
// 抢占 之类的 暂略
线程通信 生产者和消费者
-
wait()
-
notify()
-
notifyAll()
上述方法应该使用锁对象进行调用
线程池(重点)
jdk线程池接口 ExecutorService
线程池对象
-
ExecutorService
的实现类ThreadPoolExecutor
创建线程池对象 -
使用
Executors
线程池的工具类调用方法返回不同特点的线程池对象
方式1:
构造器如下:
1
2
3
4
5
6
7
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {}
-
核心线程数量
-
最大线程数量 (核心线程数量不够用时,可以多创建些,但是后续会被销毁)
-
临时线程存活时间
-
存活时间单位 秒 分 时 天
-
任务队列
-
线程工厂
-
当最大线程数量的线程都在忙时,任务队列已满的情况下,对于新任务的处理,比如忽略 比如 抛异常
ktv 例子 为例 1参数表示正式员工 一直在的 假设为3个,参数2是正式员工和临时工的数量,假设为10,参数3表示临时工的招工时间,参数5表示门口的排队座位数量,参数6表示专门招人的,是专门招临时工的(正式员工一直存活),参数7表示座位满了,对于新客人的处理
示例:
1
2
3
ExecutorService executorService = new ThreadPoolExecutor(3,5,8,
TimeUnit.SECONDS,new ArrayBlockingQueue<>(6),
Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
后俩个参数可以不填,使用默认值
ExecutorService常用方法:
方法名称 | 说明 |
---|---|
void execute(Runnable command) | 执行任务/命令,没有返回值,一般用来执行 Runnable 任务 |
Future submit(Callable task) |
执行任务,返回未来任务对象获取线程结果,一般拿来执行 Callable 任务 |
void shutdown() |
等任务执行完毕后关闭线程池 |
List shutdownNow() | 立刻关闭,停止正在执行的任务,并返回队列中未执行的任务 |
拒绝策略:
策略 | 含义 |
---|---|
AbortPolicy | 丢弃并抛出异常 默认 |
DiscardPolicy | 丢弃 不抛出 |
DiscardOldestPolicy | 抛弃等待最久的任务,加入新任务 |
CallerRunsPolicy | 主线程负责调用任务的run方法来绕过线程池直接执行 |
常见面试题
-
临时线程创建的时机:新任务提交时发现核心线程都在忙,任务队列都满了,且可以创建临时线程。此时才会创建临时线程
-
什么时候开始拒绝任务:最大线程数量都在忙,任务队列已满
简单验证线程创建和拒绝任务时机
1
2
3
4
5
6
7
8
9
10
11
12
public class MyThreadPool {
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = new ThreadPoolExecutor(3,5,8,
TimeUnit.SECONDS,new ArrayBlockingQueue<>(2),
Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
Runnable my_task = new MyThreadPoolRunnable();
for (int i = 0; i < 8; i++) {
executorService.submit(my_task);
Thread.sleep(1000);
}
}
}
创建8个任务 每创建一个 睡眠一秒
1
2
3
4
5
6
7
8
9
10
11
public class MyThreadPoolRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" now: "+new SimpleDateFormat("HH:mm:ss:SSS").format(new Date()));
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
pool-1-thread-1 now: 14:14:25:531
pool-1-thread-2 now: 14:14:26:503
pool-1-thread-3 now: 14:14:27:504
pool-1-thread-4 now: 14:14:30:525
pool-1-thread-5 now: 14:14:31:538
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@6442b0a6[Not completed, task = java.util.concurrent.Executors$RunnableAdapter@67b64c45[Wrapped task = learning.thread.MyThreadPoolRunnable@4411d970]] rejected from java.util.concurrent.ThreadPoolExecutor@60f82f98[Running, pool size = 5, active threads = 5, queued tasks = 2, completed tasks = 0]
at java.base/java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2055)
at java.base/java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:825)
at java.base/java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1355)
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:118)
at learning.thread.MyThreadPool.main(MyThreadPool.java:12)
pool-1-thread-1 now: 14:14:35:553
pool-1-thread-2 now: 14:14:36:514
简单分析:
前3个任务核心线程进行处理,每个间隔1秒,线程3任务结束后,是第28秒,放入第4个任务,休眠1秒,现在是第29秒,放入第5个任务,休眠1秒,现在是第30秒,此时,队列长度为2,最大线程数量为5,即3个线程都在忙,任务队列已满,且可以创建临时线程(线程数量未达上限),会创建第四个线程(临时线程),将该任务取出,此时会输出当前时间,即第30秒,接着第6个任务放入,休眠1秒,此时是第31秒,因为线程数量未达上线,队列又再次满了,会创建第5个线程(临时线程),处理第5个任务,输出当前时间第31秒,第5个任务会被拿出,此时队列长度是1,接着是第7个任务放入,此时队列长度为2,休眠1秒,时间为第32秒,线程不能再被创建,第8个任务到来,抛出异常,该任务被拒绝,因为队列满了,线程1的任务结束(休眠了10秒),现在是第35秒,开始处理第6个任务,过了1秒,线程2的任务结束,现在是第36秒,开始执行第7个任务。
任务队列
这里用的是ArrayBlockingQueue
常见的还有
-
LinkedBlockingQueue 长度可以到Integer最大数,基本可以认为是无限长了,但一般都会人为指定长度(不指定,默认就是Integer.MAX_VALUE),与ArrayBlockingQueue类似,线程都在忙时,任务队列满了,没有达到线程数量上限时,才会创建新的线程处理任务,如果设的很大,会全阻塞在队列中
-
SynchronousQueue 本身没有容量,是无缓冲的等待队列,来一个塞一个给消费者执行,maximumPoolSize 通常会设定为无界(Integer.MAX_VALUE),防止被拒绝执行
// ScheduleExecutorService定时器
内部为线程池 ,早先的Timer定时器是单线程 多个任务顺序执行,存在时延,也可能因为某个任务异常使Timer线程死亡
Executors工具类得到线程池对象
Executors 底层也是基于ThreadPoolExectuor创建线程池对象的
方法名称 | 说明 |
---|---|
newCachedThreadPool() | 线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了一段时间则会被回收掉。 |
newFixedThreadPool(int nThreads) | 创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。 |
newSingleThreadExecutor () | 创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。 |
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) |
创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。 |
前三者 返回类型都是 ExecutorService
可能存在的系统风险
-
newFixedThreadPool newSingleThreadExecutor 的请求队列长度为Integer.MAX_VALUE
-
newCachedThreadPool newScheduledThreadPool 线程数量上限为Integer.MAX_VALUE
大型并发系统中都可能导致OOM
未完待续…
主要参考 黑马程序员
线程状态
-
NEW 新建状态—— 创建线程对象
-
RUNNABLE 就绪状态 —— start 方法 等待CPU调度
-
BLOCKED 阻塞状态——无法获得锁对象
-
WAITING 等待状态——wait方法,(其他线程notify或notifyAll才能够唤醒)
-
TIMED_WAITING 计时等待——sleep方法
-
TERMINEATED 结束状态 run方法正常退出而死亡 或 没有捕获的异常终止了run方法而死亡