Java线程相关知识
线程是什么
就是进程里面的一个控制流,是程序最小的执行单位,进程的概念下面会提及到
线程与系统进程,生命周期
一个应用对应一个或者多个进程,一个进程里面包含许多个线程
- 线程的生命周期
- 新建状态:使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
- 就绪状态:当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待 JVM 里线程调度器的调度。
- 运行状态:如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
- 阻塞状态:如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
- 死亡状态:一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到死亡(终止)状态。
Java线程创建方式
一共分为三种方法
直接继承Thread的方式
1.数据不能共享
2.使用到start后run方法
3.需要重写run方法
使用implement实现Runnable接口
1.数据可以共享
2.创建的对象是一个可运行的,通过Thread的构造方法传递进去。
内部匿名类的方式
1.在new的时候使用,
Thread t = new Thread(){ public void run(){ System.out.println("匿名类") } }
通过Callable后future创建线程
1. 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。 2. 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。 3. 包装好的FutureTask类对象作为线程参数传递并创建启动新的线程 4. 调用FutureTask对象的get()方法获得子线程结束后的返回值
为什么要使用线程池
为了减少创建和销毁线程的次数,让每个线程可以多次使用,可根据系统情况调整执行的线程数量,防止消耗过多内存,所以我们可以使用线程池.
java中线程池的顶级接口是Executor(e可rai kei ter),ExecutorService是Executor的子类,也是真正的线程池接口,它提供了提交任务和关闭线程池等方法。调用submit方法提交任务还可以返回一个Future(fei 曲儿)对象,利用该对象可以了解任务执行情况,获得任务的执行结果或取消任务。
由于线程池的配置比较复杂,JavaSE中定义了Executors类就是用来方便创建各种常用线程池的工具类。通过调用该工具类中的方法我们可以创建单线程池(newSingleThreadExecutor),固定数量的线程池(newFixedThreadPool),可缓存线程池(newCachedThreadPool),大小无限制的线程池(newScheduledThreadPool),比较常用的是固定数量的线程池和可缓存的线程池,固定数量的线程池是每提交一个任务就是一个线程,直到达到线程池的最大数量,然后后面进入等待队列直到前面的任务完成才继续执行.可缓存线程池是当线程池大小超过了处理任务所需的线程,那么就会回收部分空闲(一般是60秒无执行)的线程,当有任务来时,又智能的添加新线程来执行.Executors类中还定义了几个线程池重要的参数,比如说int corePoolSize核心池的大小,也就是线程池中会维持不被释放的线程数量.int maximumPoolSize 线程池的最大线程数,代表这线程池汇总能创建多少线程。corePoolSize :核心线程数,如果运行的线程数少 corePoolSize,当有新的任务过来时会创建新的线程来执行这个任务,即使线程池中有其他的空闲的线程。maximumPoolSize:线程池中允许的最大线程数.
线程池使用示例
package javalearning; import java.util.Random; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPoolDemo { static class Task implements Runnable{ private String id; Task(String id){ this.id = id; } @Override public void run() { System.out.println("Thread "+id+" is working"); try { //每个任务随机延时1s以内的时间以模拟线程的运行 Thread.sleep(new Random().nextInt(1000)); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Thread "+id+" over"); } } public static void main(String[] args) { ExecutorService threadPool = Executors.newFixedThreadPool(3);//线程池中,3工作线程 threadPool.execute(new Task("a")); threadPool.execute(new Task("b")); threadPool.execute(new Task("c")); threadPool.execute(new Task("d")); threadPool.execute(new Task("e")); threadPool.shutdown(); while(!threadPool.isTerminated()){ } System.out.println("Thread Pool is over"); } }
线程池参数用法
1.corePoolSize:核心线程数
- 核心线程会一直存活,及时没有任务需要执行
- 当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理
- 设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭
2.queueCapacity:任务队列容量(阻塞队列
- 当核心线程数达到最大时,新任务会放在队列中排队等待执行
3.maxPoolSize:最大线程数
- 当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务
- 当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常
4.keepAliveTime:线程空闲时间
- 当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize
- 如果allowCoreThreadTimeout=true,则会直到线程数量=0
5.allowCoreThreadTimeout:允许核心线程超时
6.rejectedExecutionHandler:任务拒绝处理器
两种情况会拒绝处理任务:
- 当线程数已经达到maxPoolSize,切队列已满,会拒绝新任务
当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务
线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置默认是AbortPolicy,会抛出异常
ThreadPoolExecutor类有几个内部实现类来处理这类情况:
AbortPolicy 丢弃任务,抛运行时异常
CallerRunsPolicy 执行任务
DiscardPolicy 忽视,什么都不会发生
DiscardOldestPolicy 从队列中踢出最先进入队列(最后一个执行)的任务
实现RejectedExecutionHandler接口,可自定义处理器
ThreadPoolExecuto自定义线程池[^https://www.cnblogs.com/wang-meng/p/10163855.html]
有待跟进
线程池大小如何确定
考虑的因素
- CPU的个数
- 内存的大小
- 任务类型,是计算CPU密集型还是I/O密集型
- 是否需要一些稀缺资源,像数据库连接那样的
简单的估算方法
- 对于CPU密集型的应用,线程池的大小设置为N+ 1
- 对于I/O密集型的应用,线程池的大小哦设置为2N+1
详细参见[^https://blog.csdn.net/cnd2449294059/article/details/97367325]
callable和Runnable的区别
1.Runnable
其中Runnable应该是我们最熟悉的接口,它只有一个run()函数,用于将耗时操作写在其中,该函数没有返回值。然后使用某个线程去执行该runnable即可实现多线程,Thread类在调用start()函数后就是执行的是Runnable的run()函数。Runnable的声明如下 :
public interface Runnable{ public abstract void run(); }
2.Callable
Callable与Runnable的功能大致相似,Callable中有一个call()函数,但是call()函数有返回值,而Runnable的run()函数不能将结果返回给客户程序。Callable的声明如下 :
public interface Callable<V> { V call() throws Exception; }
线程池异常捕获
Java中线程执行的任务接口java.lang.Runnable 要求不抛出Checked异常,那么如果 run() 方法中抛出了RuntimeException,将会怎么处理了? 通常java.lang.Thread对象运行设置一个默认的异常处理方法:
java.lang.Thread.setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler)
而这个默认的静态全局的异常捕获方法时输出堆栈。
当然,我们可以覆盖此默认实现,只需要一个自定义的java.lang.Thread.UncaughtExceptionHandler接口实现即可。public interface UncaughtExceptionHandler { void uncaughtException(Thread t, Throwable e); }
而在线程池中却比较特殊。默认情况下,线程池 java.util.concurrent.ThreadPoolExecutor 会Catch住所有异常, 当任务执行完成(java.util.concurrent.ExecutorService.submit(Callable))获取其结果
而在线程池中却比较特殊。默认情况下,线程池 java.util.concurrent.ThreadPoolExecutor 会Catch住所有异常, 当任务执行完成(java.util.concurrent.ExecutorService.submit(Callable))获取其结果
sleep(1)sleep(0)sleep(1000)的区别
sleep(0)
当 timeout = 0, 即 Sleep(0),如果线程调度器的可运行队列中有大于或等于当前线程优先级的就绪线程存在,操作系统会将当前线程从处理器上移除,调度其他优先级高的就绪线程运行;如果可运行队列中的没有就绪线程或所有就绪线程的优先级均低于当前线程优先级,那么当前线程会继续执行,就像没有调用 Sleep(0)一样。
sleep(1)
会引发线程上下文切换:调用线程会从线程调度器的可运行队列中被移除一段时间,这个时间段约等于 timeout 所指定的时间长度。为什么说约等于呢?是因为睡眠时间单位为毫秒,这与系统的时间精度有关。通常情况下,系统的时间精度为 10 ms,那么指定任意少于 10 ms但大于 0 ms 的睡眠时间,均会向上求值为 10 ms。l
yield()、notify()、notifyAll()、sleep()、join()、wait()
yield方法是静态的方法,是设置线程优先级的,调用 getPriority 后 setPriority 方法可以得到和设置线程的优先级
notify方法后wait方法(生产者和消费者模式,直接上代码)
package ThreadStudy.WaitAndNotify; import java.util.ArrayList; import java.util.List; /** * @ClassName WaitAndNotifyTest * @Description TODO * 生产者以消费者模式 * 1、创建一个集合当作厂库,使用list集合并且创库的容量只有一个元素 * 2、创建两个线程t1 、t2,t1代表消费线程,指的是到创库中去取一个元素,t2代表的是生产给 厂库加一个元素 * 3、当list中的元素没有时候,也就是一开始的时候,线程启动的时候会去执行里面的 * while死循环(小技巧死循环会导致下一次的线程启动之后会继续的进行上面的判断) * 生产线程检测到厂库是空的就会去执行list.add()方法去为仓库生产一个元素, * 生产完之后使用一下唤醒notify唤醒一下在此对象上面睡眠的线程,不是死循环吗, * 接着在一次判断这时候仓库里面有元素了,就会停下来使用到就是wait方法,wait方法 * 使得当前生产线程暂停,并且释放锁 * 4、当生产线程暂停之后后面的排队(之所排队是因为wait and notify是建立在synchronized 之上的)的线程就开始执行了,这时候会判断出集合中有一个元素就会执行lsit.remove(); * 方法,移除仓库里面的元素就没有了 就是使用notify方法去唤醒之前在此对象上面的睡眠的 线程,然而notify不会释放锁线程还在执行,再一次的循环一次,只是后会判断仓库中没有元 素了, 就开始调用wait方法释放锁 * * @Author Zhoujian * @Date 2020/12/17
**/
public class WaitAndNotifyTest {
public static void main(String[] args) {
List list = new ArrayList();
Thread t1 = new Thread(new Producter(list));
Thread t2 = new Thread(new Customer(list));
t1.setName(“t1线程”);
t2.setName(“t2线程”);
t1.start();
t2.start();
}
}/**
生产类
*/
class Producter implements Runnable {
// 仓库对象引入
private List list;
public Producter(){}
// 传递参数
public Producter(List list){
this.list = list;
}@Override
public void run() {
// 死循环是使得在做加减元素之后执行唤醒之后再执行wait使得线程暂停之后再一次苏醒之后还能进行判 断的关键,不然线程就不会再一次
// 回到上面再一次进行判断
while (true) {
// 同步代码块是wait释放锁的关键,锁的是仓库对象
synchronized (list) {
// 这里可以修改仓库的容量,消费线程不需要
if (list.size() > 0) {
try {
// 暂停生产线程
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 创建对象,加入集合
Object o = new Object();
list.add(o);
System.out.println(“增加元素成功!”);
// 唤醒消费。本线程还是在执行的
list.notify();
}
}
}
}/**
消费
*/
class Customer implements Runnable{
private List list;
public Customer(){}
public Customer(List list){
this.list = list;
}@Override
public void run() {
// 给出一个循环
while(true) {
synchronized (list) {
// 当集合里的元素为空的时候
if (list.size() == 0) {
try {
// 线程等待,释放线程锁
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.remove(0);
System.out.println(“元素移除成功!”);
// 不会释放线程的锁这时候线程还是在执行的
list.notify();
}
}
}
}+ <font color ="#dc6c8d">notifyAll方法:</font>唤醒该对象上面所有的wait线程 + <font color ="#dc6c8d">sleep方法:</font>是一个静态的方法,哪个线程需要睡眠就在哪个线程里面写Thread.sleep(); + <font color ="#dc6c8d">join方法:</font>表示的是使某个线程1加入到某线2程执行,某线程2会停止执行直到某县城1执行完才开始 ~~~ java package JavaBasic.week9; import java.sql.SQLOutput; import java.util.NavigableMap; /** * @ClassName JoinMethod * @Description TODO * join方法 * 1、当一个线程使用join()的时候主线程会停止让出来等待使用join的线程执行完再继续执行 * 2、join的使用之后就会使得主线程停止知道使用join线程执行结束 * 3、参照下面的例子 * @Author ZHouJian * @Date 2020/12/14 **/ public class Test{ public static void main(String[] args) { JoinMethod jm = new JoinMethod("分支线程"); Thread t = new Thread(jm); t.start(); for (int a = 0; a < 30; a++){ if (a == 10){ try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("主线程" + a); } }
synchronized
同步代码块的三种写法
synchronized(要同步的对象){ 同步代码块 } 同步代码块
public synchronized void doSome(){} 表示的共享对象一定会是this,同步的是一整个的对象。
在静态的方法中使用synchronized 表示找类锁 类锁永远的只有一把 创建100个对象,那类所只有一个 对象锁 : 一个对象一个锁,100个对象100个锁 类锁 : 100个对象也只有可能只有一把类锁
volatile
翻译:不稳定的。首先简单的说就是轻量级的同步机制
Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。
而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jj7Gx5tX-1610865515930)(https://ae02.alicdn.com/kf/H32959b5186d94171814e36ff9d45452fW.png)]
ThreadLocal
翻译:线程局部变量。简单的说 ThreadLocal是一个线程内部的存储类,可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据,提供set and get 方法获取存储的参数
详情参见[^https://www.jianshu.com/p/3c5d7f09dfbd]
volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。
而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。
[外链图片转存中…(img-Jj7Gx5tX-1610865515930)]
ThreadLocal
翻译:线程局部变量。简单的说 ThreadLocal是一个线程内部的存储类,可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据,提供set and get 方法获取存储的参数
详情参见[^https://www.jianshu.com/p/3c5d7f09dfbd]