文章目录
- <1> Spring, Sping Boot和Sping Cloud的关系
- <2> Spring boot如何配置多环境
- <3> 实际工作中,如何全局处理异常?
- <4> 线程如何启动?
- <5> 实现多线程的方法有几种?
- <6> 线程有哪几种状态?生命周期是什么?
- <7> 什么是分布式?
- <8> 分布式和单体结构那个更好?
- <9> CAP理论是什么?
- <10> 为啥需要Docker?
- <11> Docker的网络模式有哪些?
- <12> Zookeeper有哪些节点类型?
- <13> 为什么要用消息队列?什么场景用?
- <14> 微服务有那两大门派?
- 彩蛋:学习编程知识的优质方法
- <15> Lock简介,地位,作用
- <16>哲学家就餐问题
- <17> HashMap为什么不安全?
- <18> final的作用是什么?有哪些用法?
- <19> 单例模式你会写吗?
<1> Spring, Sping Boot和Sping Cloud的关系
spring最初利用AOP和IOC解耦,按照这种模式搞了MVC框架,不过随着不断壮大,就变得臃肿了,就算写一个很简单的程序也要写很多样板代码很麻烦,就有了Spring Boot,Spring boot就是为了解决开发人员配置太繁琐的问题,定位是一个帮助开发人员快速开发的快速框架
所以Spring boot是在强大的Spring帝国上发展起来的,发明Spring boot是希望人们更加容易的使用Spring
Spring could和Spring boot的关系也有点像Spring boot和Spring的关系,
Spring could是在Spring boot的基础上诞生的
Spring could并没有去重复的造轮子,而是将各家公司开发的比较成熟经得起考验的服务框架给组合了起来,同样利用了Spring boot这种风格屏蔽掉了复杂的配置和实现原理,给开发者留出了一套简单易懂的容易部署容易维护的微服务框架,它是一系列框架的有序集合:比如服务注册和发现 网关 断路器等等,而且每一个模块都是具有Spring boot风格的,比如可以一键启动
总结:正因为有了Spring的IOC和AOP的强大功能才有了Spring,Spring生态不断发展蓬勃壮大之后,由于他的配置繁琐就因此诞生了Spring boot,而Spring Could 是基于Spring boot的一套微服务框架,可以看出这三者中有层次递进,逐步演化的这种关系的
<2> Spring boot如何配置多环境
开发,测试,预发,生产
<1>开发环境:一般在本地,用到的数据库一般也是用于开发的,一般会造一些模拟的数据
<2>测试环境:一般开发以后会部署到测试环境,因为测试环境一般是公司提供的一个服务器.
#1 因为一般开发是在本机开发,本机关机后测试人员就没法测试了,
#2 而且本机和测试环境不独立的话,本机继续写代码就会影响测试代码,这样也会出现很大的问题,所以一般用一台稳定的服务器把要测试的代码给部署上去,
#3 测试环境可以跟开发环境共同用一个数据库,都是模拟出来的,不需要做严格的区分
<3>预发环境:跟线上的环境是一致的,用的真实数据,这样可以进行数据验真的作用,可能找出测试环境没有发现的问题,且会进行网络隔绝,开发和测试是访问不到这个服务器环境的
<4>生产环境:也成为线上环境,真实对外提供服务的,生产环境和预发环境最大的不同就是流量的多少,通常预发环境不会对外暴露,而生产环境则是直接面向所有用户的,所以可能会存在一些并发的问题等,所以要保证好一个稳定的生产环境
配置多环境的方法:
1.开发,测试,预发,生产
2.提供多套配置的文件
3.通过改变application里的profiles.active值来加载对应的环境
代码实现,,实现的是加载含有pre的配置文件:
# application.properties
spring.profiles.active=pre
#-------------------------
# application-pre.properties
server.port=8081
#-------------------------
# application-prod.properties
server.port=8082
#-------------------------
# application-test.properties
server.port=8083
#-------------------------
上面代码对应这四个配置文件:
<3> 实际工作中,如何全局处理异常?
为什么异常需要全局处理,不处理行不行?
不行,如果我们不进行处理的话,那么这个异常会被抛出去,那么别有用新的用户或者是黑客能看到相信的异常的情况,很不安全,所以是必须要处理的,用封装一个统一处理异常的GlobalExceptionHandler的类,在类里面封装方法去管理任何可能出现的异常以及进行处理,从而实现引导用户正确避开错误且异常不外露
具体的实现要在函数(封装的类)上要加上@ControllerAdvice注解,封装方法上加上@ExceptionHandler(Exception.class)注解
<4> 线程如何启动?
用Thread调用start()执行里面的run()方法
既然start()方法会调用run()方法,为什么我们选择调用start()方法,而不是直接调用run()方法?
如果直接调用run方法,其实他就是一个普通的java方法,而且还是在主线程中调用的,根本没有起到在其他线程启动的作用,而start这个方法则是回去申请线程,这个再调用run方法后,就是再另一个线程中启动了
两次调用start()方法会出现什么情况?
会抛出线程状态不对的异常,因为在进行start()的时候,首先会进行状态的检测,看过一眼源码,里面有一个判断:只有ThreadStatu=0的时候才会启动,不等于0就会抛出一个线程状态不对的异常,也就是说只有是"new(新的,前面没有启动)"的时候才能启动
<5> 实现多线程的方法有几种?
思路:1.从不同角度有不同的答案
2.典型答案有两种
3.看原理,两种本质都是一样的
4.具体展开说其他方式
5.结论:不管发展出有多少种能创建线程的方法,其实万变不离其宗方法一:实现Runnable接口 方法二:继承Thread类
那个方法会更好?
实现Runnable接口会更好
1.从架构角度,之所以会有runnable这个接口本意就是为了把任务的执行类和任务的具体内容进行解耦,这样从架构的角度扩展性会更好,runnable更多的是去描述这个工作的内容,Thread类主要是维护线程状态的,启动,更改,结束等作用,所以不应该把runnable跟Thread过多的耦合,所以用实现runnable接口会更好
2.新建线程的损耗,Thread每次启动一个线程都要new一个对象,所以耗费的资源比较多,runnable可以把任务作为一个参数传进去线程池,然后线程池用固定的线程来执行这些任务,就不用每次都新建和销毁线程了,这样就降低了性能的开销
3.java不支持多继承,而可以实现多个接口,且可以同时继承类和实现接口(继承要写在实现接口前面),而类一旦继承了另外一个类,那么可扩展性就变小了,不利于发展
两种方法的对比
方法一:最终调用target.run();源码里面的run()方法判断有没有穿进去一个实现了runnable接口的类的target,只有传入了这个才会执行target.run(),而外面这个run()就是进行重写的run()
方法二:run()整个都被重写,因为是继承一个父类,所以重写run()的时候是重写了父类中的整个run()方法
同时使用这两种方法会怎么样?
当同时使用两种方法的时候,由于已经把父类的run方法给覆盖了,所以即便是传入了target,或者说是传入runnable也不会起到效果,真正执行的还是覆盖的Thread的run方法,所以还是Thread这个方法起作用
除了上面两个有没有其他的创建线程的方法?
表面上还可以通过定时器,lambda表达式,匿名内部类,创建线程池等方式 实际上这些方法本质上都是通过封装了上面两个方法实现的
<6> 线程有哪几种状态?生命周期是什么?
new,runnable ,blocked,waiting,timed waiting ,Terminated
其实一般把:waiting(等待),timed
waiting(计时等待),blocked(等待synchronized锁执行完毕)都称为阻塞状态,而不仅仅是blocked
<7> 什么是分布式?
分布式是为了应对大量需求,以及考虑不资源浪费而将功能模块拆分到不同服务器上进行管理运行,且需求大的功能模块可以多部署几个服务器,这样相对集群既能解决大量需求,也能避免资源的浪费
<8> 分布式和单体结构那个更好?
这个要根据具体的实际业务需求来定
#1. 传统的单体结构:业务逻辑成本高,但是部署和运维比较容易,但是隔离线非常差,基本上一损俱损,一个出了问题其他的都会被收到影响,
#2. 分布式架构:架构逻辑成本高,发布频繁,发布顺序复杂,运维也难,但是隔离线比较好,出了故障的影响范围小
从架构设计来看的话,单体的难度远远低于分布式的难度,系统性能单体响应快,但是吞吐量较小,分布式响应慢,吞吐量大,测试的成本单体低,而分布式就相对很高了
技术上单体架构技术单一且封闭,分布式多样且开放 系统扩展性单体架构比较差,分布式扩展性很好 系统管理成本,单体低,分布式高
项目快速上线是比较合适单体结构的,后续按照业务需求和实际情况再进行技术改造或者重构,利用到分布式的优点
<9> CAP理论是什么?
三者不可兼得,最多兼得两种,P一般无法避免,一般都要存在,所以一般都是AP,CP ,按照实际场景来决定用AP(图片网站等)还是CP(支付场景等)
#1 C(Consistency,一致性):读操作是否总能读到前一个写操作的结果
#2 A(Avaliablity,可用性):非故障节点应该在合理的时间内做出合理的响应
#3 P(partition tolerance,分区容错性):当出现网络分区现象后,系统能够继续运行
<10> 为啥需要Docker?
Docker是用来装程序及其环境的容器,主要解决的难题就是环境配置的难题,比如这个程序再这台电脑可以良好的运行,而在另外电脑可能就会出问题,为了解决这个情况,就直接把程序和环境进行打包放在一个Docker容器中(类似一个小型虚拟机,虚拟机也能解决环境问题,但是资源太浪费了)
Docker的用途
<1> 提供统一环境
<2> 提供快速拓展,弹性伸缩的云服务,如果碰见流量大的情况可以迅速部署几十台甚至上百台的机器
<3> 防止其他用户的进程把服务器资源占用过多,就是再执行的过程中电脑性能被业务繁重的进程抢占了
Docker的架构是什么样的?
<11> Docker的网络模式有哪些?
Bridge(桥接),Host,None(通常情况下都是用的Bridge)
1.Bridge是用的最多的,用外面主机的端口号去映射Docker内部的端口号,实现了一座桥,通过这座桥,大家就可以通信了
2.Host模式是:里面的容器不会获得一个独立的网络资源配置,它和外面的主机是使用的同一个网络,也就是说里面的容器不会虚拟出它自己独立的网卡,也不会虚拟出它的ip和端口号,而是用宿主机上的ip和端口号
3.None:就是不需要网络的模式,这种模式下就不能和外界通信了
Nginx的适用场景有哪些?
Http的方向代理服务器,可以进行负载均衡,和地址保护
动静资源的分离,如果不分离响应速度会变慢,其实静态资源没必要经过Tomcat,Tomcat只负责处理动态请求就ok了,后缀是gif的时候,Nginx会直接获取到当前请求的文件并返回,这样响应速度就会快很多,甚至可以有条件一个搭建一个静态资源服务器
Nginx的常用命令有哪些?
/user/sbin/niginx 启动
-h 帮助
-c 读取指定的配置文件
-t 测试
-v 显示当前Nginx的版本
-s 发信号:1.stop 立即停止
2.quit 优雅停止 (二者区别,收到的请求不管了直接停止,优雅 停止是不再接受新的请求了但是要执行完收到的请求后才停止)
3.reload 重启(应用场景:更改了内容,重启来获取新的内容)
4.reopen 更换日志文件
<12> Zookeeper有哪些节点类型?
zookeeper内部结构的样子,采用树结构
1.持久节点 (创建节点后不会消失,出发删掉)
2.临时节点(再链接断开之后会自动进行删除)
3.顺序节点(节点的号码是递增的且唯一)
<13> 为什么要用消息队列?什么场景用?
1.系统解耦,通过消息队列的收发消息的机制,我们可以让不用系统之间进行解耦,我不在去需要调用你的接口,也不必等着返回,我只需要发个消息就ok了,剩下的都由消息队列去完成
2.异步调用,比如有一个功能涉及到的模块很多,用户不用等待返回,只需要把消息发出去就ok了,等待的时候就会变少很多,体验感会好很多
3.流量削峰,再高并发的情况下,又可能短时间内会接到特别多的请求,我们不应该让这些请求一下子就进来,这个时候我们可以把我们的请求放到消息队列中,然后由消息队列一个个的进行处理消化,这样就控制了机器的访问压力,不至于过大的访问量导致我们的机器宕机
RabbitMQ核心概念?
发送者会把消息发送到交换机上,然后由交换机去把消息放到合适合理的队列上,而消费者就只要去关心队列就可以了,队列有啥就去消费啥
在交换机和队列的外面是虚拟主机,在同一个rabbitMQ的server下可以建立不同的虚拟主机,这些虚拟主机之间都是相互独立的,这些可以用于不同的业务线,这就是消息队列的核心概念
交换机工作模式有哪几种?
四种工作模式
1.fanout:广播,这种模式只需要将队列绑定到交换机上即可,是不需要设置路由键的
2.direct:根据RoutingKey去精准匹配
topic:生产者指定RoutingKey消息根据消费端指定的队列通过模糊匹配的方式来进行相应的转发(工作中最常用的模式)
*:代表一个单词
#:可以代替零个或者多个单词
headers:根据发送消息内容中的headers属性来进行匹配,需要我们自定义,通常情况下我们用不到这样
<14> 微服务有那两大门派?
spring cloud:众多子项目,是包括一系列项目的集合
dubbo:定位和spring could有所不同,dubbo是一个高性能的,轻量级的开源RPC框架,它提供了三大核心功能:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现
总结:dubbo提供的能力只是Spring
cloud的一部分子集,但是dubbo是可以和其他的组件进行整合的,省事省心的话可以选择Springcould,因为它对模块和组件的支持是非常成熟的,如果想要有更好的性能或者是进行一定成都的定制的话,可以选择dubbo
Spring Cloud核心组件有哪些?
核心组件:服务注册中心 Spring could Netfix Eureka
服务调用: Spring could Netfix Feign
服务网关: Spring could Netfix Zuul
断路器: Spring could Netfix Hystrix
能画一下Eureka的架构吗?
负载均衡的两种类型是什么?
1.客户端的负载均衡(Ribbon):我们在请求的时候就以及知道了这三个ip地址都能请求提供服务,我们可以一个个的进行去轮流调用,或者是经过一定的算法去调用,总之这个调用是我们客户端的
2.服务端的负载均衡(Nginx):用Nginx把用户的请求进行合理转发给不同的服务器就是负载均衡
负载均衡有哪些策略?
1.randomRule 随机策略
2.RoundRobinRule 表示轮询策略(用的最多的)
3.responseTimeWeightedRule 加权,根据每一个Server的平均响应时间动态加权(根据服务器的响应速度分配请求,快的分配多点,慢的分配少点)
为什么需要断路器?
比如我们依赖很多服务,但是突然有一个服务不能用了,如果没有断路器,用户的请求就会被卡住了,后面的请求也会进不来,会在很短的时间内服务变得不可用了,发生很严重的故障,所以需要断路器把发生故障的服务或者模块摘除掉,以至于不会影响其他模块
为什么需要网关?
鉴权:分布式中比如签名校验,登录校验的冗余问题,如果不用网关,则每个模块需要都去实现自己的鉴权功能,这就造成了资源浪费,而且维护起来也很困难
统一对外,增强安全:我们线上服务通常只会对外暴露网关一个服务,其他的都作为内部服务,不对外暴露,那么外面想访问必须通过网关,只需要在网关这个层面进行安全保护就可以了,可以对恶意IP进行拦截,对所有的请求记录进行打日志等,因为外面把所有的请求都放在一起了,比分散管理的时候方便得多
Dubbo的工作流程是什么?
彩蛋:学习编程知识的优质方法
1.看翻译过来的外国经典编程书籍
2.看官方文档
3.自己动手写代码,尝试运用到项目中(github)
4.不理解的内容参考多个知识来源,综合判断
5.学习开源项目,分析源码
<15> Lock简介,地位,作用
1.Lock锁是一种工具,用于控制对共享资源的访问(常见的类:ReentrantLock)
2.Lock和synchronized,这两个最常见的锁,它们都可以达到线程安全的目的,但是使用上和功能有较大的不同
3.Lock并不是用来替代synchronized的,而是使用synchronized不合适或者不足以满足要求的时候,用来提供高级功能的
Lock主要方法介绍
在Lock中声明四个方法来获取锁
lock(),tryLock(),tryLock(long time,TimeUnit unit)和lockInterruptibly()
这四个方法有什么区别呢?
Lock()
1.lock()就是最普通的获取锁,如果锁已被其他线程获取,则进行等待
2.lock不会想synchronized在发生异常的时候自动解锁,而是必须自己主动解锁,因此最佳实践是,在finally中释放锁,以保证发生异常的时候锁一定被释放
3.lock()方法不能被中断,这会带来很大的隐患:一旦陷入死锁,lock()会陷入永久等待
tryLock()
- tryLock()用来尝试获取锁,如果当前锁没有被其他线程占用,则获取成功,则返回true,否则返回false,代表获取锁失败
2.相比较lock(),这样的方法显然更强大了,我们可以根据是否能获取到锁来决定后续程序的行为
3.该方法会立即返回,即便是拿不到锁的时候也不会一直在那等
tryLock(long time,TimeUnit unit)
跟tryLock是一样的,只不过有个时间参数传进去,在超过这个时间内还获取不到就会返回
lockInterruptibly()
相当于tryLock(long time,TimeUnit unit),只不过时间是设置成无限的,但是等待锁的过程中是可以被中断的
unlock()解锁
要把unlock()放在finally中,让它一定会被执行
synchronized()和lock()有何异同?
相同点:
1.保护线程的安全
2.可以重入,也就是说再次获得锁的时候锁在之前不需要被解锁
3.主要用reentrantLock这个锁的实现类
不同点:
1.用法不同:synchronized可以用在方法上也可以用在同步代码块上,而对于lock而言,它的用法就不太一样了,它必须去使用lock()方法来加锁,用unlock()方法来解锁,所以lock的加解锁都是显示的,而synchronized是隐式的
2.加解锁顺序不同:由于lock的加解锁是我们程序员可以手动写代码去控制的,所以有灵活度,自己去控制,而synchronized则是java内部去控制的,在进入synchronized保护的代码的时候会加锁,退出的时候会解锁,而这些都是自动的,顺序上不能灵活的调整
3.synchronized锁不够灵活:如果有一个synchronized锁被某一个线程给获取了,如果其他的线程还想获得这个锁的话,就只能等待,直到上面一个锁释放
4.性能区别:java5之前synchronized性能较差,之后进行了优化,它的性能逐步提高,跟lock差不多了,只是早期版本的synchronized的性能差
你知道几种锁?
共享锁/独占锁(排他锁):
1.独占锁:大部分的是独占锁,也就是说当一个线程获取到锁的时候,其他的就不能来访问
2.共享锁:比如当多个操作在进行读操作的时候,你们是可以共享这把锁的,因为读操作不会带来线程安全的问题,所以读操作可以使用共享锁来提高效率
什么是共享锁和排它锁
1.排他锁,又称为独占锁,独享锁共享锁,又成为读锁,获得共享锁之后,可以查看但是无法修改和删除数据,其他线程此时可以获得共享锁,也可以查看,但是无法修改和删除数据
2.共享锁和排它锁的典型是读写锁,reentrantReadWriteLock,其中读锁是共享锁,写锁是独享锁
读写锁的作用
在没有读写锁之前,假设使用ReentrantLock,那么虽然我们保证了线程安全,但是也浪费一定的资源:多个读操作同时进行,并没有安全问题,这样就造成了资源浪费
在读的地方加读锁,在写的地方使用写锁,灵魂控制,如果没有写锁的情况下,读是无阻塞的,提高了程序的执行效率
读写锁的规则
1.多个线程只申请读锁,可以申请到
2.如果一个线程占用了读锁,有线程要申请写锁,要等读锁释放了,才能申请
3.如果一个线程占用了写锁,那其他线程都不能申请读锁和写锁了,需要等待写锁释放 总结:要么一个或者多个同时有读锁,有么是一个线程有写锁,两者不会同时出现(要么多读,要么一写)
什么是公平和非公平锁
公平指的是按照线程请求的顺序,来分配锁 非公平指的是不完全按照线程请求顺序,在一定情况下可以插队
(注意:不是盲目乱插队,而是在合适的时机插队)
为什么要有非公平锁?
1.提高效率
2.避免唤醒带来的空窗期(在等待的线程唤醒需要一定的时间的,在这个空窗期要是恰好有别的正在请求的线程,则会插队,提高效率)
公平和非公平各自的优缺点?
公平锁:优势在于各线程平等,等待一段时间,每个线程总会被执行,缺点是吞吐量更小,更慢
非公平锁:有点是更快,吞吐量大,提高效率,劣势是可能会造成线程饥饿,也就是说某一些线程可能长时间始终等不到执行
悲观锁和乐观锁
悲观锁:比如:认为自己操作的时候会有其他线程来干扰,为了确保数据万无一失,把数据锁住,不让别人访问 悲观锁的实现就是synchronized和Lock相关类
乐观锁:1.认为自己操作的时候不会有其他线程来干扰,所以不会锁住被操作的对象,
2.在更新的时候,去对比修改期间数据有没有被其他人改变过,如果没有,就说明真的只有自己在操作,就会正常去修改数据
3.如果数据不一样了,说明有人在这段时间内修改过数据,那就会放弃之前的更新数据过程了,会进行放弃,报错和重试等策略(数据库的select for update就是悲观锁,用version控制数据库就是乐观锁)
4.乐观锁的实现一般是利用CAS的算法
自旋锁和阻塞锁
阻塞或者唤醒一个java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间
自旋锁:后面请求锁的线程不放弃CPU的执行时间,等待持有锁的线程是否很快的释放,自己则进行原地自选,不去休息等待,当持有锁线程释放就立即获得锁,这样当前锁就不用去阻塞而是直接获取同步资源,避免了切换线程的开销
自旋锁的缺点:如果持有锁的线程迟迟不释放,则自旋的线程因为没有被阻塞,一直消耗cpu,就会白白浪费处理器的资源,所以自旋锁读锁的起始开销是低于悲观锁的,但是会随着自旋时间增长,开销也会呈线性增长
可重入锁和非可冲入锁
可重入的性质:也叫递归锁,我一个线程可以多次获取同一把锁,第一次获取后不是释放,继续获取这把锁
好处:是避免死锁(如果两个方法都被同一个锁给修饰了,如果线程运行到了方法一被锁住了,还想运行方法二,而方法二也是被同样的锁给修饰,如果不具备可重入性这个时候再去获取那把锁是获取不到的,因为相当于拿着这把锁还要获取这把锁是不行的,需要释放),提高封装性
可中断锁和不可中断锁
中断锁:觉得请求时间太长了,不想请求了,可以中断锁 不可中断锁:如果想要它去获取锁,则会一直等待直到获取锁,不能去中断
写一个必然死锁的例子
什么是死锁:
1.发生的场景:一定是发生在并发中的,因为我们保证并发的安全,会使用各种各样的手段,比如加锁,如果我们处理不得到就会发生死锁,单线程是不会发送死锁的
2.死锁:互不相让,两个或者多个线程互相持有对方所需要的资源,又不主动释放,导致所有人都无法继续前进,导致程序陷入无尽的阻塞,这就是死锁
多个线程造成死锁的情况
如果多个线程的依赖关系是环形,存在环路的锁的依赖关系,那么也可能发生死锁
死锁的影响
死锁的影响在不同系统中是不一样的,这取决于系统对死锁的处理能力
数据库中:检测并放弃事物(数据库根据版本不同拥有处理死锁的能力,比如检测到死锁的情况,会指派一个线程先释放)
JVM:无法自动处理(会检测到,但是不会帮程序员处理,交给程序员自己处理)
几率不高危害很大:不一定发生,但是遵循“墨菲定律”,一旦发生,多是高并发的场景,影响用户多,甚至整个系统崩溃,子系统崩溃,性能降低,上线前的压力测试无法找出所有潜在的死锁
写一个必然死锁的例子:
package com.example.profiles;
/**
* 描述:写一个必然死锁的例子
*/
public class DeadLock implements Runnable{
// 标记 作为条件
public int flag;
// 创建一个对象用来当锁对象的
static Object object1 = new Object();
static Object object2 = new Object();
@Override
public void run() {
System.out.println("开始执行");
// 实现两个不同线程在索取一把锁的同时还想获取对方的锁的方法
if (flag==1){
synchronized (object1){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (object2){
System.out.println("成功获取了两把锁");
}
}
}
if (flag==2){
synchronized (object2){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (object1){
System.out.println("成功获取了两把锁");
}
}
}
}
public static void main(String[] args) {
// 创建两个对象
DeadLock deadLock1 = new DeadLock();
DeadLock deadLock2 = new DeadLock();
// 赋值flag满足执行if里面的条件
deadLock1.flag = 1;
deadLock2.flag = 2;
// 创建两个线程,传入两个对象,并启动
new Thread(deadLock1).start();
new Thread(deadLock2).start();
}
}
以上代码思路分析:
1.当一个线程里的对象deadLock1的flag=1的时候,执行上面if判断下的代码,获取锁1,然后休眠500毫秒,然后获取锁2
2.当一个线程里的对象deadLock2的flag=2的时候,执行上面if判断下的代码,获取锁2,然后休眠500毫秒,然后获取锁1
3.这就实现了自己不释放锁,要获得对方锁的场景(下图),最终造成死锁
<16>哲学家就餐问题
每个哲学家吃面都要两只筷子才能吃 每个哲学家都是左手先拿筷子 如果同时都想吃饭了,就会造成全部只有一只筷子,吃不了饭(死锁)
如果线程拿锁跟这些哲学家拿筷子一样就会形成环路的死锁 解决方案:
1.改变一个哲学家拿筷子的顺序(这样避免了形成环形造成死锁,避免死锁)
2.有餐票才能吃,比如设置只有四张餐票(拿到餐票的先吃,剩下的一位等待回收的餐票,死锁避免策略)
3.服务员检查(先检查,检查会死锁,就等下再执行,死锁避免策略)
实际工作中如何避免死锁
1.设置超时时间 lock的tryLock(long timeout,Timeunit unit)
2. 多使用并发类,而不是自己去设置锁(ReentrantLock就是一个并发类)
3. 尽量降低锁的使用粒度 :用不同的锁而不是一个锁(这样效率低,也风险大)
4. 在使用synchronized时,如果能使用同步代码块,就不使用同步方法:自己指定锁对象
5. 给线程取一个有意义的名字:debug和排查的时候事半功倍,框架和JDK都遵守这个最佳事件
<17> HashMap为什么不安全?
指的是线程不安全,在多个线程同时去运行同一个hashmap的时候,它其实是暗藏风险的
因为HashMap中有源码类似于i++的自增代码,也没有同步代码和synchronized这样的代码而实现这个代码第一个步骤就是读取,第二步骤是增加,第三步骤是保存,而这三个步骤都是有打断风险的(如下图,两个线程造成了结果错误),所以会造成线程不安全的风险
hashmap线程不安全可能造成的问题
1.同时put碰撞造成数据丢失
2.可见性问题无法保证:一个线程对hashmap进行了赋值,但是另外一个线程是有可能看不见的,第二个线程去获取的时候可能就是获取的旧的值,这是个一个很严重的问题
结论:由于hashmap线程是不安全的,尽量在并发的情况下不去使用它
<18> final的作用是什么?有哪些用法?
final是java的关键字,版本不同也是有所不同的 作用:早期:早期的java实现版本中,会将final的方法转为内嵌调用,提高效率性能
现在:1.类防止被继承,方法防止被重写,变量防止被修改
2.天生是线程安全的,而不需要额外的同步开销
final的三种用法:
1.final修饰变量 被修饰后不能再被修改
2.final修饰方法 不允许修饰构造方法,不可被重写,也就是不能被@override
3.final修饰类 被修饰后这个类不能再被继承:例如典型的String类就是被final修饰过了的,所以不能继承String类
<19> 单例模式你会写吗?
什么是单例模式?
保证一个类只有一个实例,并且提供一个全局可以访问的入口,这就是单例模式(举个例子:分身,虽然有很多分身但是只有一个是真身)
为什么需要单例模式?
节省内存和计算,保证结果正确,方便管理
适用场景:无状态的工具类,全局的信息类
单例模式的8种写法
1.饿汉式(静态常量) [可用] 缺点:没有达到懒加载的效果
package com.example.profiles.singleton;
/**
* 描述:饿汉式 静态常量 (可用)
*/
public class Singleton1 {
// 单例模式要有私有的构造函数,不允许外界用构造函数来调用,自然也不能再新建类
private Singleton1(){
}
//实例化,但是被修饰成常量
private final static Singleton1 INSTANCE = new Singleton1();
// 对外暴露的方法:因为外界访问不到INSTANCE,就要创建一个公共的调用方法调用INSTANCE
public static Singleton1 getInstance(){
return INSTANCE;
}
}
2.饿汉式(静态代码块) [可用] 饿汉式的通病:再类加载的时候就会把对象实例化出来
package com.example.profiles.singleton;
/**
* 描述:饿汉式 静态代码块 (可用)
* 缺点:根据类加载的原则:会先加载静态代码块中的内容,
* 这样的话类就很块被实例化了,没有起到懒加载的效果,
* 也会造成一定的资源浪费,和前面一样的缺点
*/
public class Singleton2 {
// 单例模式要有私有的构造函数,不允许外界用构造函数来调用,自然也不能再新建类
private Singleton2(){
}
private final static Singleton2 INSTANCE;
// 和饿汉式(静态常量)不用的式,初始化时机:这个是静态代码块种初始化Singleton2
static {
// 初始化Singleton2
INSTANCE = new Singleton2();
}
// 对外暴露的方法:因为外界访问不到INSTANCE,就要创建一个公共的调用方法调用INSTANCE
public static Singleton2 getInstance(){
return INSTANCE;
}
}
3.懒汉式(线程不安全) [不可用]
懒汉式:再用到实例的时候才去实例化,这就实现了懒加载
package com.example.profiles.singleton;
/**
* 描述:懒汉式(线程不安全) [不可用]
* 不可用原因:线程不安全,当两个线程同时访问
* 下面代码的if判断的时候,就有可能实例化两个对象出来,
* 违背了单例模式的原则
*/
public class Singleton3 {
// 单例模式要有私有的构造函数,不允许外界用构造函数来调用,自然也不能再新建类
private Singleton3(){
}
// 加载类
private static Singleton3 INSTANCE;
// 对外暴露获取INSTANCE的方法
public static Singleton3 getInstance(){
// 判断没有没被初始化
if (INSTANCE == null){
// 没有就进行初始化
INSTANCE = new Singleton3();
}
// 否则就是被初始化过了,就直接返回INSTANCE
return INSTANCE;
}
}
4.懒汉式(线程安全,同步方法) [不推荐使用]
package com.example.profiles.singleton;
/**
* 描述:懒汉式(线程安全,同步方法) [不推荐使用]
* 缺点:因为加入了synchronized修饰方法,所以这个方法变成了同步方法;
* 线程访问需要一个个排队,这样效率很低
*/
public class Singleton4 {
// 单例模式要有私有的构造函数,不允许外界用构造函数来调用,自然也不能再新建类
private Singleton4(){
}
// 加载类
private static Singleton4 INSTANCE;
// 对外暴露获取INSTANCE的方法
// 这种方法相较于上面一个懒汉式,就是加入了synchronized修饰了方法,解决了线程不安全的问题
public synchronized static Singleton4 getInstance(){
// 判断没有没被初始化
if (INSTANCE == null){
// 没有就进行初始化
INSTANCE = new Singleton4();
}
// 否则就是被初始化过了,就直接返回INSTANCE
return INSTANCE;
}
}
5.懒汉式(线程不安全,同步代码块) [不可用]
package com.example.profiles.singleton;
/**
* 描述:懒汉式(线程不安全,同步代码块) [不可用]
* 不可用原因: 虽然用同步代码块把实例化代码包括起来,这样把同步的范围缩小,相比较上个效率会高一些
* 但是,其实还是线程不安全的,当两个线程是同时访问到这里的时候,上一个刚实例化完,下一个就进来实例化了
* 还是会造成实例化两个出来,违背单例模式的原则
*/
public class Singleton5 {
// 单例模式要有私有的构造函数,不允许外界用构造函数来调用,自然也不能再新建类
private Singleton5(){
}
// 加载类
private static Singleton5 INSTANCE;
// 对外暴露获取INSTANCE的方法
public static Singleton5 getInstance(){
// 判断没有没被初始化
if (INSTANCE == null){
// 当发现INSTANCE没有初始化的时候,进来初始化,用同步代码块包裹初始化过程
synchronized ( Singleton5.class){
INSTANCE = new Singleton5();
}
}
// 否则就是被初始化过了,就直接返回INSTANCE
return INSTANCE;
}
}
6.双重检查[推荐用](重点掌握)
package com.example.profiles.singleton;
/**
* 描述:双重检查[推荐用]***(重点掌握)
* 优点:用synchronized同步代码块缩小了同步范围,这样效率会高很多,
* 然后利用双重检查就相较上一种可用避免线程不安全的问题
*/
public class Singleton6 {
// 单例模式要有私有的构造函数,不允许外界用构造函数来调用,自然也不能再新建类
private Singleton6(){
}
// volatile是保证会完成对象的初始化,避免拿到不完整的对象,保证运行安全
private static volatile Singleton6 INSTANCE;
// 对外暴露获取INSTANCE的方法
public static Singleton6 getInstance(){
// 判断没有没被初始化
if (INSTANCE == null){
// 当发现INSTANCE没有初始化的时候,进来初始化,用同步代码块包裹初始化过程
synchronized ( Singleton6.class){
// 再检测一遍没有没被初始化
if (INSTANCE == null){
INSTANCE = new Singleton6();
}
}
}
// 否则就是被初始化过了,就直接返回INSTANCE
return INSTANCE;
}
}
7.静态内部类[推荐用]
package com.example.profiles.singleton;
/**
* 描述:静态内部类[推荐用]
* 关键点在于:它有个内部类,在这个内部类的里面把INSTANCE实例话
* 这种写法的好处在于,外面的这个类在被装载的时候,里面这个类并不会被装载
* 这就实现了懒加载,只有载用getInstance方法的时候,
* 才会访问到内部类里面的实例,才会被实例化出来,避免了内存的浪费
*/
public class Singleton7 {
private Singleton7(){
}
// 创建一个Singleton7 内部的类
private static class SingletonInstance{
private static Singleton7 INSTANCE = new Singleton7();
}
public static Singleton7 getInstance(){
return SingletonInstance.INSTANCE;
}
}
8.枚举[推荐用](重点掌握)
package com.example.profiles.singleton;
/**
* 描述:枚举[推荐用]**(重点掌握)
* 虽然还没被广泛使用,但是已经是目前实现单例模式最好的方法
* // 优点:1.写法简洁,且枚举不能实例化两个一模一样的
* 2.线程安全,枚举类的线程安全已经由虚拟机所保证了
* 3.防止反射,java规定枚举是不能被反射创建的,所以它天然的保证了反射的安全性
*/
public enum Singleton8 {
// 实例化,调用的时候只需要通过Singleton8直接调用
INSTANCE;
}
不同写法的对比
饿汉:简单,但是没有懒加载(lazy loading)
懒汉:有线程安全问题 静态内部类:可用
双重检查:面试用,推荐用
枚举:最好的写法饿汉式的缺点:没有懒加载,会造成一定的浪费资源 懒汉式的缺点:不容易保证线程安全
为什么要用双重检查?
如果只用一个检查就算有synchronized同步代码块保护也会有线程不安全的问题
为什么双重检查要用volatile?
保证一定会成功初始化,保证拿到一个完整的实例化对象,从而保证了运行安全(使用于双重检测)
哪种实现方案最好?
枚举最好,虽然还没被广泛使用,但是已经是目前实现单例模式最好的方法
优点:
1.写法简洁,且枚举不能实例化两个一模一样的 *
2.线程安全,枚举类的线程安全已经由虚拟机所保证了 *
3.防止反射,java规定枚举是不能被反射创建的,所以它天然的保证