前言
三年前,大概是21年,那会刚学完java,然后去面试,被打的一塌糊涂,今天来盘一盘之前的面试,到底是怎样的问题整住了。然后发现了去年整的线程安全东西,也贴到文章后面了。那个贴的还不太准,慢慢完善!会整完然后发B站视频。
线程安全
其实简单的来看就是线程安全的问题。现在来看这些问题好像没有什么,就是比较正常的问题。但是当时掌握的确实太不好了,然后可能面试官看我太菜了,这些回答不上来就没有接着往后续问了。大家看到这个面试题会想到什么呢,会想怎么回答和解决呢。
也可以结合一个实际的项目区演示和测试一番。写一个数字的成员变逻辑写计算然后看结果。
在Spring Boot中,处理Service层的线程安全问题是非常重要的,因为Spring的Service默认是单例的,这意味着Spring容器中只会创建一个Service实例。如果Service中包含了状态(即成员变量),那么在并发环境下可能会出现线程安全问题。以下是一些处理Service层线程安全问题的建议:
- 避免在Service中定义有状态的成员变量
尽量不要在Service中定义有状态的成员变量。如果确实需要存储状态,考虑将这些状态存储在方法内部的局部变量中,因为局部变量是线程安全的,它们存储在每个线程自己的栈中。
2. 使用线程安全的类
如果Service需要维护跨方法调用的状态,应该使用线程安全的类或数据结构,如ConcurrentHashMap、AtomicInteger等。
3. 同步访问共享资源(使用锁机制)
当多个线程需要访问同一个资源时,可以通过同步机制来保证线程安全。可以使用synchronized关键字或java.util.concurrent.locks包中的锁机制。但是,过度同步可能会导致性能问题,因此需要谨慎使用。
4. 使用ThreadLocal
ThreadLocal可以为每个线程提供一个变量的副本,使每个线程都有自己独立的变量副本,从而避免线程安全问题。但是,使用ThreadLocal时需要注意及时清理,避免内存泄漏。
- 使用@Scope注解(不建议)
如果确实需要Service有状态,可以考虑使用Spring的@Scope注解来改变Bean的作用域。例如,使用@Scope(“prototype”)注解可以让Spring容器为每个请求创建一个新的Service实例,这样可以避免多个请求共享同一个Service实例的线程安全问题。但这种方式会增加对象的创建和销毁的开销。
线程安全
后面是自己写的,还不是很完善,会补充的完善,然后B站发视频
3.线程方面
3.1线程安全
3.1. spring如何处理线程并发问题
spring的对象是默认是单例的。如果在里面声明成员变量的话。然后一个请求创建一个线程,多个线程同时请求一个资源就会出现问题。(单例bean是线程安全的)
解决方式有两种
1.将spring声明成多例
2.使用threadloacal
3.在代码块中加入同步锁 让他编程同步的 就可以解决了
相当于把并行编程了串行,会影响服务器他吞吐量。
4.成员变量声明在方法里面
3.2 tomcat线程模型
tomcat也已经支持异步了;但是他的效率比netty这种肯定还是要低的。
BIO :tomcat6默认采用的。每个请求都要创建一个线程来处理,线程开销较大,不能处理高并发
3.4 Volatile
涉及到可见性 ,禁止重排序
volatile与cas 通过乐观锁的思想来共同完成相应的reenTrankllock,具体在锁那块3.4里面有
3.5 Servlet
单实例 ,多线程
3.6 ThreadLocal
以空间换时间,给每个线程分配一个空间
ThreadLocal 是Java里一种特殊的变量。每个线程都有一个 ThreadLocal 就是每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了。如果为每个线程提供一个自己独有的变量拷贝,将大大提高效率。首先,通过复用减少了代价高昂的对象的创建个数。其次,你在没有使用高代价的同步或者不变性的情况下获得了线程安全。
3.7 ThreadLocal可能引发内存泄漏问题
1、强引用(StrongReference)
最普遍的一种引用方式,如String s = “abc”,变量s就是字符串“abc”的强引用,只要强引用存在,则垃圾回收器就不会回收这个对象。
2、软引用(SoftReference)
用于描述还有用但非必须的对象,如果内存足够,不回收,如果内存不足,则回收。一般用于实现内存敏感的高速缓存,软引用可以和引用队列ReferenceQueue联合使用,如果软引用的对象被垃圾回收,JVM就会把这个软引用加入到与之关联的引用队列中。
3、弱引用(WeakReference)
弱引用和软引用大致相同,弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。当一个对象既有弱引用又有强引用的时候不会清楚弱引用。短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器标记。
ThreadLocal采用的是弱引用的key,当没有强引用来使用Thread Local的key时,key就会被回收。当我把ThreadLocal比作一个水桶,水桶上面有一个桶盖,我们在寻找桶的时候通过桶盖来寻找,但是当没有人惦记桶的时候,桶盖会自己丢失,这样我们没办法找到桶,也没法使用桶里的东西,这样就会导致内存泄漏。
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
TreadLocalMap的Entry继承了WeakReference<ThreadLocal<?>>,即Entry的key是弱引用(该引用只存在弱引用的情况下,下一次GC]
ThreadLocal在保存的时候会把自己当做Key存在ThreadLocalMap中,正常情况应该是key和value都应该被外界强引用才对,但是现在key被设计成WeakReference弱引用了。
ThreadLocal的 key若引用 容易被GC回收,然后变成null。导入里面的 值没法被访问到,容易产生内存泄漏
3、解决方法
想象情况:如果一个线程被回收了,然后线程里面的成员变量也被回收,就没有内存泄漏的问题了。
实际情况:一般使用线程池,线程池是重复利用的,会存在内存泄漏的问题。ThreadLocal在进行数据读写的时候默认会进行一些清理的操作,找到并清理Entry里面key 为null 的数据
这个没有太好的解决方案,只能通过规范代码使用来解决最好是每次使用完ThreadLocal后,主动调用remove 方法去移除数据,把ThreadLocal 声明成全局变量,使他无法被GC回收
所以为了避免内存泄漏,在ThreadLocal使用结束以后,规范操作,执行下remove方法。
或者你可以从ThreadLocal,引出底层的Weak引用话题,再引出JVM结构以及OOM调优方面的话题。*
3.2 main方法线程(jvm创建线程过程)
一个程序创建一个jvm实例,一个进程,然后开辟一个线程
程序运行
JVM 在启动的时候会创建一个 VM Thread
,这是所有线程的祖先,其他的线程都是由这个线程派生出去的。
在 Java 中,线程分为两种:普通线程和守护线程。
普通线程就是通常业务逻辑执行的代码,代码执行完成之后,普通线程也就退出了。而守护线程一般运行在后台,比如说响应命令行的操作,守护线程会在普通线程退出之后退出。
当程序执行完成之后,JVM 也要退出,程序执行完的标志就是非守护线程都退出了。当所有的非守护线程退出之后,守护线程也会退出。
程序在执行完成之后,会调用 System.exit()
方法,然后虚拟机退出,程序彻底结束。
exit 方法接收一个整型的参数,如果传入的值为 0,那么就表示程序是正常退出的,如果是任何非零的值,那么就表示是异常退出。
其实在代码中,非常不推荐调用这个方法,因为这个方法会造成一些意向不到的情况。
作者:Rayjun
链接:https://juejin.cn/post/6866744930376220679
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
3.3线程池
线程池的几个参数
包括阻塞队列这种
3.3.1线程池几大参数
3.3.1队列
无界队列oom
3.4锁
同步块大家都比较熟悉,通过 synchronized 关键字来实现;所有加上 synchronized 的方法和块语句,在多线程访问的时候,同一时刻只能有一个线程能够访问。
ConcurrentHashMap的锁
jdk1.8 使用到了 CAS+Volatile
synchronized+volatile(1.8版本)
CAS+Volatile
心想,确实是可以实现的呀!因为 AbstractQueuedSynchronizer(简称 AQS)内部就是通过 CAS + volatile(修饰同步标志位state) 实现的同步代码块。
volatile 保证了可见性和有序性,
cas 保证了原子性,像i++是三个操作,普通的保证不了这三个操作一起执行,然后使用cas 保证原子性。优点是可以保障原子性,但是缺点是有时候自旋时间太长,也会有问题。
AQS
AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock。 简单来说,AQS定义了一套框架,来实现同步类。
AQS的核心思想是对于共享资源,维护一个双端队列来管理线程,队列中的线程依次获取资源,获取不到的线程进入队列等待,直到资源释放,队列中的线程依次获取资源。 AQS的基本框架如图所示:
AQS定义了一个实现同步类的框架,实现方法主要有tryAquire
和tryRelease
,表示独占模式的资源获取和释放,tryAquireShared
和tryReleaseShared
表示共享模式的资源获取和释放。 源码分析如上文所述。
ReenTrankLock 锁
synchronized 锁对象头
Mysql的锁
表锁 行锁
CAS加volitale实现同步代码块,然后应用有 AQS ,
3.5 多线程 生产者与消费者
生产者 - 消费者模型 Producer-consumer problem
是一个非常经典的多线程并发协作的模型,在分布式系统里非常常见。
public void run() {
synchronized (queue) {
while (queue.size() == maxCapacity) { //一定要用 while,而不是 if,下文解释
try {
System.out.println("生产者" + Thread.currentThread().getName() + "等待中... Queue 已达到最大容量,无法生产");
wait();
System.out.println("生产者" + Thread.currentThread().getName() + "退出等待");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (queue.size() == 0) { //队列里的产品从无到有,需要通知在等待的消费者
queue.notifyAll();
}
Random random = new Random();
Integer i = random.nextInt();
queue.offer(new Product("产品" + i.toString()));
System.out.println("生产者" + Thread.currentThread().getName() + "生产了产品:" + i.toString());
}
}
作者:小齐本齐
链接:https://juejin.cn/post/6872131047032553486
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。