文章目录
- StringBuffer 和 StringBuilder 的区别
- 一般的有死锁怎么形成的,怎么解决死锁
- HashMap,ConcurrentHashMap,LinkedHashMap的区别
- synchronized 和 ReentrantLock 的异同
- SpringMVC的运行原理
- 分布式锁怎么实现
- BIO 和 NIO区别
- new 一个对象,JVM 里面都干了啥
- volatile 关键字
- Synchronized 关键字在 1.6 做了哪些优化
- AQS和CAS
StringBuffer 和 StringBuilder 的区别
- 可变性。String 不可变,StringBuilder 与 StringBuffer 是可变的。
String 类中使用只读字符数组保存字符串,
private final char value [],
所以是不可变的(Java 9 中底层把 char 数组换成了 byte 数组,占用更少的空间)。
StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder
类,在 AbstractStringBuilder 中也是使用字符数组保存字符串,char [] value
,这两种对象都是可变的。
- 线程安全性。String 和 StringBuffer 是线程安全的,StringBuilder 是非线程安全的。
String 线程安全
是因为其对象是不可变的,StringBuffer 线程安全
是因为对方法加了同步锁或者对调用的方法加了同步锁。
StringBuilder
并没有对方法进行加同步锁,所以是非线程安全的
。
- 性能。
String 的性能较差,因为每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。
而 StringBuffer/StringBuilder 性能更高
,是因为每次都是对对象本身进行操作
,而不是生成新的对象并改变对象引用
。一般情况下 StringBuilder 相比 StringBuffer 可获得 10%~15% 左右的性能提升。
点评:
如果要操作少量的数据用 String; 单线程操作字符串缓冲区下操作大量数据 StringBuilder; 多线程操作字符串缓冲区下操作大量数据 StringBuffer;
一般的有死锁怎么形成的,怎么解决死锁
什么是线程死锁?
死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,
若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。
死锁产生的条件是什么?
(1) 互斥条件:
该资源任意一个时刻只由一个线程占用;
(2) 请求与保持条件:
一个线程 / 进程因请求资源而阻塞时,对已获得的资源保持不放;
(3) 不剥夺条件
:线程 / 进程已获得的资源在末使用完之前不能被其他线程 / 进程强行剥夺,只有自己使用完毕后才释放资源;
(4) 循环等待条件:
若干线程 / 进程之间形成一种头尾相接的循环等待资源关系。
- 如何避免线程死锁?
针对死锁产生的条件进行一一拆解:
(1) 破坏互斥条件:无法破坏
,因为使用锁的本意就是想让它们互斥的(临界资源需要互斥访问);
(2) 破坏请求与保持条件:
一次性申请所有的资源;
(3) 破坏不剥夺条件:
占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源;
(4) 破坏循环等待条件:
按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件(最常用)。
HashMap,ConcurrentHashMap,LinkedHashMap的区别
1.HashMap 是线程不安全的,HashTable 是线程安全的。
2.HashMap 的键需要重新计算对象的 hash 值,而 HashTable 直接使用对象的 hashCode。
3.HashMap 的值和键都可以为 null,HashTable 的值和键都不能为 null。
4.HashMap 的数组的默认初始化大小为 16,HashTable 为 11;HashMap 扩容时会扩大两倍,HashTable 扩大两倍 + 1;
LinkedHashMap维护一个双链表,可以将里面的数据按写入的顺序读出
- 基础特性不同:
HashMap 的 key 和 value 可以为 null,ConcurrentHashMap 的 key 和 value 不能为 null
。
- 内部数据结构不同:
HashMap 在 JDK1.7 中采用的数据结构是数组 + 链表,在 JDK1.8 中采用的数据结构是数组 + 链表 / 红黑二叉树;
ConcurrentHashMap 在 JDK1.7 中采用的数据结构是分段的数组
+ 链表,JDK1.8 的内部数据结构采用的数据结构是数组 + 链表 / 红黑二叉树(同 HashMap 一致)。
- 线程安全不同:
HashMap 是非线程安全的;
ConcurrentHashMap 是线程安全的;
ConcurrentHashMap
JDK1.7 中,ConcurrentHashMap 采用 HashEntry+Segment
的结构,ConcurrentHashMap 里一共 16
个 Segment,Segment 是可重入锁ReentrantLock
的子类,每个 Segment 对应一个 HashEntry 键值对数组。当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁
,因此,多线程访问容器里不同 Segment 的数据,就不会存在锁竞争,从而提升并发性能。
JDK1.8 中则摒弃了 Segment 的概念,并发控制使用 synchronized 和 CAS 来操作
,虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本。来看看核心的 put 方法。
补充
CAS
原子语义来处理加减等操作,CAS 全称Compare And Swap(比较与交换)
,通过判断内存某个位置的值是否与预期值相等,如果相等则进行值更新。CAS 是内部是通过 Unsafe
类实现,而 Unsafe 类的方法都是native
的,在 JNI
里是借助于一个 CPU 指令完成的
,属于原子
操作。
synchronized 和 ReentrantLock 的异同
1. 相同点:Lock 能完成 synchronized 所实现的所有功能;
2. 不同点:Lock 有比 synchronized 更精确的线程语义和更好的性能,而且不强制性的要求一定要获得锁。synchronized 会自动释放锁,而 Lock 则要求手工释放
。更具体地来说,有以下差异:
(1) 含义不同
Synchronized 是关键字,属于 JVM 层面
,底层是通过 monitorenter 和 monitorexit 完成,依赖于 monitor 对象来完成;
Lock 是
java.util.concurrent.locks.lock 包下的,是 JDK1.5 以后引入的新API 层面
的锁;
(2) 使用方法不同
Synchronized 不需要用户手动释放锁,代码完成之后系统自动让线程释放锁;ReentrantLock 需要用户手动释放锁,没有手动释放可能导致死锁;
(3) 等待是否可以中断
Synchronized 不可中断,除非抛出异常或者正常运行完成
;
ReentrantLock 可以中断
。
一种是通过 tryLock
(long timeout, TimeUnit unit),
另一种是lockInterruptibly ()
放代码块中,调用interrupt ()
方法进行中断;
(4) 加锁是否公平
Synchronized 是非公平锁
;
ReentrantLock 默认非公平锁
,
可以在构造方法传入 boolean 值,true 代表公平锁,false 代表非公平锁;
SpringMVC的运行原理
分布式锁怎么实现
CAP理论
即满足一致性(Consistency)
、可用性(Availability)
和分区容错性(Partition tolerance)
1.基于数据库。
基于数据库的实现方式的核心思想是:在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。
2.基于缓存环境,redis,memcache等。
(1)获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
(2)获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
(3)释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。
3.基于zookeeper。
(1)创建一个目录mylock;
(2)线程A想获取锁就在mylock目录下创建临时顺序节点;
(3)获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
(4)线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
(5)线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。
BIO 和 NIO区别
BIO(Blocking IO)阻塞IO
NIO(Non-Blocking IO)非阻塞IO
共同点:两者都是同步操作。即必须先进行IO操作后才能进行下一步操作。
不同点:
BIO多线程对某资源进行IO操作时会出现阻塞
,即一个线程进行IO操作完才会通知另外的IO操作线程,必须等待。
NIO多线程对某资源进行IO操作时会把资源先操作至内存缓冲区
。然后询问是否IO操作就绪,是则进行IO操作,否则进行下一步操作,然后不断的轮询是否IO操作就绪
,直到iIO操作就绪后进行相关操作。
new 一个对象,JVM 里面都干了啥
先是加载,验证,准备,解析,初始化
volatile 关键字
从原子性,可见性,指令重排三个方面说了
1.保证可见性:线程之间可见性(及时通知)
2.不保证原子性
3.禁止指令重排
Synchronized 关键字在 1.6 做了哪些优化
从锁消除,锁粗化,偏向锁,轻量级锁,重量级锁解锁了一遍。
1.适应自旋锁:为了减少线程状态改变带来的消耗 不停地执行当前线程
2.锁消除:不可能存在共享数据竞争的锁进行消除
3.锁粗化: 将连续的加锁 精简到只加一次锁
4.轻量级锁: 无竞争条件下 通过CAS消除同步互斥
5.偏向锁:无竞争条件下 消除整个同步互斥,连CAS都不操作。
AQS和CAS
CAS
CAS(Compare And Swap),即比较并交换。是解决多线程并行情况下使用锁造成性能损耗的一种机制, CAS操作包含三个操作数—— 内存位置(V)、预期原值(A)和新值(B)。 如果 内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。 无论哪种情况,它都会在CAS指令之前返回该位置的值。CAS有效地说明了“
我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。
AQS
AQS 的原理
抽象队列同步器
AQS(AbstractQueuedSynchronizer)核心思想是,如果被请求的资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态
;如果被请求的资源被占用,则需要一套线程阻塞等待以及唤醒分配的机制,该机制基于一个 FIFO(先进先出)的等待队列实现
。
AQS 的应用
作为一个用来构建锁和同步器的框架,AQS 能简单且高效地构造出大量同步器,事实上 java.util.concurrent.concurrent 包内许多并发类都是基于 AQS 构建。这些同步器从资源共享方式的方式来看,可以分为两类:
(1)Exclusive(独占):只有一个线程能执行,如 ReentrantLock。又可分为公平锁和非公平锁:
A、公平锁
:按照线程在队列中的排队顺序,先到者先拿到锁;
B、非公平锁
:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的。
(2) Share(共享):多个线程可同时执行,如 Semaphore/CountDownLatch/CyclicBarrier 等。
此外,也可以通过 AQS 来自定义同步器,自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队 / 唤醒出队等),AQS 已经在顶层实现好了。