线程简介
什么是线程
现代操作系统调度的最小单元是线程,也叫轻量级进程,在一个进程里可以创建很多是线程,这些线程都有自己的计数器,堆栈和局部变量等属性,并且能够访问共享的内存变量。
之所以我们感觉不到有很多个线程在切换,是因为处理器的高速切换,使得我们觉得就好像是全部线程在一起执行。
我们平常写的一个Java程序,其实就是一个多线程的程序,执行main()方法的就是一个叫作main的线程。下面我们来看一个执行一个main方法,里面都有哪些线程。
/**
* 查看我们运行一个main方法中都涉及到哪些线程
*/
public class Demo1 {
public static void main(String[] args) {
// 获取Java多线程管理 MXBean
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
// 不需要获取同步的monitor 和 synchronizer信息,仅获取线程和线程的堆栈信息
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
// 遍历线程信息,仅打印线程ID 和 线程名称信息
for (ThreadInfo threadInfo : threadInfos) {
System.out.println("["+threadInfo.getThreadId()+"]"+threadInfo.getThreadName());
}
}
}
输出
[6]Monitor Ctrl-Break [5]Attach Listener [4]Signal Dispatcher [3]Finalizer [2]Reference Handler [1]main
由上可见,一个Java程序的运行不仅仅是main() 方法的运行,而是main线程和多个其他线程的同时运行。
总结:
线程 是 进程 划分成的更⼩的运⾏单位。线程和进程最⼤的不同在于基本上各进程是独⽴的,⽽各线程则不⼀定,因为同⼀进程中的线程极有可能会相互影响。线程执⾏开销⼩,但不利于资源的管理和保护;⽽进程正相反。
并发和并行
并发: 同⼀时间段,多个任务都在执⾏ (单位时间内不⼀定同时执⾏);
并⾏: 单位时间内,多个任务同时执⾏。
为什么要使用多线程?
(1) 更多的处理器核心
随着处理器核心数量的越来越多,以及超线程技术的广泛应用,使得计算机更加擅长计算,还有工作效率大大提高。
(2) 更快的响应时间
当一个程序有很多业务的时候,例如就拿购物来说,一笔订单的创建,往往伴随着很多复杂的业务。这些业务要想让它快速执行,就需要使用到多线程技术,将这些数据一致性不强的操作委派给多个线程去处理(或者也可以使用消息队列),这样能够很好的缩短生成订单的时间,即响应时间,从而提高了用户的消费体验。
(3) 更好的编程模型
线程的优先级
在Java线程中,通过一个整型的成员变量priority来控制优先级,范围为1-10,可以使用==setPriority(int)==方法来修改优先级,一般默认的优先级都是5。
一般情况下,针对频繁堵塞(休眠或者IO操作)的线程需要设置较高的优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程设置较低的优先级,避免处理器被独占。
注意:在不同的JVM以及操作系统上,线程规划会有差异。
线程的状态(重点)
6种不同的状态
状态名称 | 说明 |
---|---|
NEW | 初始状态,线程被构建,但是还没有调用start()方法 |
RUNNABLE | 运行状态,Java线程将操作系统中的就绪和运行两种状态统称为“运行中” |
BLOCKED | 阻塞状态,表示线程阻塞于锁 |
WAITING | 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定的动作(通知或者中断) |
TIME_WAITING | 超时等待状态,该状态不同于WAITING,它是可以在指定的时间内自行返回 |
TERMINATED | 终止状态,表示当前线程已经执行完毕 |
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dw2ofNgY-1611741665609)(F:\文件\我的笔记\学习JUC.assets\image-20201228161121288.png)]
Daemon线程
所谓后台(daemon)线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这个线程并不属于程序中不可或缺的部分。因此,当所有的非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程。反过来说,只要有任何非后台线程还在运行,程序就不会终止。必须在线程启动之前调用setDaemon()方法,才能把它设置为后台线程。注意:后台进程在不执行 finally子句的情况下就会终止其 run()方法。
比如:JVM 的垃圾回收线程就是 Daemon 线程,Finalizer 也是守护线程。
创建线程
继承Thread类
继承Thread类创建线程的步骤为:
(1)创建一个类继承Thread类,重写run()方法,将所要完成的任务代码写进run()方法中;
(2)创建Thread类的子类的对象;
(3)调用该对象的start()方法,该start()方法表示先开启线程,然后调用run()方法;
public class ExtendsThread {
public static void main(String[] args) {
Thread.currentThread().setName("主线程");
System.out.println(Thread.currentThread().getName()+":"+"输出的结果!");
//创建一个新线程
Demo1 demo1 = new Demo1();
//设置名称
demo1.setName("demo1");
//开启线程
demo1.start();
}
}
class Demo1 extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":"+"输出的结果!");
super.run();
}
}
实现Runnable接口
实现Runnable接口创建线程的步骤为:
(1)创建一个类并实现Runnable接口
(2)重写run()方法,将所要完成的任务代码写进run()方法中
(3)创建实现Runnable接口的类的对象,将该对象当做Thread类的构造方法中的参数传进去
(4)使用Thread类的构造方法创建一个对象,并调用start()方法即可运行该线程
public class Thread2 {
public static void main(String[] args) {
Thread.currentThread().setName("主线程");
System.out.println(Thread.currentThread().getName()+":"+"输出的结果");
//创建一个新线程
Thread thread2 = new Thread(new ThreadDemo2());
//为线程设置名称
thread2.setName("线程二");
//开启线程
thread2.start();
}
}
class ThreadDemo2 implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":"+"输出的结果");
}
}
实现Callable接口
实现Callable接口创建线程的步骤为:
(1)创建一个类并实现Callable接口
(2)重写call()方法,将所要完成的任务的代码写进call()方法中,需要注意的是call()方法有返回值,并且可以抛出异常
(3)如果想要获取运行该线程后的返回值,需要创建Future接口的实现类的对象,即FutureTask类的对象,调用该对象的get()方法可获取call()方法的返回值
(4)使用Thread类的有参构造器创建对象,将FutureTask类的对象当做参数传进去,然后调用start()方法开启并运行该线程。
public class Thread3 {
public static void main(String[] args) throws Exception {
Thread.currentThread().setName("主线程");
System.out.println(Thread.currentThread().getName()+":"+"输出的结果");
//创建FutureTask的对象
FutureTask<String> task = new FutureTask<String>((Callable<String>) new ThreadDemo3());
//创建Thread类的对象
Thread thread3 = new Thread(task);
thread3.setName("线程三");
//开启线程
thread3.start();
//获取call()方法的返回值,即线程运行结束后的返回值
String result = task.get();
System.out.println(result);
}
}
class ThreadDemo3 implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println(Thread.currentThread().getName()+":"+"输出的结果");
return Thread.currentThread().getName()+":"+"返回的结果";
}
}
使用线程池创建
使用线程池创建线程的步骤:
(1)使用Executors类中的newFixedThreadPool(int num)方法创建一个线程数量为num的线程池
(2)调用线程池中的execute()方法执行由实现Runnable接口创建的线程;调用submit()方法执行由实现Callable接口创建的线程
(3)调用线程池中的shutdown()方法关闭线程池
public class Thread4 {
public static void main(String[] args) throws Exception {
Thread.currentThread().setName("主线程");
System.out.println(Thread.currentThread().getName()+":"+"输出的结果");
//通过线程池工厂创建线程数量为2的线程池
ExecutorService service = Executors.newFixedThreadPool(2);
//执行线程,execute()适用于实现Runnable接口创建的线程
service.execute(new ThreadDemo4());
service.execute(new ThreadDemo6());
service.execute(new ThreadDemo7());
//submit()适用于实现Callable接口创建的线程
Future<String> task = service.submit(new ThreadDemo5());
//获取call()方法的返回值
String result = task.get();
System.out.println(result);
//关闭线程池
service.shutdown();
}
}
//实现Runnable接口
class ThreadDemo4 implements Runnable{
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+"输出的结果");
}
}
//实现Callable接口
class ThreadDemo5 implements Callable<String> {
@Override
public String call() throws Exception {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+"输出的结果");
return Thread.currentThread().getName()+":"+"返回的结果";
}
}
//实现Runnable接口
class ThreadDemo6 implements Runnable{
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+"输出的结果");
}
}
//实现Runnable接口
class ThreadDemo7 implements Runnable{
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+"输出的结果");
}
}
线程池
ThreadPoolExecutor
,7 大参数,原理,四种拒绝策略,四个变型:Fixed,Single,Cached,Scheduled
什么是线程池
为了避免系统频繁的创建和销毁线程,我们可以让创建的线程复用
在线程池中,总又那么几个活跃的线程,当你需要使用线程时,可以从池子中随便拿一个空闲线程,当完成工作时,并不急着关闭线程,而是将这个线程退回到线程池中,方便其他人使用。
简而言之,在使用线程池后,创建线程变成了从线程池获得空闲线程,关闭线程变成了向线程池归还线程。
使用的好处
降低资源消耗。通过重复利⽤已创建的线程降低线程创建和销毁造成的消耗。
提⾼响应速度。当任务到达时,任务可以不需要的等到线程创建就能⽴即执⾏。
提⾼线程的可管理性。线程是稀缺资源,如果⽆限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使⽤线程池可以进⾏统⼀的分配,调优和监控。
七大参数
public ThreadPoolExecutor(int corePoolSize, //核心线程大小
int maximumPoolSize, //线程池的最大线程数
long keepAliveTime, //当线程数大于核心线程数时,多余的空闲线程存活的最长时间
TimeUnit unit, //时间单位
BlockingQueue<Runnable> workQueue, //任务队列,用来储存等待执行任务的队列
ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
RejectedExecutionHandler handler) //拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
ThreadPoolExecutor
3个重要的参数
-
corePoolSize
: 核心线程数定义了最小可以同时运行的线程数量 -
maximumPoolSize
: 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 -
workQueue
: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
ThreadPoolExecutor
其他常见参数:
keepAliveTime
:当线程池中的线程数量大于corePoolSize
的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime
才会被回收销毁;unit
:keepAliveTime
参数的时间单位。threadFactory
:executor 创建新线程的时候会用到。handler
:拒绝策略。
原理
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。他的主要特点为:线程复用;控制最大并发数;管理线程。
四种拒绝策略
线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。
ThreadPoolExecutor
拒绝策略定义:
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolTaskExecutor
定义一些策略:
ThreadPoolExecutor.AbortPolicy
:抛出RejectedExecutionException
来拒绝新任务的处理。ThreadPoolExecutor.CallerRunsPolicy
:调用执行自己的线程运行任务,也就是直接在调用execute
方法的线程中运行(run
)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。ThreadPoolExecutor.DiscardPolicy
: 不处理新任务,直接丢弃掉。ThreadPoolExecutor.DiscardOldestPolicy
: 此策略将丢弃最早的未处理的任务请求。
以上内置拒绝策略均实现了 RejectedExecutionHandler 接口,若以上策略仍无法满足实际需要,完全可以自己扩展 RejectedExecutionHandler 接口。
Java线程池工作过程
-
线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
-
当调用 execute() 方法添加一个任务时,线程池会做如下判断:
a) 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
c) 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
d) 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常 RejectExecutionException。
-
当一个线程完成任务时,它会从队列中取下一个任务来执行。
-
当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
四种线程池
Java通过Executors
提供四种线程池,分别为:
newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
synchronized 和 ReentrantLock
synchronized
synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
1、修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
public class Demo1 {
public void func1(){
synchronized(this){
for (int i = 0; i < 10; i++) {
System.out.println(i+" ");
}
}
}
public static void main(String[] args) {
Demo1 e1 = new Demo1();
ExecutorService executorService = Executors.newCachedThreadPool();
// 同一个对象调用func1
executorService.execute(()->e1.func1());
executorService.execute(()->e1.func1());
}
}
//执行结果
0 1 2 3 4 5 6 7