线程
作者:CSDN 双鱼座的老王
目录
1.4 分析一个问题:对于单核的CPU来说,真的可以做到真正的多线程并发吗?
1.多线程
1.1 什么是进程?
进程是一个应用程序。(一个软件)
线程是一个进程的执行场景/执行单元。
一个进程可以启动多个线程
进程:
1.2 对于java程序来说,当在DOS命令窗口中输入:
java HelloWorld 回车之后
会先启动JVM,而JVM就是一个进程。
JVM再启动一个主线程调用main方法,同时再启动一个垃圾回收线程负责看护,回收垃圾,那么此时,java程序中,至少有两个线程并发,一个为垃圾回收线程,一个为执行main方法的主线程
1.3 进程和线程的关系?
举个例子,淘宝是阿里巴巴旗下的产品
那么如果阿里巴巴是进程,那么阿里巴巴的员工就为线程,马云是阿里巴巴的线程,前台也是一个线程
如果京东是一个进程,那么刘强东就是京东的一个线程,奶茶妹妹就是京东的一个线程。
进程可以看作是现实生活中的公司。线程可以看作是公司当中的某个员工
注意:进程A和进程B的内从独立不共享(阿里巴巴和京东不会共享资源!!)
but!同一进程下的线程A和线程B,在Java中,线程A和线程B在堆内存和方法区内存共享。但是栈是独立的。
lol是一个进程,网易云音乐是一个进程,他们不共享资源,但是,你在打lol的时候一边打游戏一边电竞钢琴家,那么他们是共享资源的,假如启动10个线程,会有10个栈空间,每个栈和每个栈之间互不干扰,各自执行各自的,这就是多线程并发。
Java中之所以有多线程机制,目的就是为了提高程序的处理效率
1.4 分析一个问题:对于单核的CPU来说,真的可以做到真正的多线程并发吗?
什么是真正的多线程并发?
T1线程执行T1,T2线程执行T2,互不干扰,叫做真正的多线程并发。
对于单核的CPU来说,他只有一个大脑,所以,单核CPU无法做到真正的多线程并发,但是可以到给人一种多线程并发的感觉。(执行的快,例如胶卷电影,超过60帧就觉得很流畅)
1.5 java语言中,实现线程的两种方式
java支持多线程机制,并且java已经将多线程实现了,我们只需要继承即可
第一种方式:
编写一个类,直接继承java.lang.Thread 然后重写run方法
//实现线程的第一种方式:编写一个类,直接继承java.lang.Thread 然后重写run方法
public class xiancheng {
public static void main(String[] args) {
//怎么创建线程对象?怎么启动线程?
//这里是main方法,这里的代码属于主线程,在主栈中运行
//在这里要新建一个分支对象
MyThread myThread = new MyThread();
//启动线程 myThread.start();
//start的作用是,在JVM中开辟一个新的栈空间,这段代码在瞬间就结束了,
//这段代码的任务只是为了开空间,开出来就结束,线程就启动成功了
//启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部,main在主栈的栈底部,双方平级
myThread.start();
//下面的代码还在主线程中运行
for (int i = 0; i < 1000; i++) {
System.out.println("mian线程--->"+i);
}
}
}
class MyThread extends Thread{
@Override
public void run() {
//编写程序,这段程序运行在分支栈中
for (int i = 0; i < 1000; i++) {
System.out.println("线程分支--->"+i);
}
}
}
run方法的内存图如下:
第二种方式:
编写一个类,实现java.lang.Runnable接口,实现run方法
//实现多线程的第二种方式,编写一个实现类继承Runnable接口
public class dierzhong {
public static void main(String[] args) {
//先创建线程类,此时还并不是一个线程
MyRunnable myRunnable = new MyRunnable();
//封装为线程类
Thread thread = new Thread(myRunnable);
//启动线程
thread.start();
for (int i = 0; i < 1000; i++) {
System.out.println("mian线程--->"+i);
}
}
}
//这并不是一个线程类
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("线程分支--->"+i);
}
}
}
那么问题来了,我们平时更喜欢用哪种方法呢?
答案是接口类多,原因在于,java是单继承,如果我使用了继承方法,那么该类只能继承这个线程类,所以我们推荐接口,不耽误这个方法继承别的类。
由此延申,我们可以试着采用匿名内部类。
public class niming {
public static void main(String[] args) {
//创建线程对象,采用匿名内部类方式
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("线程分支--->"+i);
}
}
});
thread.start();
for (int i = 0; i < 1000; i++) {
System.out.println("mian线程--->"+i);
}
}
}
1.6 线程的生命周期
1.7 获取线程的名字
1.怎么获取当前线程的对象?
这玩意以后要用!!
什么是当前线程对象?在java.lang包下有一个Thread类中有一个currentThread方法
static Thread.currentThread();
//返回值t就是当前的线程
Thread t = Thread.currentThread();
//这段代码出现在哪,得到的就是哪个线程。
2.怎么获取线程对象的名字?
线程对象.getName();
3.修改线程对象的名字?
线程对象.setName("修改后的名字");
注意,每个线程都有默认的线程名字Thread-x ,当线程没有设置名字的时候,默认名称为Thread-第X个生成的线程。
1.8 关于线程的sleep方法
关于sleep方法:
static void sleep( long millis)
1.静态方法: Thread.sleep(1000)
2.参数是毫秒
3.作用:让当前线程进入休眠,进入"阻塞状态",放弃占有CPU的时间片,让给其他线程使用,出现在谁那里,就睡谁。
4.sleep 可以做到间隔特定的时间,去执行一段特定的代码,每隔XX执行一次。
关于sleep的注意点:‘
这段代码的MyThread线程会进入休眠吗
答案:不会
解读:t.sleep的作用是让当前进程进入睡眠,这行代码出现在main方法中,所以,HelloWorld会在五秒钟之后输出。
1.9 终止线程的睡眠。
听不懂?简单来说,就是sleep睡得太久了,你怎么叫醒?换句话说就是,你怎么把他从睡眠中途叫醒
如图所示,这个线程被我睡了一年。如何叫醒?
注意:此方法不是终端线程的执行,是终端线程的睡眠(sleep方法)
解决方案如图所示,请详细的读一下备注
t.interrupt();
//干扰,报异常,进入睡眠的异常catch,依靠异常处理机制。
结果如图,主方法唤醒的代码如下:
运行结果如下:
当然,如果你不喜欢"见红",可以把打印异常的那行注释掉(我就喜欢这么干)
1.10 强行终止线程的方法
在java中,如何强行终止一个线程的执行?
首先,我们说一种过时的方法
线程.stop()
去java输入这个就可以看见这个效果
它可以干掉这个线程,But!
他有一个致命的缺点,他的工作类型有点像小时候玩游戏卡了,然后任务管理器直接结束进程一样,所以,如果你这个终止线程,容易丢失线程未保存的数据。
那么怎么正确并且合理的终止线程的执行?
我们可以在线程类写一个boolean值,如图所示,那么,如果我需要终止这个线程,只需要更改他的run属性为false即可:
运行结果如下
当你的线程有什么没有保存的时候,可以在else的return之前写好就ok了
1.11 线程的调度概况
关于整个调度情况,了解即可,但是我觉得如果你会了,你对线程的学习会很轻松
1.1 常见的线程调度模型有那些?(了解)
抢占式调度模型:
哪个线程的优先级比较高,抢到的CPU的时间片的概率就高一些。而Java采用的就是抢占式调度模型。
均分式调度模型:
平均分配CPU时间片,每个线程占有的CPU时间片长度一样,平均分配,一切平等,java未采用这种调度模式,而有一些编程语言采用了平等的分配模式。
1.2 java中提供了哪些方法是和线程调度有关呢?
1.1优先级方法
这是一个实例方法:
void setPriority(int newPriority) //设置线程的优先级
最低优先级是1,默认优先级是5,最高优先级是10
But!他不是百分百优先,而是所谓抢夺时间片的概率大了,并不完全一定是他抢到(这点巨坑)
1.2 让位方法
static void yield() //yield 方法不是阻塞方法,而是让当前线程让位,从运行状态变为就绪抢夺时间片的状态.
你退半步的动作认真的吗?~
👆就这个意思,注意,他回到就绪状态之后,可不是百分之百能抢夺到时间片哦。但是,有可能让为了还抢到了,那么在你的视觉效果上,等于白给,还不如不让。
1.3 合并方法
插队线程。打饭没被插过队吗?
注意,不是把两个栈合并了,而是出现了某个栈等待的关系。
public class join {
public static void main(String[] args) {
System.out.println("main begin");
Thread thread = new Thread(new ceshi());
thread.setName("t");
thread.start();
try {
thread.join();
//thread合并到当前线程中,当线程受到阻塞,t线程执行到结束。
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main end");
}
}
class ceshi implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "执行了"+i);
}
}
}
执行结果:main end一定最后执行
2.多线程的线程安全问题
关于多线程并发环境下,数据的安全问题(重点!!)
2.1 为什么安全是重点?
以后在开发中,我们的项目都是运行在服务器当中,而服务器已经将线程定义了,线程对象的创建,启动都实现了,这些代码不用我们编写。
所以重要的是,我们编写的程序需要放多多线程的环境下运行,你更需要关注的是这些运行的环境是否安全。
2.2 什么是线程不安全?
小时候想过,假如两个人拿着一模一样(想象)的银行卡去两个ATM同时取钱。网络延时一下会不会取两次钱赚银行一笔?
答案一定是不能的。(年少无知),所以,如果银行的线程是不安全的,我此时就不会在这里写文档了。咔咔咔就是一顿刷卡卡银行的BUG,然后估计就蹲号去了。
现在想一想,小日子又有判头了。
那么总结一下,什么时候数据在多线程并发的环境下会存在安全问题?
三个条件:
1.多线程并发。
2.有共享数据。
3.共享数据有修改行为。
2.3 怎么解决安全问题?
当多线程并发的情况下,有共享数据,并且这个数据还涉及到修改,此时就存在线程安全问题。
那么怎么解决这个问题呢?
线程排队执行(不能并发),用排队执行解决线程安全问题,这种机制成为线程同步机制。专业术语叫:线程同步。
线程同步就是让线程去排队了,线程排队,就会牺牲效率。但是,数据安全是第一位,只有数据安全了,才能去谈效率。
2.4 两个专业术语
1.异步编程模型:
线程1和线程2,各自执行各自的,谁也不用等谁,互不干扰,这种编程模型就叫异步编程模型。其实就是多线程并发(效率较高)
2.同步变成模型
线程1和线程2,线程1执行的时候,线程2像执行必须等到线程1结束,两个线程之间发生了等待关系,这就是同步变成模型,效率较低,线程排队执行。
2.5 线程不安全的模拟演示
接下来我们编写程序模拟一下。
package threadsafe;
/**
银行账户
**/
public class Account {
private String actnode;
private double balance;
public Account(String actnode, double balance) {
this.actnode = actnode;
this.balance = balance;
}
public Account() {
}
public String getActnode() {
return actnode;
}
public void setActnode(String actnode) {
this.actnode = actnode;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
//取款的方法。
//先不考虑够不够的问题。
public void withdraw(double money){
//t1和t2并发执行这个方法(t1,t2是两个栈),两个栈造作堆中的同一个对象
//取款之前的余额
double before = this.getBalance();
//取款后的余额
double after = before-money;
//刷新余额
//假如:t1执行到这里,还没来得及刷新,t2访问了这个对象,此时一定出现线程安全问题
this.setBalance(after);
}
}
有可能这个方法不出问题,但是,你不能保证此次不出问题,关键是看t1线程能不能在时间片内执行完这个方法并且刷新余额。
参观一下,总共设置的1W,每个方法执行完之后,一人拿了5000,而且还剩下5000
这就是线程安全问题。如果在run方法里面搞个sleep模拟网络延迟,那么百分之百出问题。
我们来尝试一下用线程同步机制解决线程安全问题。
//改一改就ok,我们把取钱的方法拿过来
public void withdraw(double money){
//synchronized 线程同步代码块,后面的小括号的数据是相当关键的。这个数据必须是共享的关键数据,才能达到多线程排队
//()中写什么?
/*
* 要看你想让哪些线程同步,假设你有t1,t2,t3,t4,t5五个线程
* 你只希望 t1,t2,t3排队,t4,t5不排队
* 你就一定要在()写入123共享的对象,而对于t4,t5不共享
* 本次例子共享的对象是账户对象。
* 那么this就是账户对象
* 当然 不一定是this,这次是碰巧
* */
synchronized (this){
double before = this.getBalance();
double after = before-money;
this.setBalance(after);
}
}
这样我们再去执行,就开始排队
2.6 对于synchronized的理解
你可以理解为,去洗手间锁门,这种情况下不能并发,得锁门,而synchronized,就是锁。
在java语言中,任何一个对象都有一把锁,这把锁就是标记,上面的代码执行原理就是,假设两个线程并发,开始执行代码的时候,一个先一个后,t1先执行遇到了synchronized,这个时候会自动找后面共享对象的对象所,找到之后占有这个锁然后执行synchronized中的代码块,结束之后才会释放,而此时t2也遇到了synchronized关键字,也会去后面尝试占有共享对象的锁,此时发现t1占用了,必须等到t1结束归还这个锁。才能进入
这样就达到了线程排队执行。不过,需要注意的是,共享对象一定要是需要排队执行的这些线程对象锁共享的。
我们此时就可以更新线程生命周期的图片了。
2.7总结:synchronized有三种写法:
第一种:同步代码块,灵活
synchronized(线程共享对象){
同步代码块;
}
第二种:在实例方法尚使用synchronized
表示共享对象一定是this,并且同步代码块是整个方法体。
第三种:在静态方法上使用synchronized
表示找类锁,类锁永远只有一把。
3 锁
3.1 死锁现象
指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程
//给大家表演个死锁
public class DeadLock {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
Thread t1=new MyThread1(o1,o2);
Thread t2=new MyThread2(o1,o2);
t1.start();
t2.start();
}
}
class MyThread1 extends Thread{
Object o1;
Object o2;
public MyThread1(Object o1,Object o2){
this.o1 = o1;
this.o2 = o2;
}
@Override
public void run() {
synchronized (o1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){}
}
}
}
class MyThread2 extends Thread{
Object o1;
Object o2;
public MyThread2(Object o1,Object o2){
this.o1 = o1;
this.o2 = o2;
}
@Override
public void run() {
synchronized (o2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){}
}
}
}
之前面试的时候有面试官问我会不会写死锁,给我整蒙了,人家问的都是怎么规避死锁,而他偏偏让我写出来,记忆犹新
3.2 聊一聊,开发中如何解决线程安全问题
是一上来就选择线程同步吗? (synchronized)不是,synchronized会让程序的执行效率降低,用户体验不好。系统的用户吞吐量降低。用户体验差。在不得已的情况下再选择线程同步机制
第一种方案:尽量使用局部变量代替"实例变量和静态变量"。
第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。(一个线程对应1个对象,100个线程对应100个对象,对象不共享,就没有数据安全问题了)
第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized了。线程同步机制。
4.守护线程
java中有两种线程,一种是用户线程,例如main方法,一种是守护线程。例如垃圾回收线程就是守护线程。
守护线程的特点:
一般情况下,守护线程是个死循环,所有的用户线程只要结束,守护线程自动结束。
守护线程有什么用呢?
例如,每天00:00的时候系统数据自动备份
用法:
当main执行结束后,守护循环自动结束,死循环也不好使
5.定时器
定时器的作用:
间隔特定的时间,执行特定的程序。例如,每周要进行银行账户的总账操作,每天要进行数据的备份操作。在实际开发中,每隔多久执行一段特定的程序是很常见的
第一,我们可以用sleep的方法,但是比较low
所以我们可以利用java.util.Timer,不过也很少用了,目前很多高级框架都是支持定时任务的。例如SpringTask框架
由此可得,这个Timer是一个后台线程。
6.实现线程的第三种方式
实现Callable接口
之前的两种方式是无法获得现成的返回值的。因为run方法返回void,而Callable是可以拿到返回值内容的。
实现线程的第三种方式:实现Callable接口这种方式的优点:可以获取到线程的执行结果。这种方式的缺点:效率比较低,在获取t线程执行结果的时候,当前线程受阻塞,效率较低。
public class Thread0001 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask futureTask = new FutureTask(new Callable() {
@Override
public Object call() throws Exception {
return 10;
}
});
Thread t = new Thread(futureTask);
t.start();
Object o = futureTask.get();
System.out.println(o);
}
}
7.关于Object类中的wait和notify方法
第一: wait和notify方法不是线程对象的方法,是java中任何一个java对象都有的方法,因为这两个方式是object类中自带的。wait方法和notify方法不是通过线程对象调用,等待方法和通知方法不是通过线程对象调用
第二: wait()方法作用?
Object o = new Object () ;
o.wait( );
表示:
让正在o对象上活动的线程进入等待状态,无期限等待,直到被唤醒为止-
o.wait( );方法的调用,会让"当前线程(正在o对象上活动的线程)"进入等待状态。
第三: notify ()方法作用?
Object o = new Object () ;
o.notify () ;
表示:唤醒正在o对象上等待的线程。还有一个notifyAll()方法:这个方法是唤醒o对象上处于等待的所有线程。
8.生产者和消费者模式!
一张图解释生产者和消费者的关系。
public class Card {
private int yue=0;
private boolean bzw=false;
//没钱是F,有钱时T;
public int getYue() {
return yue;
}
public void setYue(int yue) {
this.yue = yue;
}
//有钱的时候不让存,没钱的时候不让取
//存钱的
public synchronized void cunqian(int money){
//if风改为while
//解决了程序出现负数问题
while(bzw){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//接下来是没钱存钱
yue+=money;
System.out.println(Thread.currentThread().getName()+"存入了1000元,余额为"+yue);
bzw=!bzw;
this.notifyAll();
}
//取钱
public synchronized void quqian(int money){
while(!bzw){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//接下来是没钱存钱
yue-=money;
System.out.println(Thread.currentThread().getName()+"取出了了1000元,余额为"+yue);
bzw=!bzw;
this.notifyAll();
}
}
public class Test {
public static void main(String[] args) {
Card card = new Card();
Thread ykq = new Thread(new Boy(card), "1");
Thread ykq1 = new Thread(new Girl(card), "2");
//激活线程
ykq.start();
ykq1.start();
}
}
class Boy implements Runnable{
private Card card;
public Boy(Card card) {
this.card = card;
}
@Override
public void run() {
for (int i = 0; i <10 ; i++) {
card.cunqian(1000);
}
}
}
class Girl implements Runnable{
private Card card;
public Girl(Card card) {
this.card = card;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
card.quqian(1000);
}
}
}
9.线程池
线程是宝贵的内存资源,单个线程约占1M,过多分配容易内存溢出,频繁的创建和销毁会造成虚拟机的性能下降。
线程池:
线程容器,可以设定线程分配的数量上限。
常用的线程池和在 java.util.concurrent (juc)下
Executor:线程池的顶级接口
Executor Service:线程池接口,可以通过submit提交代码;
Executors:工具类,通过此类获得一个线程池。
通过newFixedThreadPool(int nThreads) 获取固定长度的线程池。
通过newCachedThreadPool 获得动态的线程池,如果不够创建型的,没有上限
创建线程的方式
// Executor: 线程池的顶级接口。 execute方法 该方法会执行Runable类型的类型
// ExecutorService: 子接口 shutdown关闭线程池 submit()该方法可以执行Runable类型的任务和Callable类型的任务
// Executors:工具类 获取相应的线程池
public class Test {
public static void main(String[] args) {
//1.固定长度的线程池
//ExecutorService executorService = Executors.newFixedThreadPool(3);
//2.可变长度得线程池 如果存在空闲的线程,则不会创建新的。
//ExecutorService executorService = Executors.newCachedThreadPool();
//3. 创建单一线程池
//ExecutorService executorService = Executors.newSingleThreadExecutor();
//4. 创建定长延迟线程池。
// ScheduledExecutorService executorService = Executors.newScheduledThreadPool(3);
//5.ExcutorsService实现类创建.ThreadPoolExecutor
ArrayBlockingQueue blockingQueue=new ArrayBlockingQueue(2);
ExecutorService executorService=new ThreadPoolExecutor(3,5,30,TimeUnit.SECONDS,blockingQueue);
Runnable tast1=new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName()+"执行");
}
};
for(int i=0;i<8;i++) {
executorService.execute(tast1);
}
}
}
9.1池化思想
例如线程池,字符串常量池,数据库连接池。
目的是为了提高资源的利用路。
1.手动创建线程对象
2.执行任务。
3.执行完毕,释放线程对象。
例如,手机没电就扔掉,就很浪费,我们应该让手机重复利用。
线程池的优点。
1.提高线程的利用率。
2.提高程序的响应速度(节省了创建和销毁的响应时间)
3.便于统一管理线程对象
4.可以控制最大的并发数
举个例子。线程池是怎么运作的。
柜台里有五个窗口。假设,4,5窗口没上班。那么123窗口就能接待顾客,再来新的顾客,就会在等待区,而当你去银行发现,等待区满了,三个柜台也都在忙,那么银行就会打开另外两个窗口,But,如果五个窗口都打开了,等候区也满了,那么保安就会劝你明天再来或者去别的分行。
这个例子就和线程池运行原理一模一样。
当然,线程池有一个概念,存活时间,套用刚才的例子,假如人不多,三个窗口足以供应办理业务,那么,4,5窗口的工作人员就可以放假回家了,这就是线程的存活时间的概念,假如这个线程太长时间没被调用,那么线程池就会销毁这个线程
9.2 线程池的使用
线程池的真正实现类是ThreadPoolExecutor ,其构造方法有如下四种:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), handler);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
可以看到,其需要如下几个参数:
corePoolSize(必需):
核心线程数。默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
maximumPoolSize(必需):
线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。
keepAliveTime(必需):
线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
unit(必需):
指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
workQueue(必需):
任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。
threadFactory(可选):
线程工厂。用于指定为线程池创建新线程的方式。
handler(可选):
拒绝策略。当达到最大线程数时需要执行的饱和策略。
线程的使用流程如下:
// 创建线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(CORE_POOL_SIZE,
MAXIMUM_POOL_SIZE,
KEEP_ALIVE,
TimeUnit.SECONDS,
sPoolWorkQueue,
sThreadFactory);
// 向线程池提交任务
threadPool.execute(new Runnable() {
@Override
public void run() {
... // 线程执行的任务
}
});
// 关闭线程池
threadPool.shutdown(); // 设置线程池的状态为SHUTDOWN,然后中断所有没有正在执行任务的线程
threadPool.shutdownNow(); // 设置线程池的状态为 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表
9.3 线程池的工作原理
9.4功能线程池
嫌上面使用线程池的方法太麻烦?其实Executors已经为我们封装好了 4 种常见的功能线程池,如下:
定长线程池(FixedThreadPool)
定时线程池(ScheduledThreadPool )
可缓存线程池(CachedThreadPool)
单线程化线程池(SingleThreadExecutor)
9.4.1 定长线程池(FixedThreadPool)
创建方法的源码:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
使用实例:
// 1. 创建定长线程池对象 & 设置线程池线程数量固定为3
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
public void run() {
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务
fixedThreadPool.execute(task);
9.4.2 定时线程池(ScheduledThreadPool )
创建方法的源码:
private static final long DEFAULT_KEEPALIVE_MILLIS = 10L;
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
public static ScheduledExecutorService newScheduledThreadPool(
int corePoolSize, ThreadFactory threadFactory) {
return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue(), threadFactory);
}
-
特点:核心线程数量固定,非核心线程数量无限,执行完闲置 10ms 后回收,任务队列为延时阻塞队列。
-
应用场景:执行定时或周期性的任务。
使用实例:
// 1. 创建 定时线程池对象 & 设置线程池线程数量固定为5
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
public void run() {
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务
scheduledThreadPool.schedule(task, 1, TimeUnit.SECONDS); // 延迟1s后执行任务
scheduledThreadPool.scheduleAtFixedRate(task,10,1000,TimeUnit.MILLISECONDS);// 延迟10ms后、每隔1000ms执行任务
9.4.3 可缓存线程池(CachedThreadPool)
创建方法的源码:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
-
特点:无核心线程,非核心线程数量无限,执行完闲置 60s 后回收,任务队列为不存储元素的阻塞队列。
-
应用场景:执行大量、耗时少的任务。
使用实例:
// 1. 创建可缓存线程池对象
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
public void run() {
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务
cachedThreadPool.execute(task);
9.4.4 单线程化线程池(SingleThreadExecutor)
创建方法的源码:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
-
特点:只有 1 个核心线程,无非核心线程,执行完立即回收,任务队列为链表结构的有界队列。
-
应用场景:不适合并发但可能引起 IO 阻塞性及影响 UI 线程响应的操作,如数据库操作、文件操作等。
使用实例:
// 1. 创建单线程化线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
public void run() {
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务
singleThreadExecutor.execute(task);
9.5 对比线程池
9.6 总结
Executors 的 4 个功能线程池虽然方便,但现在已经不建议使用了,而是建议直接通过使用 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
其实 Executors 的 4 个功能线程有如下弊端:
FixedThreadPool 和 SingleThreadExecutor:主要问题是堆积的请求处理队列均采用 LinkedBlockingQueue,可能会耗费非常大的内存,甚至 OOM。 CachedThreadPool 和 ScheduledThreadPool:主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。
这部分借鉴了 孙强 Jimmy大佬的文章,原文如下 Java 多线程:彻底搞懂线程池_孙强 Jimmy 的博客-CSDN博客_java多线程线程池
10.Lock接口
JDK5加入,与synchronized比较,显示定义,结构更灵活。
提供了更多实用性的方法,功能更强大。(JDK7以前)
常用方法:
void lock()//获取锁 void unlock() //释放锁 boolean tryLock() //尝试获取锁,成功返回true,失败返回false,不阻塞
10.1重入锁:
ReentrantLock:lock接口的实现类,与synchronized一样具有互斥锁功能。
注意,请在finally中写入解锁,否则一旦有异常,代码断掉就不会执行unlock;
10.2 读写锁:
ReentrantReadWriteLock
一种支持一写多读的同步锁,读写分离,可分别分配读,写。
支持多次分配读锁,使多个读操作可以并发执行。
11.线程安全的集合
白色的均为线程不安全的,蓝色的为线程安全的。绿色为被淘汰的集合。