Java面试常问的知识点

  • HashMap、LinkedHashMap、ConcurrentHashMap、ArrayList、LinkedList的底层实现。 答:HashMap是基于哈希表的Map接口的非同步实现,它允许null键和null值,且HashMap依托于它的数据结构的设计,存储效率特别高,这是我用它的原因 HashMap是基于hash算法实现的,通过put(key,value)存储对象到HashMap中,也可以通过get(key)从HashMap中获取对象。当我们使用put的时候,首先HashMap会对key的hashCode()的值进行hash计算,根据hash值得到这个元素在数组中的位置,将元素存储在该位置的链表上。当我们使用get的时候,首先HashMap会对key的hashCode()的值进行hash计算,根据hash值得到这个元素在数组中的位置,将元素从该位置上的链表中取出
  • HashMap和Hashtable的区别。 Hashtable方法是同步的 HashMap方法是非同步的 Hashtable基于Dictionary类 HashMap基于AbstractMap,而AbstractMap基于Map接口的实现 Hashtable中key和value都不允许为null,遇到null,直接返回 NullPointerException HashMap中key和value都允许为null,遇到key为null的时候,调用putForNullKey方法进行处理,而对value没有处理 Hashtable中hash数组默认大小是11,扩充方式是old*2+1 HashMap中hash数组的默认大小是16,而且一定是2的指数
  • ArrayList、LinkedList、Vector的区别。 LinkedList底层是双向链表 ArrayList底层是可变数组 LinkedList不允许随机访问,即查询效率低 ArrayList允许随机访问,即查询效率高 LinkedList插入和删除效率快 ArrayList插入和删除效率低

Vector是基于可变数组的List接口的同步实现 Vector是有序的 Vector允许null键和null值 Vector已经不建议使用了

  • LinkedHashMap与HashMap的区别 LinkedHashMap有序的,有插入顺序和访问顺序 HashMap无序的 LinkedHashMap内部维护着一个运行于所有条目的双向链表 HashMap内部维护着一个单链表
  • HashMap和ConcurrentHashMap的区别。 ConcurrentHashMap基于双数组和链表的Map接口的同步实现 ConcurrentHashMap中元素的key是唯一的、value值可重复 ConcurrentHashMap不允许使用null值和null键 ConcurrentHashMap是无序的线程安全的
  • HashMap和LinkedHashMap的区别。 LinkedHashMap有序的,有插入顺序和访问顺序 HashMap无序的 LinkedHashMap内部维护着一个运行于所有条目的双向链表 HashMap内部维护着一个单链表

LinkedHashMap底层使用哈希表与双向链表来保存所有元素,它维护着一个运行于所有条目的双向链表(如果学过双向链表的同学会更好的理解它的源代码),此链表定义了迭代顺序,该迭代顺序可以是插入顺序或者是访问顺序 1.按插入顺序的链表:在LinkedHashMap调用get方法后,输出的顺序和输入时的相同,这就是按插入顺序的链表,默认是按插入顺序排序 2.按访问顺序的链表:在LinkedHashMap调用get方法后,会将这次访问的元素移至链表尾部,不断访问可以形成按访问顺序排序的链表。简单的说,按最近最少访问的元素进行排序(类似LRU算法)

  • HashMap是线程安全的吗。 不是线程安全的,方法一:通过Collections.synchronizedMap()返回一个新的Map,这个新的map就是线程安全的. 这个要求大家习惯基于接口编程,因为返回的并不是HashMap,而是一个Map的实现. 方法二:重新改写了HashMap,具体的可以查看java.util.concurrent.ConcurrentHashMap. 这个方法比方法一有了很大的改进. 方法一使用的是的synchronized方法,是一种悲观锁.在进入之前需要获得锁,确保独享当前对象,然后做相应的修改/读取. 方法二使用的是乐观锁,只有在需要修改对象时,比较和之前的值是否被人修改了,如果被其他线程修改了,那么就会返回失败.锁的实现,使用的是NonfairSync. 这个特性要确保修改的原子性,互斥性,无法在JDK这个级别得到解决,JDK在此次需要调用JNI方法,而JNI则调用CAS指令来确保原子性与互斥性. 当如果多个线程恰好操作到ConcurrentHashMap同一个segment上面,那么只会有一个线程得到运行,其他的线程会被LockSupport.park(),稍后执行完成后,会自动挑选一个线程来执行LockSupport.unpark(). 如何得到/释放锁 得到锁: 方法一:在Hashmap上面,synchronized锁住的是对象(不是Class),所以第一个申请的得到锁,其他线程将进入阻塞,等待唤醒. 方法二:检查AbstractQueuedSynchronizer.state,如果为0,则得到锁,或者申请者已经得到锁,则也能再辞得到锁,并且state也加1. 释放锁:都是得到锁的逆操作,并且使用正确,二种方法都是自动选取一个队列中的线程得到锁可以获得CPU资源. 优缺点 方法一:优点:代码实现十分简单,一看就懂. 缺点:从锁的角度来看,方法一直接使用了锁住方法,基本上是锁住了尽可能大的代码块.性能会比较差. 方法二:优点:需要互斥的代码段比较少,性能会比较好. ConcurrentHashMap把整个Map切分成了多个块,发生锁碰撞的几率大大降低,性能会比较好. 缺点:代码实现稍稍复杂些.
  • ConcurrentHashMap是怎么实现线程安全的。 ConcurrentHashMap的数据结构为一个Segment数组,Segment的数据结构为HashEntry的数组,而HashEntry存的是我们的键值对,可以构成链表。可以简单的理解为数组里装的是HashMap ConcurrentHashMap定位一个元素的过程需要进行两次Hash操作,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部,因此,这一种结构的带来的副作用是Hash的过程要比普通的HashMap要长,但是带来的好处是写操作的时候可以只对元素所在的Segment进行加锁即可,不会影响到其他的Segment。正是因为其内部的结构以及机制,ConcurrentHashMap在并发访问的性能上要比Hashtable和同步包装之后的HashMap的性能提高很多。在理想状态下,ConcurrentHashMap 可以支持 16 个线程执行并发写操作(如果并发级别设置为 16),及任意数量线程的读操作
  • HashMap 的长度为什么是2的幂次方 因为HashMap的数据结构是数组和链表的结合,其中里面数组的默认长度是16,HashMap的长度为2的幂次方的原因是为了减少Hash碰撞,尽量使Hash算法的结果均匀分布。

多线程并发相关问题(必问)

  • 创建线程的3种方式。 一、继承Thread类创建线程类 (1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。 (2)创建Thread子类的实例,即创建了线程对象。 (3)调用线程对象的start()方法来启动该线程。 二、通过Runnable接口创建线程类 (1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。 (2)创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。 (3)调用线程对象的start()方法来启动该线程。 三、通过Callable和Future创建线程 (1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。 (2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。 (3)使用FutureTask对象作为Thread对象的target创建并启动新线程。 (4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

二、创建线程的三种方式的对比 采用实现Runnable、Callable接口的方式创见多线程时,优势是: 线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。 在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

劣势是: 编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。 使用继承Thread类的方式创建多线程时优势是: 编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。

劣势是: 线程类已经继承了Thread类,所以不能再继承其他父类。

  • 什么是线程安全。 线程安全就是在多线程环境下也不会出现数据不一致,而非线程安全就有可能出现数据不一致的情况。
  • Runnable接口和Callable接口的区别。 不同之处: 1.Callable可以返回一个类型,而Runnable不可以 2.Callable能够抛出checked exception,而Runnable不可以。 3.Runnable是自从java1.1就有了,而Callable是1.5之后才加上去的 4.Callable和Runnable都可以应用于executors。而Thread类只支持Runnable.
  • wait方法和sleep方法的区别。 sleep()方法必须传入参数,参数就是休眠时间,时间到了就会自动醒来 wait()方法可以传入参数也可以不传入参数,传入参数就是在参数结束的时间后开始等待,不穿如参数就是直接等待。 sleep方法必须要捕获异常,而wait方法不需要捕获异常。
  • synchronized、Lock、ReentrantLock、ReadWriteLock。 synchronized: 是JVM实现的一种锁, 用于同步方法和代码块,执行完后自动释放锁。其中锁的获取和释放分别是monitorenter和monitorexit指令,该锁在实现上分为了偏向锁、轻量级锁和重量级锁,其中偏向锁在1.6是默认开启的,轻量级锁在多线程竞争的情况下会膨胀成重量级锁,有关锁的数据都保存在对象头中。 Lock:Lock是一个锁的接口,提供获取锁和解锁的方法(lock,trylock,unlock) ReentrantLock:ReentrantLock是k是Lock接口的实现类,基于AQS实现的,在AQS内部会保存一个状态变量state,通过CAS修改该变量的值,修改成功的线程表示获取到该锁,没有修改成功,或者发现状态state已经是加锁状态,则通过一个Waiter对象封装线程,添加到等待队列中,并挂起等待被唤醒。 ReadWriteLock :可以实现读写锁,当读取的时候线程会获得read锁,其他线程也可以获 得read锁同时并发的去读取,但是写程序运行获取到write锁的时候,其他线程是不能进行操作的,因为write是排它锁,而上面介绍的两种不管你是read还是write没有抢到锁的线程都会被阻塞或者中断,它也是个接口,里面定义了两种方法readLock()和writeLock(),他的一个实现类是ReentrantReadWriteLock。
  • 介绍下CAS(无锁技术)。 CAS(Compare and Swap),即比较并替换,实现并发算法时常用到的一种技术,CAS是通过unsafe类的compareAndSwap方法实现的。CAS的思想很简单:三个参数,一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。 缺点:CAS存在一个很明显的问题,即ABA问题。 问题:如果变量V初次读取的时候是A,并且在准备赋值的时候检查到它仍然是A,那能说明它的值没有被其他线程修改过了吗? 如果在这段期间曾经被改成B,然后又改回A,那CAS操作就会误认为它从来没有被修改过。针对这种情况,java并发包中提供了一个带有标记的原子引用类AtomicStampedReference,它可以通过控制变量值的版本来保证CAS的正确性。
  • volatile关键字的作用和原理。 作用:保持内存可见性(内存可见性(Memory Visibility):所有线程都能看到共享内存的最新状态。) 原理:观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏)它会强制将对缓存的修改操作立即写入主存如果是写操作,它会导致其他CPU中对应的缓存行无效
  • 什么是ThreadLocal。 线程变量,一个以ThreadLocal对象为键、任意对象为值的存储结构,每个ThreadLocal可以放一个线程级别的变量,但是它本事可以被多个线程共享使用,而且又可以达到线程安全的目的,且绝对线程安全。 常用的3个方法:set()、get()、remove()。都是线程安全的。
  • 创建线程池的4种方式。 1.newSingleThreadExecutor 创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。 2.newFixedThreadPool 创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。 3.newCachedThreadPool 创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程, 那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。 4.newScheduledThreadPool 创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
  • ThreadPoolExecutor的内部工作原理。
  • 分布式环境下,怎么保证线程安全。 向线程池提交一个任务后,它的主要处理流程如下图所示 一个线程从被提交(submit)到执行共经历以下流程: 线程池判断核心线程池里是的线程是否都在执行任务,如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下一个流程 线程池判断工作队列是否已满。如果工作队列没有满,则将新提交的任务储存在这个工作队列里。如果工作队列满了,则进入下一个流程。 线程池判断其内部线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已满了,则交给饱和策略来处理这个任务。

JVM相关问题

  • 介绍下垃圾收集机制(在什么时候,对什么,做了什么)。 标记-清除算法、复制算法、标记-整理算法、分代收集算法 主要是对一些静态对象做清理和整理的工作

  • 垃圾收集有哪些算法,各自的特点。 标记-清除算法、复制算法、标记-整理算法、分代收集算法

  • 类加载的过程。 整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。

  • 双亲委派模型。 双亲委托模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需要加载的类)时,子加载器才会尝试自己去加载。 使用双亲委托机制的好处是:能够有效确保一个类的全局唯一性,当程序中出现多个限定名相同的类时,类加载器在执行加载时,始终只会加载其中的某一个类。

  • 有哪些类加载器。 分别是 bootstrapClassLoader (主要加载java核心api) , ExtClassLoaders是扩展类的类加载器,AppClassLoader 程序类加载器,还有一个是用户继承ClassLoader重写的类加载器。

  • 能不能自己写一个类叫java.lang.String。 可以,但是即使你写了这个类,也没有用。 这个问题涉及到加载器的委托机制,在类加载器的结构图(在下面)中, BootStrap是顶层父类,ExtClassLoader是BootStrap类的子类,ExtClassLoader又是AppClassLoader的父类 这里以java.lang.String为例,当我是使用到这个类时,Java虚拟机会将java.lang.String类的字节码加载到内存中。

数据库相关问题,针对MySQL(必问)

  • 给题目让你手写SQL。
  • 有没有SQL优化经验。
  • MySQL索引的数据结构。
  • SQL怎么进行优化。
  • SQL关键字的执行顺序。 from->on->join->where->group by->having->select->distinct->union
  • 有哪几种索引。 普通索引:仅加速查询 唯一索引:加速查询 + 列值唯一(可以有null) 主键索引:加速查询 + 列值唯一(不可以有null)+ 表中只有一个 组合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并 全文索引:对文本的内容进行分词,进行搜索
  • 什么时候该(不该)建索引。 一般来说,在WHERE和JOIN中出现的列需要建立索引,但也不完全如此,因为MySQL只对<,<=,=,>,>=,BETWEEN,IN,以及某些时候的LIKE才会使用索引。
  • Explain包含哪些列。 expain出来的信息有10列,分别是id、select_type、table、type、possible_keys、key、key_len、ref、rows、Extra,

框架相关问题

  • Hibernate和Mybatis的区别。

  • Spring MVC和Struts2的区别。

  • Spring用了哪些设计模式。 1.工厂模式,这个很明显,在各种BeanFactory以及ApplicationContext创建中都用到了; 2.模版模式,这个也很明显,在各种BeanFactory以及ApplicationContext实现中也都用到了; 3.代理模式,在Aop实现中用到了JDK的动态代理; 4.单例模式,这个比如在创建bean的时候。 5.策略模式在Java中的应用,这个太明显了,因为Comparator这个接口简直就是为策略模式而生的。Comparable和Comparator的区别一文中,详细讲了Comparator的使用。比方说Collections里面有一个sort方法,因为集合里面的元素有可能是复合对象,复合对象并不像基本数据类型,可以根据大小排序,复合对象怎么排序呢?基于这个问题考虑,Java要求如果定义的复合对象要有排序的功能,就自行实现Comparable接口或Comparator接口. 6.原型模式:使用原型模式创建对象比直接new一个对象在性能上好得多,因为Object类的clone()方法是一个native方法,它直接操作内存中的二进制流,特别是复制大对象时,性能的差别非常明显。 7.迭代器模式:Iterable接口和Iterator接口 这两个都是迭代相关的接口,可以这么认为,实现了Iterable接口,则表示某个对象是可被迭代的;Iterator接口相当于是一个迭代器,实现了Iterator接口,等于具体定义了这个可被迭代的对象时如何进行迭代的

  • Spring中AOP主要用来做什么。 1.Spring声明式事务管理配置。 2.Controller层的参数校验。 3.使用Spring AOP实现MySQL数据库读写分离案例分析 4.在执行方法前,判断是否具有权限。 5.对部分函数的调用进行日志记录。监控部分重要函数,若抛出指定的异常,可以以短信或邮件方式通知相关人员。 6.信息过滤,页面转发等等功能,博主一个人的力量有限,只能列举这么多,欢迎评论区对文章做补充。

  • Spring注入bean的方式。 使用属性的setter方法注入 这是最常用的方式; 使用构造器注入; 使用Filed注入(接口注入).

  • 什么是IOC,什么是依赖注入。 inverse of controll IOC所谓控制反转就是把创建对象(bean)和维护对象(bean)的关系的权利从程序中转移到spring的容器文件(就是spring的配置文件),程序不再维护 依赖注入实在编译阶段商未知所需的功能来自哪个类的情况下,将其他的对象所依赖的功能对象实例化的模式。这就需要一种极致来激活相应的组件已提供特定的功能,所依赖注入是控制反转的基础。否则如果在组件不受框架控制的情况下,框架有怎么知道要创建那个组件?

  • Spring是单例还是多例,怎么修改。 spring默认是单例模式(就每个请求都是用的同一对象,对于dao层肯定是棒棒的),但是有的时候,我们需要每个请求都 产生一个新的对象,就如做微信小程序,用scoket、不可能一直都用一个来接收的,因为需要分配房间,所以需要使用到多例。 可以修改为多例,在此bean节点中添加一个属性,scope=“prototype",例如

  • Spring事务隔离级别和传播性。

  • 介绍下Mybatis/Hibernate的缓存机制。

  • Mybatis的mapper文件中#和$的区别。

  • Mybatis的mapper文件中resultType和resultMap的区别。

其他遇到问题

  • 介绍下栈和队列。
  • IO和NIO的区别。 Java NIO和IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。
  • 接口和抽象类的区别。 接口和抽象类的概念不一样。接口是对动作的抽象,抽象类是对根源的抽象。 1、抽象类和接口都不能直接实例化,如果要实例化,抽象类变量必须指向实现所有抽象方法的子类对象,接口变量必须指向实现所有接口方法的类对象。 2、抽象类要被子类继承,接口要被类实现。 3、接口只能做方法申明,抽象类中可以做方法申明,也可以做方法实现 4、接口里定义的变量只能是公共的静态的常量,抽象类中的变量是普通变量。 5、抽象类里的抽象方法必须全部被子类所实现,如果子类不能全部实现父类抽象方法,那么该子类只能是抽象类。同样,一个实现接口的时候,如不能全部实现接口方法,那么该类也只能为抽象类。 6、抽象方法只能申明,不能实现,接口是设计的结果 ,抽象类是重构的结果 7、抽象类里可以没有抽象方法 8、如果一个类里有抽象方法,那么这个类只能是抽象类 9、抽象方法要被实现,所以不能是静态的,也不能是私有的。 10、接口可继承接口,并可多继承接口,但类只能单根继承。
  • int和Integer的自动拆箱/装箱相关问题。
  • 常量池相关问题。
  • ==和equals的区别。
  • 什么是JDK?什么是JRE?什么是JVM?三者之间的联系与区别
  • Java和C++的区别
  • 重载和重写的区别。
  • String和StringBuilder、StringBuffer的区别。 1、首先说运行速度,或者说是执行速度,在这方面运行速度快慢为:StringBuilder > StringBuffer > String String最慢的原因:   String为字符串常量,而StringBuilder和StringBuffer均为字符串变量,即String对象一旦创建之后该对象是不可更改的,但后两者的对象是变量,是可以更改的。 2、在线程安全上,StringBuilder是线程不安全的,而StringBuffer是线程安全的 总结一下   String:适用于少量的字符串操作的情况   StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况   StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况
  • 静态变量、实例变量、局部变量线程安全吗,为什么。 静态变量:线程非安全。加static关键字的变量,只能放在类里,不能放到方法里。静态变量有默认初始化值。一旦静态变量被修改,其他对象均对修改可见,故线程非安全。 实例变量:单例模式(只有一个对象实例存在)线程非安全,非单例线程安全。 局部变量:线程安全。 1、局部变量只定义在局部范围内,如:函数内,for循环语句内等,只在所属的区域有效。 2、局部变量存在于栈内存中,作用的范围结束,变量空间会自动释放。 3、局部变量没有默认初始化值 4、在使用变量时需要遵循的原则为:就近原则,先找局部变量,再找实例变量(如果同名,实例变量需要用this关键字引用) 5、局部变量不能逐级重名,比如函数内定义过一个变量,就不能在for循环内定义相同的变量(两层for循环一个用i一个用j也是这个道理)由于每个线程执行时将会把局部变量放在各自栈帧的工作内存中,线程间不共享,故不存在线程安全问题。
  • try、catch、finally都有return语句时执行哪个。 任何执行try 或者catch中的return语句之前,都会先执行finally语句,如果finally存在的话。 如果finally中有return语句,那么程序就return了,所以finally中的return是一定会被return的, 编译器把finally中的return实现为一个warning。
  • 介绍下B树、二叉树。
  • 分布式锁的实现。 基于数据库实现分布式锁 基于缓存(Redis,memcached,tair)实现分布式锁 基于Zookeeper实现分布式锁 基于数据库表

要实现分布式锁,最简单的方式可能就是直接创建一张锁表,然后通过操作该表中的数据来实现了。 当我们要锁住某个方法或资源时,我们就在该表中增加一条记录,想要释放锁的时候就删除这条记录。 创建这样一张数据库表: CREATE TABLE methodLock ( id int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', method_name varchar(64) NOT NULL DEFAULT '' COMMENT '锁定的方法名', desc varchar(1024) NOT NULL DEFAULT '备注信息', update_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成', PRIMARY KEY (id), UNIQUE KEY uidx_method_name (method_name) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法'; 当我们想要锁住某个方法时,执行以下SQL: insert into methodLock(method_name,desc) values (‘method_name’,‘desc’) 因为我们对method_name做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。 当方法执行完毕之后,想要释放锁的话,需要执行以下Sql: delete from methodLock where method_name ='method_name' 上面这种简单的实现有以下几个问题: 1、这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。 2、这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。 3、这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。 4、这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。 当然,我们也可以有其他方式解决上面的问题。 数据库是单点?搞两个数据库,数据之前双向同步。一旦挂掉快速切换到备库上。 没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。 非阻塞的?搞一个while循环,直到insert成功再返回成功。 非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。 基于数据库排他锁

除了可以通过增删操作数据表中的记录以外,其实还可以借助数据中自带的锁来实现分布式的锁。 我们还用刚刚创建的那张数据库表。可以通过数据库的排他锁来实现分布式锁。 基于MySQL的InnoDB引擎,可以使用以下方法来实现加锁操作: public boolean lock(){ connection.setAutoCommit(false) while(true){ try{ result = select * from methodLock where method_name=xxx for update; if(result==null){ return true; } }catch(Exception e){

    }
    sleep(1000);
}
return false;
复制代码

} 在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。 我们可以认为获得排它锁的线程即可获得分布式锁,当获取到锁之后,可以执行方法的业务逻辑,执行完方法之后,再通过以下方法解锁: public void unlock(){ connection.commit(); } 通过connection.commit()操作来释放锁。 这种方法可以有效的解决上面提到的无法释放锁和阻塞锁的问题。 阻塞锁? for update语句会在执行成功后立即返回,在执行失败时一直处于阻塞状态,直到成功。 锁定之后服务宕机,无法释放?使用这种方式,服务宕机之后数据库会自己把锁释放掉。 但是还是无法直接解决数据库单点和可重入问题。 总结

总结一下使用数据库来实现分布式锁的方式,这两种方式都是依赖数据库的一张表,一种是通过表中的记录的存在情况确定当前是否有锁存在,另外一种是通过数据库的排他锁来实现分布式锁。 数据库实现分布式锁的优点 直接借助数据库,容易理解。 数据库实现分布式锁的缺点 会有各种各样的问题,在解决问题的过程中会使整个方案变得越来越复杂。 操作数据库需要一定的开销,性能问题需要考虑。 基于缓存实现分布式锁

相比较于基于数据库实现分布式锁的方案来说,基于缓存来实现在性能方面会表现的更好一点。而且很多缓存是可以集群部署的,可以解决单点问题。 目前有很多成熟的缓存产品,包括Redis,memcached以及我们公司内部的Tair。 这里以Tair为例来分析下使用缓存实现分布式锁的方案。关于Redis和memcached在网络上有很多相关的文章,并且也有一些成熟的框架及算法可以直接使用。 基于Tair的实现分布式锁其实和Redis类似,其中主要的实现方式是使用TairManager.put方法来实现。 public boolean trylock(String key) { ResultCode code = ldbTairManager.put(NAMESPACE, key, "This is a Lock.", 2, 0); if (ResultCode.SUCCESS.equals(code)) return true; else return false; } public boolean unlock(String key) { ldbTairManager.invalid(NAMESPACE, key); } 以上实现方式同样存在几个问题: 1、这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在tair中,其他线程无法再获得到锁。 2、这把锁只能是非阻塞的,无论成功还是失败都直接返回。 3、这把锁是非重入的,一个线程获得锁之后,在释放锁之前,无法再次获得该锁,因为使用到的key在tair中已经存在。无法再执行put操作。 当然,同样有方式可以解决。 没有失效时间?tair的put方法支持传入失效时间,到达时间之后数据会自动删除。 非阻塞?while重复执行。 非可重入?在一个线程获取到锁之后,把当前主机信息和线程信息保存起来,下次再获取之前先检查自己是不是当前锁的拥有者。 但是,失效时间我设置多长时间为好?如何设置的失效时间太短,方法没等执行完,锁就自动释放了,那么就会产生并发问题。如果设置的时间太长,其他获取锁的线程就可能要平白的多等一段时间。这个问题使用数据库实现分布式锁同样存在 总结

可以使用缓存来代替数据库来实现分布式锁,这个可以提供更好的性能,同时,很多缓存服务都是集群部署的,可以避免单点问题。并且很多缓存服务都提供了可以用来实现分布式锁的方法,比如Tair的put方法,redis的setnx方法等。并且,这些缓存服务也都提供了对数据的过期自动删除的支持,可以直接设置超时时间来控制锁的释放。 使用缓存实现分布式锁的优点 性能好,实现起来较为方便。 使用缓存实现分布式锁的缺点 通过超时时间来控制锁的失效时间并不是十分的靠谱。 基于Zookeeper实现分布式锁

基于zookeeper临时有序节点可以实现的分布式锁。 大致思想即为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。 来看下Zookeeper能不能解决前面提到的问题。 锁无法释放?使用Zookeeper可以有效的解决锁无法释放的问题,因为在创建锁的时候,客户端会在ZK中创建一个临时节点,一旦客户端获取到锁之后突然挂掉(Session连接断开),那么这个临时节点就会自动删除掉。其他客户端就可以再次获得锁。 非阻塞锁?使用Zookeeper可以实现阻塞的锁,客户端可以通过在ZK中创建顺序节点,并且在节点上绑定监听器,一旦节点有变化,Zookeeper会通知客户端,客户端可以检查自己创建的节点是不是当前所有节点中序号最小的,如果是,那么自己就获取到锁,便可以执行业务逻辑了。 不可重入?使用Zookeeper也可以有效的解决不可重入的问题,客户端在创建节点的时候,把当前客户端的主机信息和线程信息直接写入到节点中,下次想要获取锁的时候和当前最小的节点中的数据比对一下就可以了。如果和自己的信息一样,那么自己直接获取到锁,如果不一样就再创建一个临时的顺序节点,参与排队。 单点问题?使用Zookeeper可以有效的解决单点问题,ZK是集群部署的,只要集群中有半数以上的机器存活,就可以对外提供服务。 可以直接使用zookeeper第三方库Curator客户端,这个客户端中封装了一个可重入的锁服务。 public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { try { return interProcessMutex.acquire(timeout, unit); } catch (Exception e) { e.printStackTrace(); } return true; } public boolean unlock() { try { interProcessMutex.release(); } catch (Throwable e) { log.error(e.getMessage(), e); } finally { executorService.schedule(new Cleaner(client, path), delayTimeForClean, TimeUnit.MILLISECONDS); } return true; } Curator提供的InterProcessMutex是分布式锁的实现。acquire方法用户获取锁,release方法用于释放锁。 使用ZK实现的分布式锁好像完全符合了本文开头我们对一个分布式锁的所有期望。但是,其实并不是,Zookeeper实现的分布式锁其实存在一个缺点,那就是性能上可能并没有缓存服务那么高。因为每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。ZK中创建和删除节点只能通过Leader服务器来执行,然后将数据同不到所有的Follower机器上。 总结

使用Zookeeper实现分布式锁的优点 有效的解决单点问题,不可重入问题,非阻塞问题以及锁无法释放的问题。实现起来较为简单。 使用Zookeeper实现分布式锁的缺点 性能上不如使用缓存实现分布式锁。 需要对ZK的原理有所了解。 三种方案的比较

从理解的难易程度角度(从低到高)

数据库 > 缓存 > Zookeeper 从实现的复杂性角度(从低到高)

Zookeeper >= 缓存 > 数据库 从性能角度(从高到低)

缓存 > Zookeeper >= 数据库 从可靠性角度(从高到低)

Zookeeper > 缓存 > 数据库

  • 分布式session存储解决方案。
  • 常用的linux命令。
  • redis集群原理。 一、设置redis服务器先经过CRC16哈希到一个指定的Node上范围是0-16384 (平均分配,不能重复也不能缺失,否则会导致对象重复存储或无法存储,比如:三台啊服务器:节点1分配0-5600,节点二分配应该书5601-12000,节点3,12001-16384). 二、当数据要保存到redis时,通过CRC16哈希到一个指定RC16哈希值,保存在对应的节点上。 三、获取,当要获取一个数据时,先通过key获取到RC16哈希值,再通过RC16哈希值找到对应的节点,然后就能在对应的节点马上找到kye的值了。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值