在上篇博客中我总结了有关多线程的基础知识点,今天这篇博客我来总结一下有关“锁”更详细的知识,一起来看
目录
经典面试题:Hashtable和HashMap、ConcurrentHashMap 之间的区别
常见锁策略
加锁的时候的咋加的?
乐观锁VS悲观锁
乐观锁:预测接下来锁冲突的概率不大
悲观锁:预测接下来锁冲突的概率很大
synchronized自适应锁既是乐观锁又是悲观锁,当前锁冲突概率不大以乐观锁的方式运行,往往是纯用户态执行。
一旦发现锁冲突概率大了,以悲观锁的方式运行,进入内核对当前线程进行挂起等待
普通的互斥锁VS读写锁
synchronized就属于普通的互斥锁,两个加锁操作之间发生竞争
读写锁将加锁操作细化了,分为了“加读锁”,“加写锁”
在Java标准库中提供了ReentrantReadWriteLock类,来实现加读写锁
重量级锁VS轻量级锁
重量级锁:锁开销大,做的工作多,主要依赖操作系统提供的锁容易产生阻塞等待
轻量级锁:锁开销小,做的工作少,尽量在用户态完成加锁,避免用户态与内核态的切换,避免挂起等待
synchronized是自适应锁,既是重量级锁又是轻量级锁,根据冲突情况
自旋锁VS挂起等待锁
自旋锁是轻量级锁的具体实现,当发生锁冲突会迅速尝试获取锁
挂起等待锁是重量级锁的具体实现,发生冲突就挂起等待
synchronized作为轻量级锁的时候内部是自旋锁,作为重量级锁的时候内部是挂起等待锁
公平锁VS非公平锁
注意此处的公平表示为“先来后到”,synchronized 是非公平锁
操作系统的内部对于挂起等待锁是非公平的,如果想使用公平锁就要使用数据结构来控制实现
可重入锁 vs 不可重入锁
![](https://i-blog.csdnimg.cn/blog_migrate/8fb8eddcd098dfdc580a7e43ce32b350.png)
为解决上诉的问题就引入了可重入锁 ,可重入锁在内部记录锁是哪个线程获取到的,当发现加锁线程与持有锁的线程是同一个则会让直接获取,同时还会在锁内部加个计数器,记录是第几次加锁,通过计数器来控制释放锁
Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的
CAS
CAS是操作系统/硬件给JVM提供的另一种更轻量的原子操作的机制
CAS是CPU提供的一个特殊指令
boolean CAS(address, expectValue, swapValue) {//CAS实现的伪代码
if (&address == expectedValue) {
&address = swapValue;
return true;
}
return false;
}
注意以上是伪代码通过Java代码实现的,实际是由一个CPU指令完成的
CAS应用场景
1.实现原子类
public static AtomicInteger count=new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
for(int i=0;i<5000;i++) {
count.getAndIncrement();//count++;
}
});
Thread t2=new Thread(()->{
for(int i=0;i<5000;i++) {
count.getAndIncrement();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
2实现自旋锁(伪代码)
public class SpinLock {
private Thread owner = null;
public void lock(){
// 通过 CAS 看当前锁是否被某个线程持有.
// 如果这个锁已经被别的线程持有, 那么就自旋等待.
// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
while(!CAS(this.owner, null, Thread.currentThread())){
}
}
public void unlock (){
this.owner = null;
}
}
ABA问题
A->A
A->B->A(A修改数据为B后又改回A)
synchronized原理
基本特征
加锁过程(锁升级)
无锁(没加锁)
偏向锁(刚开始加锁,未产生竞争)标记表示加锁
轻量级锁(产生锁竞争)用户态自旋
重量级锁(激烈的锁竞争)内核态挂起等待
锁消除
编译器+JVM 判断锁是否可消除. 如果可以, 就直接消除
StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
sb.append("d");
每个append都会加锁解锁但是在单线程下这是白白浪费的资源
锁粗化
![](https://i-blog.csdnimg.cn/blog_migrate/a63f965f5305da2ce367f09316697e6d.png)
Callable接口
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {//重写call()方法
int sum = 0;
for (int i = 1; i <= 1000; i++) {
sum += i;
}
return sum;
}
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);//包装一下
Thread t = new Thread(futureTask);
t.start();
int result = futureTask.get();//get()获取结果,拿到结果前阻塞等待
System.out.println(result);
理解Callable
JUC的常见类
ReentrantLock(可重入锁)
public static void main(String[] args) {
ReentrantLock locker=new ReentrantLock();
try {
locker.lock();//加锁
}finally{
locker.unlock();//解锁
}
}
synchronized与ReentrantLock的区别
1.synchronized单纯的关键字,以代码块为单位进行加锁解锁
原子类
原子类内部用的是 CAS 实现,之前介绍过这里总结方法,以 AtomicInteger 举例,常见方法有
addAndGet(int delta); i += delta;decrementAndGet(); --i;getAndDecrement(); i--;incrementAndGet(); ++i;getAndIncrement(); i++;
线程池
ExecutorService 和 Executors
newFixedThreadPool(): 创建固定线程数的线程池newCachedThreadPool(): 创建线程数目动态增长的线程池newSingleThreadExecutor(): 创建只包含单个线程的线程池newScheduledThreadPool(): 设定 延迟时间后执行命令,或者定期执行命令 . 是进阶版的 Timer
ThreadPoolExecutor 构造方法
ExecutorService pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS,
new SynchronousQueue<Runnable>(),
Executors.defaultThreadFactory(),
new
ThreadPoolExecutor.AbortPolicy());
for(int i=0;i<3;i++) {
pool.submit(new Runnable() {
@Override
void run() {
System.out.println("hello");
}
});
}
信号量 Semaphore
public static void main(String[] args) throws InterruptedException {
//构造方法传入有效资源个数
Semaphore semaphore=new Semaphore(3);
semaphore.acquire();//p操作 申请资源
semaphore.release();//v操作 释放资源·
}
CountDownLatch
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch=new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
Thread t=new Thread(()-> {
try {
Thread.sleep(3000);
System.out.println("到达终点");
latch.countDown();//相当于撞线
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
}
latch.await();//等待所有线程完成撞线
//调用CountDown的次数达到初始化的值await返回,否则阻塞等待
System.out.println("结束");
}
线程安全的集合类
多线程环境使用 ArrayList
1.使用同步机制 (synchronized 或者 ReentrantLock)
2.Collections.synchronizedList(new ArrayList);//对ArrayList的封装
3.使用 CopyOnWriteArrayList
多线程下使用队列
1. ArrayBlockingQueue 基于数组实现的阻塞队列
2.LinkedBlockingQueue 基于链表实现的阻塞队列
3.PriorityBlockingQueue 基于堆实现的带优先级的阻塞队列
4.TransferQueue 最多只包含一个元素的阻塞队列
多线程环境使用哈希表
concurrentHashMap的优点
经典面试题:Hashtable和HashMap、ConcurrentHashMap 之间的区别
死锁
死锁是多线程代码中常见BUG
产生死锁的必要条件
1.互斥使用:线程1拿到锁A,其他线程无法获取到A
2.不可抢占:线程1拿到锁A,其他线程只能阻塞等待,等到线程1主动释放锁而不是强行将锁抢走
3.请求保持:当线程1拿到锁A之后,就会一直持有这个获取到锁的状态,直到说主动释放
4.循环等待:线程1等待线程2,线程2又尝试等待线程1
其中循环等待就可以通过代码编写格式来打破进而避免死锁
破坏循环等待
针对多把锁进行编号,约定获取多把锁时明确获取顺序比如从小到大
//可能造成死锁
Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread() {
@Override
public void run() {
synchronized (lock1) {
synchronized (lock2) {
// do something...
}
}
}
};
t1.start();
Thread t2 = new Thread() {
@Override
public void run() {
synchronized (lock2) {
synchronized (lock1) {
// do something...
}
}
}
};
t2.start();
//约定获取锁的顺序
Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread() {
@Override
public void run() {
synchronized (lock1) {
synchronized (lock2) {
// do something...
}
}
}
};
t1.start();
Thread t2 = new Thread() {
@Override
public void run() {
synchronized (lock1) {
synchronized (lock2) {
// do something...
}
}
}
};
t2.start();
好的以上就是有关多线程下锁的一些知识总结还有一些面试常见问题,希望对你有帮助 欢迎点赞,评论 蟹蟹!!