1.索引的分类
主键索引,联合索引,唯一索引,全文索引,普通索引
2.锁的分类
行锁:操作数据库时,锁定整行数据
表锁:操作数据库时,锁定整表数据
乐观锁:多线程情况下,认为其他线程不会去修改的数据,所以不会上锁,但是会在跟新时,判断一下数据有没有被改变,可以用版本号机制
悲观锁:每次拿数据时,都会认为别人会修改,每次去拿数据时都会上锁,例如synchronized,ReentrantLook等独占锁就是悲观锁的思想,优点,保证数据的独占性和正确性,缺点:效率不高
可重入锁:可以并发于多余一个的任务,不用担心数据的准确性,可以随时被中断,并且数据不会丢失
不可重入锁::只能并发于一个任务,一旦被中断,数据可能丢失
公平锁:线程公平竞争资源
非公平锁:线程不公平竞争资源,synchronized为不公平锁
3.如何避免线程死锁
避免一个线程同时获得多个锁
避免一个线程在锁内同时占用多个资源
尝试使用定时锁,lock.tryLock(timeOut)
4.synchronized的底层原理
synchronized保证方法或代码块,同一时刻,只能被一个线程访问,同时保证共享变量的内存可见性
java中每一个对象都可以当做synchronized的锁
底层使用monitor监视器锁的对象来完成,每个对象都有一个监视器锁,被synchronized修饰过的代码当它的monitor被占用时就会处于锁定状态,并尝试获取monitor所有权
过程:
如果monitor进入数为0,则线程进入后,将monitor进入数设置为1,该线程为monitor的所有者
如果该线程已经拥有monitor,如果重新进入,则进入数加1
如果其他线程拥有monitor,则线程进入阻塞状态,直到monitor的进入数为0,再次尝试获取monitor的所有权
synchronized可重入的原理:
重入锁是指一个线程获取到锁之后,该线程可以继续获得该锁,底层原理维护一个计数器,当线程获取到该锁时,计数器加1,再次获得该锁时,计数器再加1,释放锁时,计数器减1,当计数器为0时,表示该锁未被任何线程拥有,其他线程可以竞争获取锁
5.synchronized和volatile的区别
synchronized表示同一时间内只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程.
volatile表示变量在cpu的寄存器中是不确定的,必须从主内存中读取,保证多线程环境下变量的可见性;禁止指令重排
区别:
volatile是变量修饰符;synchronized可以修饰方法以及代码块
volatile保证变量修改的可见性,不保证原子性;synchronized保证变量修改可见性和原子性
volatile不会造成线程阻塞,synchronized可能会造成线程阻塞
volatile禁止指令重排
volatile关键字是线程同步的轻量级实现,性能比synchronized好,实际开发中用synchronized的场景比较多
6.volatile的功能是什么,禁止指令重排的目的是什么
volatile关键字的作用就是保证多线程之间共享变量的可见性,以及禁止指令重排,让程序在多线程的情况下可以安全的操作变量
指令重排会导致以下问题:
1.可见性问题,在多线程的环境下,当一个线程修改了某个变量的值时,由于重排后的指令执行顺序不确定,其他线程可能无法立即看到这个修改,这可能会导致某个线程在操作某个变量时,操作的是它的旧值,从而引发并发问题.如果一个变量被声明为volatile,那么当一个线程修改了这个变量,其他线程就可以立刻感知到这个值的变化
2.有序性问题,指令重排可能会改变指令的执行顺序,从而改变程序的执行结果.在一些需要保证指令执行顺序的场景下(如单例模式双检锁),指令重排可能会导致程序出问题
3.死锁问题,指令重排可能会导致线程出现死锁现象,例如线程a执行完语句1后,被挂起了,线程b此时得到cpu执行权,执行完语句2后,线程b也被挂起了.如果此时cpu继续执行线程a,执行语句3的同时发生了上下文切换,那么线程b就无法激活,这时就出现了死锁的问题
7.synchronized和lock的区别
1.synchronized是java内置关键字,在jvm层面,lock是个java类
2.synchronized无法判断获取锁的状态,lock可以判断是否获取到锁
3.synchronized会自动释放锁,lock 需要再finally中调用unlock()方法手动释放锁,否则容易造成线程死锁
4.用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待.如果线程1阻塞,线程2就会一直等待下去,等待锁时不可中断,而lock锁就不一定会等待下去,如果尝试获取不到锁时可中断响应
5.synchronized的锁可重入,非公平,而lock锁可重入,可公平
8.什么是cas
compare and swap的缩写,比较与交换,是一种乐观锁
cas包含3个操作数 内存位置(v),预期原值(A)和新值(B)如果内存地址的里面的值和A的值是一样的,那么就把内存里面的值更新成B.cas是通过无限循环来获取数据的,如果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能执行
java.util.concurrent.atomic包下的类大多是使用cas操作
9.cas会产生的问题
aba问题,比如一个线程1从内存位置v中取出a,这时另外一个线程2也才内存中取出a,并且线程2进行了一些操作将值变成了b,然后又将数据变成a,这时线程1进行cas操作发现内存中仍然是a,然后线程1操作成功,可能会出现浅藏的问题.从jdk1.5开始的atomic包中提供了带邮戳的引原子类AtomicStampedReference来解决aba的问题,还可以用加锁的方式解决
循环时间长,开销大,对于线程冲突严重的情况,cas自旋的概率会比较大,从而浪费更多的cpu资源,效率低于synchronized
只能保证一个共享变量的原子操作,当对一个共享变量执行操作时,我们可以使用循环cas的方式来保证原子操作,但是对于多个共享变量操作时,循环cas无法保证操作的原子性,这时可以使用锁
10.synchronized,volatile,cas比较
synchronized是悲观锁,属于抢占式,会引起其他线程阻塞
volatile提供多线程共享变量可见性和禁止指令重排序优化
cas是基于冲突检测的乐观锁
11.分布式锁
分布式锁,就是在多台服务器之间,用来解决分布式系统中控制共享资源访问的问题,以保证共享资源安全的锁
基于数据库实现分布式锁
1.基于数据表增删是最简单的方式,首先创建一张锁表,包含以下字段:类的全路径名+方法名,时间戳等字段
具体使用方式:当需要锁住某个方法时,往该表插入一条相关的记录.类的全路径名+方法名是有唯一约束的,如果有多个请求同时提交到数据库的,数据会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体的内容,执行完毕后,需要delete该记录.
2.基于数据库的排他锁
基于mysql的innoDb引擎,可以使用下面方法来实现加锁操作
在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁获得排他锁的线程即可获得分布式锁,当获得锁之后,可以执行方法的业务逻辑,执行完方法之后,释放锁connection.commit()
当某条数据被加上排他锁之后,其他线程无法获取排他锁并被阻塞
基于redis的分布式锁
synchronized只能用于jvm中的高并发,无法用于分布式集群下的高并发,只能通过分布锁
redis分布式锁,可以使用两种方式来实现
1.利用redis提供的 SET key value NX PX milliseconds指令
NX代表key不存在时才能对key进行设置操作,px 5000设置key的过期时间为5000毫秒
基于返回值的boolean值来判断锁的占用情况从而实现分布式锁
2.基于redission客户端来实现分布式锁,redission提供了分布式锁的封装方法,我们只需要调用api中的lock()和unlock()方法, 解决了封装锁实现的细节和复杂度
redission所有的指令都是通过lua脚本执行并支持lua脚本的原子性,redission中有一个warchDog(看门狗)的概念,它会在获取到锁之后,每隔10s把key的过期时间设置为30s,就算一直持有锁也不会出现key过期,看门狗的逻辑保证了没有死锁发生
可能会出现的问题,如果client向master获取锁之后同步给slave,如果client获取锁成功后master节点挂掉,并且未将该锁同步到slave,之后再sentinel的帮助下slave升级为master但是并没有之前为同步的锁的信息,此时如果有新的client要在新master获取锁,那么可能出现两个client持有同一把锁的问题
解决方案:设置一个唯一值random_value,用来校验自己的锁只能自己释放
SET lock_key random_value NX PX 5000
redlock算法原理,是在分布式环境中,假如有N个独立的节点,在每个阶段上使用的方式跟单节点一样
redlock算法要求:
1.客户端获取一个锁,需要想超过半数的节点获取锁,只有在超过半数节点获取锁成功后,才算获取成功,这个过程是有时间损耗的
2.过期时间(ttl)>业务执行时间+加锁时间+始终漂移时间
3.加锁失败后,要有重试次数限制
4.若master挂了,master要延迟启动,可以延迟ttl时间
3.基于zookeeper分布式锁
基于zookeeper分布式的方案
推荐有序节点来实现:
第一步,每个线程或进程在zookeeper上的/lock目录下创建一个临时有序的节点表示去抢占锁,所有创建的节点会按照先后顺序生成一个带有序编号的节点
第二步,线程创建节点后,获取/lock节点下面的所有子节点,判断当前线程创建的节点是否是所有节点的序号最小的
第三步,如果当前线程创建的节点是所有节点序号最小的节点,则认为获取锁成功
第四步,如果当前线程创建的节点不是所有节点序号最小的节点,则对节点序号的前一个节点添加一个事件监听,当前一个被监听的节点释放锁之后,触发回调通知,从而再次去尝试抢占锁
12.zookeeper与redis分布式锁的区别
redis分布式锁而言,它有以下缺点
1.获取锁的方式简单粗暴,如果获取不到锁,会不断尝试获取锁,比较消耗性能
2.redis是ap模型,在集群模式中由于数据的一致性会导致锁出现问题,即便使用redlock算法实现,在某些复杂的场景下,也无法保证100%可靠性
实际开发中redis分布式很常见,大部分情况下不会遇到极端复杂的场景,最重要的是redis性能比较高,高并发场景比较合适
对应zookeeper分布式锁而言
1.zookeeper天生设计定位就是分布式协调,强一致性.锁的模型健壮,简单易用,适合做分布式锁
2.如果获取不到锁,只需要添加一个监听器就可以了,不用一直轮询,性能消耗较小
如果两者之前做选择,比较推荐zookeeper实现的锁,因为对于分布式锁而言,它应该符合cp模型,但是redis是ap模型,所以在这个点上zookeeper会更加合适
13.什么是线程安全
多个线程访问某一个类对象或者方法时,对象的共享数据始终都能表现正确,不会出现数据不一致或者数据污染的情况,那么这个类对象方法就是线程安全的
标签:面试题,java,synchronized,记录,获取,线程,volatile,节点,分布式 From: https://www.cnblogs.com/zhaoruisi/p/17291261.html