volatile: 低配的同步锁,保障有序性(禁止指令重排,内存屏障),可见性(打小报告)
有序性是如何保证的:通过插入内存屏障,来禁止 屏障 之前与屏障之后的指令交换位置
可见性:
含义,底层原理,Unsafe 类理解
CAS 缺点:
多线程环境下,对共享变量的操作,要么加锁,要么CAS
神马是CAS?
CAS 底层实现?
CAS 优缺点?
CAS ABA 问题?
如何解决?
原子引用?
加锁 :保证只能同时有一个线程去操作 数据
CAS:比较交换,偏量值 ,主要思想是通过读取主内存的值 和预期旧值比较,如果相同,则将新值=预期旧值+偏量值 写入主内存(如果写入失败,重新读取旧值)
经典的使用例子便是jdk 中的atomic 包(查看源码可以看见有乐观锁的思想)
以AtomicInteger 的 getAndIncrement() 方法为例
AtomicInteger atomicInteger=new AtomicInteger();
atomicInteger.getAndIncrement();
--》
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
/**
* Atomically adds the given value to the current value of a field
* or array element within the given object o
* at the given offset
.
*
* @param o object/array to update the field/element in
* @param offset field/element offset
* @param delta the value to add
* @return the previous value
* @since 1.8
*/
// O 是指当前对象,或者说是首地址
//offset 是指内存偏移量
、// delta 是指变化量
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
// 读取当前对象+offset 内存地址上的值
v = getIntVolatile(o, offset);// 获取主内存值
} while (!compareAndSwapInt(o, offset, v, v + delta));// 如果没有交换成功,重新读取主内存值,进行下一次交换,直到成功为止
return v;
}
compareAndSwapInt 的底层就是 c语言的实现了,位于unsafe.cpp 中
UNSAFE_ENTRY(jboolean,uNSAFE_cOMPAREaNDsWAPiNT(jnieNV *env,jobject unsafe,jlong offset,iint e,jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p=JNIHandles::resolve(obj);// 对象首地址
jint * addr=(jint*) index_oop_from_field_offset_long(p,offset);// 对象的属性字段的地址
return (jint)(Atomic::cmpxchg(x,addr,e))==e;// 注意atomic 则保证此操作是原子性的不可中断的
UNSAFE_END
CAS=Compare and Swap CPU 并发原语(原子性)
主内存:初始值 1
线程 A 和线程B 读取主内存 值 为自己工作空间(拷贝一份)
线程 在CAS 过程中 将数据从主内存获取(得到1)
线程A 将数据改为 改为2 并且完成刷回主内存
线程A 将数据改为1 并且完成刷回主内存
线程B 执行 CompareAndSwap 操作 将数据 更改为3(CAS 个会成功)
因为线程B 读取到的仍是1(虽然不是最初的那个1,中途被改变,线程B无法感知)
忽略了中间过程的变化,只注意了首尾状态
Atomic 只是提供了 基本数据类型的原子操作
如果现在有User/Account 这些类型也需要原子操作呢?
class User{
String name;
int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class AtomicReferenceDemo {
public static void main(String[] args) {
User user1=new User("zhangsan",10);
User user2=new User("lisi",20);
AtomicReference<User> atomicReference=new AtomicReference<>();
atomicReference.set(user1);
System.out.println(atomicReference.compareAndSet(user1,user2)+"\t"+atomicReference.get());
System.out.println(atomicReference.compareAndSet(user1,user2)+"\t"+atomicReference.get());
}
}
如何解决ABA 问题
原子类的缺点:只能保证单个变量,而不能保证一个复杂的引用对象
AtomicReference
解决ABA可以使用AtomicStampReference(相比AtomicReference 多了一个版本号stamp)
给出解决方案(比较方案的底层实现)
类似线程不安全的 还有hashMap,hashSet(底层是HashMap) 实现
锁的功能:让临界区互斥执行
锁的内存语义:
线程释放锁时,JMM会将该线程的本地内存中的共享变量刷新到主内存中(和volatile 写有相同的内存语义,刷回主内存)
线程获取锁时,JMM会把该线程的本地内存中的的共享变量置为无效(和volatile 读有相同的内存语义,去主内存读取)。
公平vs 不公平
公平:得到锁的顺序和申请的顺序一致(FIFO)
非公平:不保证顺序,新线程申请锁是先尝试获取锁,获取失败,再排队(锁的实现默认是非公平的,性能更好)
可重入(递归锁)
持有锁的线程,可以进入 该锁 控制的所有代码
场景:单个线程重复多次访问同一个对象
自旋锁
获取锁失败,会循环尝试获取锁,而不是阻塞,
优点:不阻塞
缺点:不释放cpu ,尝试减获取失败,会影响cpu 利用率
手写一个自旋锁(可以使用AtomicReference 去实现)
public class SpinLock {
AtomicReference<Thread> lock=new AtomicReference<>();
public void lock(){
Thread t=Thread.currentThread();
System.out.println(t.getName()+" 进入 lock");
while (!lock.compareAndSet(null,t)){
// 自旋直到获取到锁
}
}
public void unLock(){
Thread t=Thread.currentThread();
lock.compareAndSet(t,null);// 只有持有锁的线程才能解锁
System.out.println(t.getName()+" unlock");
}
public static void main(String[] args) {
SpinLock spinLock=new SpinLock();
new Thread(()->{
spinLock.lock();
try {
TimeUnit.SECONDS.sleep(3);
}catch (InterruptedException e){
e.printStackTrace();
}finally {
spinLock.unLock();
}
},"thread1").start();
new Thread(()->{
spinLock.lock();
try {
TimeUnit.SECONDS.sleep(1);
}catch (InterruptedException e){
e.printStackTrace();
}finally {
spinLock.unLock();
}
},"thread2").start();
}
}
效果
thread1 进入 lock
thread2 进入 lock
thread1 unlock
thread2 unlock
CountDownLatch: 倒计时,计数为0,回到主线程
特点:
阻塞队列,首先它是一个队列,而一个阻塞队列在数据结构中所起的作用大致是:线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素。
当阻塞队列是空时,从队列中获取元素的操作将被阻塞。 当阻塞队列是满时,往队列里添加元素的操作将被阻塞。
优点:
在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦满足条件,被挂起的线程又会自动被唤醒。
为什么需要BlockingQueue?
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你包办了。
在concurrent包发布之前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给程序带来不小的复杂度。
核心方法:
方法 | 无异常 | 带异常 | 阻塞操作 | 限时阻塞 |
---|---|---|---|---|
添加 | offer(不抛异常) | add(满了会抛异常) | put | offer(E e, long timeout, TimeUnit unit) |
移除 | poll | remove(空了会抛异常) | take | offer(E e, long timeout, TimeUnit unit) |
第一个元素 | peek | element(空会抛异常) |
种类,使用场景
线程池的优点:线程复用;并发量可控;资源可控
如何使用
重要参数:
核心线程数:内部员工,
阻塞队列:任务仓库;
非核心线程:外包人员,
无空闲线程,放仓库,仓库堆满了,任务办不完,让外包人员做,如果外包人员达到最大数量,也做不完,就交给拒绝策略
底层工作原理
拒绝策略:(内置)均实现 RejectedExecutionHandler
ThreadPoolExecutor.AbortPolicy;// 抛异常
ThreadPoolExecutor.CallerRunsPolicy;// 调用者执行
ThreadPoolExecutor.DiscardOldestPolicy;// 移除最久未执行的任务
ThreadPoolExecutor.DiscardPolicy;// 静默处理,直接丢掉新任务
IO密集型,即该任务需要大量的IO,即大量的阻塞。
在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。
所以IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。
IO密集型时,大部分线程都阻塞,故需要多配置线程数:
参考公式:CPU核数/1-阻塞系数 阻塞系数在0.8-0.9之间。
什么是死锁
死锁代码
解决
lock:可中断,可非公平,可公平,多个condition(可以精确唤醒),手动释放
synchronized: 非公平,不可中断
例题:A线程打印10次,然后B打印10次,然后C 打印10次
重复11次上述流程
**
* A,B,C 三个线程,A 输出5次,B 输出10次,C输出15次 重复11轮(A->B->C)
*/
public class ReadLock {
private int state = 0;//a 0,b1,c2
private ReentrantLock lock = new ReentrantLock();
private Condition[] conditions=new AbstractQueuedSynchronizer.ConditionObject[3];// 每个线程对应一个condition
{
for (int i = 0; i < 3; i++) {
conditions[i] = lock.newCondition();
}
}
private void print(String content, int times, int threadNo) {
try {
lock.lock();
while (state != threadNo) {
conditions[threadNo].await();
}
for (int i = 0; i < times; i++) {
System.out.println(Thread.currentThread().getName() + ":" + content);
}
} catch (Exception e) {
} finally {
state+=1;
state%=3;
conditions[state].signal();// 唤醒下一个线程
lock.unlock();
}
}
public static void main(String[] args) {
ReadLock readLock=new ReadLock();
new Thread(()->{
for(int i=0;i<11;i++)
readLock.print("a",5,0);
},"A").start();
new Thread(()->{
for(int i=0;i<11;i++)
readLock.print("b",10,1);
},"B").start();
new Thread(()->{
for(int i=0;i<11;i++)
readLock.print("c",15,2);
},"C").start();
}
}
final 引用不能从构造函数溢出?
class A{
final obejct b;
A(){
b=bew Object()'}
}
写final 域的重排序规则可以确保:将引用变量被任意线程可见之前,该引用变量指向对象的final域已经在构造函数正确初始化过 。
也就是如下操作:
A a=new A();
过程细分为:
1. 进入A 构造函数
2. 初始化final 域(也就是b 属性)// 写final域的重排序规则保证这一步只能在构造函数内部完成,而不是重排序到构造函数外去完成
3. 退出A 构造函数
4. 堆上对象 被引用变量a 指向
JSR-133: