个人主页: Java李小立
后面会持续更新java面试专栏,请持续关注
如果文章对你有帮助、欢迎关注、点赞、收藏(一键三连❤️❤️❤️)
面试宝典列表(持续更新):
序号 | 内容 | 链接地址 |
---|---|---|
1 | Java基础篇 | (点击跳转)java面试宝典-基础篇 |
2 | Java集合框架篇 | (点击跳转)java面试宝典-集合框架篇 |
3 | Java多线程篇 | (点击跳转)java面试宝典- 多线程篇 |
4 | JVM篇 | 待分享 |
5 | Spring篇 | 待分享 |
6 | Mybatis篇 | 待分享 |
7 | SpringcCloud篇 | 待分享 |
8 | Redis篇 | 待分享 |
9 | Mysql篇 | 待分享 |
10 | dubbo篇 | 待分享 |
11 | zookeeper篇 | 待分享 |
12 | kafka篇 | 待分享 |
13 | RocketMq篇 | 待分享 |
14 | Nacos篇 | 待分享 |
创建线程的常用四种方式:
采用继承Thread类的方式创建线程的优缺点
Thread代码解析:Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过 Thread 类的 start()实例方法。start()方法是一个 native 方法,它将启动一个新线
程,并执行 run()方法(单独执行run方法不会创建新线程,只会在当前线程执行run方法)。
public class MyThread extends Thread {
public void run() {
System.out.println("MyThread.run()");
}
}
MyThread myThread1 = new MyThread();
myThread1.start();
优点:编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用
this即可获取当前线程
缺点:因为线程类已经继承了Thread类,Java语言是单继承的,所以就不能再继承其他父类了。
采用实现Runnable、Callable接口的方式创建线程的优缺点
Runnable代码解析 :通常情况下我们自己不会使用Thread类,因为如果已经 extends 另一个类,就无法直接 extends Thread,此时,可以实现一个
Runnable 接口。
public class MyThread extends OtherClass implements Runnable {
public void run() {
System.out.println("MyThread.run()");
}
}
//启动 MyThread,需要首先实例化一个 Thread,并传入自己的 MyThread 实例:
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
callable代码解析:有返回值的任务必须实现 Callable 接口执行,Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务
返回的 Object,再结合线程池接口 ExecutorService 就可以实现传多线程返回结果。
//创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(taskSize);
// 创建多个有返回值的任务
List<Future> list = new ArrayList<Future>();
for (int i = 0; i < taskSize; i++) {
Callable c = new MyCallable(i + " ");
// 执行任务并获取 Future 对象
Future f = pool.submit(c);
list.add(f);
}
// 关闭线程池
pool.shutdown();
// 获取所有并发任务的运行结果
for (Future f : list) {
// 从 Future 对象上获取任务的返回值,并输出到控制台
System.out.println("res:" + f.get().toString());
}
优点:线程类只是实现了Runnable或者Callable接口,还可以继承其他类,Callable接口里定义方法返回值,可以声明抛出异常。
缺点:编程稍微复杂一些,如果需要访问当前线程,则必须使用 Thread.currentThread() 方法
实际开发中一般都会使用线程池线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。
线程池的方式代码实例(固定大小线程池)
// 创建线程池(定长线程池)
ExecutorService threadPool = Executors.newFixedThreadPool(10);
while(true) {
threadPool.execute(new Runnable() { // 提交多个线程任务,并执行
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running ..");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
4 种线程池
Java 里面线程池的顶级接口是 Executor,但是严格意义上讲 Executor 并不是一个线程池,而
只是一个执行线程的工具。真正的线程池接口是 ExecutorService。
其实四种线程池都是 ThreadPoolExecutor ,只是创建参数不同
newSingleThreadExecutor: 创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
newCachedThreadPool: 创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
状态
阻塞的三种情况
1.锁池:所有需要竞争同步锁的线程都会放在锁池当中,比如当前对象的锁已经被一个线程拿到,其他线程就需要进入锁池等待,等锁释放后去竞争锁,某线程得到锁后会进入就绪态,等待cpu分配资源。
2.等待池
当我们调用wait()方法后,会释放锁池,线程会放到等待池当中,等待池的线程是不会去竞争同步锁。只有调用了
notify()或notifyAll()后等待池的线程才会开始去竞争锁,notify()是随机从等待池选出一个线程放
到锁池,而notifyAll()是将等待池的所有线程放到锁池当中
sleep和wait的区别五点
1、sleep 是 Thread 类的静态本地方法,wait 则是 Object 类的本地方法。
2、sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中。
- sleep就是把cpu的执行资格和执行权释放出去,不再运行此线程,当定时时间结束再取回cpu资源,参与cpu 的调度,获取到cpu资源后就可以继续运行了。而如果sleep时该线程有锁,那么sleep不会释放这个锁,而 是把锁带着进入了冻结状态,也就是说其他需要这个锁的线程根本不可能获取到这个锁。也就是说无法执行程 序。如果在睡眠期间其他线程调用了这个线程的interrupt方法,那么这个线程也会抛出 interruptexception异常返回,这点和wait是一样的。
3、sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。
4、sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)。
5、sleep 一般用于当前线程休眠,或者轮循暂停操作,wait 则多用于多线程之间的通信。
join和yield方法的区别
yield:执行后线程直接进入就绪状态,马上释放了cpu的执行权,但是依然保留了cpu的执行资格,
所以有可能cpu下次进行线程调度还会让这个线程获取到执行权继续执行。
join:执行后线程进入阻塞状态,例如在线程B中调用线程A的join(),那线程B会进入到阻塞队
列,直到线程A结束或中断线程
应该是可以说线程安全是内存的数据安全,堆是共享内存,可以被所有线程访问
- 当多个线程访问一个对象时进行操作,如果得到正确的结果(和单线程一致),我们就说这个对象是线程安全的。
堆是所有线程共享的一块内存区域,堆所存在的内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
栈是每个线程独有的,,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈
互相独立,因此,栈是线程安全的。
线程安全
进程内存空间独立,而不能访问别的进程的,这是由操作系统保障的。
在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存)。进程内的所有线程都可以
访问到该区域,这就是造成问题的潜在原因。
守护线程:为所有非守护线程提供服务的线程;并不是某个,而是所有线程的守护线程。
举例: GC垃圾回收线程:就是一个经典的守护线程,所以当所有线程结束垃圾回收线程是JVM上仅剩的线程时,垃圾回收线
程会自动离开。
应用场景:
(1)来为其它线程提供服务支持的情况;
(2) 或者在任何情况下,程序结束时,这个线程必须正常且立刻关闭,就可以作为守护线程来使用;
Java中的线程池比如executeService会自动将守护线程转化为用户线程。
引用类型主要分为强软弱虚四种:
ThreadLocal提供了线程内存储变量的能力。通过get和set方法就可以得到当前线程对应的值,做到了线程之间互相隔离,相比于synchronized的做法是用空间(类似map储存)来换时间。
Thread 对象含有一个 ThreadLocalMap 类型的成员变量 它存储本线程中所
有ThreadLocal对象及其对应的值
ThreadLocal有一个静态内部类ThreadLocalMap,ThreadLocalMap又包含了一个Entry数组,Entry本身是一个弱引用,他的key是指向ThreadLocal的弱引用,Entry具备了保存key value键值对的能力。
ThreadLocalMap 由一个个 Entry 对象构成
Entry 继承自 WeakReference
当执行set方法时,ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,将值存储进ThreadLocalMap对象中。
get方法执行过程类似。ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,获取对应的value。
使用场景:
Spring框架在事务开始时会给当前线程绑定一个Jdbc Connection,在整个事务过程都是使用该线程绑定的 connection来执行数据库操作,实现了事务的隔离性。Spring框架里面就是用的ThreadLocal来实现这种 隔离
内存泄露:不再被使用的对或者变量占用的内存不能被回收,这就是内存泄露,最终会导致oom。
强引用:一般创建的对象,new对象,反射 newInstance,都是强引用,内存空间不足,抛出oom也不会回收强引用。如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。
弱引用:weakRefeface,只要垃圾进行就回收,缓存用的比较多。
thread中有threadLocalMap,key为弱引用的threadLocal,线程value为变量
泄漏原因:由于Thread中包含变量ThreadLocalMap,因此ThreadLocalMap与Thread的生命周期是一样长(线程池的线程不会被回收。同一个线程,可能执行了任务一,又执行任务2),如果都没有手动删除对应key,都会导致内存泄漏。但是使用弱引用可以多一层保障:弱引用ThreadLocal(Key值)不会内存泄漏,对应的value在下一次ThreadLocalMap调用set(),get(),remove()等方法的时候会被清除。
保证三大特性才能保证线程安全
原子性:不可分割的操作,多个步骤cpu同一时间执行。多线程i++,数据不安全i++为什么线程不安全点击查看,原子性是指在一个操作中cpu不可以在中途暂停然后再调度,即不被中断操作,要不全部执行完成,要
不都不执行。
可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
有序性:程序执行顺序和代码顺序一致。虚拟机在进行代码编译时,对于那些改变顺序之后不会对最终结果造成影响的代码,虚拟机不一定会按
照我们写的代码的顺序来执行,有可能将他们重排序。实际上,对于有些代码进行重排序之后,虽然对变量的值没有造成影响,但有可能会出现线程安全问题。
synchronized关键字同时满足以上三种特性,但是volatile关键字不满足原子性。
volatile(保证有序性,可见性):
new一个对象分为三步
**由于指令重排序 多线程中上下文切换等原因,可能执行1与3步骤还没有执行2就切换到其他线程 **
使用volatile
第一(可见性):使用volatile关键字会强制将修改的值立即写入主存
第二(有序性):volatile可以禁止指令重排,这就保证了代码的程序会严格按照代码的先后顺序执行。这就保证了有序性。
需要了解as-if-serial与happens-before
大致可以总结为
1、降低资源消耗;创建线程是很消耗资源,提高线程利用率,降低创建和销毁线程的消耗。
2、提高响应速度;任务来了,直接有线程可用可执行,而不是先创建线程,再执行。
3、提高线程的可管理性;线程是稀缺资源,使用线程池可以统一分配调优监控。
核心参数:
corePoolSize: 代表核心线程数,也就是正常情况下创建工作的线程数,这些线程创建后并不会
消除。
maxinumPoolSize:代表的是最大线程数,比如当前任务较多,将核心线程数都用完了,此时就会创建新的线程,但
是线程池内线程总数不会超过最大线程数
keepAliveTime:
unit单位当前线程池数量超过 corePoolSize 时,多余的空闲线程的存活时间,即多
次时间内会被销毁。
unit:keepAliveTime 的单位。
threadFactory:线程工厂,用于创建线程,一般用默认的即可。
workQueue: 用来存放待执行的任务,假设我们现在核心线程都已被使用,还有任务进来则全部放入队列,直到整个队列被放满但任务还再持续进入则会开始创建新的线程
Handler:
主要有4种拒绝策略:
普通列队超出列队长度,无法保任务等待,阻塞队列通过阻塞可以保留住当前想要继续入队的任务。使得线程进入wait状态,释放cpu资源,列队空闲notify唤醒。
在创建新线程的时候,是要获取全局锁的,这个时候所有线程会阻塞,影响了整体效率。
其核心原理在于线程池对Thread 进行了封装,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的run方法,将 run 方法当成一个普通的方法执行,通过这种方式只使用固定的线程就将所有任务的 run 方法串联起来。
fixedThreadPool是固定长度的线程池,底层用的linkedBlockingQueue,无界阻塞队列,长度是Integer的最大值,无限放入可能造成机器内存溢出(阿里巴巴规范禁用fixedThreadPoll)。
在开发过程中:
相似点:
这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如
果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的.
区别:
在了解锁类型前受限了解一下JVM内存中对象的数据结构
JVM对象的储存
版权声明:本文为博主原创文章,未经博主允许不得转载
https://blog.csdn.net/qq_44614710/article/details/120308144