【Java并发】五、锁的优化和注意事项

【Java并发】五、锁的优化和注意事项

提高锁的性能

多线程可以提高应用的性能,但是为了保证数据的一致性,需要引入锁,这样多线程又会因为激烈的锁竞争导致性能下降,而且锁、线程的维护本身也需要消耗系统资源,因此并不是多线程一定能提高系统性能,我们需要在锁的使用上更加注意。

减少锁的持有时间

很容易理解:如果单个线程对锁的战友时间过长,那么势必导致其他线程长时间等待,竞争激烈。从而系统性能下降。所以在使用锁的时候,应该尽可能地拆分出可并行和必须同步的部分,让锁竞争真正发生在最需要同步的地方,而不是把整个过程加锁,让一些耗时的、不需要同步的过程也参与锁竞争。

减小锁粒度

JDK1.7中的ConcurrentHashMap使用synchronized + segment实现,是一个典型的减小锁粒度的例子。先把HashMap的数据分成若干个段,当需要做put()操作时,首先计算出数据将要往哪个段中插入,然后只对相应的段作插入操作,不会阻塞其他段的数据操作,这样大大增加了吞吐量。

但是,减小锁粒度会带来一个新问题:假设我需要获得全局的锁,那么小号的资源就会更多,比如我要访问ConcurrentHashMapsize()方法,这个时候就需要获取所有段的锁,求出每个段的长度,然后求和,最后释放所有段的锁,这会导致全局操作时所有段的数据操作都被阻塞,虽然ConcurrentHashMap会先尝试使用无锁方式求和,如果失败了才会再次使用加锁方式求和,但是性能肯定要低于HashMap,只有在size()类似的全局操作很少的情况下,才能真正意义上提高系统的吞吐量。

但对于读写操作频繁的系统,减小锁粒度是非常有必要的,比如Mysql数据库,当需要对某一行做更新时,只需要数据库行锁,而不是锁住整个表,让在其他不受影响的行上的操作做不必要的等待。

读写分离锁替换独占锁

在记录ReadWriteLock时有提到读写锁可以使用在读多写少的系统,特别是写操作非常耗时的情况下。相较于减小锁粒度对数据结构进行分割,读写分离锁是通过对系统功能进行分割实现的。

锁分离

相较于读写分离锁是对不同系统功能的分割,锁分离是基于同一系统功能的不同场景分离。

比如LinkedBlockingQueuetake()put()操作分别实现了从队列中获取数据和往队列中增加数据的功能,但是由于LinkedBlockingQueue是基于链表实现的,那么take()/put()操作虽然操作的是同一个链表,但是一个是操作队首元素,一个是操作队尾元素,大多数情况下并不冲突,因此完全可以使用两把锁来实现阻塞(ReentrantLock takeLock,putLock),从而实现更高的吞吐量。

锁粗化

这似乎和锁优化的目的是背离的,然而并不是。假设有一下一段代码:

public void doSth(){
	Object lock = new Object();
	for(int i = 0;i < 100;i ++){
		synchronized(lock){
			System.out.println(i);
		}
	}
}

public void doSth2(){
	Object lock = new Object();
	synchronized(lock){
		System.out.println("m1...");
	}
	synchronized(lock){
		System.out.println("m2...");
	}
	synchronized(lock){
		System.out.println("m3...");
	}
}

上面两个方法频繁的加锁是没有必要的,而且对同一把锁频繁的加锁和释放,本身就会消耗系统资源,这非常不利于性能优化。注意到这种情况,JVM会将第二种情况整合变成:

public void doSth2(){
	Object lock = new Object();
	synchronized(lock){
		System.out.println("m1...");
		System.out.println("m2...");
		System.out.println("m3...");
	}
}

而第一种场景似乎只能自己做优化了。

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

偏向锁

偏向锁设计针对加锁操作的优化,它的核心思想是:如果一个线程获得了锁,那么锁就进入了偏向模式,如果当前线程再次请求锁,就无需再做任何同步操作,如果请求来自于其它线程,那么退出偏向锁模式。下面演示使用偏向锁和不使用偏向锁的差别:

public class BiasedLockTest {

    public static void main(String[] args) throws InterruptedException {
        long s = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            compute();
        }
        System.out.println(System.currentTimeMillis() - s);
    }

    private static void compute() throws InterruptedException {
        Object o = new Object();
        o.hashCode();
    }
}

不使用同步时,用时302毫秒;在compute()方法前加上synchronized,用时512毫秒;修改VM参数,添加-XX:+UseBiasedLocking参数打开偏向锁后,484毫秒。
因此在锁竞争不是很激烈的情况下,可以使用偏向锁策略提高系统效率,因为大部分时间同步方法都被同一个线程调用,完全不需要做额外的同步,但是一旦线程很多,就没有很好的效果,因为同步总是在线程间跳动,不会有很长的时间来自同一个线程。

轻量级锁

如果偏向锁失败,线程并不会立即挂起,JVM还会使用一种轻量级锁的优化手段:JVM现在当前线程的栈针中创建Displaced Mark Word存储锁的头部信息(Mark Word)Java并发编程:Synchronized底层优化(偏向锁、轻量级锁),然后使用CAS尝试从锁对象中拷贝头部信息到栈中,如果拷贝成功,就把锁的头部指向栈中的存储,当前线程获得锁;如果拷贝,先检查锁对象的头部信息是否指向当前线程的Displaced Mark Word,如果是说明当前线程已经获得锁,开始进入同步块执行,否则说明有其他线程已经获取过锁,锁膨胀为重量级,当前线程会尝试使用自旋来获取锁。

自旋锁

在轻量级锁获取失败的时候,当前线程会通过自旋尝试获取锁。自旋的意思是:与其让当前线程直接挂起,不如做一个赌注,假设这个线程会在不久后获取到锁,虚拟机会让当前线程做几次空循环(自旋),假设在若干个循环后获取到锁,就开始进入临界区执行,否则才真正挂起。这样做的好处是可以减少一次内核和上下文的切换以及一次线程切换的维护,但假设别的线程占用锁的时间太长,一直自旋下去也不是办法,可能出现多个线程在自旋状态占用cpu资源。因此自旋一般都是有次数限制的,自适应自旋

锁消除

有些情况下其实并不需要锁,但是我们在使用Java内置的一些API时,会用到锁,比如Vector等,假设一个Vector只是某个线程的局部变量,绝对不可能出现竞争,那么JIT编译时会直接消除锁。这是根据代码逃逸技术实现的:如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。消除锁的效果肯定是肉眼可见的,因为加锁和不加锁的性能差别还是比较明显的,JVM中使用-XX:+EliminateLocks参数可以打开锁消除,使用-XX:+DoEscapeAnalysis可以打开逃逸分析。

ThreadLocal

同步的核心思想是用时间换空间:可以牺牲一点性能,延长整个任务的时间,但是必须留出足够的(共享但是同一时间只能独占)空间给每一个线程。ThreadLocal可以看作是用空间换时间:给每个线程一个单独的数据空间(不共享只独占),保证每个线程能早第一时间执行,线程之间的数据空间无法访问彼此,也没有必要访问彼此的数据。

这种情况出现在一些线程内有效的数据传输,比如一次web请求的request传递,或者某些数据需要多线程共享但是只需要读取和使用但不需要写回,但要命的是使用时还必须独占加锁否则异常的场景,比如多线程的DataFormat

public class SimpleDateFormatTest {

    private final static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    System.out.println(sdf.parse("2018-12-11 21:03:30").toString());
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

上面的程序一般都会报错java.lang.NumberFormatException: For input string: "E112"java.lang.NumberFormatException: multiple points等,原因在于调用parse方法并不是线程安全的,多线程访问会导致一些共享变量的混乱。解决这个问题其实也很简单,用传统的加锁即可:

public class SimpleDateFormatTest {

    private final static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    private final static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    lock.lock();
                    System.out.println(sdf.parse("2018-12-11 21:03:30").toString());
                } catch (ParseException e) {
                    e.printStackTrace();
                }finally {
                    lock.unlock();
                }
            }).start();
        }
    }

}

但是这样没有必要:第一,我们只是需要一个结果而已,并没有改变sdf的值,根本没有改坏的可能性,反而要我来维护一个锁降低系统的性能,这对程序和程序员来说是不公平的;第二,加锁会给程序性能带来影响,这是一个不必要的竞争。因此我们可以给每一个使用sdf的线程都分配一个sdf对象,这些对象只能被对应的线程访问,互不影响,这也是ThreadLocal的核心:

public class SimpleDateFormatTest {

    private final static ThreadLocal<SimpleDateFormat> tl = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-ddHH:mm:ss"));

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    SimpleDateFormat sdf = tl.get();
                    System.out.println(sdf.parse("2018-12-11 21:03:30").toString());

                    doAgain();

                } catch (ParseException e) {
                    e.printStackTrace();
                }

            }).start();
        }

    }

    private static void doAgain() throws ParseException {
        SimpleDateFormat sdf = tl.get();
        System.out.println("[DO AGAIN] " + sdf.parse("2018-12-11 21:03:30").toString());
    }
}
/**
 * Tue Dec 11 21:03:30 CST 2018
 * Tue Dec 11 21:03:30 CST 2018
 * Tue Dec 11 21:03:30 CST 2018
 * Tue Dec 11 21:03:30 CST 2018
 * Tue Dec 11 21:03:30 CST 2018
 * Tue Dec 11 21:03:30 CST 2018
 * [DO AGAIN] Tue Dec 11 21:03:30 CST 2018
 * [DO AGAIN] Tue Dec 11 21:03:30 CST 2018
 * [DO AGAIN] Tue Dec 11 21:03:30 CST 2018
 * [DO AGAIN] Tue Dec 11 21:03:30 CST 2018
 * [DO AGAIN] Tue Dec 11 21:03:30 CST 2018
 * [DO AGAIN] Tue Dec 11 21:03:30 CST 2018
 * Tue Dec 11 21:03:30 CST 2018
 * Tue Dec 11 21:03:30 CST 2018
 * Tue Dec 11 21:03:30 CST 2018
 * [DO AGAIN] Tue Dec 11 21:03:30 CST 2018
 * [DO AGAIN] Tue Dec 11 21:03:30 CST 2018
 * [DO AGAIN] Tue Dec 11 21:03:30 CST 2018
 * Tue Dec 11 21:03:30 CST 2018
 * [DO AGAIN] Tue Dec 11 21:03:30 CST 2018
 */

可以看出ThreadLocal会在每个线程中单独保存一份数据,而且同一个线程中任何地方都可以直接使用。web项目中,当一次请求的数据交给另外一个县城处理,并且处理数据的线程需要这次请求的Request信息时就非常有用。启动数据处理线程时,Request信息并不会传入到新的线程,一种有效的办法是从主线程传入,但假设我们没法控制Request主动传入呢?比如spring-boot-cloud中调用FeignClient的时候就会丢失Header信息,这时候可以写一个拦截器,拦截器拦截所有FeignClient请求,在请求中加入从ThreadLocal中获取到的header信息即可;如果ThreadLocal中没有header信息,那么说明该调用仍在Request线程中,可以使用RequestContextHolder直接获取,否则需要在异步调用的时候主动将header信息加入ThreadLocal

ThreadLocal原理不是特别复杂,每个Thread都有一个threadLocals域,可以看做一个简易的Map,当向ThreadLocal设置值时,先获取ThreadthreadLocals域,如果为空就初始化threadLocals;然后将这个ThreadLocal对象本身作为键、设置的值为value放入threadLocals;在A线程从ThreadLocal获取值的时候,先得到A线程的threadLocals域,然后把ThreadLocal对象作为键从threadLocals获取对应的值;之所以把threadLocals设置成Map,是因为一个线程可能有多个ThreadLocal,而ThreadLocal<T>只能存储一个类型的值,在多个ThreadLocal的情况下就可以通过不同的键获取到不同的值。

可以发现,如果线程不结束,而且ThreadLocal中的对象有十分大,那么很可能出现OOM,因为这个可是每个线程一个对象啊;但是threadLocalsEntry是一个WeakReference<ThreadLocal<?>>,当线程中不再持有Entry的强引用的时候,JVM会在下一次GC回收这个对象;线程结束的时候也会主动置空触发GC,但最好还是在线程使用完ThreadLocal主动调用ThreadLocal.remove()清理一下。

ThreadLocal能对性能带来多大的提升取决于线程中对ThreadLocal引用的次数,或者说同一线程中对同一个“共享资源”访问的次数越多,就更应该考虑从共享更改为ThreadLocal(上面参数传递的情况除外,仅仅是为了保留线主线程中的数据,供其它线程使用而已):

public class RandomTest {

    private static Random random = new Random(100);
    private static ThreadLocal<Random> randomThreadLocal = ThreadLocal.withInitial(() -> new Random(100));
    private static ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();

    public static void main(String[] args) {

        List<Future<Long>> futures = new ArrayList<>();
        //写代码验证的时候没想透彻,我这里线程池不小心设置了很大的值,并在for循环中添加了差不多数量的线程,把CPU跑死了3次...太傻了
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        for (int i = 0; i < 4; i++) {
            futures.add(executorService.submit(() -> {
                long s = System.currentTimeMillis();
                for (int j = 0; j < 10000000; j++) {
                    random.nextInt();
                }
                return System.currentTimeMillis() - s;
            }));
        }
        System.out.println("normal: " + futures.stream().map(future -> {
            try {
                return future.get();
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
                return 0L;
            }
        }).mapToLong(a -> a).sum());

        futures.clear();
        for (int i = 0; i < 4; i++) {
            futures.add(executorService.submit(() -> {
                long s = System.currentTimeMillis();
                for (int j = 0; j < 10000000; j++) {
                    randomThreadLocal.get().nextInt();
                }
                return System.currentTimeMillis() - s;
            }));
        }
        System.out.println("thread local: " + futures.stream().map(future -> {
            try {
                return future.get();
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
                return 0L;
            }
        }).mapToLong(a -> a).sum());

        futures.clear();
        for (int i = 0; i < 4; i++) {
            futures.add(executorService.submit(() -> {
                long s = System.currentTimeMillis();
                for (int j = 0; j < 10000000; j++) {
                    threadLocalRandom.nextInt();
                }
                return System.currentTimeMillis() - s;
            }));
        }
        System.out.println("thread local random: " + futures.stream().map(future -> {
            try {
                return future.get();
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
                return 0L;
            }
        }).mapToLong(a -> a).sum());
    }

}
/**
 * normal: 12425
 * thread local: 511
 * thread local random: 60
 */

其中ThreadLocalRandom是专门针对多线程使用的随机数生成方案,性能是最好的。

无锁

锁可以分为悲观锁和乐观锁,悲观锁认为每一次操作都可能会出现冲突,必须在每次操作上加锁,保证万无一失;而乐观锁并不这样思考,乐观锁认为冲突的概率是很小的,每个线程一开始都当做没有锁肆无忌惮地进行,一旦发现真的冲突再用相应的策略改正或重试。

并发控制是悲观的,它认为每次进入临界区一定会产生冲突,所以每个线程必须独占资源;无锁是一种乐观的策略,它假设每次对临界资源的访问都是没有冲突的,当真正遇到冲突的时候再通过一定的策略重试。

CAS

CAS(Compare and Swap),JDK8中很多用同步实现的逻辑都被改用CAS实现了,比如ConcurrentHashMapAtomicInteger等。CAS算法需要三个参数CAS(V,E,N),V表示将要更新的变量,E表示变量的预期值,N表示将要更新成的新值,只有V的真实值E’和预期值E相等时,才表示V的只可以被更新为N,否则说明V已经被其它线程更新了,当前线程什么都不做。CAS会返回V的真实值,那么我们可以把这个真实值设置为新的期望值进行重试,直到成功为止。

在硬件方面,大部分处理器都支持原子化的CAS指令,JVM可以根据这个指令来实现并发情况下的CAS操作。JDK中的CAS操作是在Unsafe类中的public final native boolean compareAndSwapObject(Object o, long valueOffset, Object expect, Object update);compareAndSwapLong()/compareAndSwapInt()这几个本地方法实现的,o是要更新的对象(并不是更新的目标值,而时包裹目标值的外层对象,比如包装类、数组等,而要更新的是包装类的value或数组的某个元素),valueOffset表示被更新值在o中的偏移量(由Unsafe.objectFieldOffset()计算得出),expect表示期望值,update表示将要更新的新值。

Usafe类的初始化方法为Unsafe.getUnsafe(),但是这个方法:

@CallerSensitive
public static Unsafe getUnsafe() {
    Class var0 = Reflection.getCallerClass();
    if(!VM.isSystemDomainLoader(var0.getClassLoader())) {
        throw new SecurityException("Unsafe");
    } else {
        return theUnsafe;
    }
}

不是一般人能用的,任何应用级别的类初始化获取Unsafe都会抛出异常,但是JDK内部的类可以使用,因为Java应用程序的类由AppClassLoader加载,系统核心类由Bootstrap加载,Bootstrap是本地代码实现的,所以获取核心类的ClassLoader会返回空,所以核心类是可以使用Unsafe类的,比如rt.jar里的类,其它的类就不行。因此,我们只能使用JDK提供的提供的CAS实现,却不能自己使用CAS实现其它功能,CAS相关的方法都是final的,连重写都不可能,在Java中要使用CAS操作,只能用Atomic相关的类开放出来的compareAndSet()方法。

因为无锁不会导致线程阻塞、不需要主动通信也能感知其它线程的操作、不需要维护锁消耗额外的系统资源,因此带来了一些性能提升。下面写一个使用同步和无锁的例子,看看无锁带来了多少性能提升:

public class AtomicIntegerTest {
    static long sum = 0L;

    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();
        Thread[] threads = new Thread[100000];
        for (int i = 0; i < 100000; i++) {
            int finalI = i;
            Thread thread = new Thread(() -> {
                lock.lock();
                sum += finalI;
                lock.unlock();
            });
            threads[i] = thread;
        }

        long s = System.currentTimeMillis();
        for (Thread thread : threads) {
            thread.start();
        }
        for (Thread thread : threads) {
            thread.join();
        }
        System.out.println("lock :" + (System.currentTimeMillis() - s));

        AtomicLong atomicSum = new AtomicLong(0L);
        for (int i = 0; i < 100000; i++) {
            int finalI = i;
            Thread thread = new Thread(() -> atomicSum.addAndGet(finalI));
            threads[i] = thread;
        }
        s = System.currentTimeMillis();
        for (Thread thread : threads) {
            thread.start();
        }
        for (Thread thread : threads) {
            thread.join();
        }
        System.out.println("cas :" + (System.currentTimeMillis() - s));

    }

}
/**
 * lock :8116
 * cas :7564
 */

除了AtomicInteger/Long/Boolean/IntegerArray/LongArray等类以外,还有AotimicReference<V>可以引用对象,完成对对象的无锁并发更新。还有两种AtomicStampedReference/AtomicMarkableReference类,这两个类用于解决下面的ABA问题。

ABA问题

假设一个数值是1的对象X正在被多线程修改,线程A读取到X的值为1,做了一个操作,准备将X改为2,但是就在修改之前,恰好有两个线程对X做了修改,他们先后将X改为2又改为1;线程A发现X的值是1,高高兴兴地将值改为2,这就是ABA问题,从A->B和B->A这两次修改被忽略了,好像没有发生一样。

大多数情况下,这种情况是没有问题的,因为大部分场景都不关心数据的过程状态,只关心数据的当前状态,但是在一些需要关注过程状态的情况下,就会出问题,比如充值、消费这种跟过程状态相关的场景,这时候就需要用AtomicStampedReference来解决了,对这个对象的值做修改,不仅会更新值,还会更新一个更新次数,当有线程想要更新的时候,除了值满足期望,更新次数也满足期望。另一种AtomicMarkableReference原理类似,只不过更新次数换成了true/false,两者的区别是AtomicStampedReference可以得到两次更新之间更新了多少次,AtomicMarkableReference只关心有没有更新过。很明显后者依然不能保证解决ABA问题,因为仅有true/false两种状态的来回切换,只能解决简单的AA问题。

public class ABATest {

    private static AtomicReference<Integer> atomicReference = new AtomicReference<>(0);
    private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(0,0);
    private static AtomicMarkableReference<Integer> atomicMarkableReference = new AtomicMarkableReference<>(0,false);

    public static void main(String[] args) {
        testAtomicReference();
        testAtomicStampedReference();
        testAtomicMarkableReference();
    }

    private static void testAtomicMarkableReference() {
        System.out.println("atomicMarkableReference--------------------------------------------------");
        //A
        Integer expectReference = atomicMarkableReference.getReference();
        boolean expectMark = atomicMarkableReference.isMarked();
        //B
        System.out.println("thread0 read expect reference = 0, 0->1: " + atomicMarkableReference.compareAndSet(
                expectReference,
                expectReference+1,
                expectMark,
                !expectMark)
        );//true
        //A
        System.out.println("thread1 read expect reference = 1, 1->0: " + atomicMarkableReference.compareAndSet(
                atomicMarkableReference.getReference(),
                atomicMarkableReference.getReference()-1,
                atomicMarkableReference.isMarked(),
                !atomicMarkableReference.isMarked())
        );//true
        System.out.println("thread2 read expect reference = 0, 0->1: " + atomicMarkableReference.compareAndSet(
                expectReference,
                expectReference+1,
                expectMark,
                !expectMark)
        );//true,其实已经被修改过,无法解决ABA问题
        System.out.println();
    }

    private static void testAtomicStampedReference() {
        System.out.println("atomicStampedReference--------------------------------------------------");
        //A
        Integer expectReference = atomicStampedReference.getReference();
        Integer expectStamp = atomicStampedReference.getStamp();
        //B
        System.out.println("thread0 read expect reference = 0, 0->1: " + atomicStampedReference.compareAndSet(
                expectReference,
                expectReference+1,
                expectStamp,
                expectStamp+1)
        );//true
        //A
        System.out.println("thread1 read expect reference = 1, 1->0: " + atomicStampedReference.compareAndSet(
                atomicStampedReference.getReference(),
                atomicStampedReference.getReference()-1,
                atomicStampedReference.getStamp(),
                atomicStampedReference.getStamp()+1)
        );//true
        System.out.println("thread2 read expect reference = 0, 0->1: " + atomicStampedReference.compareAndSet(
                expectReference,
                expectReference+1,
                expectStamp,
                expectStamp+1)
        );//false,可以解决ABA问题
        System.out.println();
    }

    private static void testAtomicReference() {
        System.out.println("atomicReference--------------------------------------------------");
        //A
        Integer expect = atomicReference.get();
        //B
        System.out.println("thread0 read expect = 0, 0->1: " + atomicReference.compareAndSet(
                expect,
                expect + 1)
        );//true
        //A
        System.out.println("thread1 read expect = 1, 1->0: " + atomicReference.compareAndSet(
                atomicReference.get(),
                atomicReference.get() - 1)
        );//true
        System.out.println("thread2 read expect = 0, 0->1: " + atomicReference.compareAndSet(
                expect,
                expect + 1)
        );//true,其实已经被修改过,无法解决ABA问题
        System.out.println();
    }

}
使用CAS更新对象的非Atomic属性

有这样的场景:一个变量存储在对象A的a属性中,一开始各个线程只会读这个值,所以没有使用相关Aotimic类包装,但是后来业务变更,会有多线程来并发更新这个值,这时候有多种解决方案。一是直接用Automic类包装一下a属性,二是添加一个同步方法来更新a,三是使用AtomicReferenceFieldUpdater来保证线程安全。这三者的优缺点其实一目了然,最差的是方法一,虽然有很好的心梗但违反了软件的开闭原则,修改属性必然导致多处关联的修改;其次是方法二,但同步或加锁在高并发下性能并不是最好;最好的是方法三,结合了前两个方法的有点,是最优解。

public class AtomicReferenceFieldUpdaterTest {

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        TestObj testObj = new TestObj();
        for (int i = 0; i <= 100; i++) {
            int finalI = i;
            executorService.execute(() -> {
                while(true){
                    Integer a = testObj.getA();
                    if(TestObj.updateA(testObj,a,a + finalI)){
                        break;
                    }
                }
            });

        }
        executorService.shutdown();
        while(! executorService.isTerminated()){
            Thread.sleep(10L);
        }
        System.out.println(testObj.getA());
    }

    private static class TestObj {
        public final static AtomicReferenceFieldUpdater<TestObj,Integer> atomicReferenceFieldUpdater = AtomicReferenceFieldUpdater.newUpdater(TestObj.class, Integer.class,"a");

        private volatile Integer a = 0;

        public Integer getA() {
            return a;
        }

        public static boolean updateA(TestObj testObj, Integer expect, Integer update){
            return atomicReferenceFieldUpdater.compareAndSet(testObj, expect, update);
        }
    }

}

上面的程序始终会输出5050,而且修改对于TestObj的侵入性并不算大,只是增加了一个方法并将a属性用volitale修饰了而已。同时使用AtomicReferenceFieldUpdater必须注意:一、属性必须用volitale修饰,保证每一次修改对其它线程是可见的;二、对对象属性的CAS操作是通过直接修改属性对应的对象在实例中的偏移量实现的,而计算偏移量的方法是Unsafe.objectFieldOffset(),这个方法不支持计算静态变量的偏移量,因此static修饰的变量无法使用这个方法进行CAS操作。

AQS:锁的实现原理

AQS(AbstractQueuedSynchronizer)定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch

Java并发之AQS详解

这篇文章已经讲得非常详细了,记录一下。

死锁

死锁的出现一般是因为一个线程需要占用多个资源,如果一个线程先占用了部分资源,而另一个线程占用了剩下的资源,就会出现这两个线程之间都在等待彼此释放资源,如果没有外力介入,等待将永远存在,程序出现卡死、崩溃,下面是一个简单的产生死锁的例子:

public class DeadLockTest {

    private static ReentrantLock lock1 = new ReentrantLock();
    private static ReentrantLock lock2 = new ReentrantLock();

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                try {
                    lock1.lock();
                    Thread.sleep(1L);
                    lock2.lock();
                    System.out.println("1->2 finish...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock1.unlock();
                    lock2.unlock();
                }
            }).start();
            new Thread(() -> {
                try {
                    lock2.lock();
                    Thread.sleep(1L);
                    lock1.lock();
                    System.out.println("2->1 finish...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock2.unlock();
                    lock1.unlock();
                }
            }).start();
        }
    }

}

上面的程序可能什么都打印不出来而且还一直不会结束,因为加锁顺序的原因,一个线程先获取lock1的锁,另一个线程先获取lock2的锁,很容易就出现两个线程一个线程拿到了lock1,一个线程拿到了lock2,导致两个线程都带等待彼此释放需要的另外一个锁,如果严格地按照相同的加锁顺序,是不会出现死锁的,因为资源的加锁顺序一致,某一个线程获取到第一个锁,其它线程就不可能再获取到这个锁了。

我们用jstack查看一下堆栈信息:

...
Found one Java-level deadlock:
=============================
"Thread-199":
  waiting for ownable synchronizer 0x00000000d5f60a80, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
  which is held by "Thread-1"
"Thread-1":
  waiting for ownable synchronizer 0x00000000d5f60a50, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
  which is held by "Thread-0"
"Thread-0":
  waiting for ownable synchronizer 0x00000000d5f60a80, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
  which is held by "Thread-1"
...

可以看出检测到了死锁,并且是Thread-1在等待被Thread-0持有的锁,Thread-0在等待被Thread-1持有的锁,然后就可以从其它信息中看看这两个线程到底在干嘛,最后解决死锁问题。

CAS并不会造成死锁,因为CAS不持有任和锁,自然也就不会产生死锁问题啦。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值