多线程进阶与源码分析--synchronized与volatile实践场景(二)

来源:互联网 发布:网页美工培训 编辑:程序博客网 时间:2024/05/21 10:55

上篇文章主要讲了怎么创建合理的线程池的方式、线程同步,外加部分问题定位的方法,对于简单的业务开发是可以胜任,这仅仅是入门。

       多线程带来的问题是顺序性与竞争问题,这个问题的产生于多个线程同时访问一个或多个共享变量引起的,而现代多核处理器在设计上增加了许多一二三级缓存,每个CPU有自己的缓存,在执行之前会先拿到要操作值的副本,拷贝到高速缓存区,然后处理器直接从高速缓存区获取信息进行计算,如果这个过程中处理器A与处理器B都从主内存中缓存一个数值i=1,然后俩处理器都对其进行+1操作,这个时候会出现虽然做了两次加1,但结果却是2的问题,synchronized能解决这个问题,synchronized可以修饰方法、静态方法、代码块,本质上就是通过加锁与解锁的方式保证当前代码块内只有一个线程在执行,我们知道synchronized有性能问题,显而易见,一个方法或代码块只有一个线程能跑确实存在一定的性能问题,这个问题在jdk1.6之后又做了些优化,加了偏向锁、轻量级锁、重量级锁等概念,随着线程竞争的规模而从左向右进行膨胀,但是还是存在性能问题,原因是java中的锁都是对象锁,什么意思?首先大家知道java中基础单位是对象,我们都知道Java中所有的对象都有一个基类是Object,它有它最通用的数据结构,我们来看下Java对象头的数据结构:

20151217151455512

       ok,存储结构里面貌似好多跟锁相关的,那我们可以推测出,java中所有的加锁、竞争锁的操作实际都是竞争的同一个对象的锁,然后一个对象可能有多个方法或多个同步代码块,而且很有可能存在这些方法并不是相关的,方法A与方法B,方法A占用了锁,我方法B就得阻塞,等待获取锁,严重的性能问题,我们通过下面的代码来做验证(当然实际上是每个对象都有个monitor监视器,其实就是锁,本例是静态方法,静态方法锁的是Class类本身,如果是对象的话,锁的是每个new出来的对象):

/** * 验证同步静态方法 是否锁对象 * @author zhengxun * @version 2017-11-12 */public class StaticSynchronized {    public synchronized static void test1() throws InterruptedException {        System.out.println(new Date() + "synchronized static test1");        Thread.sleep(10000);    }    public synchronized static void test2() throws InterruptedException {        System.out.println(new Date() +"synchronized static test2");
     Thread.sleep(10000);     }    public static void main(String[] args) {        new Thread(new Runnable() {            @Override            public void run() {                try {                    StaticSynchronized.test1();                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        }).start();        new Thread(new Runnable() {            @Override            public void run() {                try {                    StaticSynchronized.test2();                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        }).start();    }}

这是运行结果:

Tue Nov 14 00:00:07 CST 2017synchronized static test1Tue Nov 14 00:00:17 CST 2017synchronized static test2

        如猜想,因此一个对象中慎用多个同步代码块。

        若使用sychronized关键词:

1、尽量减少占用锁的时间,比如代码块的代码内容尽量少,一段代码中如果可以就用多个代码块来代替整个代码块;

2、尽量减少阻塞的放生,从设计上将synchronized分布到不同的对象中,减少一个对象中的synchronized整体数量;

      尽可能的多释放锁,比如使用wait()方法+循环重试,允许其他线程获取锁。

这两段代码摘自Netty,第一段代码只在需要做锁的地方加同步代码块,第二段代码即使用wait()方法的方式,当然为了避免过多的占用cpu资源我们在重试的时候可以加一个上限,一旦超过上线则直接抛异常,如第三段代码。

image

image

 

synchronized (this) {            while (!isDone()) {                incWaiters();                try {                    wait();                } finally {                    decWaiters();                }            }        }
private void incWaiters() {    if (waiters == Short.MAX_VALUE) {//超过32767则抛出异常        throw new IllegalStateException("too many waiters: " + this);    }    ++waiters;    }

        前面提到偏向锁、轻量级锁、重量级锁,这是synchronized的内部优化,即如何降低资源消耗一些策略,我们来讲下什么情况下对应什么样的锁。

偏向锁是第一个阶段,也是效率最高的,条件:不存在线程竞争的场景,意思是虽然你加了synchronized关键词,但是通常都只有一个线程会访问,ok,那这个之后资源消耗最低,仅仅通过判断对象头的偏向锁是否指向当前线程,如果是,那就获取到锁。如果存在线程竞争,会带来锁撤销的消耗(偏向锁默认开启,可通过jvm参数关闭)

轻量级锁是第二个阶段,效率次等,条件:多个线程交替进入synchronized代码块,意思是偏向锁里面指向的不是当前线程,那么就会升级到轻量级锁。轻量级锁是在没有线程竞争锁的情况下,如果存在一个线程已经获取到锁,那么第二个线程会使用CAS自旋尝试获取锁,会带来一定的cpu开销。

重量级锁是最后一个阶段,效率最差,条件是:多个线程同时进入synchronized代码块,就是线程存在竞争锁的场景,这时没有获取到锁的线程只能阻塞,当持有锁的线程释放锁之后会唤醒阻塞的线程,进行来一轮的锁竞争,如果线程过多的话,响应时效无法保证。

需要强调的是偏向锁与轻量级锁都不是对象锁,虽然做了不少优化,但是多线程竞争条件下还是会很慢,我们大致猜想一种场景,线程之间锁竞争比较少或线程数量比较少,可适当使用synchronized,另外如果要用,遵循以上提到的两点,当然新手如果用到锁还是直接用synchronized,等出现性能问题再选用其他的锁。

volatile又是什么场景用呢?

        第一volatile只保证可见性、顺序性(禁止CPU重排序优化),不保证原子性,像前面提到的多个线程操作同一个变量,就算加上voatile也不起作用。如下代码(验证volatile的可见性与非原子性):

/** * 验证volatile的可见性与非原子性 * * @author zhengxun * @version 2017-11-14 */public class VisibilityForVolatile {    private static volatile int non_atomic = 0;    public static void main(String[] args) {        try {            final CountDownLatch countDownLatchForAtomicity = new CountDownLatch(1);            for (int i = 0; i < 10; i++) {//验证非原子性                new Thread(new Runnable() {                    @Override                    public void run() {                        for (int j = 0; j < 10000; j++) {                            non_atomic++;                        }                        try {                            countDownLatchForAtomicity.await();                        } catch (InterruptedException e) {                            e.printStackTrace();                        }                    }                }).start();            }            final CountDownLatch countDownLatchForVisibility = new CountDownLatch(1);            final Visibility visibilityThread = new Visibility(countDownLatchForVisibility);            new Thread(visibilityThread).start();            countDownLatchForVisibility.await();            System.out.println("验证可见性:" + visibilityThread.getVisibility());            countDownLatchForAtomicity.countDown();            Thread.sleep(1000);            System.out.println("验证非原子性:" + non_atomic);//            Thread.sleep(Integer.MAX_VALUE);        } catch (InterruptedException e) {            e.printStackTrace();        }    }}class Visibility implements Runnable {    private CountDownLatch countDownLatchForVisibility;    public Visibility(CountDownLatch countDownLatchForVisibility) {        this.countDownLatchForVisibility = countDownLatchForVisibility;    }    private Boolean visibility = true;//    private volatile Boolean visibility = true;    @Override    public void run() {        visibility = false;        countDownLatchForVisibility.countDown();    }    public Boolean getVisibility() {        return visibility;    }    public void setVisibility(Boolean visibility) {        this.visibility = visibility;    }}

输出结果:

        程序本应输出10000,结果输出92453,证明其是非原子性的,

        可见性,方面子线程已修改为false,有时输出却还是true,而加上volatile却绝对不会出现这种情况(实际上很多次可见性不好验证,大部分情况下不加volatile也能具有可见性,后续再找机会验证这个问题)

验证可见性:true   验证非原子性:92453

 

        根据其特性,总结,volatile最适合的场景是一个线程写,其他线程读的场景,如果有多个线程写共享变量,还是直接用锁来的合适。

        还有中奇妙的思路,volatile既然不支持原子性,那么jdk中有其他的类支持,比如AtomicLongFieldUpdater原子操作类,volatile修饰的属性如果修改对其他线程是可见的,AtomicLongFieldUpdater又能保证原子性,但是这种也存在一定的性能问题,这些原子操作类都使用的CAS来保证原子操作的,如果存在大量线程,CPU自旋的时间就会过长,同样会影响性能,所以这种最好能在有限的线程池中操作。

/** * A reflection-based utility that enables atomic updates to * designated {@code volatile long} fields of designated classes. * This class is designed for use in atomic data structures in which * several fields of the same node are independently subject to atomic * updates. * * <p>Note that the guarantees of the {@code compareAndSet} * method in this class are weaker than in other atomic classes. * Because this class cannot ensure that all uses of the field * are appropriate for purposes of atomic access, it can * guarantee atomicity only with respect to other invocations of * {@code compareAndSet} and {@code set} on the same updater. * * @since 1.5 * @author Doug Lea * @param <T> The type of the object holding the updatable field */public abstract class AtomicLongFieldUpdater<T> {

        AtomicLongFieldUpdater这个原子操作类我们需要学习,源码也写的很清楚了,是专门为volatile修饰的属性设计的,内部使用CAS自旋保证修改正确,其底层也使用了unsafe的compareAndSet()

        如下代码,volatile不加锁形式的原子性实现:主要利用AtomicLongFieldUpdater的特性实现。

/** * volatile 修饰词,原子性代码实现 * @author zhengxun * @version 2017-11-14 */public class VolatileAndCAS {    private volatile long atomic = 0L;    private AtomicLongFieldUpdater<VolatileAndCAS> atomicInteger = AtomicLongFieldUpdater.newUpdater(VolatileAndCAS.class, "atomic");    public void increment(CountDownLatch countDownLatch) {        long oldValue = atomic;        long newValue = atomic + 1;        while (!atomicInteger.compareAndSet(this, oldValue, newValue)) {            System.out.println("重试中==========atomicInteger:" + atomicInteger                    + "===oldValue:" + atomic + "====newValue:" + newValue);            oldValue = atomic;            newValue = oldValue + 1;        }        countDownLatch.countDown();    }    public long getAtomic() {        return atomic;    }}class TestVolatile {    public static void main(final String[] args) {        final VolatileAndCAS volatileAndCAS = new VolatileAndCAS();        try {            final CountDownLatch countDownLatch = new CountDownLatch(500);            for (int i = 0; i < 5; i++) {                new Thread(new Runnable() {                    @Override                    public void run() {                        for (int j = 0; j < 100; j++) {                            volatileAndCAS.increment(countDownLatch);                        }                    }                }).start();            }            countDownLatch.await();            System.out.println("=======" + volatileAndCAS.getAtomic());        } catch (Exception e) {            e.printStackTrace();        }    }}

        结果:数值为500,是为原子性,中间有两次重试,还可以接受。

重试中==========atomicInteger:java.util.concurrent.atomic.AtomicLongFieldUpdater$CASUpdater@1c015d7a===oldValue:287====newValue:160重试中==========atomicInteger:java.util.concurrent.atomic.AtomicLongFieldUpdater$CASUpdater@1c015d7a===oldValue:354====newValue:260=======500

 

        以上主要以偏代码、实践层面来解析synerchronized、volatile的使用场景,网络上有更多关于其底层实现的文章:

        http://www.cnblogs.com/dennyzhangdd/p/6734638.html

        http://www.jianshu.com/u/90ab66c248e6

本篇也参考了李林峰撰写的《多线程并发编程在netty中的应用分析》

 

中间有不对的地方还请海涵,指正。

阅读全文
0 0
原创粉丝点击