1. 基本概念
1.1 程序和进程
- 程序 : 数据结构 + 算法 ,存放在硬盘上的可执行文件,即
.exe
文件 - 进程 :主要指运行在内存中的可执行文件
- 目前主流的操作系统都支持多进程,让操作系统同时执行多个任务
1.2 线程
- 线程就是进程内部的程序流,也就是说每个进程的内部,是支持多线程的
- 线程是轻量级的,会共享所在进程的系统资源。主流的开发都是多线程
- 多线程再用时间片轮转法,来保证线程的并发执行,所谓并发就是指宏观并行,围观穿行
- 从一个时间段上看,是多线程,多个线程同时执行
- 从一个时间点上看,是单线程,单个线程执行
2. 线程的创建【重点】
2.1 Thread类
java.lang.Thread
代表线程,任何线程都是Thread类的子类- Thread类是线程的模板,封装了复杂的线程开启等操作
2.2 创建方式【2】
-
自定义Thread类,重写run方法。然后创建该类对象,来调用start方法(不是直接调用run方法)
- 如果直接调用run方法,本质上就是单线程
- 本来是一个main主线程,调用start方法后,一分为二,变为两个线程
- 两个线程,没有先后顺序,有系统的调度算法决定
// subThread是 Thread的子类 Thread t = new subThread(); // JVM会自动调用run方法 t.start();
-
自定义类实现
Runnable
接口,并且重写run方法。再创建该类的对象作为实参来构造Thread类型对象,然后使用Thread类对象调用 run方法-
虽然是Thread类对象调用run方法,其实调用的是Runnable接口实现类中重写的run方法
new Thread(new Runnable() { // 匿名内部类中,重写run方法 @Override public void run() { System.out.println("Runnable接口实现线程的创建"); } }).start(); // 上次代码,使用了匿名对象和匿名内部内
-
使用
lambda
: (参数列表)->{方法体}new Thread(()->System.out.println("Runnable接口实现线程的创建");).start();
-
2.3 相关方法
方法声明 | 功能介绍 |
---|---|
Thread() | 无参构造 |
Thread(String name) | 根据参数指定线程名,来构造对象 |
Thread(Runnable target) | 实现Runnable接口,构造对象 |
Thread(Runnable target, String name) | 根据参数,指定引用和名称来构造对象 |
void run() | 1. 若使用了Runnable引用构造了线程对象,调用run方法时最终调用接口中的重写的run 2. 若没有使用Runnable接口引用构造线程对象,调用该方法时,啥也不做 |
void start() | 用于另外启动一个线程,Java虚拟机会自动调用该线程的run方法 |
Thread类中的 run方法测试
Thread thread = new Thread();
thread.run();
-
run方法源码分析:
@Override public void run() { if (target != null) { target.run(); } }
-
从上面的Java源码源码中我们可以看到,如果target 成员变量=null时,将不执行任何方法。
-
那这个target?怎么判断呢?看构造方法
-
Thread(Runnable target)
public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); }
-
Thread()
public Thread() { init(null, null, "Thread-" + nextThreadNum(), 0); }
-
其实这个target 就是Runnable接口引用,底层这个Runnable接口引用,会赋值给 target变量。所以,在构造方法时,就会确定target是否为空
- 所以,若没有使用Runnable接口引用构造线程对象作为参数,在调用该方法时,就会啥也不做。
- 使用自定义Thread类,来创建线程时,必须重写 run方法,不然也会是啥也不做
- 如果使用实现Runnable接口作为参数,来创建线程。那么:底层的target = Runnable实现类对象。所以,调用run方法,其实就是调用 Runnable实现类对象的重写的run方法
-
-
3 线程的生命周期【了解】
步骤 | 说明 |
---|---|
new Thread() | 创建线程(并没有执行) |
start() | 让JVM执行 run方法,激活线程,进入就绪状态 |
线程调度器 | 使用线程调度器调用该线程,线程进入运行状态。新线程开启 |
运行状态的3种情况:
- 如果cpu分配的时间片用完后,该线程的活还没干完,将进入就绪状态,继续等待调度器的调度
- 如果线程的任务,执行完成后,将进入消亡状态
- 在线程执行的过程中,发生了阻塞事件,如:sleep方法等。线程将进入阻塞状态,在阻塞状态解除后,进入就绪状态,等待调度器的调度
4. 线程的编号和名称【了解】
- 编号:线程的唯一标识
- 名称:线程名
方法声明 | 功能介绍 |
---|---|
long getId() | 获取当前线程的编号 |
String getName() | 获取当前线程的名称 |
void setName(String name) | 修改线程名 |
static Thread currentThread() | 获取当前正在执行线程的引用 |
5. 常用方法【重点】
方法声明 | 功能介绍 |
---|---|
static void yield() | 让当前线程让出CPU,进入就绪状态 |
static void sleep(times) | 让线程,进入阻塞状态,并且休眠 times毫秒 1. 其他线程可能会打断阻塞状态,则发生 InterruptedException异常 2. 所以使用sleep方法,要使用 try/catch 3.(因为,子类重写的方法不能抛出比父类更大的异常,只能使用try/catch,不能使用throws) |
int getPriority() | 获取当前线程的优先级 1. 最大优先级:10 2. 最小优先级:1 |
void setPriority( int newPriority) | 修改线程的优先级 优先级越高的线程不一定先执行,但该线程获取到时间片的机会会更多一些 |
void join() | 等待该线程终止 |
void join(long millis) | 参数指定等待的额毫秒数(等待结束后,直接和子线程抢夺时间片运行) |
boolean isDaemon() | 判断是否为守护线程 |
void setDaemon (boolean on ) | 设置线程为守护线程 |
-
注意,优先级越高的线程不一定先打印。只是所,优先级越高CPU分配给他的时间片几率就越高
-
守护线程:
-
必须在主线程启动前,将子线程设置为守护线程
// 创建线程 Thread t = new Thread(); // 必须在start前,设为守护线程 t.setDaemon(true); t.start();
-
守护线程,在主线程结束后。不管守护线程是否完成执行任务,都要立马结束掉线程
-
-
小应用,主程序让新线程执行5s后,停掉子线程
- 利用 flag 和 while来解决
// flag私有属性,和setter boolean flag = true; public void setFlag(boolean flag) { this.flag = flag; } public void method(){ new Thread(new Runnable() { @Override public void run() { while (flag){ System.out.println("子线程正在执行..."); try { // 让当前线程(main主线程)睡眠1秒, 效果就是1s打印一次 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); } public static void main(String[] args) { Thread01 t = new Thread01(); t.method(); try { // 让当前线程(main主线程)睡眠5秒 Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } t.setFlag(false); System.out.println("子线程结束"); }
6. 线程的同步机制【重点】
6.1 基本概念
- 多个线程访问同一资源时,需要对线程之间,进行通信协调,保证数据的一致性
- 线程安全:多个线程并发读写同一个临界资源时会发生线程并发安全问题
- 异步操作:多线程并发的操作,各自独立运行。
- 同步操作:多线程串行的操作,先后执行的顺序。
6.2 线程同步的实现
-
IDEA快捷键:
Ctrl + Alt + T
-
使用
synchronized关键字
来实现线程的同步 (也叫对象锁机制),从而保证线程执行的原子性- 部分代码的锁定
synchronized(类类型的引用) { // TODO 编写所有需要锁定的代码; }
-
全部代码的锁定
synchronized(this) { // TODO 整个方法体的代码 }
如果使用Runnable接口实现类做参数,实现的线程。那么,this就是Runnable接口的实现类引用
-
synchronized修饰类:
- 比如:StringBuffer的append方法
public synchronized StringBuffer append(char c)
-
注意:
如果synchronized的参数,不是同一个对象的引用,那么会锁不住。就可能出现线程安全问题
6.3 线程安全类、非线程安全类
- StringBuffer类是线程安全的类,但StringBuilder类非线程安全的类。
- Vector类和Hashtable类是线程安全的类,但ArrayList类和HashMap类不是线程安全的类。
Collections.synchronizedList()
和Collections.synchronizedMap()
等方法实现安全。- 即,将ArrayList、HashMap非线程安全的类,实现线程安全
6.4 死锁
-
线程-1,执行的代码:
public void run(){ synchronized(a){ //持有对象锁a,等待对象锁b synchronized(b){ // TODO 编写锁定的代码; } } }
-
线程-2,执行的代码:
public void run(){ synchronized(b){ //持有对象锁a,等待对象锁b synchronized(a){ // TODO 编写锁定的代码; } } }
-
两个线程竞争了不可剥夺的资源。比如:上面2个线程,同时执行,线程1需要锁b,才能释放锁a。而线程2需要锁a,才能释放锁b。这样相互就形成了死锁,永远都打不开
-
结论:
在以后的开发中尽量减少同步的资源,减少同步代码块的嵌套结构的使用!
6.5 使用 Lock(锁) 实现线程同步
6.5.1 基本概念
- 从Java5开始提供了更强大的线程同步机制,用显式定义的同步锁对象来实现
java.util.concurrent.locks.Lock
接口是控制多个线程,对共享资源进行访问的工具- Lock接口的实现类:
ReentrantLock
类,该类拥有和 synchronized 相同的并发性。在线程安全的控制中,经常使用 ReentrantLock类 显式示加锁和释放锁
6.5.2 常用方法
方法声明 | 功能介绍 |
---|---|
ReentrantLock() | 无参构造,构造对象 |
void lock() | 获取锁 |
void unlock() | 释放锁 |
举例:
ReentrantLock rl = new ReentrantLock();
public void run(){
// 加锁
rl.lock();
// TODO 加锁的代码部分
// 释放锁
rl.unlock();
}
6.5.3 和synchronized的比较
- Lock是显式锁,需要手动的调用去加锁、解锁;synchronized 是隐式锁,执行锁定的代码后自动释放
- Lock锁,Java虚拟机将花费更少的时间来调度线程,性能更好
- Lock锁,只能同步代码块方式的锁;synchronized,有同步代码块、同步方法这两种方式的锁
6.6 Object类中常用方法
方法声明 | 功能介绍 |
---|---|
void wait() | 将线程进入阻塞状态 |
void wait( long timeout ) | 指定参数的时间过去后,接触阻塞状态 |
void notify() | 将进入阻塞状态的线程唤醒 |
void notifyAll() | 用于唤醒所有进入阻塞的线程 |
6.7 生产者/消费者模型
7. 线程池
-
一个客户端就要创建一个新的线程
-
当大量客户端连接服务器,就要不断地销毁和创建线程,这严重影响到了服务器的性能
-
所以引入,线程池这一概念。任务直接提交给整个线程池,而不是直接交给某个线程,线程池在拿到任务
后,它就在内部找有无空闲的线程,再把任务交给内部某个空闲的线程。任务是提交给整个线程
池,一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。
7.1 相关类和方法:
-
从Java5开始提供了线程池的相关类和接口:
- java.util.concurrent.Executors类
- java.util.concurrent.ExecutorService接口。
-
Executors类
-
是个工具类和线程池的工厂类
-
常用方法如下
方法声明 功能介绍 static ExecutorService newCachedThreadPool() 创建一个可根据需要创建新线程的线程池 static ExecutorService newFixedThreadPool( int nThreads ) 创建一个可重用固定线程数的线程池 static ExecutorService newSingleThreadExecutor() 创建一个只有一个线程的线程池
-
) | 创建一个只有一个线程的线程池 |
-
ExecutorService接口
-
是真正的线程池接口,主要实现类是ThreadPoolExecutor
-
常用方法如下
方法声明 功能介绍 void execute(Runnable command) 执行任务和命令 Future submit(Callable task) 执行任务和命令 void shutdown() 启动有序关闭
-