Chp21-线程
进程的概念
操作系统(OS)中并发(同时)执行的多个程序任务
进程的特点
宏观并行, 微观串行
CPU会将一个时间段划分为若干个时间片, 程序想要执行必须拥有时间片, 且一个时间片只能被一个程序拥有, 时间片之间交替执行, 则程序之间也是如此. 当时间片的划分足够细小,交替频率足够快,就会形成宏观并行
的假象, 实际上仍然是串行
状态
线程的概念
进程中并发执行的多个任务
线程的特点
宏观并行, 微观串行
一个时间片只能被一个程序拥有, 一个程序无法同时执行多个线程, 时间片之间是交替, 线程任务的执行自然也是交替的
多线程的概念
只有多线程,没有多进程
“进程”: 正在进行中的程序, 只有拥有时间片的程序才是进程
“线程”: 不管是否在执行, 只要被创建出来就都是线程
线程的组成
线程执行的基本支持
- 时间片: 由CPU调度分配
- 数据: 每个线程任务都对应一个功能, 数据是功能执行的基本支持
- 代码: 书写数据的操作逻辑+控制线程执行
线程的创建
Thread : 线程类,每个Thread对象都是一个单独的线程任务
-
书写子类继承Thread, 重写run方法书写任务内容, 创建子类对象
public class MyThread extends Thread { public void run() { //输出1-100 for (int i = 1; i <=100; i++) { System.out.println(i); } } }
import com.by.thread.MyThread; public class Test1 { public static void main(String[] args) { //创建线程对象1 Thread t1 = new MyThread(); //创建线程对象2 Thread t2 = new MyThread(); //开启线程 t1.start(); t2.start(); } }
弊端:
- 会将线程类与线程任务内容绑定, 不有利于后续的使用和维护
- 破坏线程类的单一职责
-
直接创建Thread对象, 在构造中传入Runnable线程任务
- 创建Runnable接口的实现类, 重写run()书写任务内容
- 在Thread构造中传入实现类对象
/** * 线程任务的实现类- 适用于任务内容会多次执行时 */ public class MyRunnable implements Runnable{ @Override public void run() { //输出1-100 for (int i = 1; i <=100; i++) { System.out.println("输出-> "+i); } } }
import com.by.dao.impl.MyRunnable; public class Test2 { public static void main(String[] args) { //创建线程对象, 传入任务内容 Thread t1=new Thread(new MyRunnable()); //如果线程任务只会被当前一个线程对象使用, 可以直接使用匿名内部类创建实现类对象 Thread t2 = new Thread(new Runnable() { @Override public void run() { //遍历101-200 for (int i = 101; i <= 200 ; i++) { System.out.println("t2>> "+i); } } }); //lambda简化匿名内部类 Thread t3 = new Thread(()->{ //遍历201-300 for (int i = 201; i <= 300 ; i++) { System.out.println("t3:::: "+i); } }); //开启线程 t1.start(); t2.start(); t3.start(); } }
使用
- 通过
线程对象.start()
开启线程 - 执行: 当开启多个线程后, 线程之间开始争抢时间片, 拿到时间片的线程才能执行自身内容, 当时间片被其他线程抢走,则暂停执行再次争抢时间片, 直至线程任务内容执行结束, 线程才会停止运行
run()
来自Runnable接口, 表示线程任务内容, 当线程拿到时间片后, 会自动执行该方法- 主函数也称为主线程, 是JVM中默认存在的线程, 也一定是第一个拿到时间片的线程
- 当开启多个线程之后, JVM执行结束的标志就会从
主线程执行结束
转换为所有线程执行结束
线程状态
基础状态
线程的生命周期
- 只有就绪状态和运行状态之间可以相互切换
等待状态
-
sleep(): 使当前线程休眠指定时长, 在休眠期间会释放自身时间片, 进入有限期等待状态,直到休眠结束才能回到就绪状态
静态方法: Thread.sleep(毫秒数);//1秒 = 1000毫秒
- 该方法需要处理非运行时异常, run()不支持上抛异常, 必须try-catch处理
- 常用于定时任务
package com.by.test; public class Test3 { public static void main(String[] args) { Thread t1 = new Thread(new Runnable() { @Override public void run() { //使当前线程休眠3秒钟 try { Thread.sleep(100); } catch (InterruptedException e) { System.out.println("休眠异常!"); } for (int i = 1; i <= 100 ; i++) { System.out.println("t1>> "+i); } } }); Thread t2 = new Thread(()->{ for (int i = 101; i <= 200 ; i++) { System.out.println("t2: "+i); } }); //开启线程 t1.start(); t2.start(); } }
-
join(): 使调用者线程早于当前线程执行, 只有当调用者线程执行结束进入死亡状态, 当前线程才能进入就绪状态
线程对象.join()
- 当前线程进入的是无限期等待状态
- 需要处理非运行时异常, run()无法上抛异常, 必须try-catch处理
- 无法影响无关线程
- 常用于调整线程执行优先级
package com.by.test; public class Test4 { public static void main(String[] args) { //t2->t1-->t3 Thread t2 = new Thread(()->{ for (int i = 101; i <= 200 ; i++) { System.out.println("t2: "+i); } }); Thread t1 = new Thread(new Runnable() { @Override public void run() { try { //使t2线程在当前线程之前执行 t2.join(); } catch (InterruptedException e) { System.out.println("join异常!"); } for (int i = 1; i <= 100 ; i++) { System.out.println("t1>> "+i); } } }); Thread t3 = new Thread(()->{ try { t1.join(); } catch (InterruptedException e) { System.out.println("join异常!"); } for (int i = 201; i <= 300 ; i++) { System.out.println("t3> "+i); } }); //开启线程 t1.start(); t2.start(); t3.start(); } }
sleep()和join()的区别:
- sleep进入的是有限期等待状态, join进入的是无限期等待状态
- sleep是静态方法可以直接通过类名调用, join的非静态的, 必须通过线程对象调用
- sleep不会与其他线程产生关联, join一定会与其他线程相互影响
线程池
作用
当线程任务需要多次执行时, 为了防止线程对象的反复创建消耗运行资源, 可以将任务内容写入线程池, 以此确保任务可以反复运行, 直至线程池关闭
API
- ExecutorService: 线程池接口
- submit(任务对象): 提交任务使其执行
- 线程任务执行结束之后不会销毁, 会回到池中等待下次调用
- shutdown(): 关闭线程池
- submit(任务对象): 提交任务使其执行
- Executors: 线程池工具类, 可以用来获取线程池实现类对象
- newCachedThreadPool(): 获取一个不固定并发数量的线程池
- 所有提交执行的任务会同时并发交替运行
- newFixedThreadPool(int ): 获取一个固定并发数量的线程池
- 会设置同时运行的数量上限, 超出运行上限的任务只能暂时等待, 等执行中的任务执行结束让位之后才能进行执行
- newCachedThreadPool(): 获取一个不固定并发数量的线程池
线程任务
Runnable
-
run(): 无返回值, 不能上抛异常
package com.by.test; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Test_ES { public static void main(String[] args) { //获取线程池对象- 不固定并发数量 // ExecutorService es = Executors.newCachedThreadPool(); //固定并发数量的线程池- 最大同时执行数量为2 ExecutorService es = Executors.newFixedThreadPool(2); //创建线程任务 Runnable r1=new Runnable() { @Override public void run() { for (int i = 1; i <= 100 ; i++) { System.out.println("r1:: "+i); } } }; Runnable r2=new Runnable() { @Override public void run() { for (int i = 101; i <= 200 ; i++) { System.out.println("r2> "+i); } } }; Runnable r3=new Runnable() { @Override public void run() { for (int i = 201; i <= 300 ; i++) { System.out.println("r3>>>> "+i); } } }; //提交任务执行 es.submit(r1); es.submit(r2); es.submit(r3); //关闭线程池 es.shutdown(); } }
Callable<返回值泛型>
-
call(): 有返回值, 可以上抛异常,默认上抛Exception
-
Future<返回值泛型>: 用来接收存放Callable执行结果
- Future对象.get(): 获取内部存放的返回值
- 该方法需要处理非运行时异常
package com.by.test; import java.util.concurrent.*; public class Test_ES2 { public static void main(String[] args){ //获取线程池对象- 不固定并发数量 ExecutorService es = Executors.newCachedThreadPool(); //创建线程任务-计算1-100的和并返回 Callable<Integer> c1=new Callable<Integer>() { @Override public Integer call() throws Exception { int sum = 0; for (int i = 0; i <=100; i++) { sum += i; } return sum; } }; //提交执行 Future<Integer> future =es.submit(c1); //从future中获取返回值 try { System.out.println(future.get()); } catch (Exception e) { System.out.println("数据异常!"); e.printStackTrace(); } //关闭线程池 es.shutdown(); } }
- Future对象.get(): 获取内部存放的返回值
线程安全问题
当多个线程同时操作同一个临界资源时, 有可能破坏其原子操作, 从而导致数据缺失, 引发线程安全问题
- 临界资源: 被多个线程同时访问操作的对象
- 原子操作: 访问临界资源过程中不可缺失或更改的操作
互斥锁
每个对象都默认拥有的锁. 特点为只有一个锁标记, 且只能被一个线程拥有. 当开启互斥锁之后, 线程想要执行, 则必须同时拥有时间片和锁标记, 拥有资源的线程在执行过程中其他线程不可争抢其资源, 必须保证其原子操作执行同步. 只有当执行线程运行结束释放锁标记之后其他线程才能继续争抢.
Synchronized
作用为开启互斥锁, 使内部原子操作执行同步
同步方法
原理: 在临界资源被访问的方法上加锁
特点: 线程必须同时争抢时间片和锁标记
修饰符 synchronized 返回值类型 方法名(形参列表){
}
package com.by.util;
import java.util.ArrayList;
import java.util.List;
/**
* 工具类- 辅助操作List集合
*/
public class MyList {
private List<Integer> list = new ArrayList<>();
/**
* 往集合属性中添加元素
* @param n 被添加的数据
* synchronized: 使该方法成为同步方法, 内部内容的执行不可被破坏
*/
public synchronized void insert(int n){
list.add(n);
}
//查看集合元素及长度
public void query(){
System.out.println("集合长度为:" + list.size());
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i)+" ");
}
}
}
同步代码块
原理: 由线程上锁
特点: 上锁线程默认拥有锁标记, 所以线程执行只需要争抢时间片即可
位置: 线程访问临界资源的位置上锁
使用: 必须确保所有访问临界资源的线程都具备加锁操作
synchronized(被加锁的临界资源对象){
原子操作
}
package com.by.test;
import com.by.util.MyList;
public class Test1 {
public static void main(String[] args)throws Exception {
//创建一个工具类对象
MyList m = new MyList();
//线程1: 往工具类中的集合里添加1-5
Thread t1 = new Thread(()->{
//同步代码块
synchronized (m) {
for (int i = 1; i <= 5; i++) {
m.insert(i);
}
}
});
//线程2: 往工具类中的集合里添加6-10
Thread t2 = new Thread(()->{
//同步代码块
synchronized (m) {
for (int i = 6; i <= 10; i++) {
m.insert(i);
}
}
});
//开启线程
t1.start();
t2.start();
//使t1和t2在主线程查看之前执行
t1.join();
t2.join();
//查看工具类中集合的内容
m.query();
}
}
区别:
同步方法: 在被访问的方法上加锁, 线程需要同时争抢时间片和锁标记, 效率慢但是书写简单
同步代码块: 在线程访问临界资源的位置加锁, 线程只需要争抢时间片, 拥有时间片的线程默认拥有锁标记, 执行效率快但是书写繁琐
线程安全的集合类
悲观锁: 悲观的认为集合一定会出现线程安全问题, 所以在底层数组位统一加锁
乐观锁: 乐观的认为集合不会出现线程安全问题, 所以底层并加锁, 当安全问题发生时, 再通过算法解决问题
- JDK5.0
java.util.concurrent
-
CopyOnWriteArrayList
- 原理: 在进行写(增删改)操作时, 先复制出一个副本, 在副本中完成操作, 若副本执行中出现问题, 则舍弃该副本, 重新复制新的副本重复操作, 直至副本中无异常, 再将集合地址转换向副本地址
- 特点: 舍弃写的效率, 提高读的效率. 适用于读操作远多于写操作时
-
CopyOnWriteArraySet
- 原理: 和CopyOnWriteArrayList一致, 在此基础上会对元素进行去重
- 特点: 和CopyOnWriteArrayList一致
-
ConcurrentHashMap
- 原理: CAS算法
CAS: compare and swap 比较并交换