xylz,imxylz

关注后端架构、中间件、分布式和并发编程

   :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  111 随笔 :: 10 文章 :: 2679 评论 :: 0 Trackbacks

前面的章节主要谈谈原子操作,至于与原子操作一些相关的问题或者说陷阱就放到最后的总结篇来整体说明。从这一章开始花少量的篇幅谈谈锁机制。

上一个章节中谈到了锁机制,并且针对于原子操作谈了一些相关的概念和设计思想。接下来的文章中,尽可能的深入研究锁机制,并且理解里面的原理和实际应用场合。

尽管synchronized在语法上已经足够简单了,在JDK 5之前只能借助此实现,但是由于是独占锁,性能却不高,因此JDK 5以后就开始借助于JNI来完成更高级的锁实现。

JDK 5中的锁是接口java.util.concurrent.locks.Lock。另外java.util.concurrent.locks.ReadWriteLock提供了一对可供读写并发的锁。根据前面的规则,我们从java.util.concurrent.locks.Lock的API开始。

 

void lock();

获取锁。

如果锁不可用,出于线程调度目的,将禁用当前线程,并且在获得锁之前,该线程将一直处于休眠状态。

void lockInterruptibly() throws InterruptedException;

如果当前线程未被中断,则获取锁。

如果锁可用,则获取锁,并立即返回。

如果锁不可用,出于线程调度目的,将禁用当前线程,并且在发生以下两种情况之一以前,该线程将一直处于休眠状态:

  • 锁由当前线程获得;或者
  • 其他某个线程中断当前线程,并且支持对锁获取的中断。

如果当前线程:

  • 在进入此方法时已经设置了该线程的中断状态;或者
  • 在获取锁时被中断,并且支持对锁获取的中断,
则将抛出 InterruptedException,并清除当前线程的已中断状态。

Condition newCondition();

返回绑定到此 Lock 实例的新 Condition 实例。下一小节中会重点谈Condition,此处不做过多的介绍。

boolean tryLock();

仅在调用时锁为空闲状态才获取该锁。

如果锁可用,则获取锁,并立即返回值 true。如果锁不可用,则此方法将立即返回值 false

通常对于那些不是必须获取锁的操作可能有用。

boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁。

如果锁可用,则此方法将立即返回值 true。如果锁不可用,出于线程调度目的,将禁用当前线程,并且在发生以下三种情况之一前,该线程将一直处于休眠状态:

  • 锁由当前线程获得;或者
  • 其他某个线程中断当前线程,并且支持对锁获取的中断;或者
  • 已超过指定的等待时间

如果获得了锁,则返回值 true

如果当前线程:

  • 在进入此方法时已经设置了该线程的中断状态;或者
  • 在获取锁时被中断,并且支持对锁获取的中断,
则将抛出 InterruptedException,并会清除当前线程的已中断状态。

如果超过了指定的等待时间,则将返回值 false。如果 time 小于等于 0,该方法将完全不等待。

void unlock();

释放锁。对应于lock()、tryLock()、tryLock(xx)、lockInterruptibly()等操作,如果成功的话应该对应着一个unlock(),这样可以避免死锁或者资源浪费。

 

相对于比较空洞的API,来看一个实际的例子。下面的代码实现了一个类似于AtomicInteger的操作。

package xylz.study.concurrency.lock;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class AtomicIntegerWithLock {

    private int value;

    private Lock lock = new ReentrantLock();

    public AtomicIntegerWithLock() {
        super();
    }

    public AtomicIntegerWithLock(int value) {
        this.value = value;
    }

    public final int get() {
        lock.lock();
        try {
            return value;
        } finally {
            lock.unlock();
        }
    }

    public final void set(int newValue) {
        lock.lock();
        try {
            value = newValue;
        } finally {
            lock.unlock();
        }

    }

    public final int getAndSet(int newValue) {
        lock.lock();
        try {
            int ret = value;
            value = newValue;
            return ret;
        } finally {
            lock.unlock();
        }
    }

    public final boolean compareAndSet(int expect, int update) {
        lock.lock();
        try {
            if (value == expect) {
                value = update;
                return true;
            }
            return false;
        } finally {
            lock.unlock();
        }
    }

    public final int getAndIncrement() {
        lock.lock();
        try {
            return value++;
        } finally {
            lock.unlock();
        }
    }

    public final int getAndDecrement() {
        lock.lock();
        try {
            return value--;
        } finally {
            lock.unlock();
        }
    }

    public final int incrementAndGet() {
        lock.lock();
        try {
            return ++value;
        } finally {
            lock.unlock();
        }
    }

    public final int decrementAndGet() {
        lock.lock();
        try {
            return --value;
        } finally {
            lock.unlock();
        }
    }

    public String toString() {
        return Integer.toString(get());
    }
}

AtomicIntegerWithLock是线程安全的,此结构中大量使用了Lock对象的lock/unlock方法对。同样可以看到的是对于自增和自减操作使用了++/--。之所以能够保证线程安全,是因为Lock对象的lock()方法保证了只有一个线程能够只有此锁。需要说明的是对于任何一个lock()方法,都需要一个unlock()方法与之对于,通常情况下为了保证unlock方法总是能够得到执行,unlock方法被置于finally块中。另外这里使用了java.util.concurrent.locks.ReentrantLock.ReentrantLock对象,下一个小节中会具体描述此类作为Lock的唯一实现是如何设计和实现的。

尽管synchronized实现Lock的相同语义,并且在语法上比Lock要简单多,但是前者却比后者的开销要大得多。做一个简单的测试。

public static void main(String[] args) throws Exception{
     final int max = 10;
     final int loopCount = 100000;
     long costTime = 0;
     for (int m = 0; m < max; m++) {
         long start1 = System.nanoTime();
         final AtomicIntegerWithLock value1 = new AtomicIntegerWithLock(0);
         Thread[] ts = new Thread[max];
         for(int i=0;i<max;i++) {
             ts[i] = new Thread() {
                 public void run() {
                     for (int i = 0; i < loopCount; i++) {
                         value1.incrementAndGet();
                     }
                 }
             };
         }
         for(Thread t:ts) {
             t.start();
         }
         for(Thread t:ts) {
             t.join();
         }
         long end1 = System.nanoTime();
         costTime += (end1-start1);
     }
     System.out.println("cost1: " + (costTime));
     //
     System.out.println();
     costTime = 0;
     //
     final Object lock = new Object();
     for (int m = 0; m < max; m++) {
         staticValue=0;
         long start1 = System.nanoTime();
         Thread[] ts = new Thread[max];
         for(int i=0;i<max;i++) {
             ts[i] = new Thread() {
                 public void run() {
                     for (int i = 0; i < loopCount; i++) {
                         synchronized(lock) {
                             ++staticValue;
                         }
                     }
                 }
             };
         }
         for(Thread t:ts) {
             t.start();
         }
         for(Thread t:ts) {
             t.join();
         }
         long end1 = System.nanoTime();
         costTime += (end1-start1);
     }
     //
     System.out.println("cost2: " + (costTime));
}


static int staticValue = 0;

 

在这个例子中每次启动10个线程,每个线程计算100000次自增操作,重复测试10次,下面是某此测试的结果:

cost1: 624071136

cost2: 2057847833

尽管上面的例子不是非常正式的测试案例,但上面的例子在于说明,Lock的性能比synchronized的要好得多。如果可以的话总是使用Lock替代synchronized是一个明智的选择。



©2009-2014 IMXYLZ |求贤若渴
posted on 2010-07-05 13:37 imxylz 阅读(34075) 评论(11)  编辑  收藏 所属分类: J2EE

评论

# re: 深入浅出 Java Concurrency (6): 锁机制 part 1 2010-08-10 11:09 strong liu
呵呵,这个系列写的真不错
不过有个小问题,好像在jdk6中, synchronized和lock的开销差不多了吧
另外,文中的例子,我把这两个变量改成
final int max = 100;
final int loopCount = 10000;
之后,得到了截然相反的结果:
cost1: 5410924000
cost2: 4069950000

------------------
Mac OS x 10.6
jdk 1.6  回复  更多评论
  

# re: 深入浅出 Java Concurrency (6): 锁机制 part 1 2010-11-03 07:54 hephaistos
呵呵,我测试了下
final int max = 100;
final int loopCount = 1000;
--------------------------------
cost1: 861453448

cost2: 1536178939
--------------------------------
final int max = 100;
final int loopCount = 10000;
--------------------------------
cost1: 4082896955

cost2: 15547632560
--------------------------------
Win7 x64
JDK 1.6
i7-850

应该楼上的相反结果是由于cpu的多线程运算能力不同而有所差异吧  回复  更多评论
  

# re: 深入浅出 Java Concurrency (6): 锁机制 part 1 2010-11-03 09:24 xylz
@hephaistos
要充分利用多核CPU的特性,需要开启-server模式,这样效果会比较明显。  回复  更多评论
  

# re: 深入浅出 Java Concurrency (6): 锁机制 part 1 2011-03-07 13:04 snake
我觉得这是由于测试方法不太全面导致的把
根据《Java Concurrency in Practice》中的描述,从1.6开始,固有锁的效率跟重入锁的效率就差不多了

这是他的测试方法,请参考一下:(该方法是测试各个map的,他测试重入锁跟固有锁的方法也是一样的)
Figure 11.3 illustrates the differences in scalability between several Map implementations: ConcurrentHashMap, ConcurrentSkipListMap, and HashMap and treeMap wrapped with synchronizedMap. The first two are thread-safe by design; the latter two are made thread-safe by the synchronized wrapper. In each run, N threads concurrently execute a tight loop that selects a random key and attempts to retrieve the value corresponding to that key. If the value is not present, it is added to the Map with probability p = .6, and if it is present, is removed with probability p = .02. The tests were run under a pre-release build of Java 6 on an 8-way Sparc V880, and the graph displays throughput normalized to the onethread case for ConcurrentHashMap. (The scalability gap between the concurrent and synchronized collections is even larger on Java 5.0.)
  回复  更多评论
  

# re: 深入浅出 Java Concurrency (6): 锁机制 part 1 2012-02-27 19:43 红泪
这个系列不错,最近在看多线程,就看到这个了,呵呵。  回复  更多评论
  

# re: 深入浅出 Java Concurrency (6): 锁机制 part 1 2012-10-11 11:46 nestle
请问blog主你这里的休眠指的是什么  回复  更多评论
  

# re: 深入浅出 Java Concurrency (6): 锁机制 part 1[未登录] 2013-03-03 14:58 teasp
synchronized使用的内置锁和ReentrantLock这种显式锁在java6以后性能没多大差异,在更新的版本中内置锁只会比显式锁性能更好。这两种锁都是独占锁,java5以前内置锁性能低的原因是它没做任何优化,直接使用系统的互斥体来获取锁。显式锁除了CAS的时候利用的是本地代码以外,其它的部分都是Java代码实现的,在后续版本的Java中,显式锁不太可能会比内置锁好,只会更差。使用显式锁的唯一理由是要利用它更多的功能。  回复  更多评论
  

# re: 深入浅出 Java Concurrency (6): 锁机制 part 1 2014-06-19 13:19 徐敏
我想知道不是已经有原子操作了吗?为什么还要使用这种独占锁的机制的,希望楼主解答一下。  回复  更多评论
  

# re: 深入浅出 Java Concurrency (6): 锁机制 part 1 2014-06-19 13:24 imxylz
@徐敏
锁数据结构的实现,便于程序开发。锁也是基于CAS原子操作实现的,但是CAS难以使用。  回复  更多评论
  

# re: 深入浅出 Java Concurrency (6): 锁机制 part 1[未登录] 2014-07-03 14:47 巴比猪
我的结果也是截然相反,用关键字的反而更快
private static class SynchroInteger{
private int i;

synchronized int incrementAndGet(){
i++;
return i;
}
}  回复  更多评论
  

# re: 深入浅出 Java Concurrency (6): 锁机制 part 1 2014-07-03 15:06 imxylz
JDK6以后对synchronized进行了很多优化,而Lock基本上没有什么可优化的空间。在某些操作系统和JDK实现上,这种简单的测试结果可能都不一样。但通常情况下,高并发和长时间任务执行上,Lock的性能比synchronized的更优。  回复  更多评论
  


只有注册用户登录后才能发表评论。


网站导航:
 

©2009-2014 IMXYLZ