第十章:线程
1.程序-进程-线程
-
程序(program):是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码
-
进程((process):就是正在执行的程序,从Windows角度讲,进程是含有内存和资源并安置线程的地方
-
线程(thread):进程可进一步细化为线程,是一个进程内部的最小执行单元(执行任务)
-
程序与进程的联系与区别:
- 进程是一个动态的实体,它有自己的生命周期。反映了一个程序在一定的数据集上运行的全部动态过程。
- 一个进程肯定有一个与之对应的程序,而且只有一个。而一个程序有可能没有与之对应的进程(因为它没有执行),也有可能有多个进程与之对应(运行在几个不同的数据集上)
- 进程还具有并发性和交往性,这也与程序的封闭性不同
2.进程与线程的关系
-
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位
-
线程是进程的一个实体 , 是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.
-
一个进程可以包含多个线程,一个线程只能属于一个进程,线程不能脱离进程而独立运行
-
每一个进程至少包含一个线程(称为主线程);在主线程中开始执行程序, java程序的入口main()方法就是在主线程中被执行的
-
进程与进程之间是独立的,互不干扰的
-
在主线程中可以创建并启动其它的线程
-
一个进程内的所有线程共享该进程的内存资源
-
处理机分给线程,即真正在处理机上运行的是线程
-
划分尺度:线程更小,所以多线程程序并发性更高
-
资源分配:进程是资源分配的基本单位,同一进程内多个线程共享其资源
-
地址空间:进程拥有独立的地址空间,同一进程内多个线程共享其资源
-
处理器调度:线程是处理器调度的基本单位
-
一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行
3.多线程的概念
-
多线程之于进程的理解,可以类比多进程之于操作系统。多线程指在单个程序中可以同时运行多个不同的线程执行不同的任务。
-
何时需要多线程:
- 程序需要同时执行两个或多个任务
- 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等
- 需要一些后台运行的程序时
-
多线程的好处:
- 提高程序的响应
- 提高CPU的利用率
- 改善程序结构,将复杂任务分为多个线程,独立运行
- 进程间不能共享内存,但线程间共享内存很容易
- 创建线程的代价比进程要小得多,使用多线程实现多任务并发效率高
- Java内置多线程功能支持,不是单纯地作为底层OS的调度方式,简化了多线程编程
-
多线程的劣势:
- 线程也是程序,所以线程需要占用内存,线程越多占用内存也越多
- 多线程需要协调和管理,所以需要CPU时间跟踪线程
- 线程之间对共享资源的访问会相互影响,必须解决竞共享资源的问题
4.创建线程
- 继承Thread类
- 实现Runnable接口
1.继承Thread类
- 继承Thread类,重写run( )方法,run( ) 本身并没有操作,需要我们将需要执行的操作写入run( ),这样当线程启动时,它将执行run( )
/*
* 方法一:继承thread类(线程类),重写run()方法
* */
public class ThreadDemo1 extends Thread{
@Override
public void run() {
// super.run(); 调用父类的run()
// 在run()中编写需要独立运行的代码
for (int i = 0; i < 1000; i++) {
System.out.println("thread::"+i);
}
}
}
- 启动线程
// 创建线程对象
ThreadDemo1 thread = new ThreadDemo1();
// thread.run(); 使用run()方法调用线程的时候,就只是一个普通的方法调用
// 此方法是启动线程的方法,在线程类中
thread.start();
2.实现Runnable接口
-
java.lang.Runnable接口中仅有一个抽象方法
public void run()
-
也可以通过实现Runnable接口的方式来实现线程,只需要实现其中的run方法即可
-
实现Runnable接口的类创建的对象只是一个任务对象,不能直接的启动,需要将这个任务对象传入到线程中,才能启动。
/*
* 方法二:实现Runnable接口,将需要独立运行的程序写到run()中,这个类创建的对象是一个任务对象,
* 再通过创建一个线程对象来启动任务
* */
public class ThreadDemo1 implements Runnable{
@Override
public void run(){
for (int i = 0; i < 1000; i++) {
System.out.println("thread"+i);
}
}
}
- 启动线程
// 创建任务对象
ThreadDemo2 threadDemo2 = new ThreadDemo2();
// 构造有参线程对象
Thread thread = new Thread(threadDemo2);
thread.start();
- 实现Runnable接口的好处:
- 避免了单继承的局限性
- 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源
5.Thread类的方法
- 构造方法:
构造方法 | 说明 |
---|---|
Thread() | 创建一个线程 |
Thread(String name) | 创建一个指定名称的线程 |
Thread(Runnable target) | 利用Runnable对象创建一个线程,启动时将执行该对象的run( ) |
Thread(Runnable target, String name) | 利用Runnable对象创建一个线程,并指定该线程的名称 |
-
常用方法:final void join()
throws InterruptedException
方法原型 | 说明 |
---|---|
void start() | 启动线程 |
final void setName(String name) | 设置线程的名称 |
final String getName() | 返回线程的名称 |
final void setPriority(int newPriority) | 设置线程的优先级 |
final int getPriority() | 返回线程的优先级 |
final void join() throws InterruptedException | 等待线程终止 |
static Thread currentThread() | 返回对当前正在执行的线程对象的引用 |
static void sleep(long millis) throws InterruptedException | 让当前正在执行的线程休眠(暂停执行),休眠时间由millis(毫秒)指定 |
6.线程的优先级
- 事实上,计算机只有一个CPU,各个线程轮流获得CPU的使用权,才能执行任务
- 优先级较高的线程有更多获得CPU的机会,反之亦然
- 优先级用整数表示,取值范围是1~10,一般情况下,线程的默认优先级都是5,但是也可以通过
setPriority
和getPriority
方法来设置或返回优先级
1.调度策略
- 时间片
- 抢占式:高优先级的线程抢占CPU
2.JAVA的调度方法
- 同优先级线程组成先进先出队列(先到先服务),使用时间片策略
- 对高优先级,使用优先调度的抢占式策略
3.Thread类有下列3个静态常量来表示优先级
- MAX_PRIORITY:取值为10,表示最高优先级
- MIN_PRIORITY:取值为1,表示最底优先级
- NORM_PRIORITY:取值为5,表示默认的优先级
4.线程在它的生命周期中会处于不同的状态
-
-
新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
-
就绪:处于新建状态的线程被
start()
后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源 -
运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,
run()
方法定义了线程的操作和功能 -
阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态
-
死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
7.线程的分类
- Java中的线程分为两类:用户线程 和 守护线程
- 通俗的来说:任何一个守护线程都是所有非守护线程的保姆
- 注:
- 只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作
- 只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作
- 守护线程的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者
- 用户线程和守护线程两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果 用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了
- 设置线程为守护线程必须在启动线程之前,否则会跑出一个
IllegalThreadStateException
异常
8.线程同步
1.并发与并行
- 并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事
- 并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事
2.多线程同步
- 多个线程同时读写同一份共享资源时,可能会引起冲突。所以引入线程“同步”机制,即各线程间要有先来后到
3.同步就是排队+锁
- 几个线程之间要排队,逐个对共享资源进行操作,而不是同时进行操作
- 为了保证数据在方法中被访问时的正确性,在访问时加入锁机制
确保一个时间点只有一个线程访问共享资源。可以给共享资源加一把锁,这把锁只有一把钥匙。哪个线程获取了这把钥匙,才有权利访问该共享资源
4.同步监视器
代码:
// 使用synchronized(同步监视器)关键字同步方法或代码块
synchronized (同步监视器){
// 需要被同步的代码;
}
// synchronized还可以放在方法声明中,表示整个方法,为同步方法
public synchronized void show (String name){
// 需要被同步的代码;
}
-
同步监视器可以是任何对象 , 必须唯一,保证多个线程获得是同一个对象(锁)
-
同步监视器执行过程
- 第一个线程访问,锁定同步监视器,执行其中代码
- 第二个线程访问,发现同步监视器被锁定,无法访问
- 第一个线程访问完毕,解锁同步监视器
- 第二个线程访问,发现同步监视器没有锁,然后锁定并访问
-
**注:**一个线程持有锁会导致其他所有需要此锁的线程挂起;在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
9.线程死锁
-
死锁:两个或两个以上的进程或线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
-
-
产生死锁的四个原因:
- 互斥条件:一个资源每次只能被一个线程使用
- 请求与保持条件:当一个线程请求阻塞时,它会对自己已有的资源保持不放
- 不剥夺(不可抢占)条件:进程已获得的资源,在未使用完之前,不能被强行剥夺
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系
-
避免死锁:只要破坏产生死锁的四个条件之一就可以
- 破坏互斥条件:该条件没有办法破坏,因为用锁的意义本来就是想让他们互斥的(临界资源需要互斥访问)
- 破坏请求与保持条件:一次性申请所有的资源;或者一个资源也申请不到
- 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源
- 破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放
-
注:在java中就是通过加锁,来避免死锁。设计时考虑清楚锁的顺序,尽量减少嵌套的加锁交互数量
可死锁代码:
public class SiSuo extends Thread {
static Object objA = new Object();
static Object objB = new Object();
boolean flag;
public SiSuo(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag) {
synchronized (objA) {
System.out.println("if objA");
synchronized (objB) {
System.out.println("if objB");
}
}
}else{
synchronized (objB) {
System.out.println("else objB");
synchronized (objA) {
System.out.println("else objA");
}
}
}
}
}
测试代码:
public class TestSiSuo {
public static void main(String[] args) {
SiSuo s1 = new SiSuo(true);
s1.start();
SiSuo s2 = new SiSuo(false);
s2.start();
}
}
10.Lock(锁)
- 从JDK 5.0开始,Java提供了更强大的线程同步机制-通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当
java.util.concurrent.locks.Lock
接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象ReentrantLock
类实现了Lock
,它拥有与synchronized
相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock
,可以显式加锁、释放锁- 锁类型:
- 可重入锁(
synchronized 和 ReentrantLock
): 在执行对象中所有同步方法不用再次获得锁 - 可中断锁(
synchronized不是可中断锁, 而Lock是可中断锁
): 在等待获取锁过程中可中断 - 公平锁(
ReentrantLock 和 ReentrantReadWriteLock
): 按等待获取锁的线程的等待时间进行获取,等待时间长的具有优先获取锁权利 - 读写锁(
ReadWriteLock 和 ReentrantReadWriteLock
): 对资源读取和写入的时候拆分为2部分处理,读的时候可以多线程一起读,写的时候必须同步地写
- 可重入锁(
1.synchronized
与 Lock
的区别
类别 | synchronized | Lock |
---|---|---|
存在层次 | Java的关键字,在jvm层面上 | 是一个类 |
锁的释放 | 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 | 在finally中必须释放锁,不然容易造成线程死锁 |
锁的获取 | 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 | 分情况而定,Lock有多个锁获取的方式,具体下面会说道,大致就是可以尝试获得锁,线程可以不用一直等待 |
锁状态 | 无法判断 | 可以判断 |
锁类型 | 可重入 不可中断 非公平 | 可重入 可判断 可公平(两者皆可) |
性能 | 少量同步 | 大量同步 |
- 线程同步优先使用顺序 : Lock > 同步代码块(已经进入了方法体, 分配了相应的资源) > 同步方法
2.Lock方法
lock()
:获取锁,如果锁被暂用则一直等待unlock()
:释放锁tryLock()
: 注意返回类型是boolean,如果获取锁的时候锁被占用就返回false,否则返回truetryLock(long time, TimeUnit unit)
:比起tryLock()就是给了一个时间期限,保证等待参数时间lockInterruptibly()
:用该锁的获得方式,如果线程在获取锁的阶段进入了等待,那么可以中断此线程,先去做别的事
3.Lock锁的一般使用
lock()
必须紧邻try{} catch(){}
使用ReentrantLock
是Lock接口的实现
百数问题实现Lock使用:
public class PrintNum extends Thread {
static int num = 1;
static Object obj = new Object();
@Override
public void run(){
while(true){
synchronized (obj){
obj.notify();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(num <= 100){
System.out.println(Thread.currentThread().getName()+":"+(num++));
}else{
break;
}
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
测试代码:
public class Test {
public static void main(String[] args) {
PrintNum p1 = new PrintNum();
p1.setName("窗口1");
p1.start();
PrintNum p2 = new PrintNum();
p2.setName("窗口2");
p2.start();
}
}
11.线程通信
- 线程通信:多个线程通过消息传递实现相互牵制,相互调度,即线程间的相互作用
- 涉及方法:
wait()
:执行此方法,当前线程就进入阻塞状态,并释放同步监视器notify()
:执行此方法,唤醒被wait()
的一个线程。如果有多个线程被wait()
,就唤醒优先级高的那个notifyAll()
:执行此方法,唤醒所有被wait的线程- 都被定义在
java.lang.Object
类中
- 生产者与消费者问题
生产者:
public class Producers implements Runnable{
Table table;
public Producers(Table table){
this.table = table;
}
@Override
public void run(){
while(true){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
table.add();
}
}
}
消费者:
public class Consumers implements Runnable{
Table table;
public Consumers(Table table){
this.table = table;
}
@Override
public void run(){
while(true){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
table.sub();
}
}
}
柜台:
public class Table {
// 共享资源
int sum = 0;
public synchronized void add(){
if(sum < 5){
sum++;
System.out.println("生产者生产产品\t 目前的产品数量为::"+sum);
notify();// 唤醒消费者
}else{
try{
this.wait();// 阻塞生产者
}catch(InterruptedException in){
in.printStackTrace();
}
}
}
public synchronized void sub(){
if(sum > 0){
sum--;
System.out.println("消费者消费产品\t 目前的产品数量为::"+sum);
notify();// 唤醒生产者
}else{
try{
this.wait();// 阻塞消费者
}catch(InterruptedException in){
in.printStackTrace();
}
}
}
}
测试代码:
public class Test {
public static void main(String[] args) {
Table table = new Table();
Producers p = new Producers(table);
Consumers c = new Consumers(table);
Thread pt = new Thread(p);
pt.start();
Thread ct = new Thread(c);
ct.start();
}
}
12.新增创建线程方式
1.新增方式一:实现 Callable
接口
- 相比
run()
,可以有返回值 - 方法可以抛出异常
- 支持泛型的返回值
- 需要借助
FutureTask
类,获取返回结果。
/*
* 实现Callable接口 重写call() 可以抛出异常,可以有返回值
* */
public class CallableDemo implements Callable<Integer> {
@Override
public Integer call() throws Exception{
int sum = 0;
for(;sum<100;){
sum++;
System.out.println(sum);
}
return sum;
}
}
测试代码:
public class Test {
public static void main(String[] args) {
CallableDemo callable = new CallableDemo();
FutureTask<Integer> futureTask = new FutureTask(callable);
Thread thread = new Thread(futureTask);
thread.start();
try {
System.out.println(futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
2.新增方式二:使用线程池
-
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大
-
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具
-
优势:
- 降低资源消耗: 通过重复利用已创建的线程降低线程创建和销毁造成的消耗
- 提高响应速度: 当任务到达时,任务可以不需要等到线程创建就能立即执行
- 提高线程的可管理性: 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
-
线程工作原理:
-
线程池接口:
ExecutorService
和Executors
-
常用实现类:
ThreadPoolExecutor
-
功能性线程池:
- 定长线程池(
FixedThreadPool
)- 特点:只有核心线程,线程数量固定,执行完立即回收,任务队列为链表结构的有界队列
- 应用场景:控制线程最大并发数
- 定时线程池(
ScheduledThreadPool
)- 特点:核心线程数量固定,非核心线程数量无限,执行完闲置10ms后回收,任务队列为延时阻塞队列
- 应用场景:执行定时或周期性的任务
- 可缓存线程池(
CachedThreadPool
)- 特点:无核心线程,非核心线程数量无限,执行完闲置60s后回收,任务队列为不存储元素的阻塞队列
- 应用场景:执行大量、耗时少的任务
- 单线程化线程池(
SingleThreadExecutor
)- 特点:只有1个核心线程,无非核心线程,执行完立即回收,任务队列为链表结构的有界队列
- 应用场景:不适合并发但可能引起IO阻塞性及影响UI线程响应的操作,如数据库操作、文件操作等
- 定长线程池(
-
-
线程池工作步骤:
- 创建线程池对象
- 向线程池提交任务
- 关闭线程池
代码演示:
/*
* 线程池:事先在线程中创建若干个线程,需要时直接使用,不需要频繁的创建,销毁线程,提高执行效率.
* */
public class Test1 {
public static void main(String[] args) {
// 在线程池中创建10个线程,等待任务执行
ExecutorService executors = Executors.newFixedThreadPool(10);
// executors.execute();执行任务,一般为Runnable
ExecutorsDemo executorsDemo = new ExecutorsDemo();
//将任务交给线程池中的5个线程,分别执行
Future<Integer> future = executors.submit(executorsDemo);
// future.get();
executors.submit(executorsDemo);
executors.submit(executorsDemo);
executors.submit(executorsDemo);
executors.submit(executorsDemo);
// 停止线程池中的所有线程
executors.shutdown();
}
若有错误,欢迎私信指正。