目录
锁
乐观锁
它认为一般情况下不会出现问题,所以它在使用的时候不会加锁,只有在数据修改的时候才会判断有没有锁竞争,如果没有就会直接修改数据,如果有则会返还失败信息给用户自行处理
1.乐观锁的经典事项:
CAS(Compare And Swap)对比且替换
CAS 实现:
三个重要内容:V【内存中的值】,A【预期的旧值】,B【新值】
- 比较 A 与 V 是否相等。(比较)
- 如果比较相等,将 B 写入 V。(替换)
- 返回操作是否成功。
Java在返回失败之后会加一个自旋锁,也就是死循环(for( ; ; )),会将A刷新为V,一直自旋对比替换,
面试题1:CAS实现的原理是什么?
针对不同的操作系统,JVM 用到了不同的 CAS 实现原理,简单来讲:
- java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作;Unsafe本地类和Unsafe CompareAndSwapXXX(本地方法),它是C/C++实现的原生方法;
- unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg(原子指令);
- Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子性。
UnSafe :
JDK 给Java提供了一种可以操作操作系统手段,通过UnSafe 实现操作系统的原子性操作。
简而言之,是因为硬件予以了支持,软件层面才能做到
CAS在Java中的应用:AtomicInteger / Atomic*
在多线程中实现 i++、i--保证线程安全的方法:
- 加锁
- ThreadLocal
- AtomicInteger
面试题2:乐观锁(CAS)存在什么问题?
答:ABA问题。
例如:A给B转100块钱,不小心点了两次提交按钮
CSA:V=100,A=100,B=0
第一次:V=100 == A=100 -> true -> V=0
第二次:(此时V=0,A=100,B=0) V == A ->false ->停止运行
结果是正常的。
但是如果假设在第一次和第二次转账中间A得到了100元转账 V->100
就会让第二次(此时V=100,A=100,B=0) V == A -> true -> V=0 ---》转账成功
A只给B转账100,但是A损失了200。
这就是ABA问题。
代码1:模拟ABA问题
/*
模拟ABA问题
*/
import java.util.concurrent.atomic.AtomicReference;
public class ThreadDemo {
private static AtomicReference money = new AtomicReference(100);
public static void main(String[] args) throws InterruptedException {
//转账 -100
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("1执行转账");
boolean rs=money.compareAndSet(100,0);
System.out.println(rs);
}
});
t1.start();
t1.join();
//转账 +100
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("3执行转账");
boolean rs=money.compareAndSet(0,100);
System.out.println(rs);
}
});
t3.start();
t3.join();
//转账 -100
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("2执行转账");
boolean rs=money.compareAndSet(100,0);
System.out.println(rs);
}
});
t2.start();
t1.join();
t2.join();
}
}
运行结果:
面试题:ABA如何处理?
答:加入版本信息,例如携带 AtomicStampedReference 之类的时间戳作为版本信息,保证不会出现老的值。 每次修改的时候判断预期的旧值和版本号,每次修改成功之后更改版本号,这样即使预期的值和V值相等,也不会修改值。
代码2:Java解决ABA问题(AtomicStampedReference )
/*
解决ABA问题
*/
import java.util.concurrent.atomic.AtomicStampedReference;
public class ThreadDemo {
private static AtomicStampedReference money = new AtomicStampedReference(100,1);
public static void main(String[] args) throws InterruptedException {
//转账 -100
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("1执行转账-100");
boolean rs=money.compareAndSet(100,0,1,2);
System.out.println(rs);
}
});
t1.start();
t1.join();
//转账 +100
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("3执行转账+100");
boolean rs=money.compareAndSet(0,100,2,3);
System.out.println(rs);
}
});
t3.start();
t3.join();
//转账 -100
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("2执行转账-100");
boolean rs=money.compareAndSet(100,0,1,2);
System.out.println(rs);
}
});
t2.start();
t2.join();
}
}
运行结果:
参数1:V(期望的旧值)
参数2:B(新值)
参数3:旧版本号
参数4:新版本号
但是如果将上面的代码中的所有的100改成1000,
运行结果:
与上次的运行结果不一样,原因如下:
AtomicStampeReference注意事项:
里面的旧值对比的是引用,并不是实际的值。这就涉及到 Integer 高速缓存:-128到127(当取值为此范围的时候会使用缓存值,不会重新new对象)
悲观锁
定义:悲观锁认为只要执行多线程就会出现问题,所以在进入方法之后会直接加锁
悲观锁的实现:synchronized
synchronized有关面试题:
1.讲一下synchronized?synchronized是如何实现的?
答:在java层面:是将锁放在对象头中(每个对象都有对象头)中;
在JVM层面:是监视器锁
在操作系统层面:是互斥锁 mutex
2.synchronized在1.7有什么优化?(做了什么优化?)
答:无锁 -> 偏向锁 -> 轻量级锁(自旋锁)-> 重量级锁
公平锁和非公平锁
公平锁:获取锁的顺序按照访问的先后顺序获取。 通过设置参数为true来设置为公平锁,例如 new ReentrantLock(true)
非公平锁:获取锁的顺序不会按照访问的先后顺序获取。不传参或传入false 性能比较高,Java设计里面的默认策略
独占锁和共享锁
独占锁:指的是这一把锁只能被一个线程拥有。例如,synchronized,lock
共享锁:指的是一把锁可以被多个线程同时拥有。例如,读写锁ReentrantReadWriteLock 读写锁当中的读锁是共享锁,写锁是独占锁,读锁写锁之间是独占锁
/*
读写锁实例
*/
public class ThreadDemo {
public static void main(String[] args) {
//创建一个读写锁
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//得到读锁
ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
//得到写锁
ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
ThreadPoolExecutor executor = new ThreadPoolExecutor(10,10,0, TimeUnit.SECONDS
,new LinkedBlockingQueue<>(1000));
//TODO 任务1 读锁演示
executor.execute(new Runnable() {
@Override
public void run() {
//加锁
readLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"进入了读锁"+new Date());
Thread.sleep(3000);//如果1s之内是共享锁,否则就是独占锁
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放锁
readLock.unlock();
}
}
});
//TODO 任务2 读锁演示
executor.execute(new Runnable() {
@Override
public void run() {
//加锁
readLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"进入了读锁"+new Date());
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放锁
readLock.unlock();
}
}
});
//TODO 任务3 写锁演示
executor.execute(new Runnable() {
@Override
public void run() {
//加锁
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"进入了写锁"+new Date());
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放锁
writeLock.unlock();
}
}
});
//TODO 任务4 写锁演示
executor.execute(new Runnable() {
@Override
public void run() {
//加锁
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"进入了写锁"+new Date());
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放锁
writeLock.unlock();
}
}
});
}
}
运行结果:
可重入锁
可重入锁:一个线程在拥有了一把锁之后,可以重复的进入,就是可重入锁。经典代表 synchronized
自旋锁
自旋锁:相当于死循环,一直循环尝试获取锁。
while(true){
if(尝试获取锁){
break;
}
}
JVM 将 synchronized 锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。会根据情况,进行依次升级。无锁: 没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改失败的线程会不断重试直到修改成功。偏向锁:在线程初次访问的时候,将线程的ID放到对象头偏向锁ID字段中,每次访问时判断一下线程的id是否等于对象头中的偏向锁Id ,如果相等则表明这个线程轻量级锁: 轻量级锁是指当锁是偏向锁的时候,被第二个线程 B 所访问,此时偏向锁就会升级为轻量级锁,线程 B 会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。 当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会 升级为重量级锁;当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁。重量级锁: 指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。重量级锁通过对象内部的监视器(monitor )实现,而其中 monitor 的本质是依赖于底层操作系统的Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。