4 锁的优化及注意事项

“锁”是最常用的同步方法之一。

有助于提高“锁”性能的几点建议

减少锁持有的时间

public synchronized void syncMethod(){
  othercodel();
  mutextMethod();	//1
  othercode2();
}

在上面代码中,若只有1处方法需要同步,而其他两个方法又是两个重量级的方法。那么整个syncMethod()方法在同步的时长上就会大大增加。若这个时候并发量较大,使用整个方法做同步就会导致等待线程大大增加。

一个较为好的解决方案是:只在必要时进行同步。这样可以减少线程持有锁的时间,提高系统的吞吐量。如下面代码:

public void syncMethod2(){
  othercodel();
    synchronized(this){
        mutextMethod();	
    }
  othercode2();
}

注意:减少锁的持有时间有助于降低锁冲突的可能性,进而提升系统的并发能力。

减少锁粒度

所谓减少锁粒度,就是指缩小锁定对象的范围,从而减少锁冲突的可能性,进而提高系统的并发能力。

ConcurrentHashMap来说明减少锁粒度

它内部没有对整个对象进行加锁,而是在内部进一步细分了若干个小的HashMap,称之为段(默认分为16段)。

如果需要再ConcurrentHashMap中增加一个新的表项,并不是将整个HashMap加锁,而是首先根据Hashcode得到该表项应该被放在哪个段中,然后对该段加锁,并完成put()操作。在多线程下,只要被加入的表项不在同一个段中,线程间就可以做到真正的并行。

由于默认有16个段,够幸运的话,ConcurrentHashMap可以接受16个线程同时插入。大致如下图:

减小锁粒度_2019-09-09_10-47-26

但是有一个新问题。当系统需要取得全局锁时,其消耗的资源较多。依然以ConcurrentHashMap为例,它的put()方法分离了锁,但是当试图访问ConcurrentHashMap的全局信息时,就要同时获得所有段的锁方能顺利实施。比如ConcurrentHashMap的size()方法,size()方法的部分代码如下:

size部分代码_2019-09-09_10-24-04

不过,size方法不总是这样执行,它会先使用无锁的方式,如果失败,才会尝试这种加锁的方式。

读写分离锁替换独占锁

读写分离锁替换独占锁是减小锁粒度的一种特殊情况。如果说上节中提到的减少锁粒度是通过分割数据结构实现的,那读写锁则是对系统功能点的分割。

在读多少写的场合,使用读写分离锁可以有效提升系统的并发量

锁分离

以LinkedBlockingQueue为例来说明锁分离。

take()和put()两个操作分别作用于队列的前端和尾端,看起来两者并不冲突,但是若两者使用独占锁,那它们不可能实现真正的并发,在运行时,它们会互相等待对方释放资源。在并发激烈的环境中,会影响性能。

因此,在JDK的实现中,取而代之的是两把不同的锁,分离了take()和put()操作。

/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();
/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();
/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();
/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();

上面源码中,定义了takeLock和putLock,它们分别在take()和put()操作中使用。这样take()和put()就不存在锁竞争的关系,只是在put()与put()、take()与take()间分别存在锁竞争。从而,消弱了锁竞争的可能。

take()方法:

taike()_2019-09-09_11-43-02

put()方法:

put_2019-09-09_11-43-47

锁粗化

通常情况下,通常每个线程持有的锁的时间都尽量短,即使用完公共资源后,就应该立即释放,这样,等待在锁上的其他线程才能获得锁资源执行任务。但是,这有一个度,如果对同一个锁不停地请求、同步释放,其本身也会消耗资源。

为此,虚拟机在对不断进行请求和释放的操作时,会整合成对锁的一次请求,从而减少对锁的请求同步次数。这就叫锁粗化,比如下列代码:

锁粗化_2019-09-09_13-52-51

会整合成:

锁粗化_2019-09-09_13-53-11

尤其是在循环时,如下,没词循环都是对锁进行一次申请和释放:

锁粗化_2019-09-09_13-54-59

上面代码应改成:

锁粗化_循环_2019-09-09_13-55-16

java虚拟机对锁优化所做的努力

锁偏向

如果一个线程获得了锁,那么锁就进入了偏向模式。当这个线程再次请求锁时,无需再做任何同步操作。这样就节省了大量有关锁申请的操作,提高了程序性能。

这种模式适合集合没有锁竞争的场合,若锁竞争激烈,模式类似于失效。使用java虚拟机参数-XX:+UserBiasedLocking可以开启偏向锁。

轻量级锁

若偏向锁失败,虚拟机不会立即挂起线程。它还会使用一种称为轻量级锁的优化手段。轻量级锁只是简单地将对象头部作为指针,指向持有锁的线程堆栈的内部,来判断一个线程是否持有对象锁。如果线程获得轻量锁成功,则可以顺利进入临界区。如果轻量级锁加锁失败,则表示其他线程抢险争夺到了锁,那么当前线程的锁请求就会膨胀为重量级锁。

自旋锁

锁膨胀后,虚拟机做最后努力——自旋锁。由于当前线程暂时无法获得锁,但是什么时候可以获得锁时一个未知数。也许几个CPU后就可以重新获得锁。若这时粗暴地挂起线程可能是一种得不偿失的操作。因此,系统会进行一次赌注:假设线程在短时间内能再次获得这把锁。因此,虚拟机会让当前线程做几个空循环(这也是自旋的含义),若能得到,则进入临界区。若不能获得,则真正在操作系统层面挂起。

锁消除

锁消除是一种更彻底的锁优化。Java虚拟机在JIT编译时,通过对运行上下文的扫描,去除不能存在共享资源竞争的锁。通过锁消除,可以节省毫无意义的请求锁时间。

那如果是不可能存在锁竞争,为什么程序员要加上锁呢?

当我们使用到JDK的一些内置API时,比如StringBuffer、Vector等,内部实现就会有锁。

锁消除涉及的一项关键技术是逃逸分析。

逃逸分析必须在-server模式下进行,可以使用-XX:+DoEscapeAnalysis参数打开逃逸分析。使用-XX:+EliminateLocks参数可以打开锁消除。

人手一支笔:ThreadLocal

如果说锁是一人一支笔,那么ThreadLocal就是一人一支笔的思路了。

ThreadLocal的简单使用

下面代码中,我们使用ThreadLocal为每一个线程都产生一个SimpleDateformat对象实例:

public class ThreadLocalDemo2 {
   static ThreadLocal<SimpleDateFormat> tl=new ThreadLocal<SimpleDateFormat>();
    public static class ParseDate implements Runnable{
       int i=0;
       public ParseDate(int i){this.i=i;}
      public void run() {
         try {
            if(tl.get()==null){		//1
               tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
            }						//2
            Date t=tl.get().parse("2015-03-29 19:29:"+i%60);
            System.out.println(i+":"+t);
         } catch (ParseException e) {
            e.printStackTrace();
         }
      }
    }
   public static void main(String[] args) {
      ExecutorService es=Executors.newFixedThreadPool(10);
      for(int i=0;i<1000;i++){
         es.execute(new ParseDate(i));
      }
   }
}

上面1~2处,如果当前线程不持有SimpleDateformat对象实例。那么就新建一个并把它设置到当前线程,如果已经持有,则直接使用。

从这里也可以看到,为每一个线程人手分配一个对象的工作并不是由ThreadLocal来完成的,而是需要在应用层面保证。ThreadLocal只是起到了简单的容器作用。

ThreadLocal的实现原理

ThreadLocal是如何保证这些对象只被当前线程所访问呢?

我们首先关注的是set()和get()方法。

先说set():

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

在set时,首先获得当前线程对象,然后通过getMap()拿到当前线程的ThreadLocalMap,并将值设如ThreadLocalMap中。而ThreadLocalMap可以理解为一个Map(虽然不是,但是可以简单理解成HashMap)。其中,key为ThreadLocal当前对象,value就是我们需要的值。而ThreadLocals本身就保存了当前自己所在线程的所有“局部变量”,也就是一个ThreadLocal变量的集合。

get():

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

在get()操作时,自然就是把map中的数据拿出来。首先,get()方法先取得当前线程的ThreadLocalMap对象。然后,通过将自己作为key取得内部的实际数据。

释放ThreadLocal:ThreadLocal.remove()防止内存泄露:

当线程退出时,Thread类会进行一些清理工作,其中就包括清理ThreadLocalMap。但是如果我们使用线程池,那就意味着线程未必会退出。如果这样,将一些大大的对象设置到ThreadLocal中(实际保存在线程持有的threadLocal Map中),可能会使系统出现内存泄露的可能(它无法被回收)。所以,如果希望及时回收对象,最好使用ThreadLocal.remove()将这个变量移除。防止内存泄露。

也可以像普通变量一样释放ThreadLocal。ThreadLocal = null

public class ThreadLocalDemo_Gc {
    static volatile ThreadLocal<SimpleDateFormat> t1 = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected void finalize() throws Throwable { //重载了finalize() 当对象在GC时,打印信息
            System.out.println(this.toString() + " is gc");
        }
    };
    static volatile CountDownLatch cd = new CountDownLatch(10000);//倒计时
    public static class ParseDate implements Runnable {
        int i = 0;

        public ParseDate(int i) {
            this.i = i;
        }

        @Override
        public void run() {
            try {
                if (t1.get() == null) {
                    t1.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") {
                        @Override
                        protected void finalize() throws Throwable {
                            System.out.println(this.toString() + " is gc");
                        }
                    });
                    System.out.println(Thread.currentThread().getId() + ":create SimpleDateFormat");
                }
                Date t = t1.get().parse("2016-12-19 19:29:" + i % 60);
            } catch (ParseException e) {
                e.printStackTrace();
            } finally {
                cd.countDown();//完成 计数器减1
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService es = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10000; i++) {
            es.execute(new ParseDate(i));
        }
        cd.await();//等待所有线程 完成准备
        System.out.println("mission complete!!");
        t1 = null;
        System.gc();
        System.out.println("first GC complete!!");
        t1 = new ThreadLocal<>();
        cd = new CountDownLatch(1000);
        for (int i = 0; i < 10000; i++) {
            es.execute(new ParseDate(i));
        }
        cd.await();
        Thread.sleep(1000);
        System.gc();
        System.out.println("second GC complete!!");
    }
}

执行上面代码的代码,看结果,首先,线程池中10个线程都各自创建了一个SimpleDateFormat对象实例。接着进行第一次GC,可以看到ThreadLocal对象被回收了。接着提交了第二次任务,这次一样也创建了10个SimpleDateFormat对象。然后,进行第二次GC。可以看到,在第二次GC后,第一次创建的10个SimpleDateFormat子类实例全部被回收。可以看到我们没有手动remove(),但系统依然回收了它们。

对性能有何帮助

对于性能而言,是为每个线程分配一个独立的对象取决于共享对象的内部逻辑。如果共享对象对于竞争的处理容易引起性能损失,我们还是应该考虑使用ThreadLocal为每个线程分配单独的对象。一个典型的案例就是在多线程下产生随机数。

public class RandomDemo {
    public static final int GET_COUNT = 10000000;
    public static final int THREAD_COUNT = 4;
    static ExecutorService exe = Executors.newFixedThreadPool(THREAD_COUNT);
    public static Random rnd = new Random(123);		//1 定义一个全局的Random,线程共用
    public static ThreadLocal<Random> tRnd = new ThreadLocal<Random>() {	//2 每个线程各使用一个random对象
        @Override
        protected Random initialValue() {
            return new Random(123);
        }
    };
    public static class RndTask implements Callable<Long> {
        private int mode = 0;

        public RndTask(int mode) {
            this.mode = mode;
        }

        public Random getRondom() {
            if (mode == 0) {
                return rnd;
            } else if (mode == 1) {
                return tRnd.get();
            } else {
                return null;
            }
        }

        /**
         * Computes a result, or throws an exception if unable to do so.
         *
         * @return computed result
         * @throws Exception if unable to compute a result
         */
        @Override
        public Long call() throws Exception {
            long b = System.currentTimeMillis();
            for (long i = 0; i < GET_COUNT; i++) {
                getRondom().nextInt();
            }
            long e = System.currentTimeMillis();
            System.out.println(Thread.currentThread().getName() + " spend " + (e - b) + "ms");

            return e - b;
        }
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Future<Long>[] futs = new Future[THREAD_COUNT];		//3
        for (int i = 0; i < THREAD_COUNT; i++) {
            futs[i] = exe.submit(new RndTask(0));
        }
        long totaltime = 0;
        for (int i = 0; i < THREAD_COUNT; i++) {
            totaltime += futs[i].get();
        }
        System.out.println("多线程访问同一个Random实例:" + totaltime + "ms");		//4

        for (int i = 0; i < THREAD_COUNT; i++) {
            futs[i] = exe.submit(new RndTask(1));
        }
        totaltime = 0;
        for (int i = 0; i < THREAD_COUNT; i++) {
            totaltime += futs[i].get();
        }
        System.out.println("使用ThreadLocal包装Random实例:" + totaltime + "ms");
        exe.shutdown();
    }
}

在程序的1处定义了一个全部线程共用的Random、2处定义了 每个线程独自占用的Random。mode用来切换是否使用独占的Random(0为否,1为是)。3~4处,打印出使用同一Random花费的时间,4处后面的代码打印每个线程独占(ThreadLocal包装的)花费的时间。

无锁

并发控制分为两种策略:乐观策略和悲观策略。锁是一种悲观策略、无锁则是乐观的,无锁的策略使用一种比较交换的技术(CAS)来鉴别线程冲突。

无锁的两个好处:比有锁的程序拥有更好的性能;天生就是对死锁免疫的。

与众不同的并发策略:计较交换(CAS)

CAS的算法过程:它包含三个参数:V(要更新的变量)、E(预期值)、N(新值)。仅当V值等于E值时,才会将V值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。失败的线程不会被挂起,仅是被告知失败,并且可以再次尝试。

在硬件层面,大部分的现代处理器都已经支持原子化的CAS指令。在JDK5.0后,这种操作无处不在。

无锁的线程安全整数:AtomicInteger

与Integer不同的是,AtomicInteger是可变的,并且是线程安全的。对其进行修改等的任何操作,都是CAS进行的。

AtomicInteger的主要方法有:

AtomicInteger_2019-09-16_15-53-13

内部重要的两个值

AtomicInteger的当前实际值:value

private volatile int value;

保存AtomicInteger对象中的偏移量:valueOffset,它是AtomicInteger的关键。

private static final long valueOffset;

简单实例

public class AtomicIntegerDemo {
    static AtomicInteger i = new AtomicInteger();

    public static class AddThread implements Runnable {
        public void run() {
            for (int k = 0; k < 10000; k++)
                i.incrementAndGet();	//1
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread[] ts = new Thread[10];
        for (int k = 0; k < 10; k++) 
            ts[k] = new Thread(new AddThread());  
        for (int k = 0; k < 10; k++) 
            ts[k].start();
        for (int k = 0; k < 10; k++) 
            ts[k].join();
        System.out.println(i);
    }
}

在1处,i.incrementAndGet()方法会使用CAS操作将自己+1,并返回当前值(这里忽略了当前值)。执行完这段代码,会输出100000,这说明程序正常执行。

内部实现

基于JDK1.7的incrementAndGet(),JDK1.8有点不同。

public final int incrementAndGet(){
  for(;;){		//1
    int current = get();	//2
    int next = current + 1;
    if(compareAndSet(current, next))	//3
    	return next;
  }
}

在1处中,为什么连设置一个值都需要一个死循环呢?原因就是:CAS的操作未必是成功的,若不成功,我们就要不断的进行尝试。在2处,用get()取得当前值,接着+1后得到新值。在3处,使用compareAndSet将新值next写入,成功的条件是,当前值应该等于刚刚取得的current。如果不是这样,说明在2处到3处之间,有其他线程已经修改过了,这种情况被看做是过期状态。因此,需要进行下一次尝试,直到成功。

get():只是返回内部数据value

public final int get(){
  return value;
}

类似的类

和AtomicInteger类似的类还有AtomicLong、AtomicBoolean、AtomicReference(表示对象引用)

java中的指针:Unsafe类

java虽然抛弃了指针,但是在关键时刻,类似指针的技术还是必不可少的。下面例子中的的Unsafe实现就是最好的例子。但是,开发人员并不希望大家使用这个类,下面只是了解一下这个类。

实例讲解Unsafe

我们对之前的compareAndSet()的实现进一步研究:

public final boolean compareAndSet(int expect, int update){
  return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

sum.misc.unsafe:上面的unsafe类,封装了一些不安全的操作。java中去除了指针,而java中的Unsafe就封装了一些类似指针的操作。

CompareAndSwapInt()方法是一个navtive方法。它的几个参数如下:

public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);

第一个参数o为给定的对象,offset为对象内的偏移量(其实就是一个字段到对象头部的偏移量,通常这个偏移量可以快速定位字段),expected表示期望值,x表示要设置的值。

3.3.4中的ConcurrentLinkedQueue中的Node的一些CAS操作也都是使用Unsafe来实现的。

无锁的对象引用:AtomicReference

AtomicReference和AtomicInteger非常类似,不同之处:AtomicInteger是对整数的封装,而AtomicReference是对普通的对象引用。

CAS一个小小的例外

线程判断被修改对象是否被正确写入的条件是当前值是否等于期望值。这个逻辑一般是正确的,但是有一个例外:

CAS_2019-09-16_16-24-57

图中显示,当获得对象数据后,在准备修改为新值前。线程B将对象的值修改了两次,而两次修改后,对象的值又恢复为旧值。若是简单的数值加法,这没什么,即使这个数字再怎么修改,只要他最终改回期望值,加法计算就不会错。

但是,在现实中,还存在另一种常见,就是我们是否能修改对象的值,不仅取决于当前值,还取决于对象的变化过程。

比如:有一家蛋糕店,决定为贵宾卡里余额小于20元的客户一次性赠送20元,刺激消费者充值和消费。但条件是,每一位用户只能被赠送一次。

stotic AtomicReference<Integer> money = new AtomicReference<Integer>();

接着,启动若干个线程,不断扫描数据,并为满足条件的客户充值。

for(int i = 0; i<3; i++){
  new Thread(){
    public void run(){
      while(true){
        while(true){
          Integer m = money.get();
          if(m < 20){
            if(money.compareAndSet(m, m+20)){	//1
              System.out.println("余额小于20,充值成功!");
              break;
            }
          }else{
          	//余额大于20,无需充值
            break;
          }
        }
      }
    }
  }
}

在1处,判断用户余额并赠送金额。若此时被其他线程处理,那么当前线程就会失败。这样可以保证用户只会充值一次。

问题来了,此时,若很不幸,用户正在消费,就在金额刚刚赠送的同时,他进行了一次消费,使总金额又小于20。使得消费、赠于后的的金额等于消费前,赠予前的金额。这时,后台的赠予进程就会误以为这个账户还没有赠予。所以,存在被多次赠予的可能。虽然几率很小。

解决办法

使用AtomicStampedReference可以很好的解决这个问题。

带有时间轴的对象引用:AtomicStampedReference

AtomicReference无法解决上述问题的根本原因是对象在修改过程中,丢失了状态信息。

AtomicStampedReference不仅维护了对象值,还维护了一个时间戳(实际上可以使用任意整数表示状态值)。当AtomicStampedReference对应的数值被修改时,除了更新数据本身外,还必须要更新时间轴。当AtomicStampedReference设置对象值时,对象值以及时间轴都必须满足期望值,写入才会成功。这样,即使对象被重复读写,写回原值,有了时间戳,也能防止不恰当的写入。

**常用api **

public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)
//获得当前对象引用
public V getReference()
//获得当前时间戳
public int getStamp()
//设置当前对象引用和时间戳
public void set(V newReference, int newStamp)

例子

public class AtomicStampedReferenceDemo {
    static AtomicStampedReference<Integer> money = new AtomicStampedReference<Integer>(19, 0);

    public static void main(String[] args) {
        // 模拟多个线程同时更新后台数据库,为用户充值
        for (int i = 0; i < 3; i++) {
            final int timestamp = money.getStamp();
            new Thread() {
                public void run() {
                    while (true) {
                        while (true) {
                            Integer m = money.getReference();
                            if (m < 20) {
                                if (money.compareAndSet(m, m + 20, timestamp, timestamp + 1)) {		//1
                                    System.out.println("余额小于20元,充值成功,余额:"
                                            + money.getReference() + "元");
                                    break;
                                }
                            } else {
                                // System.out.println("余额大于20元,无需充值");
                                break;
                            }
                        }
                    }
                }
            }.start();
        }

        // 用户消费线程,模拟消费行为
        new Thread() {
            public void run() {
                for (int i = 0; i < 100; i++) {
                    while (true) {
                        int timestamp = money.getStamp();
                        Integer m = money.getReference();
                        if (m > 10) {
                            System.out.println("大于10元");
                            if (money.compareAndSet(m, m - 10, timestamp, timestamp + 1)) {
                                System.out.println("成功消费10元,余额:" + money.getReference());
                                break;
                            }
                        } else {
                            System.out.println("没有足够的金额");
                            break;
                        }
                    }
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                    }
                }
            }
        }.start();
    }
}

在1处,如果赠予成功,我们修改时间戳,使得系统不可能发生二次赠予的情况。消费线程也是,每次操作,都使得时间戳加1,使之不可能重复。

数组也能无锁:AtomicIntegerArray

除了提供基本数据类型外,JDK也封装了数组:AtomicIntegerArray、AtomicLongArray和AtomicReferenceArray,分别表示整数数组、Long型数组和普通对象数组。

以AtomicIntegerArray为例。

AtomicIntegerArray本质上是对int[]类型的封装。使用unsafe类通过CAS的方法控制int[]在多线程下的安全性。

几个核心API

//获得数组第i个下标的元素
public final int get(int i)
//获得数组的长度
public final int length()
//将数组第i个小标设置为newValue,并返回旧的值
public final int getAndSet(int i, int newValue)
//进行CAS操作,如果第i个下标的元素等于expect,则设置为update,设置成功返回true
public final boolean compareAndSet(int i, int expect, int update)
//将第i个下标的元素加1
public final int  getAndIncrement(int i)
//将第i个下标的元素减1
public final int getAndDecrement(int i)
//将第i个下标元素加delta(delta可以是负数)
public final int getAndAdd(int i, int delta)

简单示例

public class AtomicIntegerArrayDemo {
   static AtomicIntegerArray arr = new AtomicIntegerArray(10);	//1
    public static class AddThread implements Runnable{
        public void run(){
           for(int k=0;k<1000;k++)
              arr.getAndIncrement(k%arr.length());
        }
    }
   public static void main(String[] args) throws InterruptedException {
        Thread[] ts=new Thread[10];
        for(int k=0;k<10;k++){
            ts[k]=new Thread(new AddThread());
        }
        for(int k=0;k<10;k++){ts[k].start();}
        for(int k=0;k<10;k++){ts[k].join();}
        System.out.println(arr);
   }
}

上述代码中,1处申明了一个内含10个元素的数组。2处对数组内10个元素进行累加,每个元素各加100。开启10个线程数组中10个元素必然是1000。若线程不安全数组中10个元素可能部分或全部小于1000。

普通变量也可享受原子操作:AtomicIntegerFieldUpdate

有时候,由于初期考虑不周,或后期需求变化,一些普通变量也会有线程安全的需求。这时候怎么办呢?我们当然可以简单修改这个变量成线程安全的变量,但违反了开闭原则。这时候就要想到AtomicIntegerFieldUpdate了。

作用:它可以在不改动(或极少改动)原有代码的基础上,让普通的变量也可享受CAS操作带来的线程安全性,来获得线程安全的保障。

有三种:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater和AtomicReferenceFieldUpdater。分别可以对int、long和普通对象进行CAS修改。

public class AtomicIntegerFieldUpdaterDemo {
    public static class Candidate{
        int id;
        volatile int score;
    }
    public final static AtomicIntegerFieldUpdater<Candidate> scoreUpdater
            = AtomicIntegerFieldUpdater.newUpdater(Candidate.class, "score");	//1
    //检查Updater是否工作正确
    public static AtomicInteger allScore=new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException {
        final Candidate stu=new Candidate();      //新建一个候选人
        Thread[] t=new Thread[10000];
        for(int i = 0 ; i < 10000 ; i++) {
            t[i]=new Thread() {
                public void run() {
                    if(Math.random()>0.4){
                        scoreUpdater.incrementAndGet(stu);	//2
                        allScore.incrementAndGet();
                    }
                }
            };
            t[i].start();
        }
        for(int i = 0 ; i < 10000 ; i++) {  t[i].join();}
        System.out.println("score="+stu.score);
        System.out.println("allScore="+allScore);
    }
}

代码1处定义了一个AtomicIntegerFieldUpdater实例,用来对Candidate.score进行写入。用allScore来校验 scoreUpdater 的正确性。代码2处对stu的score进行增1操作。

AtomicIntegerFieldUpdater的几个注意事项:

  • Updater只能修改它可见范围内的变量。因为Updater使用反射得到这个变量。如果变量不可见,就会出错。如score设置为private,就是不可见的。
  • 为了确保变量被正确读取,它必须时volatile类型的。
  • 由于CAS操作会通过对象实例中的偏移量直接赋值,因此,它不支持静态变量。

让线程之间互相帮助:SynchronousQueue的实现

死锁

在大部分应用程序中,使用锁的情况一般要多于无锁。

死锁:死锁就是两个或者多个线程,相互占用对方需要的资源,而都不进行释放,导致彼此之间都相互等待对方释放资源,产生无限等待的现象。

对系统的影响:死锁一旦出现,如果没有外力介入,将永远存在,这对系统产生严重影响。

简单的例子:

public class DeadLock extends Thread {
   protected Object tool;
   static Object fork1 = new Object();
   static Object fork2 = new Object();
   public DeadLock(Object obj) {
      this.tool = obj;
      if(tool == fork1) {
         this.setName("哲学家A");
      }
      if(tool == fork2) {
         this.setName("哲学家B");
      }
   }
   @Override
   public void run() {
      if(tool == fork1) {
         synchronized (fork1) {
            try {
               Thread.sleep(500);
            } catch (Exception e) {
               e.printStackTrace();
            }
            synchronized (fork2) {
               System.out.println("哲学家A开始吃饭了");
            }
         }
      }
      if(tool == fork2) {
         synchronized (fork2) {
            try {
               Thread.sleep(500);
            } catch (Exception e) {
               e.printStackTrace();
            }
            synchronized (fork1) {
               System.out.println("哲学家B开始吃饭了");
            }
         }
      }
   }
   public static void main(String[] args) throws InterruptedException {
      DeadLock A = new DeadLock(fork1);
      DeadLock B = new DeadLock(fork2);
      A.start();
      B.start();
      Thread.sleep(1000);
   }
}

上面例子中,两个哲学家吃饭都需要两把叉子,当只有两把叉子时,而两个哲学家又一人枪了一把叉子,在等对方手里的第而把叉子吃饭,这时,两个哲学家都互相占用对方的叉子,于是,两个哲学家将一直等待下去,从而产生了死锁。

posted @ 2019-11-29 14:43  星记事  阅读(273)  评论(0编辑  收藏  举报