本文详尽的介绍了线程的相关知识,从概念到创建线程和其基本使用,又介绍了线程安全的相关知识,其中包含线程同步的四种实现方式与线程休眠的不同方式与其区别,最后介绍了线程优化包括线程优化、线程任务优化、锁优化等相关知识!
文章目录
- 一、线程基础
- 1.进程
- 2.线程
- (1)概念
- (2)多线程
- (3)线程的组成
- 3.线程的使用
- (1)创建
- (2)线程的常用方法
- (3)守护线程与前台线程
- (4)线程的状态
- 二、线程安全
- (1)概念
- (2)线程安全处理办法(加锁与死锁)`重点:`
- (3)线程的休眠与唤醒(sleep与wait)
- (4)多个线程共享同一个对象锁的四种实现方式
- 三、线程优化
- (1)线程优化原因及解决方案
- 原因
- 解决方案:线程池
- (2)线程池的体系结构
- (3)Executors工具类
- 常用方法:创建线程池
- 重点原理刨析: `ThreadPoolExecutor`
- 四、线程任务优化
- (1)线程任务优化的原因及解决方式
- (2)Callable与Thread结合使用
- (3)Callable与Thread结合使用
- 五、锁优化
一、线程基础
1.进程
概念: 一个正在进行的程序
- 注意:一个程序在准备运行时,系统会为其开辟一片内存空间(运行内存)
2.线程
(1)概念
- 概念: 一个执行路径
- 注意:
1,一个进程自带一个线程,该线程被称为主线程(main)
2, 一个进程可以有多个线程,一个进程中如果有多个线程,则称此进程为多线程
(2)多线程
- 概念: 一个进程中有多线程称为多线程
- 注意:
1,多个线程在宏观上是同步执行
2,多个线程在微观上是在抢夺CPU执行权
(3)线程的组成
- CPU时间片
多个线程在抢夺CPU执行权,当某个线程获取到CPU执行权后可以执行的时间- 运行数据
1,每个线程都有其独立的栈内存
2,多个线程共享一个堆内存- 代码逻辑
程序的执行顺序
3.线程的使用
(1)创建
Thread本质上是实现了Runnable接口的类,通常采用方案2创建线程任务(Runnable)
,然后在创建线程时将其作为参数传递给线程
方案1:创建Tread的子类对象,并重写run方法
- 方式1:子类继承于Thread,并重写run方法,然后再使用的地方创建子类对象
步骤:
1, 创建一个类
2, 使用步骤1中的类继承于Thread
3, 在该类中重写run方法(开发工具不会提示重写run()方法)
4, 在使用该线程的地方创建该子类的对象
步骤1的类名 对象名 = new 步骤1的类名();- 方式2:使用匿名内部类的形式创建Thread的子类对象
步骤:Thread 对象名 = new Thread(){ public void run(){ } }; 注意:因为Thread不是抽象类,所以开发工具不会提示需要写{}与run方法
方案2:将线程任务(Runnable)与线程(Thread)分别创建,并在创建Thread对象时传入Runnable的对象
- 方式1:
1, 创建一个类使其实现Runnable接口
2, 在该类中重写run方法
3, 在使用线程的地方,创建步骤1的类的对象
步骤1的类名 对象名 = new 步骤1的类名();
注意: 此时创建的是 线程任务对象
4, 创建Thread对象,并且在实参中传入步骤3创建的对象
Thread 对象名 = new Thread(步骤3的对象名);
注意: 此时创建的对象才是线程对象- 方式2:
1,使用匿名内部类创建Runnable的子类对象 Runnable 对象名 = new Runnable(){ public void run(){ } }; 2,创建Thread对象,并且在实参中传入步骤3创建的对象 Thread 对象名 = new Thread(步骤1的对象名); 或 Thread 对象名 = new Thread(new Runnable(){ public void run(){ } });
(2)线程的常用方法
(2.1)启动
- 语法: 线程对象.start();
- 注意:
1,此线程启动并加入到进程中与进程中的其他线程一起抢夺CPU的执行权力
2,线程对象.run() 只是主线程调用子线程对象的方法,并没有开启子线程(未与主线程竞争CPU)而 线程对象.statrt()开启了子线程,会与主线程竞争cpu
(2.2)关闭
- 线程一旦被启动,无法控制 (对线程的操作必须在线程的启动之前,启动后再操作线程名字等,会在在运行时抛出异常)
- 线程执行完run方法中的代码后,将进入到等待销毁状态(线程会分多个CPU时间片去执行完run()方法中的代码——
顺序执行
)
(2.3)名称
设置:
- 方式1 ,创建线程对象时设置
new Thread(线程名称);
new Thread(线程任务,线程名称);- 方式2, 使用线程对象调用setName方法
- 语法:
线程对象.setName(线程名称);注意:在线程启动前设置
获取:
- 语法:
String 变量名 = 线程对象.getName();
(2.4)获取当前正在执行的对象
Thread thread = Thread.currentThread();
(2.5)优先级
语法:
线程对象.setPriority(优先级);
注意:
- 优先级取值范围:1~10
- 会增大或减小线程抢占到CPU的概率,并不一定会真正的抢占到CPU(在大数据范围下,概率就是结果)
(2.6)sleep休眠
语法:
- Thread.sleep(休眠时间);
或- 线程对象.sleep(休眠时间);
注意:
- 单位是毫秒
- 有异常,需要解决
- 线程在休眠时不会抢夺CPU执行权(不会释放锁对象)
- 此时Thread代指正在运行的线程
(2.7)合并
语法:
- 线程对象.join();
注意:
- 在线程A的run方法中使用线程对象B调用该方法,表示将B对应的线程中的run方法中没有执行完的代码合并到线程A中(两条路径归并为一条)
(3)守护线程与前台线程
前台线程:
- 线程创建时默认就是前台线程
- 如果一个进程中有前台线程存活,那么该进程将不会被系统所回收
守护线程: (别名:后台线程)
- 如果一个进程中所有的前台线程都被销毁了,进程也将被销毁.此时如果进程中还有守护线程未执行完,那么该守护线程依旧会被销毁
如何设置一个线程为守护线程:
- 语法: 线程对象.setDaemon(true);
- 注意: 必须在线程启动前
(4)线程的状态
二、线程安全
(1)概念
- 引起线程安全的原因?
多个线程同时操作一个数据时,会导致数据不安全
- 同步
一个数据或一段代码 只能 同时被一条线程操作或执行
- 异步
一个数据或代码 可以 同时被多条线程操作或执行
- 锁对象
注意: 使用不同的同步方法,锁对象要求是不同的(同步代码块---可以自己指定,同步方法---此方法的调用者,同步静态方法---该类的类对象)
,其必须是若干条冲突线程共有的
(2)线程安全处理办法(加锁与死锁)
重点:
思路:保证同时只能有一条线程在操作数据,并且锁对象是同一个
- 使用同步要注意什么?
1,分析需要加同步的地方
2,保证锁对象是同一个对象
3,避免死锁- 产生死锁的原因?
多个线程互相持有对方所需的锁资源(锁对象)
特殊情况: 某个类创建的对象是线程安全的,其内部有同步方法,避免死锁时需要注意(eg:StringBuffer中的方法都是用synchronized修饰的同步方法)
方案1:同步代码块
语法:
synchronized (锁对象) {
要同步的代码
}
注意:
1,锁对象:任何一个类的对象都可以作为锁对象
2,同步代码块的锁对象是由我们自己指定的
方案2:同步方法
锁对象是调用该同步方法的对象:
1,可以是外部传进来的对象(此对象含有同步方法,在run方法中使用此对象调用自身的同步方法,则:此外部传进的对象就是锁对象)---- 若保证多个线程在 不同线程任务内 操作同一个外部对象,即可以达到线程同步(多个线程锁对象相同--外部对象)
2,可以是某个线程任务对象(线程任务自身含有同步方法,在run方法中调用自身的同步方法,则:此线程任务对象就是锁对象)----- 若保证多个线程任务操作同一个线程任务对象,即可达到线程同步(多个线程锁对象相同--线程任务对象)
语法:
访问权限修饰符 synchronized 返回值类型 方法名(形参列表){
方法体
return 返回值;
}
注意:
1,锁对象:锁对象是调用该方法的对象
方案3:同步静态方法
语法:
访问权限修饰符 static synchronized 返回值类型 方法名(形参列表){
方法体
return 返回值;
}
注意:
1,锁对象:该类的类对象(类对象只有一个)
(3)线程的休眠与唤醒(sleep与wait)
1.休眠: 让线程进入休眠状态,不在抢夺CPU执行权
- 有限期休眠:sleep,wait
- 无限期休眠:wait
2.sleep与wait的区别
- 相同点:
都可以让线程
进行休眠
- 不同点:
- 注意: 在同一个程序中,两个线程若未使用同一个锁对象,则在一个线程内的同步代码块中无法使用另一个线程的锁唤醒或休眠另一个线程(此处说的是wait与notify,
wait与notify只能由其所处的同步代码块或同步方法中使用
)
3.唤醒 让线程从休眠状态转换为就绪状态,准备抢夺CPU执行权(针对wait()方法而言)
- 对象.notify(): 随机唤醒一个使用该对象作为锁对象的线程
- 对象.notifyAll(): 唤醒所有使用该对象作为锁对象的线程
注意: 唤醒notify是Object提供的方法,需要对象名.notify调用(由同步方法或同步代码块的锁对象调用)
(4)多个线程共享同一个对象锁的四种实现方式
具体案例可参考我另一篇博客:线程同步—— 生产者与消费者、龟兔赛跑、双线程打印
方案一:
- 创建一个线程任务,传给四个线程对象(线程任务对象只有一个,对象的成员变量
(对象锁)
也只有一个)
方案二:
- 使用static修饰需要共享的成员变量(此静态变量
(对象锁)
属于该类创建的所有对象)
方案三:
- 创建一个工具类,里面含有静态成员变量(使用类名.变量名
(对象锁)
调用)
方案四:
- 使用构造函数(类似于接口回调:为线程任务提供一个有参构造函数,在创建线程任务时传入,可以保证多个线程任务操作同一个传进的外部对象
(对象锁)
)
三、线程优化
原因
- 一个线程在存活时大约占1MB的运行内存,线程在使用完成后,需要等待GC回收,此时因为大量的没有被回收的线程占据着内存,会导致程序运行效率降低
解决方案:线程池
- 简介:
一个用于管理线程的容器,包含线程的创建,复用与回收- 优点:
1,方便使用:使用线程池以后不用在考虑线程的创建,复用,回收等问题.
2,可以设定线程的上限,避免频繁的创建与销毁线程
Executor(接口)
- 方法:void execute(Runnable command);执行任务
- 子接口: ExecutorService(接口)
- 方法:
- void shutdown();关闭线程池
- Future> submit(Runnable task);给线程池提交要执行的任务
- 子类与子接口
- ThreadPoolExecutor(子类)
注意:该类创建时需要传入多个参数,导致创建难度较大- ScheduledExecutorService(子接口)
- ScheduledThreadPoolExecutor:调度线程池(子类:继承和实现了ThreadPoolExecutor、ScheduledExecutorService)
作用: 可以让你简单的获取到线程池对象
常用方法:创建线程池
—1.固定线程池
作用:创建一个固定长度的线程池(掌握)
public static ExecutorService newFixedThreadPool(int nThreads)
参数:线程池中的线程数量
注意:
1,使用固定线程池时关闭线程池时,
固定线程池会等线程池里面的所有线程执行完线程任务时才会关闭
2,增加线程池的线程任务时,在shutdown之前加入线程任务(之后加会报错)
—2.可变线程池(缓冲线程池)
作用:创建一个缓存线程池,该线程池中的线程数量由任务数量决定(掌握)
public static ExecutorService newCachedThreadPool()
注意:
该线程池中闲置的线程会在60秒后被回收,
如果闲置线程在60秒内,有任务需要执行,就直接使用该线程
—3.固定线程池
作用:获取一个线程数量为1的线程池(掌握)
public static ExecutorService newSingleThreadExecutor()
—4.抢占线程池(了解)
作用:获取一个抢占线程池
方法:
static ExecutorService newWorkStealingPool()
特点:
1,jdk1.8出现
2,该线程池中有一个算法,叫做窃取算法.
该算法的优点将任务平分下去,当该线程中一个线程执行完自己的任务后,
会帮助还没有完成任务的线程.为了提高线程任务的执行效率
3,该线程池的线程都是守护线程,
所以当进程中所有前台都被销毁后,该线程池中的所有线程都会被销毁,
该线程池也会被销毁
4,该线程池如果被关闭,也需等待其中任务执行完毕后才会关闭线程池.
前提时在该线程执行线程任务时,有别的前台线程存活
—5.调度线程池
作用:获取一个调度线程池
方法:
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
特点:(类似于闹钟)
1,属于ExecutorService的子接口
2,拥有特有方法,调度
特有方法:
1,schedule:延迟多长时间后在执行任务
eg:schedule(runnable01, 10, TimeUnit.SECONDS);
2,scheduleAtFixedRate:延迟多长时间后在执行任务,间隔多长后重复执行
间隔时间:前一次任务开始时间至本次任务开始时间
注意:
如果代码执行时间超过间隔时间,那么下次任务执行将会在上一次任务执行完毕后,直接执行
eg:service.scheduleAtFixedRate(runnable01, 5, 1, TimeUnit.SECONDS);
3,scheduleWithFixedDelay:延迟多长时间后在执行任务,间隔多长后重复执行
间隔时间:前一次任务结束时间至本次任务开始时间
eg:service.scheduleWithFixedDelay(runnable03, 5, 1, TimeUnit.SECONDS);
—6.单例调度线程池
作用:获取一个只有一个线程的调度线程池
方法:
static ScheduledExecutorService newSingleThreadScheduledExecutor()
重点原理刨析:
ThreadPoolExecutor
概念:可变线程池,固定线程,单例线程池获取到的线程池对象,本质就是该类的对象
因为在阿里白皮书中说过,为了深入理解线程池,建议所有开发人员使用ThreadPoolExecutor创建线程池对象
构造函数:
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
1参corePoolSize:核心线程数量,线程池中最小线程数量
2参maximumPoolSize:线程池中最大线程数量
3参keepAliveTime:当非核心线程闲置多长时间后会被系统回收
4参unit:时间单位
5参workQueue:存储线程池执行的线程任务的集合
6参threadFactory:线程工厂,在线程池中创建线程
7参handler:当线程池中线程任务多于线程时的策略
四、线程任务优化
(1)线程任务优化的原因及解决方式
原因:
- 因为Runnable在执行完线程任务后,无法返回数据,所以对线程任务进行优化
解决方式:
- 使用Callable执行线程任务,可以通过一定的方式返回数据,所以可以使用Callable实现对线程任务的优化,实现执行完线程任务后数据的返回
(2)Callable与Thread结合使用
步骤:
- 创建Callable接口的子类对象(线程任务)
- 创建FutureTask对象,并传入Callable的子类对象(构造函数,FutureTask是Runnable的子类,并在创建时可以传入Callable对象)
- 创建线程对象,并传入FutureTask对象(构造函数)
- 启动线程
- 使用FutureTask对象调用get()方法,获取Callable中的call方法的返回值
注意:
- get()方法会阻塞程序向下执行,阻塞到Callable中call方法执行完毕,拿到返回之后才会进行运行程序的下一步
- FUtureTask是Runnable的子类对象(可以上转型),可以包裹Callable对象(Thread没有提供接受Callable的构造函数,所以需要FutureTask包裹Callable传入Thread的构造函数中)
通过FutureTask对象调用get方法获取Callable执行结果时,要保证其在线程执行之后,不然程序会挂起,不会出现运行结果
示例:
public static void main(String[] args) {
//1.创建Callable线程任务对象
Callable<Integer> callable=new Callable<Integer>() {
@Override
public Integer call() throws Exception {
// TODO Auto-generated method stub
System.out.println("Callable中的call方法开始执行!");
int sum=0;
for(int i=0;i<=50;i++) {
sum+=i;
//阻塞5s
Thread.sleep(100);
}
System.out.println("Callable中的call方法执行结束!");
return sum;
}
};
//2.使用FutureTask将Callable对象包裹
FutureTask<Integer> futureTask=new FutureTask<Integer>(callable);
//3.将包裹后的线程任务对象传递给线程
Thread thread=new Thread(futureTask);
thread.start();
//4.获取结果
int result=0;
try {
result=futureTask.get();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("1-50的加法和为:"+result);
}
(3)Callable与Thread结合使用
步骤:
- 创建线程池对象
- 创建Callable线程任务对象
- 将任务提交给线程池,并
获取到Future的返回对象
(线程池对象submit()重载了接收Callable的方法
)- 关闭线程池
- 通过Future对象的get()方法获取到线程任务的返回结果
注意:
- FutureTask是Future的子类对象
- 不用关闭线程池也可以通过Future对象的get()方法获取到线程任务的返回结果(
建议关闭线程池后再获取到返回结果
)
示例:
public static void main(String[] args) {
//1.获取到线程池
ExecutorService service = Executors.newFixedThreadPool(2);
//2.创建Callable线程任务对象
Callable< Integer> callable=new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum=0;
for (int i = 1; i <= 100; i++) {
sum+=i;
}
return sum;
}
};
//3,将任务提交给线程池
Future<Integer> future = service.submit(callable);
//4.获取到返回对象
int result=0;
try {
result = future.get();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//关闭线程池
service.shutdown();
System.out.println("结果为:"+result);
}
五、锁优化
原因: 因为在使用synchronized时,发现不是很方使用,所以对其进行优化
体系
1. Lock(锁 — 接口)
- 方法:
- lock:关闭锁
- unlock:释放锁
- 子类:
- ReentrantLock:重入锁
- 概念: 重入锁也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
2.ReadWriteLock(读写锁 — 接口)
- 方法:
- readLock:提供一个Lock对象(读的锁对象,其返回值实现Lock接口)
- writeLock:提供一个Lock对象(写的锁对象,其返回值实现Lock接口)
- 子类:
- ReentrantReadWriteLock
- 读写锁特点:
- 读的锁对象与读的锁对象
互不干扰
- 写的锁对象与写的锁对象
互斥
- 读的锁对象与写的锁对象
互斥
- 读锁使用共享模式;写锁使用独占模式;读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的。当有读锁时,写锁就不能获得;而当有写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获得读锁