《Java并发编程艺术》第二章Java并发机制的底层实现

第一章Java并发挑战主要有:

  1. 上下文切换问题:
    解决思路:
    1.减少线程数量
    2.增长线程内的有效利用率(就是线程运行时间和上下文切换时间)

  2. 死锁
    解决思路:
    1.避免一个线程同时获得多个锁
    2.避免一个线程在锁内同时占用多个资源。
    3.尝试使用定时锁(lock.tryLock(timeout))
    4.对数据库锁,保证操作由一个连接完成

  3. 资源限制
    第二章:

volatile

定义: 实现线程访问共享变量时,为了确保共享变量能够准确和一致的更新。
为何需要: 由于在默认的情况下,CPU发现一个操作数是可以进行缓存的便将其缓存下来 (缓冲行填充),若下次再访问该操作数所在地址时,则不通过内存访问,而是直接访问缓冲行中的值 (缓存命中),进行操作,再判断写入的地址是否已经存于缓存行,若是则将值再写到缓冲行 (写命中)
这样在多线程的场景下,就会影响问题,线程读到的时之前的数据,各做各的也没有达到目标。
如何解决:
这时变需要使用volatile变量了,它添加到某变量上时,就保证了CPU只能从内存读值且缓存行无效。
它会给原来的指令追加一条带lock前缀的指令,这个指令保证了两件事:

  1. 将当前处理器缓存行的数据写回系统内存(实现方式1.锁总线,2.锁缓存行,破坏了缓存一致性机制,所以迫使CPU写回内存)
  2. 写回内存的操作会使得其他CPU缓存的该内存地址的数据无效。(实现方式:啥内部嗅探技术还有就是迫使缓存行无效,下次操作时强制填充)

这就很好的解决了以上所说的问题。
volatile的优化:
文中提到的LinkedTransfarQueue类 是将数据填充为64字节(或者说68字节),由于某些缓冲行为64字节,所以无法将队列头部指针和尾部指针同时缓存,使得头尾的修改无法互相锁定。
适用的条件:

  1. 缓存行64字节带宽
  2. 共享变量被频繁的改写。

synchronized

Java中每一个对象都可以所作锁使用(毕竟有对象头就可以),主要加三种表现形式:
对象头见此文相关部分

  1. 普通方法:当前实例对象
  2. 静态同步方法:当前类的Class对象
  3. 同步代码块:配置的对象

底层实现:
JVM中基于进入和退出Moniter对象来实现同步,方法同步和代码块同步的实现原理可不一样,不过都是使用monitorentermonitorexit指令,monitorenter指令在编译后插入到同步代码块的开始位置,monitorexit则插入到结束或者异常位置,当一个monitor被持有后,这段代码块便处于锁定状态。

锁的分类:
1.偏向锁:
是一种特殊的“无锁”状态,当一个线程访问同步代码块时,会在对象头栈帧中的锁记录里储存锁偏向的线程ID,只需简单的测试下对象头的Mark Word是否存储着指向当前线程的偏向锁,若是则成功获得锁,若不是,则看是否设置偏向锁标志,若是,改为本线程ID,若不是,则用CAS竞争锁。
**偏向锁的撤销:**很明显出现竞争时,偏向锁便不再适用了,等待一个没有执行的字节码的时间点(全局安全点),首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,死了,锁的对象头设置为无锁,活着,要么偏向其他线程,要么恢复到无锁,要么标记该对象不适用于偏向锁。
关闭偏向锁:

-XX:BiasedLockingStartupDelay=0
//由于偏向锁默认程序启动几秒后启动,若需要可以取消延迟
-XX:UseBiasedLocking=flase
//如果程序中所有锁通常处于竞争状态可以关闭偏向锁

在这里插入图片描述
在这里插入图片描述
2.轻量级锁
加锁:
首先将锁对象头的MarkWord复制到栈帧的锁记录中(Displaced Mark Word),再将MarkWord替换为指向锁记录的指针,若成功,线程获得锁,若失败则说明有竞争,以自旋CAS方式获取锁。若超过三个线程竞争该锁直接膨胀为重量级锁
解锁:
将锁记录中存的MarkWord写回对象头中,若成功,表示当前没有竞争,若失败,膨胀为重量级锁(因为在必然存在竞争时,无用的自旋会让CPU做无用功)。
在这里插入图片描述
在这里插入图片描述
3.重量级锁
互斥锁,在某个线程持有锁时,其他尝试竞争锁的线程都会被阻塞,当锁被释放时,再唤醒其他线程,重新竞争锁。

注意: 锁只能升级不能降级。

优点 缺点 适用场景
偏向锁 无需额外消耗 存在竞争时产生额外的锁撤销消耗 只有一个线程访问同步块时
轻量锁 不会阻塞提高相应速度 若存在长时间竞争,自旋消耗CPU 追求响应速度,同步块执行快
重量锁 不使用自旋 线程阻塞,响应慢 追求吞吐量(竞争多),同步块执行时间长

原子性

处理机制:
在这里插入图片描述

public class CASTest {
    private static AtomicInteger atomicInteger = new AtomicInteger();
    private static int i;
    public static void main(String[] args) throws InterruptedException {
        List<Thread> list = new ArrayList<>();
        for(int i=0;i<1000;i++){
            list.add(new Thread(()->{
                for(int j =0;j<1000;j++) {
                    safeAdd();
                    unsafeAdd();
                }
            }));
        }
        for(int i =0;i<1000;i++){
            list.get(i).start();
        }
        for(Thread i:list){
            i.join();
        }
        System.out.println("safe:"+atomicInteger.get());
        System.out.println("unsafe:"+i);
    }
    static void safeAdd(){//保证原子性
        while(true){
            int count = atomicInteger.get();
            if(atomicInteger.compareAndSet(count,count+1)){
                break;
            }
        }
    }
    static void unsafeAdd(){//瞎几把加
        i++;
    }
}

//再用CAS写个单例,不过没有太大实用价值,还是建议用doublecheck、饿//汉啊,当然最推荐的还是枚举的写法

public class CASSingle {
    private static AtomicReference<CASSingle>instance= new AtomicReference<>();

    private CASSingle(){}
    public static CASSingle getCasSingle() {
        while(true){
            CASSingle casSingle = instance.get();
            if(casSingle!=null){
                return casSingle;
            }
            casSingle = new CASSingle();
            System.out.println(Thread.currentThread().getName()+"新建了一个对象");
            if(instance.compareAndSet(null,casSingle)){
                return casSingle;
            }
        }
    }

//阔以感受一下,虽然最后得到的同一对象,不过可不止实例化了一个对象
在这里插入图片描述
CAS的三大问题:
1.ABA问题:A->B->A表面上没有修改,其实改过了,解决方法是追加版本号(jdk的Atomic包下的AtomicStampedReference来解决,先检查预期再检查版本号)
2.循环时间长开销大
3.只能保证一个共享变量原子操作,用原子引用解决呀,封装成一个类。

发布了18 篇原创文章 · 获赞 1 · 访问量 214

猜你喜欢

转载自blog.csdn.net/qq_38732834/article/details/105549231