声明:
- 该资料来自自己整理。
- 参考/摘录 书籍
疯狂 Java讲义(第五版) 李刚©著
,仅用作学习,非盈利!!
一、概述
1.1 并发和并行
- 并发是指一个处理器同时处理多个任务。
- 并行是指多个处理器或者是多核的处理器同时处理多个不同的任务。
- 并发是逻辑上的同时发生(simultaneous),而并行是物理上的同时发生。
【注意】:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。
1.2 线程和进程
进程
:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。线程
:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有2个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程
1.3 线程调度
-
分时调度
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。 -
抢占式调度
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
二、线程创建和启动
Java使用java.lang.Thread
类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。
2.1 继承 Thread 类创建线程
自定义多线程类:
/**
* 创建线程类的步骤:
*/
//1. 定义Thread类的子类
public class Multithreading extends Thread{
//定义指定线程名称的构造方法
public Multithreading(String threadName){
//调用父类的String参数的构造器,指定线程的名称
super(threadName);
}
//2. 重写Thread类中的run方法(该方法的方法体就代表了线程需要完成的任务)
//run() 方法也称为线程执行体
@Override
public void run() {
//获取线程名称
System.out.println(getName());
for (int i = 0; i < 10; i++) {
System.out.println("run()-->"+i);
}
}
}
多线程测试类:
public class MultithreadingTest {
public static void main(String[] args) {
//3. 创建Thread子类的实例,即创建了线程对象
Multithreading st = new Multithreading("A thread");
//4. 调用线程对象的start()方法来启动该线程
st.start();
//同一个Thread不能重复调用start方法,会报错
//st.start(); //java.lang.IllegalThreadStateException
//main线程
for (int i = 0; i < 10; i++) {
System.out.println("main-->"+i);
}
}
}
2.2 实现 Runnable 接口创建线程类
使用匿名内部类方式创建线程:
public class AnonymousInnerClassThread {
/**
* 匿名内部类作用:简化代码
* 1.把子类继承父类,重写父类方法,创建子类对象合一步完成
* 2.把实现接口,重写接口中的方法,创建接口实现类对象合成一步完成
* 匿名内部类的最终产物:子类/接口实现类对象,而这个类没有名字。
*
* 格式:
* new 父类/接口(){
* 重写父类/接口中的方法
* };
*/
public static void main(String[] args) {
//1.继承Thread类的匿名内部类
new Thread("A线程"){
//重写run方法,设置线程任务
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+"-->"+i);
}
}
}.start();//链式编程
//2.实现Runnable接口的匿名内部类
Runnable r = new Runnable(){
//重写run方法,设置线程任务
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
};
new Thread(r,"B线程").start();
//简化Runnable接口的匿名内部类
new Thread(new Runnable(){
//重写run方法,设置线程任务
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
},"C线程").start();//链式编程
}
}
2.3 使用 Callable 和 Future创建线程
import java.util.concurrent.Callable;
/**
* 实现 Callable 接口创建线程
* @author chubo
* @since 2022/9/16 11:50
*/
public class CallableThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName()+" 线程执行call()方法");
return 11*11;
}
}
import org.junit.jupiter.api.Test;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @author chubo
* @since 2022/9/16 11:54
*/
public class CallableThreadTest {
//创建 Callable接口实现类示例对象
CallableThread callableThread = new CallableThread();
//使用 FutureTask 来包装 Callable 实现类示例对象
//方式一
FutureTask<Integer> task1 = new FutureTask<Integer>(callableThread);
//方式二
// (Callable<Integer>) () :强制类型转换
FutureTask<Integer> task2 = new FutureTask<Integer>((Callable<Integer>) () -> {
System.out.println(Thread.currentThread().getName()+" 线程执行call()方法");
return 10*10;
});
@Test
void executeCallable() throws ExecutionException, InterruptedException {
new Thread(task1,"task1").start();
System.out.println("task1 线程执行的返回值:"+task1.get());
new Thread(task2,"task2").start();
System.out.println("task2 线程执行的返回值:"+task2.get());
}
}
三、线程生命周期
3.1 线程状态转换
3.1.1 新建状态、就绪状态
当程序使用 new关键字 创建了一个线程之后,该线程就处于新建状态,此时它和其他的Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。
备注:
- 只能兑处于新建状态的线程调用
start()
方法,否则将会引发IllegalThreadStateException
异常。- 处于就绪状态的线程什么时候运行取决于JVM里线程调度器的调度。
- 启动线程使用
start()
方法,而不是run()
方法。调用start()方法来启动线程,系统会把该run()方法当成线程执行体来处理;但如果调用线程对象的run()方法,则run()方法立即就会被执行,而且在run()方法返回之前其他线程无法并发执行。
3.1.2 运行和阻塞状态
3.1.3 线程死亡
四、控制线程
4.1 join线程(线程合并)
线程的合并就是:线程A在运行期间,可以调用线程B的join()
方法,这样线程A就必须等待线程B执行完毕后,才能继续执行。
join()方法有如下三种重载形式:
- join() :等待被join的线程执行完成,才可以执行调用该线程的线程。
- join(long millis) :等待被join 的线程的时间长为millis 毫秒。如果在millis 毫秒内被join的线程还没有执行结束,则不再等待。
- join(long millis,int nanos) :等待被join 的线程的时间最长为millis 毫秒加 nanos 毫微秒。
public class JoinThread extends Thread{
//提供一个有参构造器,用于设置线程名称
public JoinThread(String name) {
super(name);
}
//run方法为线程的执行体
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+" "+i);
}
}
public static void main(String[] args) throws InterruptedException {
//启动子线程
new JoinThread("A线程").start();
for (int i = 0; i < 10; i++) {
if (i == 6){
JoinThread jt = new JoinThread("Join线程");
jt.start();
//main线程调用了jt线程的join()方法,main线程必须等jt线程执行结束才会向下执行!!!
jt.join();//当执行jt线程的join()方法时,main线程处于阻塞状态
}
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
4.2 后台线程
后台线程(Daemon Thread)。JVM的垃圾回收线程就是典型的后台线程。
后台线程有个特征:如果所有的前台线程都死亡,后台线程会自动死亡。
调用Thread对象的 setDaemon(true)
方法可将指定线程设置成后台线程。
public class DaemonThread extends Thread{
//定义后台线程的线程执行体与普通线程没有任何区别
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println(getName()+" "+i);
}
}
public static void main(String[] args) {
DaemonThread dt = new DaemonThread();
//将此线程设置成后台线程
dt.setDaemon(true);
//启动后台线程
dt.start();
//判断dt线程是否是后台线程
System.out.println(dt.isDaemon());//true
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
/**
* --------程序执行到此处,前台线程(main线程)结束-------
* 后台线程也应该随之结束
*
* dt线程本应该执行到999,但是前台线程先执行完,
* 前台线程执行完后,后台线程也跟着结束执行,所以到不了999
*/
}
}
注意:
setDaemon(true)
必须在start()
方法之前调用,否则会引发IllegalThreadStateException
异常。
4.3 线程睡眠(sleep)
import java.util.Date;
public class SleepThread {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
System.out.println("当前时间:"+new Date());
//调用sleep()方法让当前线程暂停1s
Thread.sleep(1000);
}
}
}
4.4 改变线程优先级
package com.ccbx.monitoringsys.thread;
import org.junit.jupiter.api.Test;
/**
* 线程优先级测试
* @author chubo
* @since 2022/10/17 10:10
*/
public class ThreadPriorityTest {
public class PriorityTest extends Thread{
public PriorityTest(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName()+",优先级是:"+getPriority()+",i-->"+i);
}
}
}
@Test
void changePriority(){
//改变主线程的优先级
Thread.currentThread().setPriority(6);
for (int i = 0; i < 30; i++) {
if (i == 10){
PriorityTest lowPt = new PriorityTest("lowPt");
lowPt.start();
//lowPt创建之初的优先级:6
System.out.println(lowPt.getName()+"创建之初的优先级:"+lowPt.getPriority());
//设置该线程为最低优先级
lowPt.setPriority(Thread.MIN_PRIORITY);
}
if (i == 20){
PriorityTest highPt = new PriorityTest("highPt");
highPt.start();
System.out.println(highPt.getName()+"创建之初的优先级:"+highPt.getPriority());
//设置该线程的优先级
highPt.setPriority(Thread.MAX_PRIORITY);
}
}
}
}
备注:改变优先级只是执行的概率变大/变小,并不一定先/后执行。
五、线程同步
5.1 取钱案例
账户类
@Data
public class Account {
private String accountNo;
private double balance;
}
取钱类
public class DrawThread extends Thread{
//模拟用户账户
private Account account;
//当前线程所希望取的钱数
private double drawAmount;
public DrawThread(String name, Account account, double drawAmount){
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
//当多个线程修改一个共享数据时,将涉及数据安全问题
//run()方法不具有同步安全性!!
@Override
public void run() {
//账户余额大于取钱数目
if (account.getBalance() >= drawAmount){
//吐出钞票
System.out.println(getName()+",取钱 "+drawAmount+" 成功!");
//修改金额
account.setBalance(account.getBalance() - drawAmount);
System.out.println("\t余额为:"+account.getBalance());
}else{
System.out.println(getName()+" 取钱失败!余额不足!");
}
}
}
测试类
public class DrawTest {
public static void main(String[] args) {
//创建一个账户
Account account = new Account("123456789",1000);
//模拟两个线程对同一个账户取钱
new DrawThread("甲",account,800).start();
new DrawThread("乙",account,800).start();
}
}
5.2 同步代码块
//synchronized 美 [ˈsɪŋkrənaɪzd] 已同步
//同步代码块的语法格式
synchronized(obj){ //obj 为同步监视器
...
//此处就是同步代码块
}
同步监视器的目的:
阻止两个或多个线程对同一个共享资源进行并发访问,通常推荐使用可能被并发访问的共享资源充当同步监视器。
同步监视器可以理解为就是一把锁,锁住了别的线程就进不去了,直到该线程释放掉这个锁(释放锁是指持锁线程退出了synchronized同步代码块)。
同步监视器注意事项:
1、同步监视器必须是引用数据类型,不能是基本数据类型。
2、多个线程必须共用同一把锁
3、线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。
4、任何时刻只有一个线程可以获得同步监视器的锁定,当同步代码块执行完成后该线程就会释放对同步监视器的锁定。
修改存取钱案例:
public class DrawThread extends Thread{
//模拟用户账户
private Account account;
//当前线程所希望取的钱数
private double drawAmount;
public DrawThread(String name, Account account, double drawAmount){
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
//当多个线程修改一个共享数据时,将涉及数据安全问题
@Override
public void run() {
/**
* 同步监视器的目的:
* 阻止两个或多个线程对同一个共享资源进行并发访问,
* 通常推荐使用可能被并发访问的共享资源充当同步监视器。
*
* 使用account作为同步监视器,任何线程进入下面同步代码块之前
* 必须先获得对account账户的锁定----其它线程无法获得锁,也就无法修改它
* 这种做法符合:”加锁-->修改-->释放锁“ 的逻辑
*/
synchronized (account){ //account对象作为同步监视器
//账户余额大于取钱数目
if (account.getBalance() >= drawAmount){
//吐出钞票
System.out.println(getName()+",取钱 "+drawAmount+" 成功!");
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//修改金额
account.setBalance(account.getBalance() - drawAmount);
System.out.println("\t余额为:"+account.getBalance());
}else{
System.out.println(getName()+" 取钱失败!余额不足!");
}
}
}
}
5.3 同步方法
不可变类总是线程安全的,因为它的对象状态不可改变;但可变对象需要额外的方法来保证其线程安全。
使用同步方法的方式实现取钱案例:
//将前面的Account类设置为线程安全的类
public class AccountSafe {
private String accountNo;
private double balance;
public AccountSafe(){ }
public AccountSafe(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
public String getAccountNo() {
return accountNo;
}
//balance get和set方法
···
//提供一个线程安全的draw()方法来完成取钱操作
public synchronized void draw(double drawAmount){
//账户余额大于取钱数目
if (drawAmount <= balance){
//吐出钞票
System.out.println(Thread.currentThread().getName()+",取钱 "+drawAmount+" 成功!");
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//修改金额
balance -= drawAmount;
System.out.println("\t余额为:"+balance);
}else{
System.out.println(Thread.currentThread().getName()+" 取钱失败!余额不足!");
}
}
//根据accountNo来重写hashCode()和equals()方法
···
}
public class DrawThreadSafe extends Thread{
//模拟用户账户
private AccountSafe account;
//当前线程所希望取的钱数
private double drawAmount;
public DrawThreadSafe(String name, AccountSafe account, double drawAmount){
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
//当多个线程修改一个共享数据时,将涉及数据安全问题
@Override
public void run() {
/**
* 直接调用account对象的draw()方法执行取钱操作
* 同步方法的同步监视器是this,this代表调用draw()方法的对象
* 也就是说,线程进入draw()方法之前,必须先对account对象加锁
*/
account.draw(drawAmount);
}
}
public class DrawTest {
public static void main(String[] args) {
System.out.println("---------Account线程安全类----------------");
//创建一个账户
AccountSafe accountSafe = new AccountSafe("67890",1000);
//模拟两个线程对同一个账户取钱
new DrawThreadSafe("甲",accountSafe,800).start();
new DrawThreadSafe("乙",accountSafe,800).start();
}
}
备注:可变类的线程安全是以降低程序的运行效率作为代价的,为了减少线程安全带来的负面影响,程序可以采用如下策略:
5.4 释放同步监视器的锁定
5.5 同步锁
reentrant 美 [ˌriˈɛntrənt] 可重入的
class X{
//定义锁对象
private final RenntrantLock lock = new ReentrantLock();
//..
//定义需要保证线程安全的方法
public void m(){
//加锁
lock.lock();
try{
//需要保证线程安全的代码
//... method body
}
//使用finally块来保证释放锁
finally{
lock.unlock();
}
}
}
使用 ReentrantLock 对象来进行同步,加锁和释放锁出现在不同的作用范围内时,通常建议使用finally 块来确保在必要时释放锁。
5.6 死锁
如下案例:
class A{
public synchronized void foo(B b){
System.out.println("当前线程名:"+Thread.currentThread().getName()+" 进入了A实例的foo()方法");//1
try{
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前线程名:"+Thread.currentThread().getName()+" 企图调用B实例的last()方法");//3
b.last();//此时当前线程(主线程)还没有释放同步监视器
}
public synchronized void last(){
System.out.println("进入了A类的last()方法内部");
}
}
class B{
public synchronized void bar(A a){
System.out.println("当前线程名:"+Thread.currentThread().getName()+" 进入了B实例的bar()方法");//2
try{
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前线程名:"+Thread.currentThread().getName()+" 企图调用A实例的last()方法");//4
a.last();//此时当前线程(副线程)还没有释放同步监视器;a对象(也是a同步监视器)此时访问a的同步last方法,但是访问同步方法前先要对a加锁,但是a同步监视器还没解锁。
//此时就出现了,副线程保持着b的锁,等待主线程对a加锁;主线程保持着a的锁,等待副线程对a加锁;出现死锁!!
}
public synchronized void last() {
System.out.println("进入了B类的last()方法内部");
}
}
public class DeadLock implements Runnable {
A a = new A();
B b = new B();
public void init(){
Thread.currentThread().setName("主线程");
//调用a对象的foo()方法
a.foo(b);
System.out.println("进入了主线程之后");
}
@Override
public void run() {
Thread.currentThread().setName("副线程");
//调用b对象的bar()方法
b.bar(a);
System.out.println("进入了副线程之后");
}
public static void main(String[] args) {
DeadLock deadLock = new DeadLock();
//以deadLock启动新线程
new Thread(deadLock).start();
//调用init()方法
deadLock.init();
}
}
六、线程通信
6.1 传统线程通信
借助 Objects
类提供的 wait()
、notify()
、notifyAll()
三个方法。这三个方法必须由同步监视器对象来调用。
/**
* 实现存钱取钱交替进行(不能连续存两次,也不能连续取两次)
*/
public class AccountWaitNotify {
private String accountNo;
private double balance;
//标识账户中是否已有存款的开关,false表示未存款
private boolean flag = false;
public AccountWaitNotify(){ }
public AccountWaitNotify(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
public String getAccountNo() {
return accountNo;
}
public double getBalance() {
return balance;
}
//设置取钱的同步方法
public synchronized void draw(double drawAmount){
try{
//如果falg为假,表明账户中还没有人存钱进去,取钱方法阻塞
if (!flag){
wait();
}else{
//执行取钱操作
System.out.println(Thread.currentThread().getName()+" 取钱:"+drawAmount);
balance -= drawAmount;
System.out.println("账户余额为:"+balance);
//将标识符设置为已存款
flag = false;
//唤醒此同步监视器上的其它线程
notifyAll();
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
//设置存钱的同步方法
public synchronized void deposit(double depositAmount){
try {
//如果flag为真,表明账户已有人存钱进去,存钱方法阻塞
if (flag){
wait();
}else{
//执行存款操作
System.out.println(Thread.currentThread().getName()+" 存款:"+depositAmount);
balance += depositAmount;
System.out.println("账户余额:"+balance);
//改变状态为已存钱
flag = true;
//唤醒此同步监视器上的其它线程
notifyAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
取钱线程
public class WaitNotifyDrawThread extends Thread{
private AccountWaitNotify account;
private double drawAmount;
public WaitNotifyDrawThread(String name, AccountWaitNotify account, double drawAmount){
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
//重复20次执行取钱操作
@Override
public void run() {
for (int i = 0; i < 20; i++) {
//account对象调用同步方法,则account对象就是同步监视器
account.draw(drawAmount);
}
}
}
存钱线程
public class WaitNotifyDepositeThread extends Thread{
private AccountWaitNotify account;
private double depositeAmount;
public WaitNotifyDepositeThread(String name, AccountWaitNotify account, double depositeAmount){
super(name);
this.account = account;
this.depositeAmount = depositeAmount;
}
//重复20次执行存钱操作
@Override
public void run() {
for (int i = 0; i < 20; i++) {
//account对象调用同步方法,则account对象就是同步监视器
account.deposit(depositeAmount);
}
}
}
public class WaitNotifyThreadTest {
public static void main(String[] args) {
//创建一个账户
AccountWaitNotify acc = new AccountWaitNotify("2020520",0);
new WaitNotifyDrawThread("取钱者",acc,800).start();
new WaitNotifyDepositeThread("存钱者甲",acc,800).start();
new WaitNotifyDepositeThread("存钱者乙",acc,800).start();
new WaitNotifyDepositeThread("存钱者丙",acc,800).start();
}
}
注意:
哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。
总结如下:
- 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态;
6.2 使用 Condition 控制线程通信
在 Condition 对象中,与 wait
、notify
和 notifyAll
方法对应的分别是 await
、signal
和 signalAll
。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class AccountCondition {
//创建锁对象
private final Lock lock = new ReentrantLock();
//获得指定Lock对象的Condition
private final Condition cond = lock.newCondition();
private String accountNo;
private double balance;
//有参、无参构造器
//···
public String getAccountNo() {
return accountNo;
}
public double getBalance() {
return balance;
}
//账户中是否已有存款默认false
private boolean flag = false;
//取钱方法
public void draw(double drawAmount){
//加锁
lock.lock();
try{
if (!flag){
//Condition类的await()方法使当前线程阻塞;
//当前线程释放对此Lock对象的锁定!!!
cond.await();
}else{
//取钱
System.out.println(Thread.currentThread().getName()+" 取钱:"+drawAmount);
balance -= drawAmount;
System.out.println("账户余额:"+balance);
flag = false;
//唤醒当前锁对象上的其它所有阻塞线程
cond.signalAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放锁
lock.unlock();
}
}
//存钱方法
public void deposit(double depositAmount){
//加锁
lock.lock();
try{
if (!flag){
//Condition类的await()方法使当前线程阻塞;
//当前线程释放对此Lock对象的锁定!!!
cond.await();
}else{
//取钱
System.out.println(Thread.currentThread().getName()+" 存钱:"+depositAmount);
balance += depositAmount;
System.out.println("账户余额:"+balance);
flag = true;//变为已有存款
//唤醒当前锁对象上的其它所有阻塞线程
cond.signalAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放锁
lock.unlock();
}
}
//根据accountNo来重写hashCode()和equals()方法
//···
}
显式地使用Lock对象来充当同步监视器时,需要使用Condition对象来暂停(
await()
)、唤醒(signal()
)指定线程。
6.3 使用阻塞队列(BlockingQueue)控制线程通信(待完善)
待完善
6.4 线程组和未处理异常(待完善)
待完善
七、线程池(Executors)
简单理解:线程池就是存放了很多可以复用线程的一个“容器”。可以很大程度上降低创建线程带来的开销。
7.1 使用线程池的好处
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。
7.2 创建线程
Java 5 新增了一个 Executors 工厂类
来创建线程池,通过该类的几个静态工厂方法来创建线程池。
静态方法 | 解释 |
---|---|
newCachedThreadPoll() | 创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将被缓存在线程池中。 |
newFixedThreadPoll(int nThreads) | 创建一个可重用的、具有固定线程数的线程池。 |
newSingleThreadExecutor() | 创建一个只有单线程的线程池,它相当于调用 newFixedThreadPoll(1)。 |
newScheduledThreadPoll(int corePoolSize) | 创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务。corePoolSize指池中所保存的线程数,即使线程是空闲的也被保存在线程池内。 |
newSingleThreadScheduledExecutor() | 创建只有一个线程的线程池,它可以在指定延迟后执行线程任务。 |
newWorkStealingPool(int parallelism) | 创建持有足够的线程的线程池来支持给定的并行级别,该方法还会使用多个队列来减少竞争。 |
newWorkStealingPool() | 如果当前机器有四个cpu,则目标并行级别被设置为4,也就相当于 Executors.newWorkStealingPool(4)。 |
7.2.1 ExecutorService
ExecutorService
代表尽快执行线程的线程池(只要线程池中有空闲线程,就立即执行线程任务),程序只要将一个 Runnable 对象或 Callable 对象(代表线程任务)提交给该线程池,该线程池就会尽快执行该任务。
7.2.2 ScheduledExecutorService
ScheduledExecutorService
代表可在指定延迟后或周期性地执行线程任务的线程池。
7.2.3 线程池执行线程任务步骤(重点)
- 调用 Executors 类的静态工厂方法来创建一个 ExecutorService 对象,该对象代表一个线程池。
- 创建 Runnable 实现类或 Callable 实现类的实例,作为线程执行任务。
- 调用 ExecutorService 对象的
submit()
方法来提交 Runnable 实例或 Callable 实例。 - 当不想提交任何任务时,调用 ExecutorService 对象的
shutdown()
方法来关闭线程池。
案例
@Test
void createPool(){
// 创建一个具有固定线程数的线程池
ExecutorService pool = Executors.newFixedThreadPool(4);
// 创建Runnable接口的示例
Runnable target = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + " 的 i 值为:" + i);
}
}
};
// 向线程池中提交三个线程
pool.submit(target);
pool.submit(target);
pool.submit(target);
// 关闭线程池
pool.shutdown();
}
备注
- 用完一个线程池后,应该调用该线程池的
shutdown()
方法,该方法将会启动线程池的关闭序列,调用shutdown() 方法后的线程池不再接收新任务,但会将以前的所有已提交的任务执行完成。- 当线程池中的所有任务都执行完成后,池中的所有线程都会死亡;另外也可以调用线程池的 shutdownNow()方法来关闭线程池,该方法视图停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列表。
7.2.4 ForkJoinPool(待完善)
待完善
八、线程相关类
8.1 ThreadLocal(待完善)
待完善
用途:隔绝多个线程之间的共相冲突时使用 ThreadLocal。
8.2 线程安全/不安全集合
8.2.1 举例
分类 | 示例 |
---|---|
常见的线程安全 集合 | ConcurrentHashMap 、ConcurrentSkipListMap 、ConcurrentSkipListSet 、ConcurrentLinkedQueue 、ConcurrentLinkedDeque 、CopyOnWriteArrayList 、CopyOnWriteArraySet 、等。 |
常见的线程不安全 集合 | ArrayList 、LinkedList 、HashSet 、TreeSet 、HashMap 、TreeMap 、等。 |
线程不安全集合:当多个并发线程向这些集合中存、取元素时,可能破坏这些集合的数据完整性。
8.2.2 包装线程不安全集合为线程安全集合
使用 Collections
提供的类方法把线程不安全的集合包装成线程安全的集合。
Collections静态方法 | 解析 |
---|---|
<T> Collection<T> synchronizedCollection(Collection<T> c) | 返回指定 collection 对应的线程安全的 collection。 |
<T> List<T> synchronizedList(List<T> list) | 返回指定 List 对象对应的线程安全的 List对象。 |
<K,V> Map<K,V> synchronizedMap(Map<K,V> map) | 返回指定 Map 对象对应的线程安全的 Map对象。 |
<T> Set<T> synchronizedSet(Set<T> set) | 返回指定 Set 对象对应的线程安全的 Set 对象。 |
<K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> map) | 返回指定 SortedMap 对象对应的线程安全的 SortedMap 对象。 |
<T> SortedSet<T> synchronizedSortedSet(SortedSet<T> set) | 返回指定 SortedSet 对象对应的线程安全的 SortedSet 对象。 |
@Test
void wrapCollections(){
// 使用 Collections 的方法将线程不安全的集合包装成线程安全的集合
List<String> synchronizedList = Collections.synchronizedList(new ArrayList<String>());
}
备注:如果想要把某个集合包装成线程安全的集合,则在创建之后应该立即包装。
8.3 发布-订阅框架(待完善JDK9)
待完善JDK9的功能