介绍
在开发Java多线程应用程序中,由于各个线程之间需要共享资源,就必须用到锁机制。Java很好的提供了多种多线程锁机制的实现方式,我们常见的比如有synchronized、ReentrantLock、Semaphore、AtomicInteger等。而每种锁的机制都有优缺点和各自的适用场景,必须熟练掌握他们的特点才能在Java多线程应用开发时得心应手。下面就针对这四种常见的锁给大家介绍一下...
一. synchronized
1. 介绍
synchronized关键字常被Java用于维护数据的一致性。通过synchronized给共享资源上锁,只有拿到锁才可以访问共享资源,这样就可以保证对访问共享资源的顺序。
2. 使用方式
在需要同步的方法,类或者代码块中加入该关键字即可,这样就可以保证在同一个时刻最多只有一个线程执行同一个对象的同步代码,可保证修饰的代码在执行过程中不会被其他线程干扰。synchronized (obj) {
//方法
…….
}
3. 特点及使用场景
synchronized修饰的代码具有原子性和可见性,在需要进程同步的程序中使用的频率非常高,可以满足一般的进程同步要求。
4. 性能及注意事项
synchronized实现的机理依赖于软件层面上的JVM,因此其性能会随着Java版本的不断升级而提高;但需要注意的是线程通过synchronized等待锁时是不能被Thread.interrupt()中断的,因此程序设计时必须检查确保合理,否则可能会造成线程死锁的尴尬境地。
二. ReentrantLock
1. 介绍
ReentrantLock可重入锁,顾名思义,这个锁可以被线程多次重复进入进行获取操作。ReentantLock继承接口Lock并实现了接口中定义的方法,除了能完成synchronized所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。
2. 使用方式
ReentrantLock通过方法lock()与unlock()来进行加锁与解锁操作,与synchronized会被JVM自动解锁机制不同,ReentrantLock加锁后需要手动进行解锁。为了避免程序出现异常而无法正常解锁的情况,使用ReentrantLock必须在finally控制块中进行解锁操作。通常使用方式如下所示:Lock lock = new ReentrantLock();
try {
lock.lock();
//…进行任务操作5 }
finally {
lock.unlock();
}
3. 特点及使用场景
ReentantLock继承接口Lock,而Lock实现的机理依赖于特殊的CPU指定,可以认为不受JVM的约束,并可以通过其他语言平台来完成底层的实现 ;多在高并发量情况下使用ReentrantLock。
4. 性能
在并发量较小的多线程应用程序中,ReentrantLock与synchronized性能相差无几,但在高并发量的条件下,synchronized性能会迅速下降几十倍,而ReentrantLock的性能却能依然维持一个水准。
三. Semaphore
1.介绍
Semaphore(信号量),用于做限流处理。是一种计数器,用来保护一个或者多个共享资源的访问。如果线程要访问一个资源就必须先获得信号量。如果信号量内部计数器大于0,信号量减1,然后允许共享这个资源;否则,如果信号量的计数器等于0,信号量将会把线程置入休眠直至计数器大于0.当信号量使用完时,必须释放。
2.使用方式
案例:同时只允许5五个人访问,超过五个人访问就需要等待,类似这样的需求,下面的案例可以看出执行是五个五个的执行,等上一个五个执行完了,才会执行下一个import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
public class UseSemaphore {
public static void main(String[] args) {
// 线程池
ExecutorService exec = Executors.newCachedThreadPool();
// 只能5个线程同时访问
final Semaphore semp = new Semaphore(5);
// 模拟20个客户端访问
for (int index = 0; index < 20; index++) {
final int NO = index;
Runnable run = new Runnable() {
public void run() {
try {
// 获取许可
semp.acquire();
System.out.println("Accessing: " + NO);
//模拟实际业务逻辑
Thread.sleep((long) (Math.random() * 10000));
// 访问完后,释放
semp.release();
} catch (InterruptedException e) {
}
}
};
exec.execute(run);
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//System.out.println(semp.getQueueLength());
// 退出线程池
exec.shutdown();
}
}
3. 特点及使用场景
在多线程环境下使用的一种设施, 它负责协调各个线程, 以保证它们能够正确、合理的使用公共资源。使用场景如在实际复杂的多线程应用程序中,可能存在多个临界资源,这时候我们可以借助Semaphore信号量来完成多个临界资源的访问。
四. AtomicInteger
1.介绍
AtomicInteger一种无锁的线程安全整数,一个提供原子操作的Integer的类。在Java语言中,++i和i++操作并不是线程安全的,在使用的时候,不可避免的会用到synchronized关键字。而AtomicInteger则通过一种线程安全的加减操作接口。(注:AtomicInteger是一系列相同类的代表之一,常见的还有AtomicLong、AtomicLong等,他们的实现原理相同,区别在与运算对象类型的不同)
2.性能
通过相关资料显示,通常AtomicInteger的性能是ReentantLock的好几倍。
总结
综合比较:
1.synchronized
适用于资源竞争小,偶尔需要同步的情况下,原因在于编译程序通常会尽可能的进行优化synchronize,另外可读性非常好 。
2. ReentrantLock
适用于资源竞争大,高并发大情况下,这样相对于synchronized,效率更优。
3. Atomic
资源竞争小,性能略逊synchronized,反而竞争大,性能高于synchronized和ReentrantLock。但是存在缺点,只能同步一个值,一段代码中只能出现一个Atomic的变量,多于一个同步无效。因为他不能在多个Atomic之间同步。
综合比较考虑,在使用时优先考虑synchronized,在不熟悉ReentrantLock和Atomic情况下,还是不要使用它,如果用的不好反而会带来灾难。