Java并发编程的艺术(一)

看《java并发编程的艺术》这本书,想着看的时候做个简单的总结,方便以后直接看重点。

一.并发编程的挑战

1.上下文切换

Cpu时间片通过给每个线程分配CPU时间片来实现多线程机制,时间片一般是几十毫秒。任务从保存到再加载的过程就是一次上下文切换。

如何减少上下文切换?

  • 无锁并发编程:多线程处理数据时,避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
  • CAS算法:Compare and Swap,即比较再交换。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。当两者进行比较时,如果相等,则证明共享数据没有被修改,替换成新值,然后继续往下运行;如果不相等,说明共享数据已经被修改,放弃已经所做的操作,然后重新执行刚才的操作。容易看出 CAS 操作是基于共享数据不会被修改的假设,采用了类似于数据库的commit-retry 的模式。当同步冲突出现的机会很少时,这种假设能带来较大的性能提升。
public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
  • 使用最少线程:避免创建不需要的线程
  • 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

2.死锁

避免死锁的几个常见方法:

  • 避免一个线程同时获取多个锁
  • 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
  • 尝试使用定时锁,使用lock.trylock(timeout)来替代使用内部锁机制
  • 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。

二.java并发机制的底层实现原理

  java使用的并发机制依赖于JVM的实现和CPU的指令

1.volatile的应用

  volatile是轻量级的synchronized,它保证了共享变量的“可见性”,就是说当一个线程修改一个共享变量的时候,另外一个线程能读到这个修改的值。它不会引起线程上下文的切换和调度。

那么volatile如何保证可见性的呢?

volatile变量修饰的共享变量在进行写操作的时候会多出第二行汇编代码,有一个lock前缀指令:

  • 将当前处理器缓存行的数据写回到系统内存
  • 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效

2.synchronized的实现原理与应用

synchronized实现同步的基础:java中的每一个对象都可以作为锁

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

当一个线程视图访问同步代码块时,首先必须得到锁,退出或抛出异常时必须释放锁。

synchronized在jvm里的实现原理:JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,通过两个指令,monitorenter和monitorexit

monitorenter指令是在编译后插入到同步代码块的开始位置,monitorexit是插入到方法结束处和异常处,jvm要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。

synchronized用的锁是存在java对象头里的

3.原子操作的实现原理

处理器如何实现原子操作?

  1. 使用总线锁保证原子性:总线锁就是使用处理器提供的一个Lock#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞,那么该处理器就可以独占共享内存。
  2. 使用缓存锁保证原子性:同一时刻,只需保证对某个内存地址的操作是原子性即可,但总线锁把CPU和内存之间的通信锁住了,使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大。采用缓存锁定的方式来实现复杂的原子性。

java如何实现原子操作?

java中可以通过锁和循环CAS的方式来实现原子操作

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;


/**
 * java使用循环CAS实现原子操作
 */
public class Counter {
    private AtomicInteger  atomic1 = new AtomicInteger(1);
    private  int i=1;

    public static void main(String[] args) {
        final  Counter cas = new Counter();
        List<Thread> ts = new ArrayList<>(600);
        long start = System.currentTimeMillis();
        for(int j=0;j<100;j++){
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i=0;i< 10000;i++){
                        cas.count();
                        cas.safeCount();
                    }
                }
            });
            ts.add(t);
        }
        for (Thread t:ts){
            t.start();
        }
        for (Thread t:ts){
            try {
                t.join();
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
        System.out.println(cas.i);
        System.out.println(cas.atomic1.get());
        System.out.println(System.currentTimeMillis()-start);
    }

    /**
     * 使用CAS实现线程安全计数器
     */
    private  void safeCount(){
        for(;;){
            int i = atomic1.get();
            boolean suc = atomic1.compareAndSet(i,++i);
            if(suc){
                break;
            }
        }
    }

    /**
     * 非线程安全计数器
     */
    private  void count(){
        i++;
    }
}

 运行结果如下:

 

 可见线程安全计数器正确显示了结果。

CAS实现原子操作的三大问题:

  • ABA问题:CAS需要在操作值的时候检查值有没有发生变化,如果没有发生变化就更新。但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时就会发现它的值没有发生变化,但是实际上变化了。ABA问题的解决思路就是使用版本号,A->B->A变成了1A->2B->3A,  java中提供了一个类来解决这个ABA问题:AtomicStampedReference
 
 /**
     * Atomically sets the value of both the reference and stamp
     * to the given update values if the
     * current reference is {@code ==} to the expected reference
     * and the current stamp is equal to the expected stamp.
     *
     * @param expectedReference the expected value of the reference
     * @param newReference the new value for the reference
     * @param expectedStamp the expected value of the stamp
     * @param newStamp the new value for the stamp
     * @return {@code true} if successful
     */
public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }

这个方法的作用就是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

  • 循环时间长开销大。
  • 只能保证一个共享变量的原子操作。解决办法是把多个共享变量合并成一个共享变量来操作·,比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。java中提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。

使用锁机制实现原子操作:锁机制保证了只有获得锁的线程才能操作锁定的内存区域,JVM内部实现了很多种锁机制,有偏向锁,轻量级锁和互斥锁,但是除了偏向锁,JVM实现锁的方式都用了循环CAS

转载于:https://www.cnblogs.com/Dream-chasingGirl/p/11458341.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值