Java并发整理(三)

这节主要记录一些常用的同步代码和同步类。

1. Atomic***和volatile 一般用在用线程并发访问,又没有synchronized和lock的情况下,高效地处理简单场景。比如Runnable的run方法。

1
2
3
4
5
6
7
8
final AtomicInteger counter = new AtomicInteger( 0 );
Runnable runnable = new Runnable(){
     @Override
     public void run() {
         counter.incrementAndGet();
         dowork();
     }
};

2. state-dependent method 试用于多线程并发下,有条件的执行,结构如下:

1
2
3
4
5
6
synchronized (lock){
     while (!condition){
         lock.wait();
     }
     dowork();
}

跟个简单线程池的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class ThreadPool {
     List<Thread> threads;
     int          poolSize;
 
     public ThreadPool( int size) {
         threads = new ArrayList<Thread>();
         for ( int i = 0 ; i < size; i++) {
             threads.add( new Thread());
         }
         this .poolSize = size;
     }
 
     private synchronized boolean isFull() {
         return threads.size() == poolSize;
     }
 
     private synchronized boolean isEmpty() {
         return threads.size() == 0 ;
     }
 
     public synchronized Thread get() throws InterruptedException {
         while (isEmpty()) {
             wait();
         }
         Thread thread = threads.remove(poolSize - 1 );
         notifyAll();
         return thread;
     }
 
     public synchronized void put(Thread thread) throws InterruptedException {
         while (isFull()) {
             wait();
         }
         threads.add(thread);
         notifyAll();
     }
 
}

3. wait and notify wait:使当前线程进入wait状态,直到其它线程将其唤醒。notify和notifyAll:唤醒在当前object monitor上等待的线程。

注意:这几个方法在调用前必须获得该object monitor(第一个object都有一个monitor,相当于一个信号量,只允许一个线程拥有)的锁,必须是同一个object。比如:

1
2
3
4
5
6
7
Object lock = new Object();
 
public void f(){
     synchronized (lock){
         wait();
     }
}

这样的话就会抛出IllegalMonitorStateException。因为wait等价于this.wait。所以如果要这样用,应该是lock.wait wait之后,当前线程就释放了该object monitor。而此时其它线程就可以竞争获取该object monitor(也就是说当前线程wait后,其它线程才能执行该synchronized方法之类的)。 notify,不是notify后,那些wait的线程就可以执行了,需要等notify的线程释放锁之后被唤醒的线程才能执行。 其实object monitor的概念不太好,我觉得如果一个object拥有monitor和owner两个概念更好理解:wait:释放lock object,从lock object的owner变成monitor,处于等待监听状态;notify:唤醒该lock object上处于monitor状态的线程,然后被唤醒的线程等notify线程释放lock object后,竞争成为owner。 notify只唤醒一个,唤醒哪一个由JVM决定。所以一般情况用notifyAll,除非一般只有一个线程在wait中。

4. Lock and Condition 前面的线程池例子可以看出,因为notify会把所有wait的线程的都给唤醒,比如get()方法里,最后会把所有wait的线程都给唤醒,包括get线程和put线程,按道理只需要通知put线程就可以。所以,在state-dependent wait and notify的基础上,有了Lock和Condition,它们提供逻辑更清楚,操作更便捷的锁机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class ThreadPoolLock {
     private Lock         lock     = new ReentrantLock( false );
     private int          poolsize;
     private int          count;
     private List<Thread> threads;
     private Condition    notEmpty = lock.newCondition();
     private Condition    notFull  = lock.newCondition();
 
     public ThreadPoolLock( int size) {
         this .poolsize = size;
         threads = new ArrayList<Thread>();
         for ( int i = 0 ; i < size; i++) {
             threads.add( new Thread( "thread-" + i));
         }
         this .count = poolsize;
     }
 
     public Thread get() {
         lock.lock();
         try {
             while (count == 0 ) {
                 notEmpty.await();
             }
             Thread thread = threads.remove(threads.size() - 1 );
             count--;
             notFull.signalAll();
             return thread;
         } catch (InterruptedException e) {
             System.out.println(Thread.currentThread().getName() + "Interrupted!" );
         } finally {
             lock.unlock();
         }
         return null ;
     }
 
     public void put(Thread thread) {
         lock.lock();
         try {
             while (count == poolsize) {
                 notFull.await();
             }
             threads.add(thread);
             count++;
             notEmpty.signal();
         } catch (InterruptedException e) {
             System.out.println(Thread.currentThread().getName() + " Interrupted!" );
         } finally {
             lock.unlock();
         }
     }
}

lock的主要方法还有lock.tryLock(<time>),试图获取锁,返回成功与否 condition的主要方法还有condition.await(<time>),进入等待,直到被唤醒,或被打断,或等待时间截止。被唤醒后同样要继续请求monitor,直到有了monitor该方法才真的返回。

5. lock.lock()与lock.lockinterruptibly()

lockinterruptibly()允许在等待请求monitor时,被Thread.interrupt()中断,直接返回一个InterruptedException,然后,它会接着做后面的任务,此时是在没有获得monitor的情况下。而lock()方法在被中断后,仍然会去请求monitor,直接获得monitor后,会把当前线程置为Interrupted状态,然后接着做后面的任务。 所以lock()可以这样写:

1
2
3
4
5
6
7
8
lock.lock();
 
try {
     dowork();
}
finally {
     lock.unlock();
}

而lockinterruptibly()则不能这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
try {
     lock.lockinterruptibly();
}
catch (InterruptedException e){
     doExceptionHanding();
}
 
try {
     dowork();
}
finally {
     lock.unlock();
}

因为如果该线程还没有获得monitor的时候就被中断了,那么它做finally的unlock就会抛IllegalMonitorStateException。因此,正确的lockinterruptibly使用是在doExceptionHanding()里throw InterruptedException。这就导致了像run()方法里无法用lockinterruptibly方法,因为run()方法不能重写为可以throw exception的方法(当然如果是RuntimeException就可以,问题InterruptedException是checkedException)。

所以,如果要处理线程的interrupt,使用lock.lockinterruptibly()后,在其的异常处理中一定要把捕获的InterruptedException抛出去,否则它会不请求monitor,继续执行下去。使得lock失去意义,并且遇到lock.unlock时还会抛异常。 关于interrupted状态,当一个线程为interrupted状态时,在遇到wait,join,sleep之类方法,才会触发当前线程抛出InterruptedException,否则一般的任务是不会抛异常退出的,会一直执行完。

6. sleep与wait,await的区别:sleep不会让当然线程丢失对lock object的monitor,而wait和await都会,所以wait和await被唤醒后,被唤醒的所有线程还要竞争lock object monitor。

7. ReadWriteLock,读写锁,允许并行读,串行写,当读更频繁时,效率更高,尤其是在对一些公共资源的操作时,如分类。 这里简单讲下ReentrantReadWriteLock,可重入的读写锁,它有以下特点:a. 获得读锁可以重复获得读锁,获得写锁可以重复获得写锁,即可重入b. 写锁可以降级,即获得写锁后,可以获得读锁,反之不行c. 读,写锁继承可重入锁,同样可中断,支持Condition d. 支持锁状态检测,可以检查当前在等待队列的线程,不过由于锁状态变化快,这些检测方法返回的结果并不是完全可信的,所以一般只用在监测系统中,尽量不要用作程序的判断逻辑。

注意:a. 读写锁的串行写,不光指写线程,而是说包括所有读写线程在内,只有一个写线程。也就是说,ReentrantReadWriteLock当前的ReadLock和WritLock上只有一个写线程时才能写。同时,可以有多个线程执行readLock.lock()

b. 关于可重入:允许writeLock.lock(); readLock.lock(); 不允许readLock.lock(); writeLock.lock(); 如果想读后写,那么请先释放,readLock.lock(); readLock.unlock(); writeLock.lock();

8. Collections 说到集合类,就想到最常见的面试题了,Hashtable与HashMap的区别:a. Hashtable是线程安全的,而HashMap是不安全的,性能更高 b. HashMap允许空值做为key/value。Vector与ArrayList的区别:a. Vector是线程安全的,而ArrayList不是 b. 当数据增长时,Vector是翻倍,而ArrayList是增长一半。

回到同步集合,对于HashMap,如果你需要同步,你需要怎么做:

a. 自己实现,需要对该map进行同步的操作时候少,大部分时间是可以不需要同步的,可以自己对操作语句加上synchronized,如

1
2
3
4
5
6
7
8
HashMap<String, String> map = new HashMap<String, String>();
synchronized (map){
     map.get( "key1" );
}
 
synchronized (map){
     map.put( "key1" , "value1" );
}

不过一般这种情形比较少,只有当该map存在于多个生命周期,比如刚开始时是不需要同步的,而当程序运行起来后,需要同步时。

b. Collections.synchronized***(),大部分情况是这种,整个过程都需要同步,那么可以调用Collections的同步操作,对该map进行封装,封装后,对map的所有操作都是同步的了。如

1
2
3
Map<String, String> map = Collections.synchronizedMap( new HashMap<String, String>());
map.get( "key1" );
map.put( "key1" , "value1" );

前面两种同步,其实都是对整个map进行加锁来达到同步的目的的,但是如果仅仅只是get/put操作来说,对整个map进行加锁,性能丢失比较大,所以有了ConcurrentHashMap

c. ConcurrentHashMap:细粒度锁 细粒度锁的实现主要是针对不同的hashbucket有不同的写锁,不像前面的只有一个锁,理论上说,如果put操作都是针对的不同的hashbucket时,那么有多少个写锁,就可以有多少个put操作。从而达到最小化锁保持时间,尽量减少获取锁的等待时间,并且读是不需要获取锁的(实现了一个基于ReentrantLock的Segment,在读时可以尽量降低同步开销)。所以ConcurrentHashMap的性能比前者高,推荐使用,特别是在一些get场合多的情况下,如缓存之类的。

另外相比于HashMap,ConcurrentHashMap的构造函数多了一个concurrencyLevel,这个参数可以估计大概的并发数(the estimated number of concurrently updating threads. The implementation performs internal sizing to try to accommodate this many threads)。如果在知道大概有多少并发的情况下,并且并发比较稳定时,可以使用这个参数进一步提到性能。

这里补一个,如果对HashMap迭代操作比较多时,因为迭代耗时与容量相关,所以一般情况下initialCapacity不要设得太大,loadFactor不要太小。 但是,ConcurrentHashMap的使用也要细心,不要以为是线程安全的就可以随便用,比如put-if-absent情况,下面就是错误写法:

1
2
3
4
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<String, String>();
if (!map.containsKey( "key1" )){
     map.put( "key1" , "value" );
}

因为当你get为空时,可能有另外一个线程正好此时把此key塞进去了。 正确的作法是下面两种:

1
2
3
4
5
6
7
synchronized (map){
     if (!map.containsKey( "key1" )){
         map.put( "key1" , "value" );
     }
}
 
map.putIfAbsent( "key1" , "value" );

CopyOnWriteArrayList也是对ArrayList线程安全的一种实现。考虑到如果一个线程在迭代的过程中,别一个线程也来迭代会发生ConcurrentModificationException<参见HashSet线程不安全引起的ConcurrentModificationException问题>。在多线程迭代操作时,要么你在迭代时锁住整个collection,要么在迭代前clone一个备份,两种方式性能损失都比较大。Collections提供了一个在多线程下迭代操作性能比较好的类,这就是CopyOnWrite***。因为CopyOnWriteArrayList在写时先arraycopy出一个副本,在副本上加锁进行写,这期间需要同原来的进行多次arraycopy,性能较差,而get读操作只是在原来的array中直接返回,没有锁。所以一般CopyOnWrite***也合ConcurrentHashMap一样用于读操作多的情况。并且写不阻塞读,与读写锁不一样。

下面再整理些有时会用到的同步类

9. Semphore 信号量,表示有多少个可用,用时得先申请,使用完后得释放。比如前面的线程池用这个实现便成了这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class ThreadPoolSemaphore {
     private int          poolSize;
     private List<Thread> threads = new ArrayList<Thread>();
     //0-1 lock, only one thread can access, like synchronized or lock
     private Semaphore    mutex   = new Semaphore( 1 );
     private Semaphore    notFull, notEmpty;
 
     public ThreadPoolSemaphore( int size) {
         poolSize = size;
         for ( int i = 0 ; i < poolSize; i++) {
             threads.add( new Thread( "new-thread-" + i));
         }
         notFull = new Semaphore(poolSize);
         notEmpty = new Semaphore( 0 );
     }
 
     public void put(Thread thread) throws InterruptedException {
         notFull.acquire();
         mutex.acquire();
         try {
             threads.add(thread);
         } finally {
             mutex.release();
             notEmpty.release();
         }
     }
 
     public Thread get() throws InterruptedException {
         notEmpty.acquire();
         mutex.acquire();
         try {
             return threads.remove(threads.size() - 1 );
         } finally {
             mutex.release();
             notFull.release();
         }
     }
 
}

有10个可用,也就是说可以有10个资源请求,但是最后对资源的操作还是只能原子操作(使用了一个mutex的互斥信号量)

10. CountDownLatch:计数器。一个或多个线程等待直到计数器为零时被唤醒。

这是最近写的一个场景,测并发下启动,并且多实例并存时的成功与失败的数量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class TestCountDownLatch {
     AtomicInteger success   = new AtomicInteger( 0 );
     AtomicInteger unsuccess = new AtomicInteger( 0 );
     Object lock = new Object();
 
     public void start() {
     }
     public void end(){
 
     }
 
     public void dowork() {
         try {
             start();
             success.incrementAndGet();
         } catch (RuntimeException e) {
             unsuccess.incrementAndGet();
             throw e;
         }
 
         synchronized (lock){
             try {
                 lock.wait();
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
 
         end();
     }
 
     public static void main(String[] args) {
         final TestCountDownLatch test = new TestCountDownLatch();
         int threadNum = 10 ;
         Thread[] threads = new Thread[ 10 ];
         for ( int i = 0 ; i < threadNum; i++) {
             threads[i] = new Thread( new Runnable() {
 
                 @Override
                 public void run() {
                     test.dowork();
                 }
 
             }, "new-thread-" + i);
             threads[i].start();
         }
 
         while (test.success.get() + test.unsuccess.get() < threadNum) {
             try {
                 Thread.sleep( 1000 );
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
 
         test.lock.notifyAll();
 
         System.out.println( "sucess: " + test.success.get() + "\tunsuccess: " + test.unsuccess.get());
     }
}
1
这里用CountDownLatch就可以这样实现,整个来看比前者更清爽些:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public class TestCountDownLatch2 {
     AtomicInteger          success   = new AtomicInteger( 0 );
     AtomicInteger          unsuccess = new AtomicInteger( 0 );
     private CountDownLatch count;
 
     public TestCountDownLatch2( int count) {
         this .count = new CountDownLatch(count);
     }
 
     public void start() {
     }
 
     public void end() {
 
     }
 
     public void dowork() {
         try {
             start();
             success.incrementAndGet();
             count.countDown();
         } catch (RuntimeException e) {
             unsuccess.incrementAndGet();
             count.countDown();
             throw e;
         }
 
         try {
             count.await();
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
 
         end();
     }
 
     public static void main(String[] args) {
         int threadNum = 10 ;
         final TestCountDownLatch2 test = new TestCountDownLatch2(threadNum);
 
         Thread[] threads = new Thread[ 10 ];
         for ( int i = 0 ; i < threadNum; i++) {
             threads[i] = new Thread( new Runnable() {
 
                 @Override
                 public void run() {
                     test.dowork();
                 }
 
             }, "new-thread-" + i);
 
         }
 
         try {
             test.count.await();
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
 
         System.out.println( "sucess: " + test.success.get() + "\tunsuccess: " + test.unsuccess.get());
     }
}

注意,CountDownLatch当计数器为零时,被唤醒的线程不需要在去竞争什么monitor之类的,直接往下运行就行了。

11. CyclicBarrier:循环计数器,当一组线程全部到达某一状态后,再接着运行。意思就是一组属于该CyclicBarrier的线程(可以是不相同的线程),运行到某一步后,调用cyclicBarrier.await(),只有这组线程都调用await后,才接着往下做。另外因为是cyclic的,所以这个是可以循环使用的,可以再接着都await。实际中没用到过,暂时不写例子。

总结:其实同步,常用的也就那几个,外带可能会用到些同步的辅助类。多注意下monitor就行,哪些操作是需要的,哪些不需要。所以一般锁这一类的,使用都是这种格式

1
2
3
4
5
6
lock.acquire()
try {
     dowork();
} finally {
     lock.release();
}

注意中断,当发生中断时,一般你抛出去就行,如果需要特殊处理,自己catch住后,尤其要注意中断后是否还获得monitor,以及后序操作有没有还需要monitor的释放锁操作,千万不要自以为是,顺手catch后printStackTrace就完事了。我整理这篇文章里,就遇到这种问题。

 

2013.3.20:

并发还有一种情况:比如有一个静态变量,有多个线程同时访问这个类的实例,涉及到对这个变量的访问,并且要求对于每个线程而言,这个变量的相对独立的,就是不受其它线程的影响(相当于这个变量是singleton的),此时一般用ThreadLocal来解决(每个线程都有一份自己的变量副本,相对于其它线程独立,用空间来实现多线程的并发),如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class ConnectionManager {
 
     private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
         @Override
         protected Connection initialValue() {
             Connection conn = null ;
             try {
                 conn = DriverManager.getConnection(
                         "jdbc:mysql://localhost:3306/test" , "username" ,
                         "password" );
             } catch (SQLException e) {
                 e.printStackTrace();
             }
             return conn;
         }
     };
 
     public static Connection getConnection() {
         return connectionHolder.get();
     }
 
     public static void setConnection(Connection conn) {
         connectionHolder.set(conn);
     }
}

 

原文地址:http://www.ikrady.com/2012/08/java-concurrency-3/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值