多线程是我们在编程中必然会遇到的、非常基础、非常重要的知识。我们在编程时,头脑中,必须要有多线程的意识(高并发的意识)。虽然很基础,但是也有其难度。这篇博客,将简单介绍面对多线程时,加锁的处理方式。
线程安全的定义
多个线程之间的操作,无论采用何种执行时序或交替方式,都要保证不变性条件不被破坏。
当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么这个类是线程安全的。
内置锁
java的内置锁,相当于一种互斥体(互斥锁)。性能较低。可重入。
同步代码块 - synchronized
- 作为锁的对象引用
每个Java对象,都可以用做一个实现同步的锁。
之所以每个对象都有一个内置锁,只是为了免去显示地创建锁对象。
-
作为由这个锁保护的代码块
-
可重入
如果某个线程试图获取一个已经由它自己持有的锁,那么这个请求就会成功。
当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数值置为1。如果同一个线程再次获取这个锁,计数值将递增,而当线程退出同步代码块时,计数器会相应地递减。当计数值为0时,这个锁将被释放。
当执行时间较长的计算或者可能无法快速完成的操作(例如:网络IO或者控制台IO),一定不要持有锁!
1. Synchronized
互斥锁。
synchronized实现的机理依赖于软件层面上的JVM。
如果当前多线程的性能瓶颈在锁上,可以考虑更换其他机制的锁。
自旋锁 - 避免用户线程和内核的切换。,尽可能的减少阻塞的可能性。对于占用锁时间比较短的代码块来说性能能大幅度的提升!
偏向锁 - 主要用于解决无竞争下面锁的性能问题。
2. Lock
互斥锁。
纯Java实现。
ReentrantLock、ReadWriteLock(ReentrantReadWriteLock)、AbstractQueuedSynchronizer(是一个同步器,支持获取锁和释放锁两个操作)
高并发下,ReentrantLock的性能比Synchronized高很多。
lock()、unlock(),使用ReentrantLock必须在finally控制块中进行解锁操作。
可轮询锁 - 提供了tryLock()轮询方法来获得锁,如果锁可用则获取锁,如果锁不可用,则此方法返回false,并不会为了等待锁而阻塞线程,这极大地降低了死锁情况的发生。
定时锁 - 使用Lock.tryLock(long timeout, TimeUnit unit)来指定让线程在timeout单位时间内去争取锁资源,如果超过这个时间仍然不能获得锁,则放弃锁请求,定时锁可以避免线程陷入死锁的境地。
3. Semaphore
通过acquire()与release()方法来获得和释放临界资源。释放锁的操作也必须在finally代码块中完成。
提供了可轮询锁、定时锁、公平锁、非公平锁。
支持多个临界资源,而ReentrantLock只支持一个临界资源
4. AtomicInteger
AtomicReference
通常AtomicInteger的性能是ReentantLock的好几倍。