2、多线程
1、基本概念
1、程序、进程、线程
程序(program):是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
进程(process):是程序的一次执行过程,或是正在运行的一个程序。动态过程:有它自身的产生、存在和消亡的过程。
- 运行中的QQ,运行中的MP3播放器;
- 程序是静态的,进程是动态的;
- 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。
线程(thread):进程可进一步细化为线程,是一个程序内部的一条执行路径。
- 若一个进程同一时间并行执行多个线程,就是支持多线程的;
- 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小;
- 一个进程中的多个线程共享相同的内存单元/内存地址空间,它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患;
- 每个Java程序都有一个隐含的主线程:main方法。
2、并发和并行
单核CPU,其实是一种假的多线程,因为在同一个时间单元内,只能执行一个线程的任务,实际还是串行执行的。操作系统中有一个组件叫做任务调度器,将CPU的时间片分给不同的程序使用,只是由于CPU在线程间(时间片很短)的切换非常快,人类感觉是同时运行的。
总结:微观串行,宏观并行。
如果是多核的话,才能更好的发挥多线程的效率,现在的服务器都是多核的。多核CPU每个核都可以调度运行线程,这时候线程可以是并行的。
并发concurrent:同一时间应对多件事情的能力;
并行parallel:同一时间动手做多件事情的能力。
举例:
- 家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做这多件事,这时就是并发;
- 家庭主妇雇了个保姆,她们一起这些事,这时既有并发,也有并行(这时会产生竞争,例如锅只有一口,一个人用锅时,另一个人就得等待);
- 雇了3个保姆,一个专做饭、一个专打扫卫生、一个专喂奶,互不干扰,这时是并行。
3、多线程应用场合
程序需要同时处理多个任务;
比如用户输入、文件读写、网络操作或搜索等,这些任务可能需要等待。
此外,还需要一些程序在后台运行。
2、线程的创建和使用
在Java中,JVM允许程序运行多个线程,通过
java.lang.Thread
类实现。每个线程通过Thread
对象的run()
方法来完成操作,通常将run()
方法的主体称为线程体。线程的启动是通过Thread
对象的start()
方法,而不是直接调用run()
。
Thread
类的构造器包括:
Thread()
:创建新的Thread
对象。Thread(String threadname)
:创建线程并指定线程实例名。Thread(Runnable target)
:指定创建线程的目标对象,该对象实现了Runnable
接口中的run
方法。Thread(Runnable target, String name)
:创建新的Thread
对象。创建线程的方式有四种:
- 继承
Thread
类。- 实现
Runnable
接口。- 实现
Callable
接口。- 使用线程池创建线程。
1、继承Thread类
步骤:
- 定义子类继承
Thread
类- 子类重写
Thread
类中的run
方法- 创建
Thread
子类对象,即创建线程对象- 调用线程对象
start
方法(启动线程,调用run
方法)
run() 和 start() 的区别?
- start():用来启动线程,通过该线程调用 run 方法执行 run 方法中所定义的逻辑代码。start 方法只能被调用一次。
- run():封装了要被线程执行的代码,可以被调用多次。
2、实现Runnable接口
步骤:
- 定义子类实现 Runnable 接口;
- 子类中重写 Runnable 接口中的 run 方法;
- 通过 Thread 类构造方法创建线程对象;
- 将 Runnable 接口的子类对象作为实际参数传递给 Thread 类的构造方法中;
- 调用 Thread 类的 start 方法:开启线程,调用 Runnable 子类接口的 run 方法。
优势:
- 避免了单继承的局限性;
- 多个线程可以共享一个接口实现类的对象,适合多个相同线程处理同一份资源。(但不是独有的)
3、Thread类的相关方法
start()
: 启动线程,并执行对象的run()
方法。run()
: 线程在被调用时执行的操作。getName()
: 返回线程的名称。setName()
: 设置线程的名称。currentThread()
: 返回当前线程。
3、线程的调度和生命周期
1、线程的调度
调度策略:
- 基于时间片轮转;
- 抢占式:高优先级的线程抢占CPU。
调度方法:
- 同优先级线程组成先进先出队列,使用时间片策略;
- 对高优先级,使用优先调度的抢占式策略;
- Java中线程优先级的范围是1~10,默认的优先级是5:
- MAX_PRIORITY(10)
- MIN_PRIORITY(1)
- NORM_PRIORITY(5)
涉及的方法:
- setPriority(int newPriority):设置线程的优先级;
- getPriority():获取线程的优先级;
- yield():线程让步:
- 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程;
- 若队列中没有同优先级的线程,忽略此方法。
- join():
- 当某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到join()方法加入的join线程执行完为止
- 低优先级的线程也可以获得执行
- sleep(long millis):
- 令当前线程在规定时间内放弃对CPU的控制,使其他进程能有机会被调用,事件结束后重排队
- stop():强制线程生命期结束
- isAlive():判断线程是否还活着
2、线程的生命周期
新建 (NEW)
- 线程对象创建后,但未调用
start
方法时,处于新建状态。 - 此时未与操作系统底层线程关联。
可运行 (RUNNABLE)
- 调用
start
方法后,线程从新建状态进入可运行状态。 - 此时与底层线程关联,由操作系统调度执行。
终结 (TERMINATED)
- 线程内代码执行完毕后,由可运行状态进入终结状态。
- 此时会取消与底层线程的关联。
阻塞 (BLOCKED)
- 当获取锁失败后,线程从可运行状态进入 Monitor 的阻塞队列,处于阻塞状态,不占用 CPU 时间。
- 当持锁线程释放锁时,会唤醒阻塞队列中的线程,使其进入可运行状态。
等待 (WAITING)
- 当获取锁成功后,但由于条件不满足,调用
wait()
方法,线程从可运行状态释放锁进入 Monitor 的等待集合等待,同样不占用CPU时间 - 当其它持锁线程调用
notify()
或notifyAll()
方法,会按照一定规则唤醒等待集合中的等待线程,恢复为可运行状态
有时限等待(TIMED_WAITING)
- 当获取锁成功后,但由于条件不满足,调用了
wait(long)
方法,此时从可运行状态释放锁进入Monitor等待集合进行有时限等待,同样不占用CPU时间 - 当其它持锁线程调用
notify()
或notifyAll()
方法,会按照一定规则唤醒等待集合中的有时限等待线程,恢复为可运行状态,并重新去竞争锁 - 如果等待超时,也会从有时限等待状态恢复为可运行状态,并重新去竞争锁
- 还有一种情况是调用
sleep(long)
方法也会从可运行状态进入有时限等待状态,但与Monitor无关,不需要主动唤醒,超时时间到自然恢复为可运行状态
4、线程同步
现象:
- 多个窗口卖同一张票;
- 票的编号出现了0或者负值。
问题的原因:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
解决思路:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
Java对于多线程的安全问题提供了专业的解决方式:同步机制。
1、synchronized代码块
语法:
synchronized (唯一的对象){
代码块;
}
A进程占用此 唯一对象 时,B、C…等其他进程都要在A进程完成 代码块 后释放了锁,才能挨个的执行该代码块
2、synchronized方法
语法:
synchronized public void 方法() {
方法体;
}
A占有了该锁,那么只有A进程完成了该方法,才轮到其他进程调用。
3、Lock锁
从JDK 5.0开始,Java引入了更强大的线程同步机制,通过显式定义同步锁对象来实现同步,这个锁对象就是 Lock
。
java.util.concurrent.locks.Lock
接口用于控制多个线程对共享资源的访问,确保每次只有一个线程可以对 Lock
对象加锁,从而实现对共享资源的独占访问。
ReentrantLock
类实现了 Lock
接口,它与 synchronized
关键字具有相同的并发性和内存语义,但在实现线程安全控制时,ReentrantLock
更为常用,因为它允许显式地加锁和释放锁。
4、线程安全的单例模式懒汉式
public class SingleObj {
private static SingleObj singleObj;
private SingleObj() {
}
synchronized public static SingleObj getSingleObj() {
if (singleObj == null) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
singleObj = new SingleObj();
}
return singleObj;
}
}
当A线程运行至创建新对象前停止,B进程进行时,obj仍为null,此时就会出现,单例设计模式无法实现的情况,懒汉式是线程不安全的。饿汉式则是安全的
5、死锁
应注意是否两个进程被彼此所限制,从而造成无法解决的死锁。
5、线程通信
方法:
wait()
方法让当前线程挂起并释放 CPU,同时放弃对同步资源的控制,等待其他线程调用notify()
或notifyAll()
方法来唤醒。线程被唤醒后,需要重新获得对监视器的所有权才能继续执行。notify()
方法用于唤醒正在等待同步资源的线程中优先级最高的那个线程,使其结束等待状态。notifyAll()
方法会唤醒所有正在等待该资源的线程,让它们都结束等待状态。这些方法只能在
synchronized
方法或synchronized
代码块中使用。
生产者消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个线程——即所谓的生产者和消费者——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者在缓冲区添加数据之后,再唤醒消费者。
Java中
wait
和sleep
方法的不同点和共同点。共同点
wait()
、wait(long)
和sleep(long)
的效果都是让当前线程暂时放弃CPU的使用权,进入阻塞状态。不同点
方法归属不同
sleep(long)
是Thread
的静态方法。- 而
wait()
、wait(long)
都是Object
的成员方法,每个对象都有。醒来时机不同
- 执行
sleep(long)
和wait(long)
的线程都会在等待相应毫秒后醒来。wait(long)
和wait()
还可以被notify
唤醒,wait()
如果不唤醒就一直等下去。- 它们都可以被打断唤醒。
锁特性不同(重点)
wait
方法的调用必须先获取wait
对象的锁,而sleep
则无此限制。wait
方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃CPU,但你们还可以用)。- 而
sleep
如果在synchronized
代码块中执行,并不会释放对象锁(我放弃CPU,你们也用不了)。
6、JDK5.0新增创建线程的方法
1、实现Callable接口
与使用
Runnable
相比,Callable
功能更强大些
- 相比
run()
方法,可以有返回值;- 方法可以抛出异常;
- 支持泛型的返回值;
- 需要借助
FutureTask
类,比如获取返回结果。关于
Future
接口
- 可以对具体
Runnable
、Callable
任务的执行结果进行取消、查询是否完成、获取结果等;FutureTask
是Future
接口的唯一的实现类;FutureTask
同时实现了Runnable
、Future
接口。它既可以作为Runnable
被线程执行,又可以作为Future
得到Callable
的返回值。
Runnable 和 Callable 的区别?
Runnable
接口的run
方法没有返回值,Callable
接口的call
方法有返回值,是个泛型,和Future
、FutureTask
配合可以用来获取异步执行的结果;Callable
接口支持返回执行结果,需要调用FutureTask.get()
得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞;Callable
接口的call()
方法允许抛出异常,而Runnable
接口的run()
方法的异常只能在内部消化,不能继续上抛。
2、使用线程池
1、存在问题及解决思路
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。
好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理:
corePoolSize
:核心池的大小maximumPoolSize
:最大线程数keepAliveTime
:线程没有任务时最多保持多长时间后会终止
2、线程池相关API
JDK 5.0起提供了线程池相关API:
ExecutorService
和Executors
ExecutorService
:真正的线程池接口。常见子类ThreadPoolExecutor
void execute(Runnable command)
:执行任务/命令,没有返回值,一般用来执行Runnable
Future submit(Callable task)
:执行任务,有返回值,一般用来执行Callable
void shutdown()
:关闭连接池
Executors
:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
Executors.newCachedThreadPool()
:创建一个可根据需要创建新线程的线程池Executors.newFixedThreadPool(n)
:创建一个可重用固定线程数的线程池Executors.newSingleThreadExecutor()
:创建一个只有一个线程的线程池Executors.newScheduledThreadPool(n)
:创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行