多线程并发面临的挑战:
多线程资源共享
线程死锁
锁的选择
由于多个线程是共同占用所属进程的资源和地址空间的,如果多个线程要同时访问某个资源怎么办?
其实在Java并发编程中,经常遇到多个线程访问同一个 共享资源,这时候作为开发者必须考虑如何维护数据一致性,这就是Java锁机制(同步问题)的来源
Java提供了多种多线程锁机制的实现方式,常见的有:
Synchronized
ReentrantLock
Semaphore
Atomiclnteger等
每种机制都有优缺点与各自的适用场景
Synchronized(依赖于JVM,可靠性高,性能随Java版本升级而提高)
在Java中Synchronized关键字被常用于维护数据一致性
Synchronized机制是给共享资源上锁,只有拿到锁的线程才可以访问共享资源,这样就可以强制使得对共享资源的访问都是按顺序的
Synchronized来实现多线程的同步操作是很简单的,在需要同步的对方的代码块,类,方法中加入此关键字,它能保证在同一个时刻最多只有一个线程执行同一个对象的同步代码,可保证修饰的代码在执行过程中不会被其他线程干扰。
当线程使用Synchronized等待锁时 是不能被Thread.interrupt () 中断的,否则会造成线程死锁
Synchronized修饰的代码具有原子性和可见性,在需要进程同步的程序中使用的频率非常高,可满足一般的进程同步要求
ReentrantLock(可重入锁)
ReentrantLock(可重入锁)可以被线程多次重复进行获取操作
ReentrantLock继承接口Lock并实现了接口中定义的方法,除了能完成Synchronized所能完成的所有工作,还提供了可响应中断锁,可轮询锁请求,定时锁等 避免多线程死锁的方法
Lock依赖于特殊的CPU,可以认为不受JVM的约束,可以通过其他语言平台来完成底层的实现
Synchronized与ReentrantLock的性能比
在并发量较小的多线程应用程序中,ReentrantLock与Synchronized性能相差无几
在高并发量的情况下,Synchronized性能迅速下降几十倍,而ReetrantLock的性能仍然维持一个水准
建议:在高并发量的情况下使用ReentrantLock
ReentrantLock引入了两个概念: 公平锁与非公平锁
公平锁指的是锁的分配机制是公平的,通常 先对锁提出获取请求的线程 会先被分配到锁。
非公平锁指的是JVM按随机,就近原则分配锁的机制被称为非公平锁
ReentrantLock在构造函数中提供了是否公平锁的初始化,默认是非公平锁
非公平锁实际执行的效率远超公平锁,除非程序有特殊要求,否则常用非公平锁的分配机制
ReentrantLock通过 lock () 与 unlock () 来进行加锁与解锁操作,与Synchronized会被JVM自动解锁机制不同,ReentrantLock加锁后需要手动进行解锁
为了避免程序出现异常而无法正常解锁的情况,使用ReentrantLock必须在finally控制块中进行解锁操作
使用方式的示例代码如下:
Lock lock=new ReentrantLock();
try{
lock.lock();
}catch(){
}finally{
lock.unlock();
}
Semaphore
由于Synchronized和ReentrantLock是互斥锁,互斥是进程同步关系的一种特殊情况,相当于只存在一个临界资源,因此最多只能给一个线程提供服务。在实际复杂的多线程应用程序中可能存在多个临界资源,这时可以借助Semaphore信号量来完成多个临界资源的访问
Semaphore基本能完成ReentrantLock的所有工作,使用方法类似,通过acquire () 与 release () 方法来获取和释放临界资源
Semaphore.accquire () 方法默认为可响应中断锁,与ReentrantLock.lockInterruptibly () 作用效果一致,也就是说在等待临界资源的过程中可以被Thread.interrupt () 方法中断
Semaphore也实现了 可轮询的锁请求 与 定时锁 的功能,除了方法名accquire与lock不同,使用方法与ReentrantLock几乎一致。Semaphore也提供了公平锁与非公平锁机制,也可以在构造函数中进行初始化设定
Semaphore的锁释放操作也由手动进行,与ReentrantLock一样,为避免线程因抛出异常而无法正常释放锁的情况发生,释放锁的操作也必须在finally代码块中完成
AtomicInteger
AtomicInterger是一系列相同类的代表之一,常见的还有AtomicLong等,它们的实现原理都相同,区别就在于运算对象类型的不同
在多线程应用程序中,如++i ,i++等运算不具有原子性,是不安全的线程操作之一
通常使用Synchronized将该操作变成一个原子操作,但JVM为此类操作特意提供了一些同步类,从而使得更加方便,并且程序运行效率变得更高,
AtomicInteger的性能是ReentrantLock的好几倍
Java线程锁总结:
Synchronized:
在资源竞争不是很激烈的情况下,偶尔有同步的情况下,Synchronized是很合适的,因为编译程序通常会尽可能的优化Synchronized,可读性也很好
ReentrantLock:
在资源竞争不激烈的情况下,性能稍微比Synchronized差一点,但是当同步很激烈情况下,Synchronized的性能一下子能下降好几十倍,而ReentrantLock依然能维持常态
在高并发量的情况下使用ReentrantLock
Atomic:
在资源竞争不激烈的情况下,性能比Synchronized差一点,而资源竞争激烈的情况下,也能维持常态
在资源竞争激烈的情况下,Atomic的性能会优于ReentrantLock一倍左右
缺点在于,只能同步一个值,一段代码中只能出现一个Atomic的变量,多余一个同步无效,它不能在多个Atomic之间同步
个人建议:
写同步的时候,优先考虑Synchronized,如果有特殊的情况,再进一步优化
ReentrantLock和Atomic如果用的不好,不仅不能提高性能,还可能带来灾难