Java并发(4)- synchronized与CAS

引言

上一篇文章中咱们说过,volatile经过lock指令保证了可见性、有序性以及“部分”原子性。但在大部分并发问题中,都须要保证操做的原子性,volatile并不具备该功能,这时就须要经过其余手段来达到线程安全的目的,在Java编程中,咱们能够经过锁、synchronized关键字,以及CAS操做来达到线程安全的目的。java

synchronized

在Java的并发编程中,保证线程同步最为程序员所熟悉的就是synchronized关键字,synchronized关键字最为方便的地方是他不须要显示的管理锁的释放,极大减小了编程出错的几率。程序员

在Java1.5及之前的版本中,synchronized并非同步最好的选择,因为并发时频繁的阻塞和唤醒线程,会浪费许多资源在线程状态的切换上,致使了synchronized的并发效率在某些状况下不如ReentrantLock。在Java1.6的版本中,对synchronized进行了许多优化,极大的提升了synchronized的性能。只要synchronized能知足使用环境,建议使用synchronized而不使用ReentrantLock。编程

synchronized的三种使用方式

  1. 修饰实例方法,为当前实例加锁,进入同步方法前要得到当前实例的锁。
  2. 修饰静态方法,为当前类对象加锁,进入同步方法前要得到当前类对象的锁。
  3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码块前要得到给定对象的锁。

这三种使用方式你们应该都很熟悉,有一个要注意的地方是对静态方法的修饰能够和实例方法的修饰同时使用,不会阻塞,由于一个是修饰的Class类,一个是修饰的实例对象。下面的例子能够说明这一点:安全

public class SynchronizedTest {

    public static synchronized void StaticSyncTest() {

        for (int i = 0; i < 3; i++) {
            System.out.println("StaticSyncTest");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }

    public synchronized void NonStaticSyncTest() {

        for (int i = 0; i < 3; i++) {
            System.out.println("NonStaticSyncTest");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
}

public static void main(String[] args) throws InterruptedException {

    SynchronizedTest synchronizedTest = new SynchronizedTest();
    new Thread(new Runnable() {
        @Override
        public void run() {
            SynchronizedTest.StaticSyncTest();
        }
    }).start();
    new Thread(new Runnable() {
        @Override
        public void run() {
            synchronizedTest.NonStaticSyncTest();
        }
    }).start();
}

//StaticSyncTest
//NonStaticSyncTest
//StaticSyncTest
//NonStaticSyncTest
//StaticSyncTest
//NonStaticSyncTest

代码中咱们开启了两个线程分别锁定静态方法和实例方法,从打印的输出结果中咱们能够看到,这两个线程锁定的是不一样对象,能够并发执行。并发

synchronized的底层原理

咱们看一段synchronized关键字通过编译后的字节码:ide

if (null == instance) {   
    synchronized (DoubleCheck.class) {
        if (null == instance) {   
            instance = new DoubleCheck();   
        }
    }
}



能够看到synchronized关键字在同步代码块先后加入了monitorenter和monitorexit这两个指令。monitorenter指令会获取锁对象,若是获取到了锁对象,就将锁计数器加1,未获取到则会阻塞当前线程。monitorexit指令会释放锁对象,同时将锁计数器减1。性能

JDK1.6对synchronized的优化

JDK1.6对对synchronized的优化主要体如今引入了“偏向锁”和“轻量级锁”的概念,同时synchronized的锁只可升级,不可降级:
优化

这里我不打算详细讲解每种锁的实现,想了解的能够参照《深刻理解Java虚拟机》,只简单说下本身的理解。this

偏向锁的思想是指若是一个线程得到了锁,那么就从无锁模式进入偏向模式,这一步是经过CAS操做来作的,进入偏向模式的线程每一次访问这个锁的同步代码块时都不须要再进行同步操做,除非有其余线程访问这个锁。线程

偏向锁提升的是那些带同步但无竞争的代码的性能,也就是说若是你的同步代码块很长时间都是同一个线程访问,偏向锁就会提升效率,由于他减小了重复获取锁和释放锁产生的性能消耗。若是你的同步代码块会频繁的在多个线程之间访问,可使用参数-XX:-UseBiasedLocking来禁止偏向锁产生,避免在多个锁状态之间切换。

偏向锁优化了只有一个线程进入同步代码块的状况,当多个线程访问锁时偏向锁就升级为了轻量级锁。

轻量级锁的思想是当多个线程进入同步代码块后,多个线程未发生竞争时一直保持轻量级锁,经过CAS来获取锁。若是发生竞争,首先会采用CAS自旋操做来获取锁,自旋在极短期内发生,有固定的自旋次数,一旦自旋获取失败,则升级为重量级锁。

轻量级锁优化了多个线程进入同步代码块的状况,多个线程未发生竞争时,能够经过CAS获取锁,减小锁状态切换。当多个线程发生竞争时,不是直接阻塞线程,而是经过CAS自旋来尝试获取锁,减小了阻塞线程的几率,这样就提升了synchronized锁的性能。

synchronized的等待唤醒机制

synchronized的等待唤醒是经过notify/notifyAll和wait三个方法来实现的,这三个方法的执行都必须在同步代码块或同步方法中进行,不然将会报错。

wait方法的做用是使当前执行代码的线程进行等待,notify/notifyAll相同,都是通知等待的代码继续执行,notify只通知任一个正在等待的线程,notifyAll通知全部正在等待的线程。wait方法跟sleep不同,他会释放当前同步代码块的锁,notify在通知任一等待的线程时不会释放锁,只有在当前同步代码块执行完成以后才会释放锁。下面的代码能够说明这一点:

public static void main(String[] args) throws InterruptedException {
    waitThread();
    notifyThread();
}

private static Object lockObject = new Object();
    
private static void waitThread() {
    
    Thread watiThread = new Thread(new Runnable() {
        
        @Override
        public void run() {
            
            synchronized (lockObject) {
                System.out.println(Thread.currentThread().getName() + "wait-before");
                
                try {
                    TimeUnit.SECONDS.sleep(2);
                    lockObject.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                System.out.println(Thread.currentThread().getName() + "after-wait");
            }
            
        }
    },"waitthread");
    watiThread.start();
}

private static void notifyThread() {
    
    Thread watiThread = new Thread(new Runnable() {
        
        @Override
        public void run() {
            
            synchronized (lockObject) {
                System.out.println(Thread.currentThread().getName() + "notify-before");
                
                lockObject.notify();
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } 
                
                System.out.println(Thread.currentThread().getName() + "after-notify");
            }
            
        }
    },"notifythread");
    watiThread.start();
}

//waitthreadwait-before
//notifythreadnotify-before
//notifythreadafter-notify
//waitthreadafter-wait

代码中notify线程通知以后wait线程并无立刻启动,还须要notity线程执行完同步代码块释放锁以后wait线程才开始执行。

CAS

在synchronized的优化过程当中咱们看到大量使用了CAS操做,CAS全称Compare And Set(或Compare And Swap),CAS包含三个操做数:内存位置(V)、原值(A)、新值(B)。简单来讲CAS操做就是一个虚拟机实现的原子操做,这个原子操做的功能就是将旧值(A)替换为新值(B),若是旧值(A)未被改变,则替换成功,若是旧值(A)已经被改变则替换失败。

能够经过AtomicInteger类的自增代码来讲明这个问题,当不使用同步时下面这段代码不少时候不能获得预期值10000,由于noncasi[0]++不是原子操做。

private static void IntegerTest() throws InterruptedException {

    final Integer[] noncasi = new Integer[]{ 0 };

    for (int i = 0; i < 10; i++) {
        Thread thread = new Thread(new Runnable() {

            @Override
            public void run() {
                for (int j = 0; j < 1000; j++) {
                    noncasi[0]++;
                }
            }
        });
        thread.start();
    }
    
    while (Thread.activeCount() > 2) {
        Thread.sleep(10);
    }
    System.out.println(noncasi[0]);
}

//7889

当使用AtomicInteger的getAndIncrement方法来实现自增以后至关于将casi.getAndIncrement()操做变成了原子操做:

private static void AtomicIntegerTest() throws InterruptedException {

    AtomicInteger casi = new AtomicInteger();
    casi.set(0);

    for (int i = 0; i < 10; i++) {
        Thread thread = new Thread(new Runnable() {

            @Override
            public void run() {
                for (int j = 0; j < 1000; j++) {
                    casi.getAndIncrement();
                }
            }
        });
        thread.start();
    }
    while (Thread.activeCount() > 2) {
        Thread.sleep(10);
    }
    System.out.println(casi.get());
}

//10000

固然也能够经过synchronized关键字来达到目的,但CAS操做不须要加锁解锁以及切换线程状态,效率更高。

再来看看casi.getAndIncrement()具体作了什么,在JDK1.8以前getAndIncrement是这样实现的(相似incrementAndGet):

private volatile int value;

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

经过compareAndSet将变量自增,若是自增成功则完成操做,若是自增不成功,则自旋进行下一次自增,因为value变量是volatile修饰的,经过volatile的可见性,每次get()都能获取到最新值,这样就保证了自增操做每次自旋必定次数以后必定会成功。

JDK1.8中则直接将getAndAddInt方法直接封装成了原子性的操做,更加方便使用。

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

CAS操做是实现Java并发包的基石,他理解起来比较简单但同时也很是重要。Java并发包就是在CAS操做和volatile基础上创建的,下图中列举了J.U.C包中的部分类支撑图:

相关文章
相关标签/搜索