Atomic原理

发布于:2021-09-13 12:31:07

1?术语补充

1.1 特级权


对于一个操作系统来说,创建一个进程是核心功能。创建进程有很多的工作,会消耗很多的物理资源。比如分配物理内存,父子信息拷贝等,这些都需要特定的进程去做,这样就有了特级权的概念。最关键的工作需要交到特级权最大的进程进行集中管理,减少有限资源的访问和冲突。inter x86架构的cpu一共有四个级别,0-3级,0级特权级最高,3级特权级最低。


1.2?用户态和内核态


当一个进程再执行用户自己的代码时候处于用户运行态(用户态),此时特权最低,为3级。是普通的用户进程运行的特权级,大部分用户直接面对的程序是运行再用户态。Ring3状态不能访问Ring0的地址空间,包括代码和数据;当一个进程因为系统调用陷入内代码中执行处于内核运行态(内核态),此时特权级最高为0级。执行的内核代码会使用当前进程的内核栈,每个进程都有自己的内核栈。


用户调用一个程序,该程序创建的进程开始时候会运行自己的代码,处于用户态。如果要执行文件操作、网络数据发送操作必须通过writesend等系统的调用,这些系统调用会调用内核的代码。进程会切换到Ring0,然后进入3G-4G的内核地址空间去执行内核代码来完成相应的操作。内核态的进程执行完后又切换到Ring3,回到用户态。这样,用户态的进程就不能随意操作内核地址空间,具有一定的安全保护作用。这说的保护模式是指通过内存页操作等机制,保证进程间的地址空间不会相互冲突,一个进程的操作不会修改另外一个进程地址空间的数据。


1.3?用户态和内核态的切换


当在系统中执行一个程序时,大部分时间是运行在用户态下的,在其需要操作系统帮助完成一些用户态自己没有特权和能力完成的操作时就会切换到内核态。


    系统调用

这是用户进程主动要求切换到内核态的一种方式。用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作。例如:fork()就是执行一个创建新进程的系统调用。系统调用的机制和新是使用了操作系统为用户特别开发的一个中断来实现,如:liunx的int80h中断。


插入:fork的使用


public class Test {
static final class SumTask extends RecursiveTask {
private static final long serialVersionUID = 1L;
final int start; //开始计算的数
final int end; //最后计算的数
SumTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
//如果计算量小于1000,那么分配一个线程执行if中的代码块,并返回执行结果
if (end - start < 1000) {
System.out.println(Thread.currentThread().getName() + " 开始执行:" + start + "-" + end);
int sum = 0;
for (int i = start; i <= end; i++)
sum += i;
return sum;
}
//如果计算量大于1000,那么拆分为两个任务
SumTask task1 = new SumTask(start, (start + end) / 2);
SumTask task2 = new SumTask((start + end) / 2 + 1, end);
//执行任务
task1.fork();
task2.fork();
//获取任务执行的结果
return task1.join() + task2.join();
}
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
ForkJoinPool pool = new ForkJoinPool();
ForkJoinTask task = new SumTask(1, 10000);
pool.submit(task);
System.out.println(task.get());
}
}
执行结果如下:
ForkJoinPool-1-worker-1 开始执行:1-625
ForkJoinPool-1-worker-7 开始执行:6251-6875
ForkJoinPool-1-worker-6 开始执行:5626-6250
ForkJoinPool-1-worker-10 开始执行:3751-4375
ForkJoinPool-1-worker-13 开始执行:2501-3125
ForkJoinPool-1-worker-8 开始执行:626-1250
ForkJoinPool-1-worker-11 开始执行:5001-5625
ForkJoinPool-1-worker-3 开始执行:7501-8125
ForkJoinPool-1-worker-14 开始执行:1251-1875
ForkJoinPool-1-worker-4 开始执行:9376-10000
ForkJoinPool-1-worker-8 开始执行:8126-8750
ForkJoinPool-1-worker-0 开始执行:1876-2500
ForkJoinPool-1-worker-12 开始执行:4376-5000
ForkJoinPool-1-worker-5 开始执行:8751-9375
ForkJoinPool-1-worker-7 开始执行:6876-7500
ForkJoinPool-1-worker-1 开始执行:3126-3750
50005000

2)异常


当cpu在执行运行在用户态下的程序时,发生了一些没有预知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关进程中,也就是切换到了内核态,如缺页异常。


3)外围设备的中断


当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令而转到与中断信号对应的处理程序去执行,如果前面执行的指令时用户态下的程序,那么转换的过程自然就会是 由用户态到内核态的切换。如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后边的操作等。


这三种方式是系统在运行时由用户态切换到内核态的最主要方式,其中系统调用可以认为是用户进程主动发起的,异常和外围设备中断则是被动的。从触发方式上看,切换方式都不一样,但从最终实际完成由用户态到内核态的切换操作来看,步骤有事一样的,都相当于执行了一个中断响应的过程。系统调用实际上最终是中断机制实现的,而异常和中断的处理机制基本一致。


2?术语补充


这里,我们来看看AtomicInteger是如何使用非阻塞算法来实现并发控制的。AtomicInteger的关键域只有一下3个:


public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
/**
* Creates a new AtomicInteger with the given initial value.
*
* @param initialValue the initial value
*/
public AtomicInteger(int initialValue) {
value = initialValue;
}
/**
* Creates a new AtomicInteger with initial value {@code 0}.
*/
public AtomicInteger() {
}
......
}

这里unsafe是java提供的获取对象内存地址访问的类,他的作用就是再更新操作时候,提供“比较并替换”的作用。ValueOffset是用来记录value本身再内存的编译地址的,这个记录也主要是为了再更新操作内存中找到value的位置,方便比较。getAndIncrement这个类似i++的函数,可以发现,是调用了UnSafe中的getAndAddInt。


/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
/**
* Atomically sets to the given value and returns the old value.
*
* @param newValue the new value
* @return the previous value
*/
public final int getAndSet(int newValue) {
return unsafe.getAndSetInt(this, valueOffset, newValue);
}

public final int getAndSetInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
//使用unsafe的native方法,实现高效的硬件级别CAS
} while(!this.compareAndSwapInt(var1, var2, var5, var4));
return var5;
}

如何保证原子性:自旋 + CAS(乐观锁)。在这个过程中,通过compareAndSwapInt比较更新value值,如果更新失败,重新获取旧值,然后更新。


优缺点


??????CAS相对于其他锁,不会进行内核态操作,有着一些性能的提升。但同时引入自旋,当锁竞争较大的时候,自旋次数会增多。cpu资源会消耗很高。换句话说,CAS+自旋适合使用在低并发有同步数据的应用场景。


?

相关推荐