zoukankan      html  css  js  c++  java
  • 并发机制的底层实现

    JAVA并发机制的底层实现

    volatile

    在并发编程中最容易出现的是”数据竞争“,多个线程对共享变量进行操作时,一个线程对数据进行了修改,而其它进程却不知道。这时,我们可以用volatitle来解决这个问题。从下面几个方面来理解volatile的原理。

    Cache

    Cache被翻译为高速缓存,它位于CPU内部,是CPU和内存的缓冲带。因为CPU的工作频率比内存要高的多,所以CPU的运行速度往往受制于内存。Cache是一个高速设备,我们可以先把数据从内存中装入到Cache中,CPU直接从Cache中取数据,这样可以提高CPU的运行速率。

    问题

    Cache引入虽然提高了执行速率,但是会引起Cache的不一致性问题。比如双核CPU,A核和B核都有自己的Cache,在这两个核上各跑一个线程,这两个线程会对同一个变量进行操作,则它们会将这个变量加入各自的Cache中。假如A线程在某一时刻修改了变量的值,它也仅仅只是在自己的Cache中修改,并不会将该值写回内存中,更不会通知另一个线程了。

    解决方案

    在共享变量前加volatile关键字可以解决Cache一致性问题。java代码最后要翻译成机器指令,当对一个被volatile修饰的变量做写操作时(写操作机器指令),会在该操作指令后面加一条额外的指令---Lock指令。
    该指令在多核处理器下会引发两件事情

    • 当前处理器修改的数据写会到系统中内存中
    • 这个写回操作会使其它CPU里面缓存的对应数据无效

    思考

    volatie是否能实现锁的功能呢?当然不是。它只是提供了内存的可见性而已。假如两个线程都同时对同一个变量修改,那么最后的变量仍然存在一致性问题

    下面的程序会退出吗?

    import java.util.concurrent.TimeUnit;
    
    public class Test {
    	private static boolean bChanged = true;
    	public static void main(String[] args) throws InterruptedException {
    		new Thread() {
    
    			@Override
    			public void run() {
    				while (bChanged);
    				System.exit(0);
    			}
    		}.start();
    		TimeUnit.SECONDS.sleep(1);
    		while (true) {
    			bChanged = false;
    		}
    	}
    }
    

    synchronized

    Java中的每个对象都可以作为锁,具体为一些3种形式:

    • 对于普通的同步,锁是当前实例对象
    • 对于静态方法的同步,锁是当前类的Class对象
    • 对于同步块,锁是synchronized括号里配置的对象

    实现方式

    synchroized是在JVM里面实现的,JVM基于获取和退出Monitor来实现方法同步和代码块同步的,但是两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步使用的是另外的一种方式实现的。任何一个对象都有一个monitor与之关联,当一个monitor被劫持后,它将处于锁定状态。线程执行到monitor指令时,将会尝试获取对象对应的monitor所有权,即获取对象的锁。每个对象都有个对象头,在对象头中有个锁标志位来标志锁的状态。

    锁的升级和对比

    Java中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在java中锁总共有四种状态:无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态。这几个状态会随着竞争的情况逐渐升级。

    偏向锁

    偏向锁是为了让线程获取锁的代价更低才引入的。假如对象O处于无锁状态,A线程要执行同步代码块,它先检查对象头中是否存储其ID,如果没有(这时一定是没有的),则使用CAS技术将填充自己的ID并将该对象设为偏置状态。然后在执行同步代码块(退出同步代码块后并不会将对象复原到无锁状态)。如果线程A再次进入同步代码块,则查看对象头中是否有自己的ID(这时一定有的),则直接执行同步代码块的内容。假如在某个时刻,线程2也要访问同步代码块,这时会出现锁竞争,如果竞争成功(A进程已死),则将线程ID设置为当前线程ID。如果竞争失败,则获得偏向锁的线程将被挂起,偏向锁将升级为轻量级锁,然后挂起的线程继续执行。

    特点:适用于只有一个线程访问同步块的场景。如果一旦出现竞争,则会上升为轻量级锁。

    轻量级锁

    线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,则自旋获取锁,当自旋获取锁仍然失败时,表示存在其他线程竞争锁(两条或两条以上的线程竞争同一个锁),则轻量级锁会膨胀成重量级锁。

    特点:线程不会阻塞,减少了上下文切换,但是有可能会空耗CPU。

    重量级锁

    重量级锁就是synchronize,它不会引起线程自旋,不消耗CPU。

    下面的线程执行完毕有先后顺吗
    public class Test {
    	synchronized static void test1() {
    		// TODO sleep 3s
    	}
    	synchronized void test2() {
    		// TODO sleep 3s
    	}
    	public static void main(String[] args) {
    		final Test t = new Test();
    		new Thread(new Runnable() {
    
    			@Override
    			public void run() {
    				t.test1();
    			}
    		}).start();
    
    		new Thread(new Runnable() {
    
    			@Override
    			public void run() {
    				t.test2();
    			}
    		}).start();
    	}
    }
    

    Java中的原子操作

    CAS:比较和交换,在进行CAS操作时需要输入两个数值,一个旧值(期望操作前的值),一个新值。在操作期间先比较旧值有没有发生变化,如果没有变化,才交换新值,发生了变化则不交换。

    使用CAS实现原子操作
    AtomicInteger atomicI = new AtomicInteger(0)
    private void safeCount() {
        for (;;) {
            int i = atomicI.get();
            boolean suc = atomicI.compareAndSet(i, i+1);
            if (suc) {
                break;
            }
        }
    }
    

    上面的代码我们要对Integer进行加1操作。先用get()方法获取初值,然后通过compareAndSet(CAS)来修改(i表示原始值,i+1是修改后的值),如果返回值为true表示修改成功,否则不断循环修改。

  • 相关阅读:
    spring aop的@Before,@Around,@After,@AfterReturn,@AfterThrowing的理解
    详解Spring 框架中切入点 pointcut 表达式的常用写法
    Spring计时器StopWatch使用
    Java反射-解析ProceedingJoinPoint的方法参数及参数值
    MySQL 中 datetime 和 timestamp 的区别与选择
    java在注解中绑定方法参数的解决方案
    spring aop的@Before,@Around,@After,@AfterReturn,@AfterThrowing的理解
    LocalDateTime和Date的比较与区别
    idea启动java服务报错OutOfMemoryError: GC overhead limit exceeded解决方法
    java在注解中绑定方法参数的解决方案
  • 原文地址:https://www.cnblogs.com/xidongyu/p/6531285.html
Copyright © 2011-2022 走看看