网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
-
同步处理的流程容易发生阻塞,可以用线程来实现异步处理,提高程序处理实时性
-
线程可以认为是轻量级的进程,所以线程的创建、销毁 比进程更快 (性能开销更小)
1.2.线程解决了什么问题?
单位时间内处理复杂且庞大的数据或业务时提升效率
1)如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创
建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
2)如果大量线程在执行,会涉及到线程间上下文的切换,会极大的消耗CPU运算资源
1.3 如何创建线程?
- 继承Thread
2)实现Runnable接口
- 使用 Callable接口 (可以使用CompletableFuture )
注意:
我们项目中使用多线程编程一定要使用线程池,否则可能会导致线程创建过多发生异常
1.4 线程安全
多个线程在对共享数据进行读改写的时候,可能导致的数据错乱就是线程的安全问题了
如何判断当前程序中是否存在线程安全问题?
-
是否存在多线程环境
-
在多线程环境下是否存在共享变量
-
在多线程环境下是否存在对共享变量 “写” 操作
2.线程的生命周期?线程有几种状态
1.线程通常有五种状态,创建,就绪,运行、阻塞和死亡状态。
- 新建状态(New):新创建了一个线程对象。
- 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start方法。该状态的线程位于
可运行线程池中,变得可运行,等待获取CPU的使用权。
3 .运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
- 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进
入就绪状态,才有机会转到运行状态。
- 死亡状态(Dead):线程执行完了或者因异常退出了run方法,该线程结束生命周期。
2.阻塞的情况又分为三种:
(1)、等待阻塞:运行的线程执行wait方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待
池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify或notifyAll方法才能被唤
醒,wait是object类的方法
(2)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放
入“锁池”中。
(3)、其他阻塞:运行的线程执行sleep或join方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状
态。当sleep状态超时、join等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
sleep是Thread类的方法
3. wait 和 sleep 的区别
共同点
- wait() ,wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态
不同点
-
方法归属不同
-
- sleep(long) 是 Thread 的静态方法
- 而 wait(),wait(long) 都是 Object 的成员方法,每个对象都有
- sleep(long) 是 Thread 的静态方法
-
醒来时机不同
-
- 执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来
- wait(long) 和 wait() 还可以被 notify 唤醒,wait() 如果不唤醒就一直等下去
- 它们都可以被打断唤醒
- 执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来
-
锁特性不同(重点)
-
- wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制
- wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu,但你们还可以用)
- 而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃 cpu,你们也用不了)
- wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制
4. volatile的作用和原理
4.1 JMM内存模型
JMM让java程序与硬件指令进行了隔离
由于JVM运行程序的实体是线程,创建每个线程时,java 内存模型会为其创建一个工作内存(我们一般称为栈),工作内存是每个线程的私有数据区域。
Java内存模型规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问。
但线程对变量的操作(读取、赋值等)必须在工作内存中进行。因此首先要将变量从主内存拷贝到自己的工作内存,然后对变量进行操作,操作完成后再将变量写会主内存中。
4.2 Java并发编程要解决的三个问题(三大特征)
原子性
一个线程在CPU中操作不可暂定,也不可中断,要不执行完成,要不不执行
内存可见性
默认情况下变量,当一个线程修改内存中某个变量时,主内存值发生了变化,并不会主动通知其他线程,即其他线程并不可见
有序性
程序执行的顺序按照代码的先后顺序执行。
4.3 Volatile
volatile帮我们解决了:
内存可见性问题
指令重排序问题
不能保证变量操作的原子性(Atomic)
- 被volatile修饰的共享变量对所有线程总是可见的,也就是当一个线程修改了一个被volatile修
饰共享变量的值,新值总是可以被其他线程立即得知。(会主动通知)
我们可以通过如下案例验证
import java.util.Date;
public class MyData {
private boolean flag=false;
public void setFlag(boolean flag) {
this.flag = flag;
}
public boolean isFlag() {
return flag;
}
public static void main(String[] args) throws Exception {
MyData myData = new MyData();
// 线程1 修改值
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 子线程3s 后 修改为true
myData.setFlag(true);
}
}).start();
System.out.println(new Date());
while (!myData.isFlag()){
// (如果不用 volatile) 理论上 3s 这里的死循环会结束,但是 实际上3s 后主线程一直在死循环
// 如果不用 volatile 主线程并没有感知到 子线程修改了变量
}
System.out.println("已经被修改了"+new Date());
}
}
注意: volatile 并不保证线程安全的,即多个线程同时操作某个变量依旧会出现线程安全问题
如下案例
public class MyData {
private volatile int number=1;
public void addNum(){
number++;
}
public static void main(String[] args) {
MyData myData = new MyData();
// 启动20个线程,每个线程将myData的number值加1000次,那么理论上number值最终是20000
for (int i=0; i<20; i++) {
new Thread(() -> {
for (int j=0; j<1000; j++) {
myData.addNum();
}
}).start();
}
// 程序运行时,有主线程和垃圾回收线程也在运行。如果超过2个线程在运行,那就说明上面的20个线程还有没执行完的,就需要等待
while (Thread.activeCount()>2){
Thread.currentThread().getThreadGroup().activeCount();
Thread.yield();// 交出CPU 执行权
}
System.out.println("number值加了20000次,此时number的实际值是:" + myData.number);
}
}
- 禁止指令重排序优化。
int a = 0;
bool flag = false;
public void write() {
a = 2; //1
flag = true; //2
}
public void multiply() {
if (flag) { //3
int ret = a * a;//4
}
}
write方法里的1和2做了重排序,线程1先对flag赋值为true,随后执行到线程2,ret直接计算出结果,
再到线程1,这时候a才赋值为2,很明显迟了一步。
但是用 flag 使用 volatile修饰之后就变得不一样了
使用volatile关键字修饰后,底层执行时会禁止指令重新排序,按照顺序指令
5.为什么用线程池?解释下线程池参数?
1、降低资源消耗;提高线程利用率,降低创建和销毁线程的消耗。
2、提高响应速度;任务来了,直接有线程可用可执行,而不是先创建线程,再执行。
3、提高线程的可管理性;线程是稀缺资源,使用线程池可以统一分配调优监控。
/*
corePoolSize 代表核心线程数,也就是正常情况下创建工作的线程数,这些线程创建后并不会
消除,而是一种常驻线程
maxinumPoolSize 代表的是最大线程数,它与核心线程数相对应,表示最大允许被创建的线程
数,比如当前任务较多,将核心线程数都用完了,还无法满足需求时,此时就会创建新的线程,但
是线程池内线程总数不会超过最大线程数
keepAliveTime 、 unit 表示超出核心线程数之外的线程的空闲存活时间,也就是核心线程不会
消除,但是超出核心线程数的部分线程如果空闲一定的时间则会被消除,我们可以通过keepAliveTime 、
unit 表示超出核心线程数之外的线程的空闲存活时间,
也就是核心线程不会 setKeepAliveTime 来设置空闲时间
workQueue 用来存放待执行的任务,假设我们现在核心线程都已被使用,还有任务进来则全部放
入队列,直到整个队列被放满但任务还再持续进入则会开始创建新的线程
ThreadFactory 实际上是一个线程工厂,用来生产线程执行任务。我们可以选择使用默认的创建
工厂,产生的线程都在同一个组内,拥有相同的优先级,且都不是守护线程。当然我们也可以选择
自定义线程工厂,一般我们会根据业务来制定不同的线程工厂
Handler 任务拒绝策略,有两种情况,第一种是当我们调用 shutdown 等方法关闭线程池后,这
时候即使线程池内部还有没执行完的任务正在执行,但是由于线程池已经关闭,我们再继续想线程
池提交任务就会遭到拒绝。另一种情况就是当达到最大线程数,线程池已经没有能力继续处理新提
交的任务时,这是也就拒绝
*/
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.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
java 中常见的几种线程池
// 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,
//若无可回收,则新建线程。
Executors.newCachedThreadPool();//
//创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
Executors.newFixedThreadPool(10);
//创建一个定长线程池,支持定时及周期性任务执行。
Executors.newScheduledThreadPool(10);// 核心线程数10
//创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,
//保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
Executors.newSingleThreadExecutor();
//看源码,解释一下这三个创建线程池方法的作用
Executors.newFixedThreadPool(1);
Executors.newCachedThreadPool();
Executors.newSingleThreadExecutor();
6.项目中线程池的使用?
- tomcat 自带线程池
- CompletableFuture 创建线程时指定线程池,防止创建线程过多
CompletableFuture<Integer> task1 = CompletableFuture.supplyAsync(()->{
result.setCluesNum(reportMpper.getCluesNum(beginCreateTime, endCreateTime, username));
return null;
},指定线程池);
案例:CompletableFuture异步和线程池讲解 - 不懒人 - 博客园
7 synchronized
synchronized 锁释放时机
● 当前线程的同步方法、代码块执行结束的时候释放
-
正常结束
-
异常结束出现未处理的error或者exception导致异常结束的时候释放
● 程序执行了 同步对象 wait 方法 ,当前线程暂停,释放锁
8. Sychronized和ReentrantLock的区别
- sychronized是⼀个关键字,ReentrantLock是⼀个类
- sychronized的底层是JVM层⾯的锁(底层由C++ 编写实现),ReentrantLock是API层⾯的锁 (java 内部的一个类对象)
- sychronized会⾃动的加锁与释放锁,ReentrantLock需要程序员⼿动加锁与释放锁
- sychronized是⾮公平锁,ReentrantLock可以选择公平锁或⾮公平锁
注: 假设多个线程都要获取锁对象,满足先等待的线程先获得锁则是公平锁,否则是非公平锁
- sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识
来标识锁的状态
- sychronized底层有⼀个锁升级的过程(访问对象线程数由少到多,竞争由不激烈到激烈,底层会通过一种锁升级机制 无锁->偏向锁->轻量级锁->重量级锁,保证性能) ,会使用自旋 线程频繁等待唤醒会浪费性能,特别是锁的获取也许只需要很短的时间 ,不限于等待,直接执行简单代码while(true)执行完抢锁 来优化性能
代码演示
可重入演示
public static void main(String[] args) {
// 可重入锁演示
save();
}
public synchronized static void save() {
System.out.println("save");
![img](https://img-blog.csdnimg.cn/img_convert/333b03079ae20aa2c8f3ee44d7d1d165.png)
![img](https://img-blog.csdnimg.cn/img_convert/d6dbe291273ff4b67eff4c6ac7dd3ccf.png)
![img](https://img-blog.csdnimg.cn/img_convert/946815f324f657853055b88a856df64a.png)
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!**
**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**
**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**
代码演示
可重入演示
public static void main(String[] args) {
// 可重入锁演示
save();
}
public synchronized static void save() {
System.out.println(“save”);
[外链图片转存中…(img-CYJHl4Ch-1715665367687)]
[外链图片转存中…(img-ly0YutLE-1715665367687)]
[外链图片转存中…(img-7iIUe9T6-1715665367687)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新