Java技能树之“多线程”
一. 线程池概述
1. 什么是线程池
线程池和和字符串常量池, 数据库连接池一样, 都是为了提高程序的运行效率, 减少开销; 随着并发程度的提高, 当我们去频繁的创建和销毁线程, 此时程序的开销还是挺大的, 为了进一步提高效率, 就引入了线程池, 程序中所创建的线程都会加载到一个 “池子” 中, 当程序需要使用线程的时候, 可以直接从池里面获取, 用完了就将线程还给池, 这样在多线程的环境中就不用去重复的创建和销毁线程, 从而使程序的运行效率提高, 线程池是管理线程的方式之一.
🎯那为什么从线程池中“拿”线程会比直接创建线程要更加高效呢?
这是因为创建线程和销毁线程, 是交由操作系统内核完成的, 而我们使用线程池调度线程是在用户态实现的(用户代码中就能实现的,不必交给内核操作);
如果将任务交给内核态, 就需要通过系统调用, 让内核来执行任务, 但此时你不清楚内核身上背负着多少任务(内核不是只给一个应用程序服务, 是要给所有的程序都提供服务), 当使用系统调用, 执行内核代码的时候, 无法确定内核都要做哪些工作, 整体过程"不可控"的;
相比于内核来说, 用户态, 程序执行的行为是可控的, 用户态只去完成你所指定的任务, 效率更高, 开销更小.
2. Java标准库提供的线程池
Java中提供了线程池相关的标准类ThreadPoolExecutor, 也被称作多线程执行器, 该类中的线程包括两类, 一类是核心线程, 另一类是非核心线程, 当核心线程都被占用还不能满足程序任务执行的需求时, 就会启用非核心线程, 直到任务量少了, 随之非核心线程也就会销毁.
jdk8中提供了4个构造方法, 这里主要介绍和理解参数最多的那一个构造方法, 其他构造方法只是基于这里的减少了参数而已.
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
在Java标准库中提供了4个拒绝策略, 如下:
Modifier and Type Class and Description
static class ThreadPoolExecutor.AbortPolicy 如果任务太多, 队列满了, 直接抛出异常RejectedExecutionException .
static class ThreadPoolExecutor.CallerRunsPolicy 如果任务太多, 队列满了, 多出来的任务, 谁加的, 谁负责执行.
static class ThreadPoolExecutor.DiscardOldestPolicy 如果任务太多, 队列满了, 丢弃最旧的未处理的任务.
static class ThreadPoolExecutor.DiscardPolicy 如果任务太多, 队列满了, 丢弃多出来的任务
多线程的实现方式(一)
-
继承Thread类
1、自定义一个类MyThread类,用来继承与Thread类
2、在MyThread类中重写run()方法
3、在测试类中创建MyThread类的对象
4、启动线程
- 代码如下:
/** * @author 小贺. * @Description */ public class Demo01 { public static void main(String[] args) { //创建线程 MyThread t01 = new MyThread(); MyThread t02 = new MyThread(); MyThread t03 = new MyThread("线程03"); //开启线程 // t01.run(); // t02.run(); // t03.run(); // 不会启动线程,不会分配新的分支栈。(这种方式就是单线程。) // start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。 // 这段代码的任务只是为了开启一个新的栈空间,只要新的栈空间开出来,start()方法就结束了。线程就启动成功了。 // 启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)。 // run方法在分支栈的栈底部,main方法在主栈的栈底部。run和main是平级的。 t01.start(); t02.start(); t03.start(); //设置线程名(补救的设置线程名的方式) t01.setName("线程01"); t02.setName("线程02"); //设置主线程名称 Thread.currentThread().setName("主线程"); for (int i = 0; i < 50; i++) { //Thread.currentThread() 获取当前正在执行线程的对象 System.out.println(Thread.currentThread().getName() + ":" + i); } } } class MyThread extends Thread{ public MyThread() { } public MyThread(String name) { super(name); } //run方法是每个线程运行过程中都必须执行的方法 @Override public void run() { for (int i = 0; i < 50; i++) { System.out.println(this.getName() + ":" + i); } } }
此处最重要的为start()方法。单纯调用run()方法不会启动线程,不会分配新的分支栈。
start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。线程就启动成功了。
启动成功的线程会自动调用run方法(由JVM线程调度机制来运作的),并且run方法在分支栈的栈底部(压栈)。
run方法在分支栈的栈底部,main方法在主栈的栈底部。run和main是平级的。
单纯使用run()方法是不能多线程并发的。
2.1 - 数据安全问题
是否具备多线程的环境
是否有共享数据
是否有多条语句操作共享数据
例如:我和小明同时取一个账户的钱,我取钱后数据还没返回给服务器,小明又取了,这个时候小明的余额还是原来的。
如何解决?线程排队执行(不能并发),线程同步机制。
2.1.1 -变量对线程安全的影响
实例变量:在堆中。
静态变量:在方法区。
局部变量:在栈中。
以上三大变量中:
局部变量永远都不会存在线程安全问题。
因为局部变量不共享。(一个线程一个栈。)
局部变量在栈中。所以局部变量永远都不会共享。
实例变量在堆中,堆只有1个。
静态变量在方法区中,方法区只有1个。
堆和方法区都是多线程共享的,所以可能存在线程安全问题。
局部变量+常量:不会有线程安全问题。
成员变量:可能会有线程安全问题。
- 点赞
- 收藏
- 关注作者
评论(0)