>  기사  >  Java  >  Java 동시성 제어 메커니즘에 대한 자세한 설명

Java 동시성 제어 메커니즘에 대한 자세한 설명

黄舟
黄舟원래의
2017-02-20 10:44:171303검색

일반적인 개발 과정에서 많은 학생들이 Java 동시 개발 모델을 다룰 때 몇 가지 기본적인 방법만 사용하는 것을 종종 볼 수 있습니다. 예를 들어 휘발성, 동기화됨입니다. Lock 및 Atomic과 같은 고급 동시성 패키지는 많은 사람들이 자주 사용하지 않습니다. 나는 대부분의 이유가 원칙의 속성이 부족하기 때문에 발생한다고 생각합니다. 바쁜 개발 작업에서 올바른 동시성 모델을 정확하게 파악하고 사용할 수 있는 사람은 누구입니까?

그래서 최근에는 이 아이디어를 바탕으로 동시성 제어 메커니즘에 대한 이 부분을 기사로 정리할 계획입니다. 제가 터득한 지식에 대한 추억일 뿐만 아니라, 이 글에서 언급한 내용이 대부분의 개발자들에게 도움이 되기를 바랍니다.

병렬 프로그램 개발에는 필연적으로 멀티스레딩, 멀티태스크 협업, 데이터 공유 등의 문제가 수반됩니다. JDK에서는 여러 스레드 간의 동시성 제어를 달성하기 위한 여러 가지 방법이 제공됩니다. 예를 들어 일반적으로 사용되는 내부 잠금, 재진입 잠금, 읽기-쓰기 잠금 및 세마포어가 있습니다.

Java 메모리 모델

Java에서 각 스레드에는 모든 스레드가 공유하는 주 메모리에 변수 값을 저장하는 작업 메모리 영역이 있습니다. . 스레드가 실행되면 자체 작업 메모리에 있는 이러한 변수에 대해 작동합니다.

공유 변수에 액세스하기 위해 스레드는 일반적으로 먼저 잠금을 획득하고 작업 메모리 영역을 지웁니다. 이렇게 하면 공유 변수가 모든 스레드의 공유 메모리 영역에서 스레드의 메모리 영역으로 올바르게 로드됩니다. 작업 메모리 영역. 스레드가 잠금 해제되면 작업 메모리 영역의 변수 값이 공유 메모리에 연결되는 것이 보장됩니다.

스레드가 변수를 사용할 때 프로그램이 스레드 동기화 작업을 올바르게 사용하는지 여부에 관계없이 얻은 값은 자체 또는 다른 스레드가 변수에 저장한 값이어야 합니다. 예를 들어 두 스레드가 서로 다른 값이나 객체 참조를 동일한 공유 변수에 저장하는 경우 변수 값은 이 스레드나 해당 스레드에 속하고 공유 변수 값은 두 스레드 모두에 속하지 않습니다. 참조 값의 조합입니다.

변수는 자바 프로그램이 접근할 수 있는 주소로, 기본형 변수, 참조형 변수뿐만 아니라 배열형 변수도 포함됩니다. 주 메모리 영역에 저장된 변수는 모든 스레드에서 공유할 수 있지만 한 스레드가 다른 스레드의 매개변수나 로컬 변수에 접근하는 것은 불가능하므로 개발자는 로컬 변수의 스레드 안전성에 대해 걱정할 필요가 없습니다.

휘발성 변수 - 여러 스레드 사이에서 볼 수 있음

각 스레드에는 자체 작업 메모리 영역이 있으므로 스레드가 자체 작업 메모리의 데이터를 변경할 때 변경되지 않을 수 있습니다. 다른 스레드에 표시됩니다. 이를 위해 휘발성 키워드를 사용하면 모든 스레드가 메모리에서 변수를 읽고 쓸 수 있으므로 여러 스레드에서 휘발성 변수를 볼 수 있습니다.

휘발성으로 선언된 변수는 다음을 보장할 수 있습니다.

1、其他线程对变量的修改,可以及时反应在当前线程中;

2、确保当前线程对volatile变量的修改,能及时写回到共享内存中,并被其他线程所见;

3、使用volatile声明的变量,编译器会保证其有序性。


동기화 키워드 동기화

동기화 키워드 동기화는 Java 언어에서 가장 일반적으로 사용되는 동기화 방법 중 하나입니다. JDK 초기 버전에서는 동기화 성능이 그다지 좋지 않았으며, 잠금 경쟁이 특별히 치열하지 않은 상황에 적합했습니다. JDK6에서는 동기화 잠금과 불공정 잠금 간의 격차가 좁아졌습니다. 더 중요한 것은 동기화가 더 간결하고 명확하며 코드를 더 읽기 쉽고 유지 관리하기 쉽다는 것입니다.

객체를 잠그는 방법:

public synchronized void method(){}


method() 메서드가 호출되면 호출 스레드는 먼저 현재 객체를 가져와야 합니다. 개체가 잠겨 있습니다. 호출 스레드는 위반이 완료된 후 개체 잠금이 해제됩니다.

public void method(){
synchronized(this){
// do something …
}
}

둘째, 동기화된 동기화 블록을 사용할 수도 있습니다. 동기화 방법과 비교하여 동기화 블록은 동기화 코드의 범위를 더 정확하게 제어할 수 있습니다. 작은 동기화 코드는 잠금 안팎으로 매우 빠르므로 시스템의 처리량이 더 높아집니다.

public void method(Object o){
// before
synchronized(o){
// do something ...
}
// after
}

synchronized는 정적 함수에도 사용할 수 있습니다.

public synchronized static void method(){}


여기서 주의하세요. 동기화된 잠금이 추가되었습니다. in현재 Class 객체 에서 따라서 이 메서드에 대한 모든 호출은 Class 객체의 잠금을 획득해야 합니다.

동기화를 사용하면 객체나 코드 세그먼트의 스레드 안전성을 보장할 수 있지만 동기화만으로는 복잡한 논리로 스레드 상호 작용을 제어하기에는 충분하지 않습니다. 여러 스레드 간의 상호 작용을 달성하려면 Object 개체의 wait() 및 inform() 메서드도 사용해야 합니다.

일반적인 사용법:

synchronized(obj){
    while(<?>){
        obj.wait();
        // 收到通知后,继续执行。
    }
}

wait() 메소드를 사용하기 전에 객체 잠금을 획득해야 합니다. wait() 메서드가 실행되면 현재 스레드는 다른 스레드에서 사용할 수 있도록 obj의 배타적 잠금을 해제할 수 있습니다.

obj를 대기 중인 스레드가 obj.notify()를 수신하면 obj의 독점 잠금을 다시 얻고 계속 실행할 수 있습니다. inform() 메소드 는 현재 객체를 기다리는 스레드를 무작위로 깨운다는 점에 유의하세요.
다음은 차단 대기열의 구현입니다.


public class BlockQueue{
 private List list = new ArrayList();
 public synchronized Object pop() throws InterruptedException{
 while (list.size()==0){
 this.wait();
 }
 if (list.size()>0){
 return list.remove(0);
 } else{
 return null;
 }
 }
 public synchronized Object put(Object obj){
 list.add(obj);
 this.notify();
 }
}

wait(), 통지()와 동기화는 Java 개발자가 마스터해야 합니다. 기본 기술.

Reentrantlock重入锁

Reentrantlock称为重入锁。它比synchronized拥有更加强大的功能,它可以中断、可定时。在高并发的情况下,它比synchronized有明显的性能优势。

Reentrantlock提供了公平和非公平两种锁。公平锁是对锁的获取是先进先出,而非公平锁是可以插队的。当然从性能上分析,非公平锁的性能要好得多。因此,在无特殊需要,应该优选非公平锁,但是synchronized提供锁业不是绝对公平的。Reentrantlock在构造的时候可以指定锁是否公平。

在使用重入锁时,一定要在程序最后释放锁。一般释放锁的代码要写在finally里。否则,如果程序出现异常,Loack就永远无法释放了。synchronized的锁是JVM最后自动释放的。

经典使用方式如下:

try {
 if (lock.tryLock(5, TimeUnit.SECONDS)) { //如果已经被lock,尝试等待5s,看是否可以获得锁,如果5s后仍然无法获得锁则返回false继续执行
 // lock.lockInterruptibly();可以响应中断事件
 try { 
 //操作
 } finally {
 lock.unlock();
 }
 }
} catch (InterruptedException e) {
 e.printStackTrace(); //当前线程被中断时(interrupt),会抛InterruptedException 
}

Reentrantlock提供了非常丰富的锁控制功能,灵活应用这些控制方法,可以提高应用程序的性能。不过这里并非是极力推荐使用Reentrantlock。重入锁算是JDK中提供的高级开发工具。

ReadWriteLock读写锁

读写分离是一种非常常见的数据处理思想。在sql中应该算是必须用到的技术。ReadWriteLock是在JDK5中提供的读写分离锁。读写分离锁可以有效地帮助减少锁竞争,以提升系统性能。读写分离使用场景主要是如果在系统中,读操作次数远远大于写操作。使用方式如下:

private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private Lock readLock = readWriteLock.readLock();
private Lock writeLock = readWriteLock.writeLock();
public Object handleRead() throws InterruptedException {
    try {
        readLock.lock();
        Thread.sleep(1000);
        return value;
    }finally{
        readLock.unlock();
    }
}
public Object handleRead() throws InterruptedException {
    try {
        writeLock.lock();
        Thread.sleep(1000);
        return value;
    }finally{
        writeLock.unlock();
    }
}

Condition对象

Conditiond对象用于协调多线程间的复杂协作。主要与锁相关联。通过Lock接口中的newCondition()方法可以生成一个与Lock绑定的Condition实例。Condition对象和锁的关系就如用Object.wait()、Object.notify()两个函数以及synchronized关键字一样。

这里可以把ArrayBlockingQueue的源码摘出来看一下:

public class ArrayBlockingQueue extends AbstractQueue
implements BlockingQueue, java.io.Serializable {
/** Main lock guarding all access */
final ReentrantLock lock;
/** Condition for waiting takes */
private final Condition notEmpty;
/** Condition for waiting puts */
private final Condition notFull;
public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity <= 0)
        throw new IllegalArgumentException();
    this.items = new Object[capacity];
    lock = new ReentrantLock(fair); 
    notEmpty = lock.newCondition(); // 生成与Lock绑定的Condition
    notFull =  lock.newCondition();
}
public void put(E e) throws InterruptedException {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == items.length)
            notFull.await();
        insert(e);
    } finally {
        lock.unlock();
    }
}
private void insert(E x) {
    items[putIndex] = x;
    putIndex = inc(putIndex);
    ++count;
    notEmpty.signal(); // 通知
}
public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0) // 如果队列为空
            notEmpty.await();  // 则消费者队列要等待一个非空的信号
        return extract();
    } finally {
        lock.unlock();
    }
}
private E extract() {
    final Object[] items = this.items;
    E x = this.<E>cast(items[takeIndex]);
    items[takeIndex] = null;
    takeIndex = inc(takeIndex);
    --count;
    notFull.signal(); // 通知put() 线程队列已有空闲空间
    return x;
}
// other code
}

Semaphore信号量

信号量为多线程协作提供了更为强大的控制方法。信号量是对锁的扩展。无论是内部锁synchronized还是重入锁ReentrantLock,一次都允许一个线程访问一个资源,而信号量却可以指定多个线程同时访问某一个资源。从构造函数可以看出:

public Semaphore(int permits) {}
public Semaphore(int permits, boolean fair){} // 可以指定是否公平
permits指定了信号量的准入书,也就是同时能申请多少个许可。当每个线程每次只申请一个许可时,这就相当于指定了同时有多少个线程可以访问某一个资源。这里罗列一下主要方法的使用:

 

public void acquire() throws InterruptedException {} //尝试获得一个准入的许可。若无法获得,则线程会等待,知道有线程释放一个许可或者当前线程被中断。
public void acquireUninterruptibly(){} // 类似于acquire(),但是不会响应中断。
public boolean tryAcquire(){} // 尝试获取,如果成功则为true,否则false。这个方法不会等待,立即返回。
public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException {} // 尝试等待多长时间
public void release() //用于在现场访问资源结束后,释放一个许可,以使其他等待许可的线程可以进行资源访问。

下面来看一下JDK文档中提供使用信号量的实例。这个实例很好的解释了如何通过信号量控制资源访问。

public class Pool {
private static final int MAX_AVAILABLE = 100;
private final Semaphore available = new Semaphore(MAX_AVAILABLE, true);
public Object getItem() throws InterruptedException {
    available.acquire();
    // 申请一个许可
    // 同时只能有100个线程进入取得可用项,
    // 超过100个则需要等待
    return getNextAvailableItem();
}
public void putItem(Object x) {
    // 将给定项放回池内,标记为未被使用
    if (markAsUnused(x)) {
        available.release();
        // 新增了一个可用项,释放一个许可,请求资源的线程被激活一个
    }
}
// 仅作示例参考,非真实数据
protected Object[] items = new Object[MAX_AVAILABLE]; // 用于对象池复用对象
protected boolean[] used = new boolean[MAX_AVAILABLE]; // 标记作用
protected synchronized Object getNextAvailableItem() {
    for (int i = 0; i < MAX_AVAILABLE; ++i) {
        if (!used[i]) {
            used[i] = true;
            return items[i];
        }
    }
    return null;
}
protected synchronized boolean markAsUnused(Object item) {
    for (int i = 0; i < MAX_AVAILABLE; ++i) {
        if (item == items[i]) {
            if (used[i]) {
                used[i] = false;
                return true;
            } else {
                return false;
            }
        }
    }
    return false;
}
}

此实例简单实现了一个对象池,对象池最大容量为100。因此,当同时有100个对象请求时,对象池就会出现资源短缺,未能获得资源的线程就需要等待。当某个线程使用对象完毕后,就需要将对象返回给对象池。此时,由于可用资源增加,因此,可以激活一个等待该资源的线程。

ThreadLocal线程局部变量

在刚开始接触ThreadLocal,笔者很难理解这个线程局部变量的使用场景。当现在回过头去看,ThreadLocal是一种多线程间并发访问变量的解决方案。与synchronized等加锁的方式不同,ThreadLocal完全不提供锁,而使用了以空间换时间的手段,为每个线程提供变量的独立副本,以保障线程安全,因此它不是一种数据共享的解决方案。

ThreadLocal是解决线程安全问题一个很好的思路,ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本,由于Key值不可重复,每一个“线程对象”对应线程的“变量副本”,而到达了线程安全。

特别值得注意的地方,从性能上说,ThreadLocal并不具有绝对的又是,在并发量不是很高时,也行加锁的性能会更好。但作为一套与锁完全无关的线程安全解决方案,在高并发量或者所竞争激烈的场合,使用ThreadLocal可以在一定程度上减少锁竞争。

下面是一个ThreadLocal的简单使用:

public class TestNum {
 // 通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值
 private static ThreadLocal seqNum = new ThreadLocal() {
 public Integer initialValue() {
 return 0;
 }
 };
 // 获取下一个序列值
 public int getNextNum() {
 seqNum.set(seqNum.get() + 1);
 return seqNum.get();
}public static void main(String[] args) {
 TestNum sn = new TestNum();
 //3个线程共享sn,各自产生序列号
 TestClient t1 = new TestClient(sn);
 TestClient t2 = new TestClient(sn);
 TestClient t3 = new TestClient(sn);
 t1.start();
 t2.start();
 t3.start();
 }
private static class TestClient extends Thread {
 private TestNum sn;
public TestClient(TestNum sn) {
 this.sn = sn;
 }
public void run() {
 for (int i = 0; i < 3; i++) {
 // 每个线程打出3个序列值
 System.out.println("thread[" + Thread.currentThread().getName() + "] --> sn["
 + sn.getNextNum() + "]");
 }
 }
 }
 }

输出结果:

thread[Thread-0] –> sn[1]
thread[Thread-1] –> sn[1]
thread[Thread-2] –> sn[1]
thread[Thread-1] –> sn[2]
thread[Thread-0] –> sn[2]
thread[Thread-1] –> sn[3]
thread[Thread-2] –> sn[2]
thread[Thread-0] –> sn[3]
thread[Thread-2] –> sn[3]

输出的结果信息可以发现每个线程所产生的序号虽然都共享同一个TestNum实例,但它们并没有发生相互干扰的情况,而是各自产生独立的序列号,这是因为ThreadLocal为每一个线程提供了单独的副本。

锁的性能和优化

“锁”是最常用的同步方法之一。在平常开发中,经常能看到很多同学直接把锁加很大一段代码上。还有的同学只会用一种锁方式解决所有共享问题。显然这样的编码是让人无法接受的。特别的在高并发的环境下,激烈的锁竞争会导致程序的性能下降德更加明显。因此合理使用锁对程序的性能直接相关。

1、线程的开销

在多核情况下,使用多线程可以明显提高系统的性能。但是在实际情况中,使用多线程的方式会额外增加系统的开销。相对于单核系统任务本身的资源消耗外,多线程应用还需要维护额外多线程特有的信息。比如,线程本身的元数据,线程调度,线程上下文的切换等。

2、减小锁持有时间 

在使用锁进行并发控制的程序中,当锁发生竞争时,单个线程对锁的持有时间与系统性能有着直接的关系。如果线程持有锁的时间很长,那么相对地,锁的竞争程度也就越激烈。因此,在程序开发过程中,应该尽可能地减少对某个锁的占有时间,以减少线程间互斥的可能。比如下面这一段代码:

public synchronized void syncMehod(){
beforeMethod();
mutexMethod();
afterMethod();
}

此实例如果只有mutexMethod()方法是有同步需要的,而在beforeMethod(),和afterMethod()并不需要做同步控制。如果beforeMethod(),和afterMethod()分别是重量级的方法,则会花费较长的CPU时间。在这个时候,如果并发量较大时,使用这种同步方案会导致等待线程大量增加。因为当前执行的线程只有在执行完所有任务后,才会释放锁。

下面是优化后的方案,只在必要的时候进行同步,这样就能明显减少线程持有锁的时间,提高系统的吞吐量。代码如下:

public void syncMehod(){
beforeMethod();
synchronized(this){
mutexMethod();
}
afterMethod();
}

3、减少锁粒度

减小锁粒度也是一种削弱多线程锁竞争的一种有效手段,这种技术典型的使用场景就是ConcurrentHashMap这个类。在普通的HashMap中每当对集合进行add()操作或者get()操作时,总是获得集合对象的锁。这种操作完全是一种同步行为,因为锁是在整个集合对象上的,因此,在高并发时,激烈的锁竞争会影响到系统的吞吐量。

如果看过源码的同学应该知道HashMap是数组+链表的方式做实现的。ConcurrentHashMap在HashMap的基础上将整个HashMap分成若干个段(Segment),每个段都是一个子HashMap。如果需要在增加一个新的表项,并不是将这个HashMap加锁,二十搜线根据hashcode得到该表项应该被存放在哪个段中,然后对该段加锁,并完成put()操作。这样,在多线程环境中,如果多个线程同时进行写入操作,只要被写入的项不存在同一个段中,那么线程间便可以做到真正的并行。具体的实现希望读者自己花点时间读一读ConcurrentHashMap这个类的源码,这里就不再做过多描述了。

4、锁分离 

在前面提起过ReadWriteLock读写锁,那么读写分离的延伸就是锁的分离。同样可以在JDK中找到锁分离的源码LinkedBlockingQueue。

public class LinkedBlockingQueue extends AbstractQueue
implements BlockingQueue, java.io.Serializable {
/* 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();
public E take() throws InterruptedException {
    E x;
    int c = -1;
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lockInterruptibly(); // 不能有两个线程同时读取数据
    try {
        while (count.get() == 0) { // 如果当前没有可用数据,一直等待put()的通知
            notEmpty.await();
        }
        x = dequeue(); // 从头部移除一项
        c = count.getAndDecrement(); // size减1
        if (c > 1)
            notEmpty.signal(); // 通知其他take()操作
    } finally {
        takeLock.unlock(); // 释放锁
    }
    if (c == capacity)
        signalNotFull(); // 通知put()操作,已有空余空间
    return x;
}
public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();
    // Note: convention in all put/take/etc is to preset local var
    // holding count negative to indicate failure unless set.
    int c = -1;
    Node<E> node = new Node(e);
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    putLock.lockInterruptibly(); // 不能有两个线程同时put数据
    try {
        /*
         * Note that count is used in wait guard even though it is
         * not protected by lock. This works because count can
         * only decrease at this point (all other puts are shut
         * out by lock), and we (or some other waiting put) are
         * signalled if it ever changes from capacity. Similarly
         * for all other uses of count in other wait guards.
         */
        while (count.get() == capacity) { // 队列满了 则等待
            notFull.await();
        }
        enqueue(node); // 加入队列
        c = count.getAndIncrement();// size加1
        if (c + 1 < capacity)
            notFull.signal(); // 如果有足够空间,通知其他线程
    } finally {
        putLock.unlock();// 释放锁
    }
    if (c == 0)
        signalNotEmpty();// 插入成功后,通知take()操作读取数据
}
// other code     
}

这里需要说明一下的就是,take()和put()函数是相互独立的,它们之间不存在锁竞争关系。只需要在take()和put()各自方法内部分别对takeLock和putLock发生竞争。从而,削弱了锁竞争的可能性。

5、锁粗化

上面说到的减小锁时间和粒度,这样做就是为了满足每个线程持有锁的时间尽量短。但是,在粒度上应该把握一个度,如果对用一个锁不停地进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而加大了系统开销。

我们需要知道的是,虚拟机在遇到一连串连续的对同一锁不断进行请求和释放的操作时,便会把所有的锁操作整合成对锁的一次请求,从而减少对锁的请求同步次数,这样的操作叫做锁的粗化。下面是一段整合实例演示:

public void syncMehod(){
synchronized(lock){
method1();
}
synchronized(lock){
method2();
}
}
JVM整合后的形式:
public void syncMehod(){
synchronized(lock){
method1();
method2();
}
}

因此,这样的整合给我们开发人员对锁粒度的把握给出了很好的演示作用。

无锁的并行计算

上面花了很大篇幅在说锁的事情,同时也提到过锁是会带来一定的上下文切换的额外资源开销,在高并发时,”锁“的激烈竞争可能会成为系统瓶颈。因此,这里可以使用一种非阻塞同步方法。这种无锁方式依然能保证数据和程序在高并发环境下保持多线程间的一致性。

1、非阻塞同步/无锁

非阻塞同步方式其实在前面的ThreadLocal中已经有所体现,每个线程拥有各自独立的变量副本,因此在并行计算时,无需相互等待。这里笔者主要推荐一种更为重要的、基于比较并交换(Compare And Swap)CAS算法的无锁并发控制方法。

CAS算法的过程:它包含3个参数CAS(V,E,N)。V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后CAS返回当前V的真实值。CAS操作时抱着乐观的态度进行的,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余俊辉失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作及时没有锁,也可以发现其他线程对当前线程的干扰,并且进行恰当的处理。

2、原子量操作

JDK的java.util.concurrent.atomic包提供了使用无锁算法实现的原子操作类,代码内部主要使用了底层native代码的实现。有兴趣的同学可以继续跟踪一下native层面的代码。这里就不贴表层的代码实现了。

下面主要以一个例子来展示普通同步方法和无锁同步的性能差距:

public class TestAtomic {
private static final int MAX_THREADS = 3;
private static final int TASK_COUNT = 3;
private static final int TARGET_COUNT = 100 * 10000;
private AtomicInteger acount = new AtomicInteger(0);
private int count = 0;
synchronized int inc() {
    return ++count;
}
synchronized int getCount() {
    return count;
}
public class SyncThread implements Runnable {
    String name;
    long startTime;
    TestAtomic out;
    public SyncThread(TestAtomic o, long startTime) {
        this.out = o;
        this.startTime = startTime;
    }
    @Override
    public void run() {
        int v = out.inc();
        while (v < TARGET_COUNT) {
            v = out.inc();
        }
        long endTime = System.currentTimeMillis();
        System.out.println("SyncThread spend:" + (endTime - startTime) + "ms" + ", v=" + v);
    }
}
public class AtomicThread implements Runnable {
    String name;
    long startTime;
    public AtomicThread(long startTime) {
        this.startTime = startTime;
    }
    @Override
    public void run() {
        int v = acount.incrementAndGet();
        while (v < TARGET_COUNT) {
            v = acount.incrementAndGet();
        }
        long endTime = System.currentTimeMillis();
        System.out.println("AtomicThread spend:" + (endTime - startTime) + "ms" + ", v=" + v);
    }
}
@Test
public void testSync() throws InterruptedException {
    ExecutorService exe = Executors.newFixedThreadPool(MAX_THREADS);
    long startTime = System.currentTimeMillis();
    SyncThread sync = new SyncThread(this, startTime);
    for (int i = 0; i < TASK_COUNT; i++) {
        exe.submit(sync);
    }
    Thread.sleep(10000);
}
@Test
public void testAtomic() throws InterruptedException {
    ExecutorService exe = Executors.newFixedThreadPool(MAX_THREADS);
    long startTime = System.currentTimeMillis();
    AtomicThread atomic = new AtomicThread(startTime);
    for (int i = 0; i < TASK_COUNT; i++) {
        exe.submit(atomic);
    }
    Thread.sleep(10000);
}
}

测试结果如下:

testSync():
SyncThread spend:201ms, v=1000002
SyncThread spend:201ms, v=1000000
SyncThread spend:201ms, v=1000001
testAtomic():
AtomicThread spend:43ms, v=1000000
AtomicThread spend:44ms, v=1000001
AtomicThread spend:46ms, v=1000002

相信这样的测试结果将内部锁和非阻塞同步算法的性能差异体现的非常明显。因此笔者更推荐直接视同atomic下的这个原子类。

结束语

终于把想表达的这些东西整理完成了,其实还有一些想CountDownLatch这样的类没有讲到。不过上面的所讲到的绝对是并发编程中的核心。也许有些读者朋友能在网上看到很多这样的知识点,但是个人还是觉得知识只有在对比的基础上才能找到它合适的使用场景。因此,这也是笔者整理这篇文章的原因,也希望这篇文章能帮到更多的同学。

 以上就是Java并发控制机制详解 的内容,更多相关内容请关注PHP中文网(www.php.cn)!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.