多线程基本了解
1.多线程_线程和进程
进程:在内存中执行的应用程序
线程:是进程中最小的执行单元
线程作用
:
负责当前进程中程序的运行
.
一个进程中至少有一个线程
,
一个进程还可以有多个线程
,
这样的应用程
序就称之为多线程程序
简单理解
:
一个功能就需要一条线程取去执行
2.并发和并行
并行
:
在同一个时刻
,
有多个执行在多个
CPU
上
(
同时
)
执行
(
好比是多个人做不同的事儿
)
比如
:
多个厨师在炒多个菜
并发
:
在同一个时刻
,
有多个指令在单个
CPU
上
(
交替
)
执行
比如
:
一个厨师在炒多个菜
3.CPU调度
1.
分时调度
:
让所有的线程轮流获取
CPU
使用权
,
并且平均分配每个线程占用
CPU
的时间片
2.
抢占式调度
:
多个线程轮流抢占
CPU
使用权
,
哪个线程先抢到了
,
哪个线程先执行
,
一般都是优先级高的先抢到
CPU
使用权的几率大
,
我们
java
程序就是抢占式调度
4.主线程介绍
主线程:CPU和内存之间开辟的转门为main方法服务的线程。
创建线程的方式(重点)
1.第一种方式_extends Thread
对于
Java
而言,每一个独立的线程都是
java.lang.Thread
类的一个对象,而线程中独立于其他线程所 执行的指令代码由Thread
类的
run
方法提供
因此,要想在
Java
中实现多线程协作运行,就需要创建多个独立的
Thread
类对象
而线程与线程之间所执行的指令代码会存在差异,由此可见,
Java
中实现独立执行不同指令代码的
多线程引用程序最简单直接的方法就是:继承
Thread
类重写
run
方法并构建其对象
1.
定义一个类
,
继承
Thread
2.
重写
run
方法
,
在
run
方法中设置线程任务
(
所谓的线程任务指的是此线程要干的具体的事儿
,
具体执行的代
码
)
3.
创建自定义线程类的对象
4.
调用
Thread
中的
start
方法
,
开启线程
,
jvm
自动调用
run
方法
Thread
类的构造方法有:
2.多线程在内存中的运行原理
注意
:
同一个线程对象不能连续调用多次
start
,
如果想要再次调用
start
,
那么咱们就
new
一个新的线程对象
3.Thread类中的方法
void
start
()
->
开启线程
,
jvm
自动调用
run
方法
void
run
()
->
设置线程任务
,
这个
run
方法是
Thread
重写的接口
Runnable
中的
run
方法
String
getName
()
->
获取线程名字
void
setName
(
String
name
)
->
给线程设置名字
static
Thread
currentThread
()
->
获取正在执行的线程对象
(
此方法在哪个线程中使用
,
获取的就是
哪个线程对象
)
static void sleep(long millis)->线程睡眠,超时后自动醒来继续执行,传递的是毫秒值
1.线程优先级
void
setPriority
(
int
newPriority
)
->
设置线程优先级
,
优先级越高的线程
,
抢到
CPU
使用权的几
率越大
,
但是不是每次都先抢到
int
getPriority
()
->
获取线程优先级
void
setDaemon
(
boolean
on
)
->
设置为守护线程
,
当非守护线程执行完毕
,
守护线程就要结束
,
但是守
护线程也不是立马结束
,
当非守护线程结束之后
,
系统会告诉守护线程人家结束了
,
你也结束吧
,
在告知的过程
中
,
守护线程会执行
,
只不过执行到半路就结束了
static
void
yield
()
->
礼让线程
,
让当前线程让出
CPU
使用权
void
join
()
->
插入线程或者叫做插队线程
2.守护线程
Java
中将线程划分为了两类:
用户线程
(User Thread)
守护线程
(Daemon Thread)
所谓守护线程,是
指在程序运行的时候在后台提供一种通用服务的线程
,比如垃圾回收线程就是一
个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因 此,当所有的非守护线程结束
时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,
程序就不会终止
用户线程和守护线程两者几乎没有区别,唯一的不同之处就在于虚拟机的退出:如果用户线程已经
全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没
有工作可做了,也就没有继续运行程序的必要了。
将线程转换为守护线程可以通过调用
Thread
对象的
setDaemon(true)
方法来实现(默认守护线程的
属性为
false
,即默认创建的线程对象为非守护的用户线程),在使用守护线程时需要注意一下几点:
thread.setDaemon(true)
必须在
thread.start()
之前设置,否则会抛出一个
IllegalThreadStateException
异常,不能把正在运行的常规线程设置为守护线程
在
Daemon
线程中产生的新线程也是
Daemon
的
不是所有的应用都可以分配给
Daemon
线程来进行服务,比如读写操作或者计算逻辑。因为在
Daemon Thread
还没来的及进行操作时,虚拟机可能已经退出了
可以通过线程对象的
isDaemon()
方法判定该线程是否守护线程
如:
如:
3.礼让线程
场景说明
:
如果两个线程一起执行
,
可能会执行一会儿线程
A
,
再执行一会线程
B
,
或者可能线程
A
执行完毕了
,
线 程B
在执行 那么我们能不能让两个线程尽可能的平衡一点
->
尽量让两个线程交替执行
注意
:
只是尽可能的平衡
,
不是绝对的你来我往
,
有可能线程
A
线程执行
,
然后礼让了
,
但是回头
A
又抢到
CPU
使用权了
2.第二种方式_实现Runnable接口
直接继承
Thread
类实现线程的方法存在局限性:由于
Java
是典型的单亲继承体系,因此可以通过实
现
java.lang.Runnable
接口的方式来实现线程:
Runnable
中只有一个签名和
Thread
中一致的
run()
方法,
Runnable
接口的实现类并不是线程类
,
我们只是通过这种形式向线程类提供
run()
方法指令代码,最
后还需借助
Thread
类和
Runnable
接口的依赖关系和
Thread
类的
Thread
(
Runnable runnable
)构造方 法构建线程对象。
1.
创建类
,
实现
Runnable
接口
2.
重写
run
方法
,
设置线程任务
3.
利用
Thread
类的构造方法
:
Thread
(
Runnable target
),
创建
Thread
对象
(
线程对象
),
将自定义的类当
参数传递到
Thread
构造中
->
这一步是让我们自己定义的类成为一个真正的线程类对象
4.调用Thread中的start方法,开启线程,jvm自动调用run方法
两种实现多线程的方式区别
1.
继承
Thread
:
继承只支持单继承
,
有继承的局限性
2.
实现
Runnable
:
没有继承的局限性
,
MyThread
extends
Fu
implements
Runnable
3.
Thread
线程之间不能共享资源,实现
Runnable
可以实现资源共享。
3.第三种方式_匿名内部类创建多线程
严格意义上来说
,
匿名内部类方式不属于创建多线程方式其中之一
,
因为匿名内部类形式建立在
实现
Runnable
接口的基础上完成的
匿名内部类回顾
:
1.
new
接口
/
抽象类
(){
重写方法
}.
重写的方法
();
2.
接口名
/
类名 对象名
=
new
接口
/
抽象类
(){
重写方法
}
对象名
.
重写的方法
();
线程安全(同步)
1.什么时候发生:当多个线程访问同一个资源时,导致了数据有问题
线程安全问题-->线程不安全的代码(异步)
2.解决线程安全问题的第一种方式(使用同步代码块)
1.普通同步方法_非静态
1.
格式
:
synchronized
(
任意对象
){
线程可能出现不安全的代码
}
2.
任意对象
:
就是我们的锁对象
3.
执行
:
一个线程拿到锁之后
,
会进入到同步代码块中执行
,
在此期间
,
其他线程拿不到锁
,
就进不去同步代码块
,
需要 在同步代码块外面等待排队
,
需要等着执行的线程执行完毕
,
出了同步代码块
,
相当于释放锁了
,
等待的线程才能 抢到锁
,
才能进入到同步代码块中执行
2.静态同步方法
1.
格式
:
修饰符
static synchronized
返回值类型
方法名
(
参数
){
方法体
return
结果
}
2.
默认锁
:
class
对象
死锁(了解)
死锁介绍
(
锁嵌套就有可能产生死锁
)
指的是两个或者两个以上的线程在执行的过程中由于竞争同步锁而产生的一种阻塞现象
;
如果没有外力的作用
,
他们将无法继续执行下去
,
这种情况称之为死锁.
2.线程状态图
产生死锁的四个必备条件
1.互斥条件
:
一个资源每次只能被一个进程使用
;
线程使用的资源中至少有一个是不能共享
.
2.请求与保持条件 :至少有一个进程持有一个资源
,
并且它在等待获取一个当前被别的进程持有
的资源
.
也就是说
,
要发生死锁。
3.不剥夺条件
:
进程已获得的资源
,
在末使用完之前
,
不能强行剥夺
;
4.必须有循环等待:这时
,
一个进程等待其它进程持有的资源
,
后者又在等待另一个进程持有的资
源
,
这样一直下去
,
直到有一个进程在等待第一个进程持有的资源
,
使得大家都被锁住
因为要发生死锁的话
,
所有这些条件必须全部满足
,
所以你要防止死锁的话
,只需破坏其中一个即可
线程状态(重点)
1.线程状态介绍
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周 期中,有几种状态呢?在API
中
java
.
lang
.
Thread
.
State
这个枚举中给出了六种线程状态:
这里先列出各个线程状态发生的条件,下面将会对每种状态进行详细解析。
2.线程状态图
创建状态:
--new
当利用
new
关键字创建线程对象实例后,它仅仅作为一个对象实例存在,
JVM
没有为其分配
CPU
时间片和其他线程运行资源
就绪状态:
---start()
在处于创建状态的线程中调用
start
方法将线程的状态转换为就绪状态。这时,线程已经得到除
CPU
时间之外的其它系统资源,只等
JVM
的线程调度器按照线程的优先级对该线程进行调度,
从而使该线程拥有能够获得
CPU
时间片的机会
运行状态
:
就绪态的线程
获得
cpu
就进入运行态
等待
/
阻塞:
线程运行过程中被剥夺资源或者,等待某些事件就进入等
待
/
阻塞状态
,
suspend()
方法被调用
,
sleep()
方法被调用,线程使用
wait()
来等待条件变量
;
线程处于
I/O
等待等,调用
suspend
方
法将线程的状态转换为挂起状态。这时,线程将释放占用的所有资源,但是并不释放锁,所以
容易引发死锁,直至应用程序调用
resume
方法恢复线程运行。等待事件结束或者得到足够的
资源就进入就绪态
死亡状态:
当线程体运行结束或者调用线程对象的
stop
方法后线程将终止运行,由
JVM
收回线程占用的资
源.
3.等待唤醒机制(线程通信)
线程通信的目标是使线程间能够互相
发送信号
。另一方面,线程通信使线程也能够等待其他线程的
信号
例如,线程
B
可以等待线程
A
的一个信号,这个信号会通知线程
B
数据已经准备好了。
例如收取鸡蛋的流程:
Java
有一个内建的等待机制来允许线程在等待信号的时候变为
非运行状态
。
java.lang.Object
类定义 了三个方法,wait()
、
notify()
和
notifyAll()
来实现这个等待机制(由于
Object
类是
Java
继承树的根节点,因此所有的Java
类都具备这三个方法,这三个方法都是
final
的)
一个线程一旦调用了任意对象的
wait()
方法,就会变为非运行状态,直到另一个线程调用了
同一个对
象
的
notify()/notifyAll()
方法
为了调用
wait()
或者
notify()
,线程必须先获得那个对象的锁。也就是说,
线程必须在同步块里调用
wait()
或者
notify()
,
JVM
是这么实现的,当你调用wait时候它首先要检查下当前线程是否是锁的拥有 者,不是则抛出IllegalMonitorStateException
wait 和 notify 方法需要锁对象调用 , 所以需要用到同步代码块中 , 而且必须是同一个锁对象
wait 和 notify 方法需要锁对象调用 , 所以需要用到同步代码块中 , 而且必须是同一个锁对象
当一个线程调用一个对象的
notify()
方法,正在等待该对象的所有线程中将有一个线程被唤醒并
允许执行(这个将被唤醒的线程
是随机的,不可以指定唤醒哪个线程
)。同时也提供了一个
notifyAll()
方法来唤醒正在等待一个给定对象的所有线程
一旦线程调用了
wait()
方法,它就
释放了所持有的监视器对象上的锁
。这将允许其他线程也可
以调用
wait()
或者
notify()
(这是
wait
方法和
Thread
类
sleep
方法的主要区别)
wait()
方法也具备有时间参数的重载版本,超时时间到了之后即使没有其他线程调用
notify
方
法,线程也将唤醒(如果超时时间设置为
0
,则和无参版本
wait方法功能保持一致.
Lock锁
Lock
对象的介绍和基本使用
1.
概述
:
Lock
是一个接口
2.
实现类
:
ReentrantLock
3.
方法
:
lock
()
获取锁
unlock
()
释放锁
synchronized
:
不管是同步代码块还是同步方法
,
都需要在结束一对
{}
之后
,
释放锁对象
Lock
:
是通过两个方法控制需要被同步的代码
,
更灵活
Callable接口_实现多线程方式三
1.
概述
:
Callable
<
V
>
是一个接口
,
类似于
Runnable
2.
方法
:
V
call
()
->
设置线程任务的
,
类似于
run
方法
3.
call
方法和
run
方法的区别
:
a
.
相同点
:
都是设置线程任务的
b
.
不同点
:
call
方法有返回值
,
而且有异常可以
throws
run
方法没有返回值
,
而且有异常不可以
throws
4.
<
V
>
a
.
<
V
>
叫做泛型
b
.
泛型
:
用于指定我们操作什么类型的数据
,
<>
中只能写引用数据类型
,
如果泛型不写
,
默认是
Object
类型数
据
c
.
实现
Callable
接口时
,
指定泛型是什么类型的
,
重写的
call
方法返回值就是什么类型的
5.
获取
call
方法返回值
:
FutureTask
<
V
>
,
是一个结合了
Future
和
Runnable
接口的类,它允许异步执行任
务并获取结果。
a
.
FutureTask
<
V
>
实现了一个接口
:
Future
<
V
>
b
.
FutureTask
<
V
>
中有一个方法
:
1.
V
get
()
->
获取
call
方法的返回值
,
可以在需要时获取异步计算的结果。如果计算尚未完成,该
方法会阻塞直到计算完成。
此外,
FutureTask
实现了
Runnable
接口,可以直接提交给线程池执行,这使得它在并发编程中非常有用,
能够简化多线程开发的复杂性
线程池_实现多线程方式四
1.
问题
:
之前来一个线程任务
,
就需要创建一个线程对象去执行
,
用完还要销毁线程对象
,
如果线程任务多了
,
就 需要频繁创建线程对象和销毁线程对象,
这样会耗费内存资源
,
所以我们就想线程对象能不能循环利用
,
用的时 候直接拿线程对象,用完还回去
1.线程池概述
在
java
中,如果每个请求到达就创建一个新线程,开销是相当大的
当需要处理的任务较少时,我们可以自己创建线程去处理,但在高并发场景下,我们需要处理的任
务数量很多,由于创建销毁线程开销很大,这样频繁创建线程就会大大降低系统的效率。
此时,我们就可以使用线程池,线程池中的线程执行完一个任务后可以复用,并不被销毁。合理使
用线程池有以下几点好处:
1
、减少资源的开销。通过复用线程,降低创建销毁线程造成的消耗。
2
、多个线程并发执行任务,提高系统的响应速度。
3、可以统一的分配,调优和监控线程,提高线程的可管理性。
一个比较简单的线程池至少应包含:
线程池管理器(
ThreadPool
)
创建、销毁并管理线程池,将工作线程放入线程池中
工作线程 (
PoolWorker
)
一个可以循环执行任务的线程,在没有任务时进行等待
任务列队 (
taskQueue
)
提供一种缓冲机制,将没有处理的任务放在任务列队中
任务接口 (
Task
)
每个任务必须实现的接口,主要用来规定任务的入口、任务执行完后的收尾工作、任务的执行
状态等,工作线程通过该接口调度任务的执行
2.线程池创建
通过
ThreadPoolExecutor
来创建一个线程池。
java.uitl.concurrent.ThreadPoolExecutor
类是线程池中最核心的一个类,它有四个构造方法。
创建方法:
new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime,
milliseconds,runnableTaskQueue, threadFactory, handler);
corepollsize
:
核心池的大小,默认情况下,在创建线程池后,每当有新的任务来的时候,如果此时 线程池中的线程数小于核心线程数,就会去创建一个线程执行(就算有空线程也不复用),当创建的线 程数达到核心线程数之后,再有任务进来就会放入任务缓存队列中。当任务缓存队列也满了的时候,就 会继续创建线程,直到达到最大线程数。如果达到最大线程数之后再有任务过来,那么就会采取拒绝服务策略。
Maximumpoolsize
:
线程池中最多可以创建的线程数
keeplivetime
:
线程空闲状态时,最多保持多久的时间会终止(空闲线程等待新任务的最长时
间)。默认情况下,当线程池中的线程数大于
corepollsize
时,才会起作用 ,直到线程数不大于
corepollsize
。
workQuque
:
阻塞队列,用来存放等待的任务
rejectedExecutionHandler
:任务拒绝处理器(这个注意一下),有四种
(
1
)
abortpolicy
丢弃任务,抛出异常
(
2
)
discardpolicy拒绝执行,不抛异常
3.向线程池提交任务
(
3
)
discardoldestpolicy
丢弃任务缓存队列中最老的任务
(
4
)
CallerRunsPolicy 线程池不执行这个任务,主线程自己执行。
在
java
中,并不提倡直接使用
ThreadPoolExecutor
,而是使用
Executors
类中提供的几个静态方
法来创建线程池,其内部也是调用
ThreadPoolExecutor的构造方法,只不过参数已提前配置:
1.
如何创建线程池对象
:
用具类
->
Executors
2.
获取线程池对象
:
Executors
中的静态方法(
4
个)
:
static
ExecutorService
newFixedThreadPool
(
int
nThreads
)
a
.
参数
:
指定线程池中最多创建的线程对象条数
b
.
返回值
ExecutorService
是线程池
,
用来管理线程对象
3.
执行线程任务
:
ExecutorService
中的方法
void
execute
(
Runnable task
)
提交一个
Runnable
任务用于执行
,
但是不能接收返回值
Future
<?>
submit
(
Runnable task
)
提交一个
Runnable
任务用于执行
Future
<
T
>
submit
(
Callable
<
T
>
task
)
提交一个
Callable
任务用于执行
4.
submit
方法的返回值
:
Future
接口
用于接收
run
方法或者
call
方法返回值的
,
但是
run
方法没有返回值
,
所以可以不用
Future
接收
,
执行
call
方法需要用
Future
接收
Future
中有一个方法
:
V
get
()
用于获取
call
方法返回值
5.
ExecutorService
中的方法
:
void
shutdown
()
启动有序关闭,其中先前提交的任务将被执行,但不会接受任何新任务
创建线程池的4种方式:
1、newFixedThreadPool 定长线程池
一个有指定的线程数的线程池,有核心的线程,里面有固定的线程数量,响应的速度快。正规的并
发线程,多用于服务器。固定的线程数由系统资源设置。核心线程是没有超时机制的,队列大小没有限
制,除非线程池关闭了核心线程才会被回收。
2、newCachedThreadPool 可缓冲线程池
只有非核心线程,最大线程数很大,每新来一个任务,当没有空余线程的时候就会重新创建一个线
程,这边有一个超时机制,当空闲的线程超过
60s
内没有用到的话,就会被回收,它可以一定程序减少
频繁创建
/
销毁线程
,
减少系统开销,适用于执行时间短并且数量多的任务场景。
3、ScheduledThreadPool 周期线程池
创建一个定长线程池,支持定时及周期性任务执行,通过过
schedule
方法可以设置任务的周期执行
4、newSingleThreadExecutor 单任务线程池
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序
(FIFO, LIFO,
优先级
)执行,每次任务到来后都会进入阻塞队列,然后按指定顺序执行
3.向线程池提交任务
使用
execute
提交任务,但
execute()
方法没有返回值,无法判断任务是否被线程池执行成功。
execute
方法输入的任务是一个
Runnable
类的实例。
execute
(
new
Runnable
() {
@Override
public
void
run
() {
}
});
使用
submit()
方法提交任务,会返回一个
future,
那么我们可以通过这个
future
来判断任务是否
执行成功。
Future<T> future = executor.submit(new Callable<T>(){
@Override
public T call() throws Exception {
// TODO Auto-generated method stub
return null;
}
});
T result = null;
try {
result = future.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
return result;
4. 线程池的关闭
调用
shutdown()
和
shutdownNow()
方法关闭线程池。
定时器_Timer
1.
概述
:
定时器
2.
构造
:
Timer
()
3.
方法
:
void
schedule
(
TimerTask task
,
Date firstTime
,
long
period
)
task
:
抽象类
,
是
Runnable
的实现类
firstTime
:
从什么时间开始执行
period
:
每隔多长时间执行一次
,
设置的是毫秒值
public class Demo01Timer {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("第天早上7点,准时起床了~~~");
}
},new Date(),2000L);
}
}