程序、进程、线程的区别 #
- 程序(program)
- 概念:是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码。
- 进程(process)
- 概念:程序的一次执行过程,或是正在运行的一个程序。
- 说明:进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
- 线程(thread)
- 概念:进程可进一步细化为线程,是一个程序内部的一条单一的执行路径(顺序控制流),可以共享所属进程的数据。
- 说明:线程作为调度和执行的单位,每个线程拥独立的运行栈和程序计数器(pc),线程切换的开销小。
多线程程序的优点 #
-
提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
-
提高计算机系统CPU的利用率
-
改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改
何时需要多线程 #
- 程序需要同时执行两个或多个任务。
- 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
- 需要一些后台运行的程序时。
并发和并行 #
- 并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
- 并发:一个CPU(采用分时间片)执行多个任务。比如:秒杀、多个人做同一件事,同一个时间只能做一件事
创建多线程 #
一个Java应用程序java.exe,其实至少两个线程:
- 主线程
main(),必然存在 - 垃圾回收线程
gc(),必然存在 - 异常处理线程,当然如果发生异常,会影响主线程,有可能存在
方式一:继承Thread类 #
- 创建一个继承于Thread类的子类
- 重写Thread类的
run():将此线程执行的操作声明在run()中 (线程要完成的任务) - 创建Thread类的子类的对象
- 通过此对象调用
start():启动当前线程,调用当前线程的run()
- 注意点
-
我们启动一个线程,必须调用
start(),不能调用run()的方式启动线程。 -
如果再启动一个线程,必须重新创建一个Thread子类的对象,调用此对象的
start().
-
// 继承Thread
static class MyThread extends Thread{
// 重写run
@Override
public void run() {
while (true){
System.out.println(Thread.currentThread().getName());
}
}
}
public static void main(String[] args) throws Exception {
// 创建两个线程
MyThread t1 = new MyThread();
t1.start();
MyThread t2 = new MyThread();
t2.start();
}
方式二:实现Runnable接口 #
- 创建一个实现了Runnable接口的类
- 实现类去实现Runnable中的抽象方法:
run() - 创建实现类的对象
- 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
- 通过Thread类的对象调用
start()
// 实现Runnable
static class MyThread implements Runnable{
// 实现run
@Override
public void run() {
while (true){
System.out.println(Thread.currentThread().getName());
}
}
}
public static void main(String[] args) throws Exception {
// 创建两个线程
Thread t1 = new Thread(new MyThread());
t1.start();
Thread t2 = new Thread(new MyThread());
t2.start();
}
继承Thread和实现Runable的对比 #
开发中:优先选择实现Runnable接口的方式
-
原因:
- 实现的方式没类的单继承性的局限性
- 实现的方式更适合来处理多个线程共享数据的情况。
-
联系:
public class Thread implements Runnable
-
相同点:
- 两种方式都需要重写
run(),将线程要执行的逻辑声明在run()中。 - 目前两种方式,要想启动线程,都是调用的Thread类中的
start()。
- 两种方式都需要重写
方式三:实现Callable接口 #
- 创建一个实现Callable的实现类
- 实现call方法,将此线程需要执行的操作声明在
call()中 - 创建Callable接口实现类的对象
- 将此Callable接口实现类的对象作为传递到
FutureTask构造器中,创建FutureTask的对象 - 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用
start() - 获取Callable中
call()方法的返回值:get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
-
如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
call()可以有返回值的。call()可以抛出异常,被外面的操作捕获,获取异常的信息- Callable是支持泛型的
-
缺点
- 调用
get()方法,会导致主线程阻塞
- 调用
// 实现Runnable
static class MyThread implements Callable<String> {
@Override
public String call() throws Exception {
return Thread.currentThread().getName();
}
}
public static void main(String[] args) throws Exception {
// 创建FutureTask
FutureTask<String> futureTask = new FutureTask<>(new MyThread());
// 创建Thread
Thread t1 = new Thread(futureTask);
t1.start();
// 阻塞获取返回值
System.out.println(futureTask.get());
}
Thread类 #
java.lang.Thread
构造方法 #
Thread():创建新的Thread对象Thread(String threadname):创建线程并指定线程名Thread(Runnable target):指定创建线程的目标对象Thread(Runnable target,String threadname):指定创建线程的目标对象,并指定线程名
常用方法 #
-
void start():启动线程并执行run()方法 -
run():线程被调度时执行的操作,不可主动调用! -
String getName():返回线程名称 -
static Thread currentThread():返回当前线程 -
static void yield():线程让步,线程进入就绪状态- 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
- 若队列没有满足条件的线程,此方法会被忽略
-
join():立刻执行调用此方法线程,其他线程将被阻塞直到调用方法的线程执行完毕,可以保证线程的串行化执行- 例如在线程a中,调用线程b的
join()方法,线程a会进入阻塞,直到线程b执行完毕
- 例如在线程a中,调用线程b的
-
static void sleep(long millis):当前正在活动的线程在millis时间段内放弃CPU控制,时间到后重新排队,线程进入阻塞状态 -
stop():强行结束线程生命周期,不推荐使用 -
boolean isAlive():判断线程是否还活着 -
int activeCount():获取线程组中线程的数量
获取和设置优先级 #
getPriority():获取线程的优先级
setPriority(int p):设置线程的优先级
说明:高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只当高优先级的线程执行完以后,低优先级的线程才执行。
守护线程和用户线程 #
- Java中的线程分为两类
- 守护线程
- 在调用
start()前使用thread.setDaemon(ture)方法将一个用户线程变为守护线程。 - 它是在后台运行的,当所有用户线程结束时,守护线程也会随之结束。
- 在调用
- 用户线程
- 创建的线程默认为用户线程
- 用户线程是主线程的子线程,当主线程结束时,用户线程也会结束。
- 守护线程
Thread userThread = new Thread("User Thread") {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
};
Thread daemonThread = new Thread("Daemon Thread"){
@Override
public void run() {
while (true){
System.out.println(Thread.currentThread().getName());
}
}
};
userThread.start();
// 设置为守护线程,即使自己有死循环,也会随着所有用户线程的结束而退出
daemonThread.setDaemon(true);
daemonThread.start();
线程的生命周期 #
JDK中用Thread.State类定义了线程的几种状态
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态
- 新建:当一个Thread类或子类对象被声明和创建时
- 就绪:处于新建状态的线程调用
start()方法后,线程进入队列等待CPU时间片,此时已经具备了运行的条件,只是还没分配到CPU资源 - 运行:就绪的线程被调度,并且获得CPU资源,进行运行状态,执行
run() - 阻塞:被人为挂起或输入输出操作时,让出CPU并临时中止自己的执行
- 死亡:线程完成了全部工作或被强制性终止(例如出现异常)
线程同步 #
通过同步机制,来解决线程的安全问题。
方式一:synchronized隐式锁 #
同步代码块 #
synchronized(同步监视器){
//需要被同步的代码
}
- 一般用于同步操作共享数据(多个线程共同操作的变量)的代码
- 同步监视器(Object类型),俗称:锁。任何一个类的对象,都可以充当锁。要求**:多个线程必须要共用同一把锁(不变)**。
- synchronized修饰的方法,无论方法正常执行完毕还是抛出异常,都会释放锁
- 一把锁只能同时被一个线程获取,没有获得锁的线程只能阻塞等待
同步方法 #
public synchronized void method(){}
-
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。
-
同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
- 非静态的同步方法,同步监视器是:
this - 静态的同步方法,同步监视器是:
当前类.class
- 非静态的同步方法,同步监视器是:
可重入锁(递归锁) #
所谓重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程内部可以再次获取本对象上的锁,而其他的线程是不可以的。
可以重复使用的锁,锁的创建只有一次,可以重复调用lock()和unlock()
synchronized和ReentrantLock都是可重入锁。
public class Demo {
public static void main(String[] args) {
Thread t = new Thread("A") {
@Override
public void run() {
// 线程第一次获取锁
synchronized (Demo.class){
for (int i = 0; i < 10000; i++) {
// 可重入,再次获取锁
synchronized (Demo.class){
System.out.println(Thread.currentThread().getName());
}
}
}
}
};
t.start();
}
}
方式二:Lock显式锁 #
常用方法 #
void lock():获取锁,如果锁不可用,则出于线程调度的目的,当前线程将被禁用,并且在获取锁之前处于休眠状态。boolean tryLock():如果锁可用立即返回true,如果锁不可用立即返回false;boolean tryLock(long time, TimeUnit unit) throws InterruptedException:如果锁可用,则此方法立即返回true。 如果该锁不可用,则当前线程将出于线程调度目的而被禁用并处于休眠状态,直到发生以下三种情况之一为止- 当前线程获取到该锁
- 当前线程被其他线程中断,并且支持中断获取锁
- 经过指定的等待时间如果获得了锁,则返回true,没获取到锁返回false。
void unlock():释放锁。释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。
ReentrantLock(可重入锁) #
实现Lock接口,是一种可重入锁
public class Demo {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock(true);
Thread t = new Thread("A") {
@Override
public void run() {
try {
// 线程第一次获取锁
lock.lock();
for (int i = 0; i < 10000; i++) {
// 可重入,再次获取锁
lock.lock();
System.out.println(Thread.currentThread().getName());
}
}finally {
// 释放锁
lock.unlock();
}
}
};
t.start();
}
}
ReentrantReadWriteLock(读写锁) #
writeLock():获取写锁readLock():获取读锁-
两个线程都是写锁:互斥,同步执行
-
两个线程一写一读:互斥,同步执行
-
两个线程都是读锁:共享,异步执行
-
公平锁和非公平锁 #
公平锁的效率比非公平锁低
公平锁总是可以保证让所有线程中等待时间最长的线程先执行
在new ReentrantLock(true)或new ReentrantReadWriteLock(true)时,参数为true,创建的就是公平锁;不传参默认为false,就是非公平锁
synchronized与Lock的异同 #
- 相同:
- 二者都可以解决线程安全问题
- 不同:
- synchronized是Java语言的关键字;而Lock是一个接口。
- synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器;而Lock需要手动的启动同步
lock(),同时结束同步也需要手动的实现unlock(),如果没有主动释放锁,就有可能导致出现死锁现象。 - Lock可以配置公平策略,实现线程按照先后顺序获取锁;而synchronized不可以。
- Lock提供了
trylock()方法可以试图获取锁,获取到或获取不到时,返回不同的返回值让程序可以灵活处理。 lock()和unlock()可以在不同的方法中执行,程序更加灵活。
死锁问题 #
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
说明:
- 出现死锁后,不会出现异常,不会出现提示,只是所的线程都处于阻塞状态,无法继续
- 我们使用同步时,要避免出现死锁。
解决死锁 #
1、调整锁的顺序,避免可能出现的死锁
2、调整锁的范围,避免在一个同步代码块中使用另一个同步代码块
3、使用可重入锁
判断锁会不会释放 #
- 会释放
- 线程的同步方法或同步代码块执行结束
- 线程的同步方法或同步代码块中遇到
break、return终止了代码块中的循环或方法的执行 - 线程的同步方法或同步代码块出现
Error或Exception未处理 - 线程的同步代码块、同步方法执行了
wait()造成线程阻塞并释放锁
- 不会释放
- 线程的同步方法或同步代码块中执行
Thread.sleep()或Thread.yield() - 调用了
suspend()将线程挂起(避免使用suspend()或resume()控制线程)
- 线程的同步方法或同步代码块中执行
线程通信 #
使用synchronized强同步 #
wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则,会出现IllegalMonitorStateException异常wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。
注意:wait方法等待的线程,在哪等待,被唤醒后,就从哪里开始执行,可能会出现虚假唤醒的问题,所以建议wait循环使用在while循环中
使用Lock接口下的可重入锁 #
- 获取Condition对象
Condition condition = lock.newCondition(); - 使用Condition对象的方法
await():当前线程等待,对应wait()signal():唤醒一个线程,对应notify()signalAll():唤醒所有线程,对应notifyAll()
sleep()和wait()的异同 #
- 相同点
- 一旦执行方法,都可以使得当前的线程进入阻塞状态。
- 不同点
- 两个方法声明的位置不同:Thread类中声明
sleep(), Object类中声明wait() - 调用的要求不同:
sleep()可以在任何需要的场景下调用。wait()必须使用在同步代码块或同步方法中
- 两个方法声明的位置不同:Thread类中声明
- 关于是否释放同步监视器
- 如果两个方法都使用在同步代码块或同步方法中,
sleep()不会释放锁,wait()会释放锁。
- 如果两个方法都使用在同步代码块或同步方法中,
ThreadLocal类 #
可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据(可以存放线程范围内的局部变量)
实际使用场景 #
在JavaWeb项目里面,我们做MyBatis的工具类的时候,为了可以满足多个线程中,各线程只关闭自己的SqlSession对象的需求,而不会产生线程安全的问题,所以,我们需要将SqlSession对象存入到ThreadLocal对象中
常用方法 #
set()用于向ThreadLocal对象中存值get()用于向ThreadLocal对象中取值remove()用于从ThreadLocal对象中删除值
CAS自旋锁 #
解决的问题 #
例如,一个int类型的变量,在高并发的情况下进行加减,通常会导致线程不安全,数值不准确的问题,例如
public class Demo {
static int count = 0;
public static void main(String[] args) {
// 创建100个线程
for (int i1 = 0; i1 < 100; i1++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
count++;
}
}
}).start();
}
// 确保其他线程都执行完成了,除了GC和主线程
while (Thread.activeCount() > 2){
Thread.yield();
}
// 结果打印出来的数据不一定是正确的10000,结果比10000少
System.out.println(count);
}
}
原因分析 #
由于加减操作例如count++、count = count - 1并不是一个原子操作,在执行的时候分了三步:1、读取值;2、修改值;3、写入值。所以例如现在有两个线程A和B,原子性操作如下:
- 线程A:读取值为0
- 线程B:开始执行
- 线程B:读取值为0
- 线程B:修改读到的值为1
- 线程B:将值1写入变量
count - 线程A:开始执行
- 线程A:修改读到的值为1
- 线程A:将值1写入到变量
count
所以造成了变量count的值经过两次加1,应该为2;但是实际上值为1
解决方式 #
1、使用synchronized同步锁的方式 #
缺点是:每次进行非原子操作前,都需要加锁,操作完解锁,效率低下
public class Demo {
static int count = 0;
public static void main(String[] args) {
for (int i1 = 0; i1 < 100; i1++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
// 使用同步代码块将非原子操作同步
synchronized (Demo.class){
count++;
}
}
}
}).start();
}
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(count);
}
}
2、通过CAS自旋锁的方式进行 #
通过AtomicInteger,可以在线程提交的时候,用自己读到的数值,与现在变量中的数值进行比对。如果一致的话,就写入变量;如果数值已经发生改变的话,那么会进行自旋,也就是重新进行运算,再进行比较
public class Demo {
static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) {
for (int i1 = 0; i1 < 100; i1++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//读取数据并自增,等同于count++
count.getAndIncrement();
}
}
}).start();
}
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(count);
}
}
解决ABA问题 #
但是,自旋锁的方式,有可能会产生ABA的问题,例如现在有三个线程A、B、C,操作流程如下
- 线程A:读取值为0
- 线程B:开始执行
- 线程B:读取值为0
- 线程A:开始执行
- 线程A:
+ 1;验证变量值为0,一致;写入变量;变量值为1 - 线程C:开始执行
- 线程C:读取值为1;
- 1;验证变量为1,一致;写入变量,变量值为0 - 线程B:开始执行
- 线程B:
+ 1;验证变量值和自己读取的都为0,一致,写入变量;变量值为1
这样看下来,虽然变量值和预期的值相同,但是中间变量其实已经修改过了,线程B比较的0已经不是自己当时读到的0了,在某些情况下,可能会出现问题
在CAS的思想上,利用版本戳的思想,从原始数据开始,给每次的数据都加上一个版本,每次对数据发生修改,版本也会进行迭代
具体的实现类,AtomicStampedReference,这个类在实例化时,第二个参数为版本号
public class Demo {
// 第一个参数为保存的数据,第二个参数为版本戳
static AtomicStampedReference<Integer> count = new AtomicStampedReference(0,0);
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
// 比较并且设置
// 1:比较的原始值,2:设置的新参数值
// 3、比较的原始版本戳,4、设置的新版本戳
count.compareAndSet(
count.getReference()
, count.getReference() + 1
, count.getStamp()
, count.getStamp() + 1
);
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
count.compareAndSet(
count.getReference()
, count.getReference() - 1
, count.getStamp()
, count.getStamp() + 1
);
}
}
}).start();
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(count.getReference());
System.out.println(count.getStamp());
}
}
线程池 #
-
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程, 对性能影响很大。
-
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用
-
好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
-
相关API
- JDK 5.0起提供了线程池相关API:
ExecutorService和Executors - 线程池所在的包:
java.util.concurrent
- JDK 5.0起提供了线程池相关API:
线程池的执行顺序 #
1、用户提交任务,判断核心线程是否正在处理,如果核心线程有空闲,直接使用核心线程执行
2、如果核心线程没有空闲,判断队列是否满了,如果没有满,就把任务放进队列中
3、如果队列已经满了,那么判断线程池中线程+新任务线程是否大于最大线程数,如果大于,抛出异常,拒绝执行
4、如果小于等于最大线程数,创建新的线程,执行任务
线程池的状态 #
| 状态 | 说明 |
|---|---|
| RUNNING | 允许提交并处理任务 |
| SHUTDOWN | 不允许提交新的任务,但是会处理完已提交的任务 |
| STOP | 不允许提交新的任务,也不会处理阻塞队列中未执行的任务,并设置正在执行的线程的中断标志位 |
| TIDYING | 所有任务执行完毕,池中工作的线程数为0,等待执行terminated()勾子方法 |
| TERMINATED | terminated()勾子方法执行完毕 |
- 线程池的
shutdown()方法,将线程池由 RUNNING(运行状态)转换为 SHUTDOWN状态 - 线程池的
shutdownNow()方法,将线程池由RUNNING 或 SHUTDOWN 状态转换为 STOP 状态。
SHUTDOWN 状态 和 STOP 状态 先会转变为 TIDYING 状态,最终都会变为 TERMINATED
线程池的创建 #
1、ThreadPoolExecutor(建议) #
/* 创建线程池
* 参数1:线程池创建后,核心线程数
* 参数2:最大线程数
* 参数3:核心线程外,新创建的线程空闲时,最大存活时间
* 参数4:最大存活时间的单位
* 参数5:用于存放任务的阻塞队列
* 参数6:用于创建线程的线程工厂
* 参数7:任务被拒绝后的策略
*/
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
2
, 5
, 60
, TimeUnit.SECONDS
, new ArrayBlockingQueue<>(5)
, new ThreadPoolExecutor.AbortPolicy()
);
拒绝策略 #
| 策略 | 描述 |
|---|---|
| ThreadPoolExecutor.AbortPolicy() | 抛出RejectedExecutionException异常。默认策略 |
| ThreadPoolExecutor.CallerRunsPolicy() | 由向线程池提交任务的线程来执行该任务 |
| ThreadPoolExecutor.DiscardPolicy() | 抛弃当前的任务 |
| ThreadPoolExecutor.DiscardOldestPolicy() | 抛弃最旧的任务(最先提交而没有得到执行的任务) |
线程池中,提交任务submit()与execute()之间的区别 #
-
execute()是专门用来提交Runnable接口任务的;submit()既可以提交Runable任务也可以提交Callable任务 -
execute()没有返回结果;submit()可以使用Future接收线程的返回结果 -
execute()方法中不能处理任何异常;submit()支持处理任务中的异常,使用Funture.get()
2、Executors #
可以使用Executors的静态方法,来创建线程池
| 创建方法 | 线程池 | 阻塞队列 | 使用场景 |
|---|---|---|---|
| newSingleThreadExecutor() | 单线程的线程池 | LinkedBlockingQueue | 适用于串行执行任务的场景,⼀个任务⼀个任务地执行 |
| newFixedThreadPool(n) | 固定数目线程的线程池 | LinkedBlockingQueue | 适用于处理CPU密集型的任务,适用执行长期的任务 |
| newCachedThreadPool() | 可缓存线程的线程池 | SynchronousQueue | 适用于并发执行大量短期的小任务 |
| newScheduledThreadPool(n) | 定时、周期执行的线程池 | DelayedWorkQueue | 周期性执行任务的场景,需要限制线程数量的场景 |
例如 #
public static void main(String[] args) {
// 创建一个固定大小的线程池:
ExecutorService es = Executors.newFixedThreadPool(4);
Future<Integer> future = es.submit(new Task("a"));
System.out.println(future);
// 关闭线程池:
es.shutdown();
}
阿里巴巴Java开发手册规定:线程池不允许使用Executors去创建,要求通过
new ThreadPoolExecutor()的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。