目录
一、线程安全的实现
1.1 阻塞同步
也就是使用锁实现。具体采用锁,有两种选择:内置锁也就是synchronized关键字、JUC下具体锁的实现
1.2 非阻塞同步
基于CAS操作。最直接的实现是JUC下各种原子类的实现。虽然CAS避免了锁带来的性能开销,不过其仅适用于少部分同步场景,没有阻塞同步更加具有普适性。
使用锁会带来一些问题:频繁的线程阻塞、唤醒操作以及用户内核态的切换带来的性能问题,所以引入非阻塞同步
缺点:
- 未获取同步资源的线程陷入自旋状态,所以对于CPU的消耗很高
- 仅能操作单个共享资源,对于组合类型还是需要加锁处理,或者重新组合为一个共享资源
1.3 无同步方案
线程的本地存储,主要用于对于一个共享资源都尽可能在同一个线程中执行
二、线程锁的特点、性能和使用场景
2.1 多线程的理由
和进程相比,它是一种花销小、切换快、更“节俭”的多任务操作方式
在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种”昂贵”的多任务工作方式。而在进程中的同时运行多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。
2.2 多线程的问题
由于多个线程是共同占有所属进程的资源和地址空间的,如果多个线程要同时访问同一个共享资源,这时作为开发者必须考虑如何维护数据一致性,这就是java锁机制(同步机制)的来源。
2.3 四种线程锁
1、synchronized
在需要同步的方法、类或代码块中加入关键字,它能够保证同一时刻最多只有一个线程执行同一个对象的同步代码,可保证修饰的代码在执行过程中不会被其他线程干扰。
使用synchronized修饰的代码具有原子性和可见性。
synchronized (obj) { //⽅法
.......
}
尽管Java实现的锁机制有很多种,并且有些锁机制性能也比synchronized高,但还是强烈推荐在多线程应用程序中使用该关键字。因为实现方便,后续工作由JVM来完成,可靠性高。只有在确定锁机制是当前多线程程序的性能瓶颈时,才考虑使用其他机制,如ReentrantLock等。
2、ReentrantLock
ReentantLock继承接口Lock并实现了接口中定义的方法,除了能完成synchronized所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。
Lock实现的机理依赖于特殊的CPU指令,可以认为不受JVM的约束。在并发量较小的多线程应用程序中,ReentrantLock与synchronized性能相差无几,但在高并发量的条件下, synchronized性能会迅速下降几十倍,而ReentrantLock的性能却能依然维持一个水准。
因此我们建议在高并发情况下使用ReentrantLock。
ReentrantLock引入两个概念:公平锁与非公平锁。
ReentrantLock通过方法lock()和unlock()进行加锁和解锁操作,与synchronized会被JVM自动解锁机制不同。ReentrantLock加锁后需要手动进行解锁。为了避免程序出现异常而无法正常解锁的情况,使用ReentrantLock必须在finally控制块中进行解锁操作。
Lock lock = new ReentrantLock();
try {
lock.lock();
//...进⾏任务操作
}
finally {
lock.unlock();
}
3、Semaphore
上述两种锁机制都是互斥锁,互斥是进程同步关系的一种特殊情况,相当于只存在一个临界资源。但是,在实际应用中,可能存在多个临界资源,这时候可以借助Semaphore信号量来完成多个临界资源的访问。
Semaphore基本能完成ReentrantLock的所有工作,使用方法也与之类似,通过acquire()和release()方法来获取和释放临界资源。
经实测,Semaphone.acquire()方法默认为可响应中断锁,与ReentrantLock.lockInterruptibly()作用效果一致, 也就是说在等待临界资源的过程中可以被Thread.interrupt()方法中断。
此外,Semaphore也实现了可轮询的锁请求与定时锁的功能,除了方法名tryAcquire与tryLock不同,其使用方法与ReentrantLock几乎一致。
Semaphore也提供了公平与非公平锁的机制,也可在构造函数中进行设定。Semaphore的锁释放操作也由手动进行,因此与ReentrantLock一样,为避免线程因抛出异常而无法正常释放锁的情况发生,释放锁的操作也必须在finally代码块中完成。
4、AtomicInteger
在多线程程序中,诸如++i、i++等运算不具有原子性,是不安全的线程操作之一。通常我们会使用synchronized将该操作变成一个原子操作,但JVM为此类操作特意提供了一些同步类,使得使用更方便,使程序运行效率变得更高效。通常AtomicInteger的性能是ReentrantLock的好几倍。
总结:
1、synchronized:在资源竞争不是很激烈的情况下,偶尔会有同步的情况下,synchronized是很合适的。
2、ReentrantLock:在资源竞争不激烈的情况下,性能稍微比synchronized差点。但当同步非常激烈的时候,synchronized性能一下子下降几十倍,而ReentrantLock能维持常态。高并发量情况下使用ReentrantLock。
3、Atomic:和上面类似,不激烈情况下,性能比synchronized略逊;而在激烈时,也能维持常态。激烈的时候,Atomic性能会优于ReentrantLock一倍左右。但有个缺点,就是只能同步一个值,一段代码中只能出现一个Atomic的变量。
所以,我们写同步的时候,优先考虑synchronized,如果有特殊需要,再进一步优化。ReentrantLock和Atomic如果用得不好,不仅不能提高性能,还可能带来灾难。