【JAVA EE】多线程、锁、线程池的使用
目录
创建线程
方法一:继承Thread类来创建一个线程类
class MyThread extends Thread{
@Override
public void run() {
while(true)
{
System.out.println("1111");
}
}
}public class Main {
public static void main(String[] args) {
//创建Mythread类的实例
MyThread myThread=new MyThread();//调用start方法启动线程
myThread.start();
}
}
继承Thread,使用匿名内部类
Thread t =new Thread(){
@Override
public void run() {
System.out.println("111");
}
};
t.start();
方法二:实现Runnable,重写run
class MyRunnable implements Runnable{
@Override
public void run() {
while (true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class Main {public static void main(String[] args) {
Runnable runnable=new MyRunnable();
Thread t =new Thread(runnable);
t.start();
}
}
使用Runnable和继承Thread方法的区别在于:解耦合
TIPS
.start 和 .run方法的区别:
.run代码中不会创建出新的线程,只有一个主线程,这个主线程里面只能依次执行循环,执行完一个循环再执行另一个
我们可以使用IDEA或者jconsole来观察进程里多线程的情况
jconsole是jdk自带的工具,我们在jdk的bin目录里可以找到
在启动jconsole前,确保idea程序已经跑起来了,如下会列出当前机器上运行的所有java进程
Thread-0就是我们新建的线程
如果要更明显的找到新线程,我们在创建线程的时候,给它设置一下名字
Thread t =new Thread(()->{
while(true){
System.out.println("hello");
}
},"这是新线程名字");
t.start();
线程属性
属性 | 获取方法 |
---|---|
ID | getid() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isinterrupted() |
- 这里的id是java分配的,不是系统api提供的
- 后台线程不结束,不影响整个进程结束,设置后台线程 t.setDaemon(true)
- 前台线程没有执行结束,整个进程是一定不会结束的
- isAlive判定内核线程是不是已经没了,回调方法执行完毕,线程就没了,但是Thread对象可能还在。
lambda表达式有一个语法规则,变量捕获,是可以自动捕获到上层作用域中涉及到的局部变量,所谓变量捕获,就是让lambda表达式,把当前作用域中的变量在lambda内部复制了一份
只能捕获一个final或者实际上是final的常量(变量没有使用final,但是没有进行修改)
在java中并不能像C++一样,直接中断一个线程,只能让线程任务做的快一点,依靠标志位来决定,具体示例如下:
Thread t =new Thread(()->{
while(!Thread.currentThread().isInterrupted()){
System.out.println("hello");
}
},"这是新线程名字");
t.start();
System.out.println("让t线程结束");
t.interrupt();
设置标志位的相关方法
方法 | 说明 |
---|---|
public void interrupt() | 中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,否则设置标志位 |
publi static boolean interrupted() | 判断当前线程的中断标志位是否设置,调用后清除标志位 |
public boolean isinterrupted() | 判断对象关联的线程的标志位是否设置,调用后不清除标志位 |
线程等待
让一个线程等待另一个线程执行结束,再继续执行,本质就是控制线程结束的顺序
join线程等待核心方法
方法 | 说明 |
public void join() | 等待线程结束 |
public void join(long millis) | 等待线程结束,最多等millis毫秒 |
public void join(long millis,int nanos) | 等待时间精度更高 |
主线程中,调用t.join() 此时就是主线程等待t线程先结束
Thread t =new Thread(()->{
for(int i=0;i<5;i++)
{
System.out.println("t线程工作");
}
},"这是新线程名字");
t.start();
System.out.println("主线程开始等待");
t.join();
System.out.println("主线程等待结束");
一旦调用join,主线程就会发生阻塞,一直阻塞到t执行完毕了(没有设置等待时间的话),join才会解除阻塞,主线程才会继续执行
获取当前线程引用
方法 | 说明 |
---|---|
public static Thread currentThread() | 返回当前线程对象的引用 |
休眠当前线程
方法 | 说明 |
---|---|
public static void sleep(long millis) | 休眠当前线程millis毫秒 |
public static void sleep(long millis,int nanos) | 更高精度 |
线程的状态
JAVA中有六个线程状态:
- NEW:安排了任务,还未开始行动
- RUNNABLE:可工作的,又可以分为正在工作中和即将开始工作(也就是就绪状态)
- BLOCKED:排队等待,由于锁竞争导致的阻塞
- WAITING:排队等待,由wait这种不固定时间的方式产生的阻塞
- TIMED_WAITING:排队等待,由于sleep这种固定时间的方式产生的阻塞
- TERMINATED:工作完成了,Thread对象还在,内核中的线程已经没了
获取线程状态可以通过getState方法,如下:
System.out.println(t.getState());
t.start();
t.join();
System.out.println(t.getState());
synchronized
因为线程的调度顺序是不确定的,这会导致线程不安全的情况,因此我们需要引入锁,再Java的一个对象对应的内存空间中,除了自己定义的一些属性之外,还有一些自带的属性,对象头,对象头中,其中就有属性表示当前对象是否已经加锁
synchronized的特性
1、互斥
synchronized会起到互斥效果,某个线程执行到某个对象的synchronized中时,其他线程如果也执行到同一个对象,synchronized就会阻塞等待。
- 进入synchronized修饰的代码块,相当于加锁
- 退出synchronized修饰的代码块,相当于解锁
synchronized除了修饰代码块之外,还可以修饰一个实例方法,或者修饰一个静态方法
对于普通方法,有两种写法:
第一种:
synchronized public void increase()
{
count++;
}
第二种:
public void increase()
{
synchronized (this)
{
count++;
}
}
这两种方法是一样的,第一种可以看成第二种的简写 。
如果是修饰静态方法,相当于是针对类对象加锁
class Counter{
public int count;//第一种方法
synchronized public static void increase()
{}
//第二种方法
public static void increase()
{
synchronized (Counter.class)
{
count++;
}
}
}
两种方法也是等价的。
2、刷新内存
synchronized的工作过程:
- 获得互斥锁
- 从主内存拷贝变量到最新副本工作的内存
- 执行代码
- 将更改后的共享变量的值刷新到主内存
- 释放互斥锁
当出现如下代码:
synchronized (locker)
{
synchronized (locker){……
}
}
3、可重入
第一次加锁,假设能够加锁成功,此时的locker就属于是“被锁定的”状态,进行第二次加锁,很明显,locker已经是锁定状态了,第二次加锁操作,原则上是应该要“阻塞等待”的,应该要等待到锁被释放,才能加锁成功的, 但是实际上,一旦第二次加锁的时候阻塞了,就会出现死锁情况。
因此synchronized的可重入性,也就是记录当前持有锁的线程,如果是持有锁的线程再进行加锁,则允许加锁。但是释放锁是要到最外层结束才释放,这里使用了引用计数,锁对象中,不仅要记录谁拿到了锁,还要记录,锁被加了几次,每加锁一次,计数器就+1,每解锁一次,计数器就-1,直到0,才真正释放锁
死锁
死锁的四个必要条件
- 互斥使用(锁的基本特性):当一个线程持有一把锁之后,另一个线程也想获取到锁,就需要阻塞等待
- 不可抢占(锁的基本特性):当锁已经被线程1拿到之后,线程2只能等线程1主动释放,不能强行抢占过来
- 请求保持(代码结构):一个线程尝试获取多把锁,先拿到锁1,再拿锁2的时候,锁1不释放
- 循环等待/环路等待:等待的依赖关系,形成环
避免死锁
解决死锁,破坏上面三、四条件即可
- 对于3来说,调整代码结构,避免写“锁嵌套”逻辑
- 对于4来说,可以约定加锁顺序,就可以避免循环等待,针对锁进行编号,比如约定加多把锁的时候,先加编号小的锁,后加编号大的锁
volatile关键字
1、保证内存可见性
用volatile修饰的变量,每次都从内存中读取值,而不会因为编译器优化,将常访问未修改的变量值读取到寄存器,内存中对应变量值修改后,程序还是访问的寄存器(java中叫做工作内存)的值,从而导致“内存可见性”问题(也是线程安全的问题)。
关于内存可见性,还涉及到一个关键概念,JMM(Java内存模型)
2、禁止指令重排序
TIPS
volatile和synchronized都能对线程安全起到一定的积极作用,但是volatile不能保证原子性,而synchronized保证原子性,synchronized也能保证内存可见性。
wait方法
wait做的事情:
- 使当前执行代码的线程进行等待,把线程放到等待队列中
- 释放当前的锁
- 满足一定条件时被唤醒,重新尝试获取这个锁
notify方法
notify时唤醒等待的线程
- 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其他线程,对其发出通知,并使它们重新获取该对象的锁
- 如果多个线程等待,则由线程调度器随机挑选出一个wait状态的线程(没有先来后到)
定时器
在标准库中java.util.Timer
如下是一个简单的使用示例:
主线程执行schedule方法的时候,就是把这个任务给放到timer对象中,与此同时,timer里头也包含一个线程,这个线程叫做“扫描线程”,一旦时间到,扫描线程就会执行刚才安排的任务了,Timer里可以安排多个任务的。
Timer timer=new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("时间到了");
}
},2000);
System.out.println("程序开始");
此处传参使用匿名内部类的写法,继承了TimerTask,并且创建出一个实例,TimerTask实现了Runnable接口,以此来重写run方法,通过run描述任务的详细情况。
public abstract class TimerTask implements Runnable
线程池
线程池就是把线程创建好,放到池子里,后续用的时候直接从池子里取,这样效率会比我们每次都新创建线程,效率更高。
那么为什么从线程池里取的效率会比新创建线程效率更高呢?
线程池的创建
线程池对象不是我们直接new的,而是通过一个专门的方法,返回一个线程池对象,如下:
ExecutorService service= Executors.newCachedThreadPool();
这种方法就是采用工厂模式 ,Executors是一个工厂类,newCachedThreadPool()是一个工厂方法。
newCachedThreadPool线程池里的线程用过之后不着急释放,以备随时再使用,此时构造出来的线程池对象,有一个基本的特点:线程数目是能够动态适应的,随着往线程池里添加任务,这个线程中的线程会根据需要自动被创建出来。
newFixedThreadPool这个工厂方法就是创建一个固定大小的线程池,如下是创建一个包含3个线程的线程池
ExecutorService service= Executors.newFixedThreadPool(3);
除了上述两种工厂方法,还有 newScheduledThreadPool(),newSingleThreadExecutor(),这两种并不常用,这里便不再介绍。
这几个工厂方法生成的线程池,本质上都是对一个类(ThreadPoolExecutor)进行的封装。
ThreadPoolExecutor构造参数:
- int corePoolSize 核心线程数
- int maximumPoolSize 最大线程数
这两个参数描述了线程池中,线程的数目,这个线程池里线程的数目是可以动态变化的,变化范围就是[corePoolSize,maximumPoolSize]
- long keepAliveTime 允许非核心线程数存留时间
- TimeUnit unit 时间单位 ms,s,min
- BlockingQueue<Runable> workQueue 阻塞队列,用来存放线程池中的任务,可以根据需要,灵活设置这里的队列是啥,需要优先级就可以设置PriorityBlockingQueue,如果不需要优先级,并且任务数目是相对恒定的,可以使用ArrayBlockingQueue,如果不需要优先级,并且任务数目变动较大的,使用LinkedBlockingQueue
- ThreadFactory threadFactory 工厂模式的体现,不用手动设置属性
- RejectedExecutionHandler handler 线程池的拒绝策略,一个线程池能容纳的任务数量,有上限,当到达上线的时候,做出什么样的反应,如下是常用拒绝策略:
ThreadPoolExecutor.AbortPolicy | 直接抛出异常 |
ThreadPoolExecutor.CallerRunsPolicy | 新添加的任务,由添加任务的线程负责执行 |
ThreadPoolExecutor.DiscardOldestPolicy | 丢弃任务队列中最老的任务 |
ThreadPoolExecutor.DiscardPolicy | 丢弃当前新加的任务 |
常见问题:
使用线程池,需要设置线程的数目,数目设置多少合适?
使用实验的方式,对程序进行性能测试,测试过程中尝试修改不同的线程池的线程数目,看哪种情况更符合要求
往线程池里添加任务
通过submit往线程池里注册任务
service.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
原文地址:https://blog.csdn.net/lzb_kkk/article/details/143199023
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!