一、概述
1. 进程和线程
- 进程:进程指正在运行的程序。
- 线程:线程是进程中的一个执行单元,是程序 使用cpu 的基本单位(调度)。负责当前进程中程序的执行。是进程中单个顺序控制流(执行路径),是一条单独执行的路径
- 一个程序运行后至少有一个进程,一个进程中可以包含多个线程
- 在操作系统中,进程是资源分配的基本单位,线程是调度的基本单位。
在没有出现线程之前, 进程既是操作系统进行资源分配的基本单位,又是调度的基本单位
- 单线程程序:若有多个任务只能依次执行。当上一个任务执行结束后,下一个任务开始执行。程序只有一条执行路径
- 多线程程序:若有多个任务可以同时执行。程序有多条执行路径
操作系统发展:单道批处理操作——>多道批处理操作系统——>分时操作系统(多进程的)——>线程
批处理:程序在执行过程中,不会响应用户的请求
单道批处理操作:一次只能运行一个程序,如果要在计算机运行多个程序,这多个程序,只能一个一个的顺序执行,如果这个正在运行的程序,在运行过程中,执行了一些非常耗时的IO操作(传输数据的过程是没有使用到cpu),这样一来,cpu就闲下来了,但是cpu是计算机中,最为昂贵的
多道批处理操作系统:同时运行多个程序,显著的提高了cpu的利用率,但是我们一旦一个,程序运行起来,都是是需要使用计算机资源的
分时操作系统:每一个进程,有一个固定的时间片,在运行一个固定的时间片后,紧接着轮到下一个进程运行(切换)
- 为什么还会有线程呢? 进程切换的代价太高了,这样一来,每一次进程切换,都需要付出不小的额外的代价,为了减小进程切换的代价,引入了线程,提高CPU的利用率
2. 线程调度
- 分时调度:所有线程 轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
- 抢占式调度:优先让 优先级高的 线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度
- 体现了:程序运行的不确定性
- CPU 使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。
- 多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。
3. 线程控制
- 线程睡眠 static void sleep(long millis):在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
//让主线程,休眠3秒 Thread.sleep(3000); //sleep新的写法 TimeUnit.SECONDS.sleep(3); System.out.println("睡醒了"); - 线程加入 public final void join():(让当前线程)等待该线程(新加入的线程终止)终止。
SecondThread sec = new SecondThread("zs"); SecondThread thr = new SecondThread("lsii"); SecondThread fou = new SecondThread("wangwu");
//启动这三个线程对象 sec.start(); //sec线程上调用join方法 ,让当前线程(main)等待sec线程执行完毕 sec.join(); thr.start(); fou.start(); - 线程礼让public static void yield():暂停当前正在执行的线程对象,并执行其他线程
只是让当前线程放弃cpu执行权,但是 不能阻止它放弃后继续抢夺cpu执行权
- 后台线程(守护线程)public final void setDaemon(boolean on):将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出;该方法必须在启动线程前调用。
SecondThread sec = new SecondThread("zs"); SecondThread thr = new SecondThread("lsii"); //将sec和thr线程设置为守护线程 sec.setDaemon(true); thr.setDaemon(true); //启动 sec.start(); thr.start(); //之后发现sec和thr是守护线程,就会中断 - 中断线程:public void interrupt():让一个线程控制另外一个线程(有条件的:受阻),可以利用该方法终止另一个线程的运行
- 如果线程在调用 Object 类的 wait()、wait(long) 或 wait(long, int) 方法,或者该类的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 等阻塞方法处于阻塞状态,它还将收到一个 InterruptedException
- 中断一个不处于活动状态的线程不需要任何作用。
二、Thread 类
1. 概述
- Thread是程序中的执行线程。Java 虚拟机允许应用程序并发地运行多个执行线程
- 不是抽象类
2. 构造方法
- Thread(): 分配新的 Thread 对象
- Thread(String name):分配新的 Thread 对象,将指定的 name 作为其线程名称
3. 常用方法
- void start():使该线程开始执行,Java虚拟机调用线程的 run 方法
- void run():该线程要执行的操作,
- static void sleep(long millis):在指定毫秒内让当前正在执行的线程休眠,暂停执行
- static Thread currentThread():返回当前正在执行的线程对象的引用Thread.currentThread()
4. 创建新执行线程的两种方法
- 将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法。创建对象,开启线程。run方法相当于其他线程的main方法。
- 声明一个实现 Runnable 接口的类。该类然后实现 run 方法。然后创建 Runnable 的子类对象,传入到某个线程的构造方法中,开启线程。
虽然实现线程有两种方式,其实从客观来讲,线程本身只代表独立的执行路径, 执行的具体内容其实是Task本身,和执行路径的实现本身没有联系;只是我们开发者,想将一个task放在某条独立的执行路径(Thread 类对象,也就是一个线程中)来运行
三、创建线程:继承 Thread 类
- 创建线程的步骤
- 定义一个类继承 Thread
- 重写 run方法
- 创建子类对象,就是创建线程对象
- 调用 start 方法,开启线程并让线程执行,同时还会告诉jvm去调用 run 方法
四、创建线程:实现 Runnable 接口
1. Runnable 接口的构造方法
Thread(Runnable target)
: 分配新的 Thread 对象,以便将 target 作为其运行对象Thread(Runnable target,String name)
: 分配新的 Thread 对象,以便将 target 作为其运行对象;并将指定的 name 作为其名称
2. 创建线程的步骤
- 定义类实现 Runnable 接口。
- 覆盖接口中的 run方法
- 创建 Thread类的 对象
- 将 Runnable接口 的子类对象作为参数传递给 Thread 类 的构造方法。
- 调用 Thread类的 start() 开启线程。
Thread 类的构造函数:
Thread()
: 分配新的 Thread 对象Thread(String name)
:分配新的 Thread 对象,将指定的 name 作为其线程名称
3. 实现 Runnable 接口的原理
为什么需要定一个类去实现Runnable接口呢?继承Thread类和实现Runnable接口有啥区别呢?
只有创建Thread类的对象才可以创建线程。线程任务已被封装到Runnable接口的run方法中,而这个run方法所属于Runnable接口的子类对象,所以将这个子类对象作为参数传递给Thread的构造函数,这样,线程对象创建时就可以明确要运行的线程的任务
4. 两种方式的比较
- 继承 Thread 类方式
- 如果某个类已经有父类,则无法再继承 Thread 类
- 实现 Runnable 接口方式
- 解决了方式一的单继承的局限性
- 还有一个优点,便于多线程共享数据
第二种方式实现Runnable接口避免了单继承的局限性,所以较为常用。实现Runnable接口的方式,更加的符合面向对象;线程分为两部分,一部分线程对象,一部分线程任务。
- 继承Thread类,线程对象和线程任务耦合在一起。一旦创建Thread类的子类对象,既是线程对象,有又有线程任务。
- 实现runnable接口,将线程任务单独分离出来封装成对象,类型就是Runnable接口类型。Runnable接口对线程对象和线程任务进行解耦
五、线程优先级
1. 概述
- 我们可以通过 Thread 类中:
getPriority 方法
:获取 线程的优先级setPriority 方法
:设置 线程的优先级
2. 线程优先级的范围
- 如果设置线程优先级的范围,超出了规定范围,会抛出异常;
MAX_PRIORITY 10 //最大优先级
MIN_PRIORITY 1 //最小优先级
NORM_PRIORITY 5 //默认优先级
六、线程的生命周期
1. 新建状态
- 创建线程对象
2. 就绪状态
- start() 之后
- sleep() 睡醒之后
- yield() 之后
- 有执行资格,等待cpu调度
3. 运行状态
- 取得执行权,开始执行
4. 阻塞状态
- 无执行资格,无执行权
5. 死亡状态
- 执行完毕,等待垃圾回收
七、同步代码块
1. 问题引入——线程安全问题
- 发生线程安全问题的三个条件:
- 多线程运行环境
- 多线程访问线程共享数据(存在共享数据)
- 访问共享数据的操作不是原子操作。
注:原子操作:不可分割的操作 ,相当于一次性完成的操作
- 当这三个条件同时满足的时候,才会发生多线程的数据安全问题
- 解决多线程的线程安全问题:如何实现线程对共享资源的排他性访问(只有我访问完了你们才能修改)?
- 使用
同步代码块
2. 同步代码块
- 同步代码块实现 线程同步
- 线程同步:就是利用锁对象,完成多线程运行环境中,对共享资源的排他性访问(我走你不能走, 你走我不能走)
- 优点:解决了多线程的安全问题
- 缺点:消耗资源(当线程很多时,每个线程运行的时候都需要去判断同步锁,这个是很耗费系统资源的)
线程异步:线程之间,互不干扰,各自独立运行(我走我的,你走你的)
- 格式:
synchronized(对象){ //critical section 对共享资源的访问的代码 }
- 同步代码块中所使用的对象,称之为锁对象 ——> 锁的角色
锁对象中,有一个标志位:可以表示两种状态,加锁 和 解锁
- 持有锁的是线程(一个线程给一个锁对象加锁,我们就说这个线程持有了锁对象)
- 同步代码块何时给锁对象加锁?进入同步代码块的同时,就给锁对象加锁
- 线程何时释放持有的锁? 当执行完同步代码块的时候,就会释放同步代码块的锁对象
- 判断和修改锁对象标志位的操作,这是由 jvm 保证一定是原子操作
6. 锁对象,究竟是什么对象? java语言中任意一个对象,都可以充当锁对象的角色(因为任意一个对象中都有一个表示加锁,解锁状态的标志位)
- 注意点:
虽然锁对象可以是任意对象,但是针对同一个(同样的多个共享变量) 的所有操作,都必须保证在同步代码块中,使用同一个锁对象,才能避免线程安全问题。
3. 同步方法
- 当同步代码块的范围扩大到整个方法的方法体的时候,我们可以将整个方法定义成同步方法,
在方法声明上加上synchronized
- 普通的同步代码块:
synchronized(锁对象) {}
- 同步方法:
void synchronized 方法名(){}
- 同步方法的锁对象就是
this
八、线程中的常用方法说明:
- start():
在使用 new 关键字创建一个线程后(New 状态),并不表现出任何的线程活动状态(非 New、Terminated 状态,可以使用 isAlive 方法检测线程的活动状态),CPU 也不会执行线程中的代码。
只有在 start() 方法执行后,才表示这个线程可运行了(Runnable 状态),至于何时真正运行还要看线程调度器的调度。
在线程死亡后,不要再次调用 start() 方法。只能对新建状态的线程调用且只能调用一次 start() 方法,否则将抛出 IllegalThreadStateException 异常。 - run():
启动线程是 start() 方法,而不是 run() 方法。
如果直接调用 run() 方法,这个线程中的代码会被立即执行,多个线程就无法并发执行了。 - join():
等待该线程完成的方法,其他线程将进入等待状态(Waiting 状态),通常由使用线程的程序(线程)调用,如将一个大问题分割为许多小问题,要等待所有的小问题处理后,再进行下一步操作。 - sleep():
主动放弃占用的处理器资源,该线程进入阻塞状态(Blocked 状态),指定的睡眠时间超时后,线程进入就绪状态(Runnable),等待线程调度器的调用。 - yield():
主动放弃占用的处理器资源,线程直接进入就绪状态(Runnable),等待线程调度器的调用。
可能的情况是当线程使用 yield 方法放弃执行后,线程调度器又将该线程调度执行。 - interrupt():
没有任何强制线程终止的方法,这个方法只是请求线程终止,而实际上线程并不一定会终止,在调用 sleep() 方法时可能会出现 InterruptedException 异常,你可能会想在异常捕获后(try-catch语句中的catch)请求线程终止,而更好的选择是不处理这个异常,抛给调用者处理,所以这个方法并没有实际的用途,还有 isInterrupted() 方法检查线程是否被中断。 - setDaemon():
设置守护进程,该方法必须在 start() 方法之前调用,判断一个线程是不是守护线程,可以使用 isDaemon() 方法判断。 - setPriority():
设置线程的优先级,理论上来说,线程优先级高的线程更容易被执行,但也要结合具体的系统。
每个线程默认的优先级和父线程(如 main 线程、普通优先级)的优先级相同,线程优先级区间为 1~10,三个静态变量:MIN_PRIORITY = 1、NORM_PRIORITY = 5、MAX_PRIORITY = 10。
使用 getPriority() 方法可以查看线程的优先级。 - isAlive():
检查线程是否处于活动状态,如果线程处于就绪、运行、阻塞状态,方法返回 true,如果线程处于新建和死亡状态,方法返回 false。
3. 同步方法
- 当同步代码块的范围扩大到整个方法的方法体的时候,我们可以将整个方法定义成同步方法,
在方法声明上加上synchronized
- 普通的同步代码块:
synchronized(锁对象) {}
- 同步方法:
void synchronized 方法名(){}
- 同步方法的锁对象就是
this
八、线程池:
1. 什么是线程池,有哪几种创建方法
线程池就是提前创建若干个线程,如果有任务需要处理,线程池里的线程就会处理任务,处理完之后线程并不会被销毁,而是等待下一个任务。由于创建和销毁线程都是消耗系统资源的,所以当你想要频繁的创建和销毁线程的时候就可以考虑使用线程池来提升系统的性能。
java 提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池。
四种线程池的创建:
1.newCachedThreadPool创建一个可缓存线程池
2.newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数。
3.newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
4.newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务。
2. 线程池的优点
1.重用存在的线程,减少对象创建销毁的开销。
2.可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
3.提供定时执行、定期执行、单线程、并发数控制等功能。