锁的使用建议
- 减少锁持有时间
- 减少锁粒度
- 读写锁替代独占锁
- 锁分离
- 锁粗化
减少锁的持有时间
减少锁的持有时间有助于降低冲突的可能性,进而提升并发能力
减少锁粒度
例如ConcurrentHashMap,内部分为16个segment,加锁时不会像hashmap一样全局加锁,只需要对相应segment加锁,但是如果需
要计算map所有的大小size(),则需依次获取所有segment的锁
读写锁替代独占锁(ReadWriteLock)
读多写少的场景下使用读写锁可以显著提高系统的并发能力
锁分离
在读写锁的前提上再进行升级,对独占锁进行分离,如LinkedBlockingQueue,由于是基于链表结构,take()和put()分别作用于队列的前端和尾端.两者并不冲突,故可以使用takeLock和putLock,从而削弱锁竞争的可能性
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly(); //take锁加锁
try {
while (count.get() == 0) {
notEmpty.await(); //如果无可用数据则一直等待,知道put()方法的通知
}
x = dequeue();
c = count.getAndDecrement(); //原子操作-1
if (c > 1)
notEmpty.signal(); //通知其他take方法
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull(); //通知put方法 已有空间
return x;
}
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
// Note: convention in all put/take/etc is to preset local var
// holding count negative to indicate failure unless set.
int c = -1;
Node node = new Node(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
while (count.get() == capacity) {
notFull.await();
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}
锁粗化
一连串对同一个锁不停地进行请求和释放会被整合成对锁的一次操作,从而减少对锁请求同步次数
for(int i=0;i<100;i++){
synchronized(lock){ //此时把锁放到循环外边去最好
//...
}
}
JVM对锁的优化
- 锁偏向
- 轻量级锁
- 自旋锁
- 锁消除
锁偏向
原理:一个线程获得锁,则进入偏向模式,当这个线程再次请求锁时,无需再做任何同步操作
场景:几乎没有锁竞争的场合
操作:竞争激烈时建议关闭. -XX:+UseBiasedLocking可以开启偏向锁
轻量级锁
原理:偏向锁失败后,会膨胀为轻量级锁,对象头部会尝试指向持有锁的线程堆栈内部,来判断是否持有对象锁,如果获得轻量级锁成功,则进入临界区,否则表示其他线程抢占到锁,当前线程膨胀为重量级锁(在膨胀前可以自旋再去尝试获得)
场景:不存在锁竞争或竞争不激烈
自旋锁
原理:锁膨胀后,尝试自旋,若干次后若仍得不到锁,转入重量级锁,将线程挂起
场景:竞争不激烈,且持有锁时间较短
- 锁消除
原理:去除不可能存在共享的资源竞争锁,如某些jdk内部自带的类
场景:单线程下的加锁操作会被锁消除
逃逸分析:观察某变量是否会逃逸出某个作用域,如果该变量没有逃逸出该作用域,则虚拟机会把该变量内部的锁消除掉
操作:-XX:+DoEscapeAnalysis 打开逃逸分析; -XX:+EliminateLocks 打开锁消除
ThreadLocal
线程的局部变量,仅当前线程可以访问,故实现线程安全.
实现原理
ThreadLocal类的get, set方法:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); //获取threadlocalmap
if (map != null)
map.set(this, value); //键位当前threadlocal对象,value为保存值
else
createMap(t, value);
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
回收清理:
线程类Thread在退出时,会进行一些清理工作,其中包括清理ThreadLocalMap
private void exit() {
if (group != null) {
group.threadTerminated(this);
group = null;
}
/* Aggressively null out all reference fields: see bug 4006245 */
target = null;
/* Speed the release of some of these resources */
threadLocals = null; //置空
inheritableThreadLocals = null; //置空
inheritedAccessControlContext = null;
blocker = null;
uncaughtExceptionHandler = null;
}
注意:
使用线程池未必会清除threadlocal,因为线程总会存在,而不会退出,多次使用后可能造成内存泄漏.故此处推荐使用ThreadLocal.remove()方法来移除变量.
ThreadLocalMap实现了弱引用,即进行GC时,一旦发现弱引用则立即回收.ThreadLocalMap中每一个Entry是一个WeakReference,ThreadLocal为key. 当ThreadLocal的外部强引用被回收时,ThreadLocalMap的key将会变成null.放系统进行ThreadLocalMap清理时(添加新变量进入表中时,会自动进行清理),垃圾数据会被回收.
无锁操作
无锁属于乐观锁,不采用重量级锁,主要实现原理为CAS比较交换.
比较交换CAS
特点:非阻塞性,免疫死锁问题,线程间影响比锁要小,总结来说,性能更强
CAS(V,E,N) V表示要更新的变量,E表示预期值,N表示新值.当V=E时才会去更新V=N,否则不作任何操作,多个线程去CAS更新时,只会有一个成功
线程安全整数:AtomicInteger
特点:线程安全,使用CAS,性能强
Java中的指针Unsafe类
sun.misc.Unsafe类型,仅能在JDK中使用,第三方无法调用
AtomicReference
AtomicReference与AtomicInteger相似,是对普通对象的引用.
在CAS中,存在一个问题,当一个变量连续被修改两次,最后一次修改成原来的值,这样当前对象就无法判断该对象是否被修改过
当存在这样一个场景时,AtomicReference便无效了:
//有一家蛋糕店,当客户余额低于20元时,主动给客户赠送20元余额,每位客户只能被赠送一次
//如果使用原子类的话,当客户低于20元,获赠20元余额时,客户再次消费20元整数额,此时,赠予前和赠予后金额一致,故可能存在多次赠予的情况
public class AtomicReferenceDemo {
static AtomicReference money=new AtomicReference();
public static void main(String[] args) {
money.set(19);
//模拟多个线程同时更新后台数据库,为用户充值
for(int i = 0 ; i < 3 ; i++) {
new Thread() {
public void run() {
while(true){
while(true){
Integer m=money.get();
if(m<20){
if(money.compareAndSet(m, m+20)){
System.out.println("余额小于20元,充值成功,余额:"+money.get()+"元");
break;
}
}else{
//System.out.println("余额大于20元,无需充值");
break ;
}
}
}
}
}.start();
}
//用户消费线程,模拟消费行为
new Thread() {
public void run() {
for(int i=0;i<100;i++){
while(true){
Integer m=money.get();
if(m>10){
System.out.println("大于10元");
if(money.compareAndSet(m, m-10)){
System.out.println("成功消费10元,余额:"+money.get());
break;
}
}else{
System.out.println("没有足够的金额");
break;
}
}
try {Thread.sleep(100);} catch (InterruptedException e) {}
}
}
}.start();
}
}
带时间戳的AtomicStampedReference
为解决上述问题,AtomicStampedeference内部维护了对象值,也维护了更新时间戳.
public class AtomicStampedReferenceDemo {
static AtomicStampedReference money=new AtomicStampedReference(19,0);
public static void main(String[] args) {
//模拟多个线程同时更新后台数据库,为用户充值
for(int i = 0 ; i < 3 ; i++) {
final int timestamp=money.getStamp();
new Thread() {
public void run() {
while(true){
while(true){
Integer m=money.getReference();
if(m<20){
if(money.compareAndSet(m, m+20,timestamp,timestamp+1)){ //CAS修改值,并且修改时间戳
System.out.println("余额小于20元,充值成功,余额:"+money.getReference()+"元");
break;
}
}else{
//System.out.println("余额大于20元,无需充值");
break ;
}
}
}
}
}.start();
}
//用户消费线程,模拟消费行为
new Thread() {
public void run() {
for(int i=0;i<100;i++){
while(true){
int timestamp=money.getStamp();
Integer m=money.getReference();
if(m>10){
System.out.println("大于10元");
if(money.compareAndSet(m, m-10,timestamp,timestamp+1)){
System.out.println("成功消费10元,余额:"+money.getReference());
break;
}
}else{
System.out.println("没有足够的金额");
break;
}
}
try {Thread.sleep(100);} catch (InterruptedException e) {}
}
}
}.start();
}
}
无锁的数组AtomicIntegerArray
原子数组还有AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
public class AtomicIntegerArrayDemo {
static AtomicIntegerArray arr = new AtomicIntegerArray(10);
public static class AddThread implements Runnable{
public void run(){
for(int k=0;k<10000;k++)
arr.getAndIncrement(k%arr.length()); //k%length索引处+1
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] ts=new Thread[10];
for(int k=0;k<10;k++){
ts[k]=new Thread(new AddThread());
}
for(int k=0;k<10;k++){ts[k].start();}
for(int k=0;k<10;k++){ts[k].join();}
System.out.println(arr);
}
}
普通变量升级原子变量AtomicIntegerFieldUpdater
updater升级有三种: AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,AtomicReferenceFieldUpdater
public class AtomicIntegerFieldUpdaterDemo {
public static class Candidate{
int id;
volatile int score; //不支持静态变量,且必须申明为volatile,且访问权限要支持(private不行)
}
public final static AtomicIntegerFieldUpdater scoreUpdater
= AtomicIntegerFieldUpdater.newUpdater(Candidate.class, "score"); //声明要升级的类以及成员变量.
//检查Updater是否工作正确
public static AtomicInteger allScore=new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
final Candidate stu=new Candidate();
Thread[] t=new Thread[10000];
for(int i = 0 ; i < 10000 ; i++) {
t[i]=new Thread() {
public void run() {
if(Math.random()>0.4){
scoreUpdater.incrementAndGet(stu); //使用升级类去做CAS原子自增
allScore.incrementAndGet(); //验证升级类是否正确
}
}
};
t[i].start();
}
for(int i = 0 ; i < 10000 ; i++) { t[i].join();}
System.out.println("score="+stu.score); //最终打印 score和allscore一致.
System.out.println("allScore="+allScore);
}
}
注意:
1.只能修改可见范围内的变量,因为Updater使用反射得到变量.
2.为了保证变量被正确读取,必须为volatile
3.CAS通过对象实例中的偏移量进行复制,因此,不支持static变量
无锁的LockFreeVector
待续......
SynchronousQueue
此类的容量为0,每一个写都要对应一个读操作,其内部也有很多无锁操作
对SynchronousQueue来说,它将put()和take()均抽象为一个共同方法Transferer.transfer().从字面上来看,就是数据传递
Object transfer(Object e,boolean timed,long nanos) //e不为空时,表示入队,e为空时表示出队
//timed 表示是否有超时,nanos表示超时时间
//返回值为空表示失败,不为空表示成功
SynchronousQueue内部会维护一个线程等待队列,保存等待线程及相关数据信息,如生产者将数据放入SynchronousQueue中,如果没有消费者接受,则数据本身和对象都会打包在队列中等待(SynchronousQueue本身容量为0,数据是不能放入的,但是内部的线程等待队列可以存放)
Transferer.transfer()函数的实现是SynchronousQueue的核心,大提分为三个步骤.
(1)如果等待队列为空,或者队列中节点的类型和本次操作是一致的,那么将当前操作压入队列等待。比如,等待队列中是读线程等待,本次操作也是读,因此这两 个读都需要等待。进入等待队列的线程可能会被挂起,它们会等待一个“匹配” 操作
(2)如果等待队列中的元素和本次操作是互补的(比如等待操作是读,而本次操作是写),那么就插入一个“完成”状态的节点,并且让它“匹配”到一个等待节点 上。接着弹出这两个节点,并且使得对应的两个线程继续执行。
(3)如果线程发现等待队列的节点就是“完成”节点,那么帮助这个节点完成任务,其流程和步骤(2)是一致的。
死锁
定位:
jps:查看java进程的进程ID
jstack (ID):查看线程的线程堆栈
为了避免死锁问题,可以使用无锁函数以及重入锁,通过重入锁的中断或者限时等待可以有效规避死锁带来的问题