A、锁
1、可重入锁的原理
重入锁实现可重入性原理或机制是:每一个锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁。
2、锁的分类
1、互斥锁:用来保证数据的完整性,可以保证任一时刻,只有一个线程访问该对象。如果一个线程获取到互斥锁,其他线程想要获取这个锁,就会获取失败,进入睡眠,等待被唤醒。
2、自旋锁:为了实现保护共享资源。跟互斥锁一样,任何时候都会只能有一个线程获取到锁。互斥锁是如果资源被占用,资源申请者只能进入睡眠,等待被唤醒。自旋锁不会引起调用者的睡眠,如果自旋锁被占用,其他线程就会一直循环看自旋锁是否被释放,等待获取。自旋锁的底层是由“do-while”循环实现的。因为一直自旋,占用着CPU资源,所以如果不能短时间的获取锁,就会造成CPU的浪费,所以比较适合能短时间拿到锁的情况。
3、读写锁:是一种特殊的自旋锁。它将对共享资源的访问对象划分为读锁和写锁,读锁只能对资源进行访问,写锁只能对资源进行写操作。这种锁可以提高并发性,在多线程系统中,允许多个线程来访问共享数据,最大的读者线程数为实际的逻辑CPU数。写锁是排他性的,一个读写锁同时只能有一个写者或者多个读者,但不能同时既有读者又有写者。(如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里,直到没有任何写者或读者。如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁)
4、悲观锁:每次去拿数据时都会认为别人会修改,所以每次拿数据时都会上锁。数据库中的行锁、表锁、读锁、写锁都是悲观锁。
5、乐观锁:每次拿数据的时候都认为别人不会修改,所以不会上锁。但是在更新的时候,会判断在自己拿锁的期间别人有没有修改这个数据,如果数据一致,则更新成功,否则重试更新操作,直到更新成功。一般使用版本号机制或者CAS机制。
版本号方式:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
CAS操作方式:即compare and swap 或者 compare and set,涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,一般情况下是一个自旋操作,即不断的重试。
3、synchronized为什么可重入
因为synchronized锁定线程时是基于“每线程(per-thread)”,而不是基于“每调用(per-invocation)”的。每一个synchronized锁关联线程时都会关联这个线程持有者和计数器。当一个线程获取到synchronized时,JVM会记录这个线程,并将计数器设置为1。其他线程想要获取锁,必须等待。当该线程如果想要再次获取锁,将计数器累加,当线程退出锁时,计数器会递减。如果计数器为0,则会释放锁。
4、synchronized与Lock的区别(ReentrantLock)
1、Lock(ReentrantLock)使用起来比较灵活,但是必须有释放锁的配合动作。
2、Lock(ReentrantLock)必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁。
3、Lock(ReentrantLock)只适用于代码块锁,而 synchronized 可用于修饰类、方法、代码块等。
5、乐观锁与悲观锁的区别
悲观锁:每次去拿数据时都会认为别人会修改,所以每次拿数据时都会上锁;
乐观锁:每次拿数据的时候都认为别人不会修改,所以不会上锁。但是在更新的时候,会判断在自己拿锁的期间别人有没有修改这个数据,如果数据一致,则更新成功,否则重试更新操作,直到更新成功。
乐观锁适用于写不较少的情况,这样可以省去锁的开销,加大了系统的吞吐量。但如果写比较多,就比较适用于悲观锁。
6、死锁有哪些情况?有没有遇到过死锁
死锁一般出现在操作数据库时,当事务并发时,每个事务都持有锁或者已经在等待锁,每个事务都需要再继续持有锁,然后事物之间产生加锁的循环等待,形成死锁。
避免预防死锁:A、类似的业务逻辑以固定的顺序访问表和行。
B、大事务拆小。大事务更倾向于死锁,如果业务允许,将大事务拆小。
C、在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁概 率。
D、降低隔离级别,如果业务允许,将隔离级别调低也是较好的选择 。
E、为表添加合理的索引。可以看到如果不走索引将会为表的每一行记录添加上锁(或者说是表锁)
F、给锁加上等待超时时间 innodb_lock_wait_timeout
7、synchronized用在方法和类上各有什么作用
synchronized用在方法上,表示对当前类的实例进行加锁,防止其他线程同时访问该类的该实例的所有synchronized块。
synchronized用在类上就相当于时 static synchronized,相当于控制了这个类的所有实例的并发访问,此时类对象的所有同步方法都将被Lock。
B、分布式锁
1、说一下分布式锁
分布式锁满足的条件:1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
2、高可用的获取锁与释放锁;
3、高性能的获取锁与释放锁;
4、具备可重入特性;
5、具备锁失效机制,防止死锁;
6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。
分布式锁实现的方式:1、基于数据库实现分布式锁;
2、基于缓存(Redis等)实现分布式锁;
3、基于Zookeeper实现分布式锁;
2、基于数据库实现分布式锁
实现思想:在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。
缺点:1、这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
2、这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
3、这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
4、这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。
解决方案:1、数据库是单点?搞两个数据库,数据之前双向同步。一旦挂掉快速切换到备库上。
2、没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。
3、非阻塞的?搞一个while循环,直到insert成功再返回成功。
4、非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。
3、基于缓存(Redis等)实现分布式锁
使用setNX,当Key不存在时,设置一个字符串类型的k,返回1,当k存在,则直接返回。用expire设置k的超时时间,避免出现死锁。使用delete删除k。
实现思想:1、获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
2、获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
3、释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。
缺点:
在这种场景(主从结构)中存在明显的竞态:
客户端A从master获取到锁,
在master将锁同步到slave之前,master宕掉了。
slave节点被晋级为master节点,
客户端B取得了同一个资源被客户端A已经获取到的另外一个锁。安全失效!
4、基于Zookeeper实现分布式锁
Zookeeper的数据存储结构就像一棵树,这棵树由节点组成,规定同一个目录下只能有一个唯一的文件名。
实现步骤:1、创建一个目录myLock;
2、线程A想获取锁,就在myLock目录下创建临时的顺序结点;
3、获取myLock目录下的所有子结点,然后获取比自己小的兄弟结点,如果不存在,说明当前线程的顺序最小,获取锁;
4、线程B获取所有的结点,判断自己是不是最小的,并监听比自己次小的结点;
5、线程A处理完,删除自己的结点,线程B监听到变更事件,判断自己是不是最小的结点,如果是,则获取锁。
5、三种实现分布式锁的选择
1、从理解的难易程度角度(从低到高)
数据库 > 缓存 > Zookeeper
2、从实现的复杂性角度(从低到高)
Zookeeper >= 缓存 > 数据库
3、从性能角度(从高到低)
缓存 > Zookeeper >= 数据库
4、从可靠性角度(从高到低)
Zookeeper > 缓存 > 数据库
5、分布式锁使用场景
分布式锁为了解决分布式场景下全局加锁的问题。在单体项目中可以使用synchronize完成对于不同线程之间的资源争抢问题。但是在分布式场景下,synchronize只能对其中一个项目进行资源控制,进程之间的资源增强仍然无法解距。换言之,可以将分布式锁理解为对于整个分不是系统的synchronize。通常使用独立与线程之外的工具控制资源,如redis及框架redisson。
C、线程池
1、线程池有哪些
1、Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
2、Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程池
3、Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池
4、Executors.newScheduledThreadPool(n):创建一个可以在给定延迟后运行命令或者定期地执行的线程池
2、线程池的参数
1、int corePoolSize:线程池中的常驻核心线程数
2、int maximumPoolSize:线程池中能够容纳同时执行的最大线程数,此值一定大于等于1
3、long keepAliveTime:多余空闲线程的存活时间
4、TimeUnit unit:long keepAliveTime 时间单位
5、BlockingQueue workQueue:任务队列,被提交但尚未被执行的任务
6、ThreadFactory threadFactory:生成线程的线程工厂,,一般默认即可
7、handler:拒绝策略,当队列满了之后,并且工作线程大于corePoolSize最大线程数,时来拒绝请求执行的runnale策略
3、线程池的拒绝策略
1、AbortPolicy:ThreadPoolExecutor中默认的拒绝策略就是AbortPolicy。直接抛出异常也不处理
2、CallerRunsPolicy:CallerRunsPolicy在任务被拒绝添加后,会调用当前线程池的所在的线程去执行被拒绝的任务。
3、DiscardPolicy:该策略默默地丢弃无法处理的任务,不会抛异常也不会执行,如果允许任务丢失,则这是最好的策略。
4、DiscardOldestPolicy:DiscardOldestPolicy策略的作用是,当任务被拒绝添加时,会抛弃任务队列中最旧的任务也就是最先加入队列的,再把这个新任务添加进去。
4、线程创建的方法,线程池创建的方法
1、继承Tread类
创建一个集成于Thread类的子类 (通过ctrl+o(override)输入run查找run方法)
重写Thread类的run()方法
创建Thread子类的对象
通过此对象调用start()方法
2、实现Runnable接口
创建一个实现了Runable接口的类
实现类去实现Runnable中的抽象方法:run()
创建实现类的对象
将此对象作为参数传递到Thread类中的构造器中,创建Thread类的对象
通过Thread类的对象调用start()
//比较
优先使用runnable的方式
因为它没有继承类,不会存在单继承的局限性,同时tread也是实现自runnable接口,都是需要调用run方法,将线程要执行的逻辑生命在run方法中
3、实现callable接口 jdk5.0新增
创建一个实现callable的实现类
实现call方法,将此线程需要执行的操作声明在call()中
创建callable实现类的对象
将callable接口实现类的对象作为传递到FutureTask的构造器中,创建FutureTask的对象
将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start方法启动(通过FutureTask的对象调用方法get获取线程中的call的返回值)
与使用runnable方式相比,callable功能更强大些:
runnable重写的run方法不如callaalbe的call方法强大,call方法可以有返回值
方法可以抛出异常
支持泛型的返回值
需要借助FutureTask类,比如获取返回结果
4、使用线程池创建方法
5、线程的几种状态?顺序是什么?
新建、就绪、运行、阻塞、死亡
6、线程池的好处
提高响应速度(减少了创建新线程的时间)
降低资源消耗(重复利用线程池中线程,不需要每次都创建)
便于线程管理
7、线程池的底层工作原理
1、主线程通过execute创建一个线程,发现corePool为空,则直接放入corePool
2、如果corePool满了,其余线程则进入workQueue等待,如果workQueue也满了
3、如果workQueue也满了,则执行扩容机制,扩容至最大线程数maximumPoolSize
4、如果maximumPoolSize也满了,则执行拒绝策略
5、当线程数逐渐少了下来之后,keepAliveTime时间到之后,则会清除多余空闲线程
D、分布式事务
1、分布式事务怎么做、为什么要有分布式事务?
如果一张表数据量比较大,有1000万条数据,就需要分库分表。此时如果有一个操作需要访问不同表中的数据,而且还需要保证数据的一致性,就需要用到分布式事务。
分布式事务通过三种方式可以实现:1、基于数据库实现分布式锁;
2、基于缓存(Redis等)实现分布式锁;
3、基于Zookeeper实现分布式锁;
2、seata原理
Seata 是一款开源的分布式事务解决方案,是一个业务层的XA(两阶段提交)解决方案。致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了一系列的(AT、TCC、SAGA和XA)事务模式,为用户打造一站式的分布式解决方案。
XA的两阶段提交分为Prepare阶段和Commit阶段:
1、阶段一为准备(prepare)阶段。即所有的RM锁住需要的资源,在本地执行这个事务(执行sql,写redo/undo log等),但不提交,然后向Transaction Manager报告已准备就绪。
2、阶段二为提交阶段(commit)。当Transaction Manager确认所有参与者都准备好(ready)后,向所有参与者发送commit命令。
包含三种角色:
1、Transaction Coordinator (TC): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
2、Transaction Manager (TM): 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
3、Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
一个分布式事务在Seata中的执行流程:
1、TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。
2、XID 在微服务调用链路的上下文中传播。
3、RM 向 TC 注册分支事务,接着执行这个分支事务并提交(重点:RM在第一阶段就已经执行了本地事务的提交/回滚),最后将执行结果汇报给TC。
4、TM 根据 TC 中所有的分支事务的执行情况,发起全局提交或回滚决议。
5、TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
E、异步编排 CompletableFuture
1、异步编排怎么实现
CompletableFuture实现了Future接口,拥有Future的所有功能,可以获取异步执行的结果,和取消正在执行的任务。
CompletableFuture 有四个静态的构造函数:
1、CompletableFuture.runAsync(Runnable runnable);
2、CompletableFuture.runAsync(Runnable runnable, Executor executor);
3、CompletableFuture.supplyAsync(Supplier<U> supplier);
4、CompletableFuture.supplyAsync(Supplier<U> supplier, Executor executor)
其中runAync接受的是Runable的实例,他没有返回值;supplyAsync又返回值。都可以带有 Executor ,表示让任务在制定的线程池中执行。如果没有指定,则默认在 ForkJoinPool.commonPool() 线程池中执行。
2、异步编排中阻塞主线程完成的方法是什么?
jion() 和 get() 方法
不同处在于:join() 会抛出 unchecked Exception异常
F、线程安全
1、Java中线程安全的类
1、Java.util.concurrent.atomic包下的原子类
AtomicInteger、AtomicBoolean、AtomicLong
2、常见的集合类
List:Vector
Map:ConcurrentHashMap HashTable
3、可变字符串 StringBuffer
G、Volatile
Volatile关键字
1、读操作:每次都是从主内存中读取数据,然后写入虚拟机栈中
2、写操作:Java内存模型JMM会触发 Store -- write 操作,将修改后的变量值修改到虚拟机栈中,从而保证了线程间的可见性。
3、volatile与synchronized的区别:由volatile修饰的类型,不会执行加锁操作,也不会执行线程阻塞,比synchronized更轻量。
4、volatile可以保证并发编程的可见性、有序性、部分原子性(基本数据类型 及 内存地址)
可见性:通过写操作
有序性:通过禁止指令重排的方式做到有序性的。
【
Java代码在编译阶段没有做指令重排
Java代码在运行阶段做了指令重排。是因为CPU的乱序执行导致的(乱序执行会造成线程拿到没有初始化的对象)
加volatile关键字禁止了指令重排序。
】
5、volatile关键字的作用:1、使异步写刷回内存的时间更短;
2、为了读写的有序性和可见性。
1、CAS
```java
比较并交换,预防ABA问题。
即compare and swap 或者 compare and set,涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,一般情况下是一个自旋操作,即不断的重试。
2、volatile可以保证线程安全吗?
volatile不能保证线程的安全性,如果想要线程安全,就必须得满足原子性。
H、ThreadLock
1、ThreadLock原理
ThreadLock是为每一个使用共享变量的线程都提供一个独立的副本,使访问这个共享变量的多个线程之间不会彼此影响。与线程的同步机制相比较而言,它采用的是“以空间换时间”的逻辑,所以相对而言更高效。
2、ThreadLocal为什么会内存泄漏
由于TreadLock为每一个线程都提供了一个变量副本,当所有的副本总量超出内存大小时,就会出现内存泄漏问题。
I、线程
1、Runnable与Callable的区别
与使用runnable方式相比,callable功能更强大些:
runnable重写的run方法不如callaalbe的call方法强大,call方法可以有返回值,返回值还是范型的;
callaalbe接口,方法可以抛出异常,runnable一场只能内部消化,无法往外抛;
callaalbe接口可以借助FutureTask类,获取返回结果。
2、高并发的项目需求,你要怎么进行架构设计?
从以下几个方面考虑:
1、系统拆分,将一个系统拆分成多个系统,然后每一个系统连接一个数据库;
2、利用缓存,大部分的高并发场景都是读多写少,将数据同步到缓存,然后用户读取是从缓存读取;
3、使用消息中间件MQ,MQ有削峰填谷的作用,让消息在队列中慢慢消费,提高并发性。
4、读写分离,可以设置主从数据库,主库写入,从库读取,实现读写分离;
5、利用ElasticSearch,因为es是分布式的,可以随便扩容,分布式天然可以支撑高并发。
3、对于高并发环境,除了搭建集群还可以怎么解决?
1、HTML静态化:如果页面不需要对大量内容频繁更新,可以采用HTML静态化的方式;
2、图片服务器分离:对web服务器来讲,图片是最消耗资源的。这是大型网站采用的策略,可以降低提供页面访问请求的服务器系统的压力。
4、缓存:对于不需要实时更新的数据进行缓存;
5、镜像:可以在EduNet教育网站内搭建镜像站点,数据进行定时更新或者实时更新。
6、负载均衡:终极解决方案。
4、sleep和wait的区别
1、相同点:
一旦执行方法以后,都会使得当前的进程进入阻塞状态
2、不同点:
1.两个方法声明的位置不同,Thread类中声明sleep,Object类中声明wait。
2.调用的要求不同,sleep可以在任何需要的场景下调用,wait必须使用在同步代码块或者同步方法中
3.关于是否释放同步监视器,如果两个方法都使用在同步代码块或同步方法中,sleep不会释放,wait会释放
5、线程的可见性
1、加volatile关键字
2、加synchronized关键字
6、如何实现子线程的有序性
1、使用volatile关键字
2、使用synchronized关键字
3、使用显示锁Lock来保证有序性
持续更新中。。。