简介
本篇主要介绍一下java中锁
synchronized 和 ReentrantLock
1)、Lock中,正在等待获得锁的线程是可以响应中断的,而synchronized 只能一直等待,直到获得锁
2)、Lock中,能够获得当前线程是否获得锁
3)、Lock可以设置等待获得锁的时间,如果超过限制的时间还没有获得锁,就返回false, 否则返回true
4)、多个线程共同读一个文件时,通过读锁,可以并发的读,而synchronized只能等到获得锁后才能读
5)、Lock需要用户主动释放锁,否则会出现死锁现象,而系统会自动释放synchronized锁
6)、Lock可以实现公平锁,而synchronized只能是非公平锁
7)、synchronized的线程等待是Object.wait和Object.notify 而ReentrantLock实现线程等待的方法是Condition.await和Condition.signal 或 Condition.signalAll .
如何使用Lock
Lock接口位于java.util.concurrent.locks包下
Lock接口的主要方法:
void lock(); 比较常用的上锁,其他线程等待获得锁
boolean tryLock(); 获得锁返回true否则返回false,可以避免一直等待获得锁
void lockInterruptibly(); 通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态
boolean tryLock(long time, TimeUnit unit)throws InterruptedException; 可以在指定的时间内获得锁,如果获得了,就返回true, 超过指定的时间未获得锁,就返回false
void unlock(); 解锁,通常放在finally中,确保在异常的时候,锁也能够释放
Lock的主要实现类:ReentrantLock
将读写分离的锁接口
解决问题:将文件的读写操作分开,分成两个锁来分配给线程,可以使多个线程同时读文件
接口:ReadWriteLock
方法:Lock readLock(); 读锁
Lock writeLock(); 写锁
主要实现类: ReentrantReadWriteLock
锁都有哪些类型
1)、可重入锁,像synchronized和ReentrantLock都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。
2)、可中断锁 ReentrantLock是可中断锁,而synchronized是不可中断锁,可以在线程等待获得锁的时候,中断他,会抛出InterruptedException异常
3)、读写锁 可以使多个线程同时读
4)、公平锁 按等待获得锁的时间来分配锁,避免一些线程始终获取不到锁,synchronized是不公平锁,ReentrantLock 是可以通过配置来设置是否是公平的锁
锁优化
1、减小锁持有时间,只对需要同步的代码加锁
2、减小锁粒度,例如CurrentHashMap
3、读写分离来替换独占锁ReentrantReadWriteLock
4、锁分离 例如LinkedBlockingQueue来实现put和take分离
5、锁粗化。如果对一个锁不停的请求释放,会消耗宝贵的系统资源,意思可以把一些锁合并在一起。
锁偏向
如果一个线程获得了锁,那么锁就进入偏向模式,当这个线程再次请求锁时,无需再做任何同步操作。这样就节省了大量有关锁申请的操作,从而就节省了大量有关锁申请的操作,从而提高了程序性能。启用偏向锁:-XX:+UseBiasedLocking
轻量级锁
如果偏向锁失败,虚拟机并不会立即挂起线程,它还会使用一种轻量级锁的优化手段,轻量级锁的操作也很方便,它只是简单地将对象头部作为指针,指向持有锁的线程堆栈的内部,来判断一个线程是否持有对象锁。如果线程获得轻量级锁成功,则可以顺利进入临界区。如果轻量级锁加锁失败,则表示其他线程抢先争夺到了锁,那么当前线程的锁请求就会膨胀为重量级锁。
自旋锁
虚拟机为了避免在操作系统层面挂起,在当前线程做几个空循环。在经过若干次循环后,如果可以得到锁,那么就顺利进入临界区,若个还不能获得锁,才会真是的将线程在操作系统层面挂起。通过-XX:+UseSpinning参数来开启,jdk1.6引入了自适应的自旋锁,自旋地时间不是固定的,可以使用
-XX:PreBlockSpin来更改自旋的次数。
锁消除
java虚拟机在JIT编译时,通过对上下文的扫描,去除不可能存在的共享资源竞争的锁。使用-XX:+EliminateLocks可以打开锁消除,使用-XX:+DoEscapeAnalysis参数打开逃逸分析。
ThreadLocal
线程的局部变量,只有当前线程可以访问,时间换空间
乐观锁
使用一种比较交换的策略,一旦检测到冲突,重试当前操作,直到没有冲突为止。
与悲观锁相比,使用CAS会使程序复杂些,但由于其非阻塞性,他对死锁天生免疫,并且,线程之间的相互影响也远远比基于锁的方式要小,更重要的是,使用无锁的方式,完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,他要比基于锁的方式
拥有更优越的性能。
CAS算法:CAS(V,E,N)。V表示要更新的变量,E表示预期值,N表示新值。近当V值等于E值时,才会将V的值更新为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。
在硬件层面,大部分的现代处理器都已经支持了原子化的CAS指令。JDK1.5以后,虚拟机便可以使用这个指令来实现并发操作和并发数据结构。
无锁的一些java API
为了使程序员能够收益于CAS的Cpu指令,JDK并发包中有一个atomic包,里面实现了一些直接使用CAS操作的线程安全类型。
无锁的线程安全整数:AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference
java中的指针:Unsafe类
Unsafe封装了一些类似指针的操作,compareAndSwapInt()方法是一个native方法,他的几个参数含义如下
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
第一个参数o为给定的对象,offset为对象内的偏移量(其实就是一个字段到对象头部的偏移量,通过这个偏移量可以快速定位字段),expected表示期望值,x表示要设置的值。
不难看出compareAndSwapInt()方法的内部,必然使用了CAS原子指令来完成的。此外,Unsafe类还提供了一些方法,主要有几个
//获取给定对象偏移量上的int值
public native int getInt(Object var1, long offset);
//设置给定对象偏移量上的int值
public native void putInt(Object var1, long offset, int var4);
//获得字段在对象中的偏移量
public native long objectFieldOffset(Field var1);
//获得给定对象的int值,使用volatile语义
public native int getIntVolatile(Object var1, long offset);
//设置给定对象的int值,使用volatile语义
public native void putIntVolatile(Object var1, long offset, int x);
//和putIntVolatile()一样,但是他要求被操作字段就是volatile类型的
public native void putOrderedInt(Object var1, long offset, int x);
我们的应用程序无法直接使用Unsafe类,他是一个JDK内部使用的专属类。
无锁的对象引用:AtomicReference 和带有时间戳的对象引用:AtomicStampedReference
AtomicReference只保存了对象的数值本身,不包含状态信息,当要处理与对象变化过程有关的对象时,它就无能为力了
AtomicStampedReference正是为了解决这样的问题,她不仅维护了对象值,还维护了一个时间戳,当AtomicStampedReference设置对象是,对象值以及时间戳都必须满足期望值,写入才会成功。
数组无锁:AtomicIntegerArray
除了提供基本的数据类型外,JDK还为我们准备了数组等符合结构。当前可用的原子数组有:AtomicIntegerArray,AtomicLongArray和AtomicReferenceArray,分别表示整数数组,long型数组和普通对象数组。
public final int length() {
return array.length;
}
public final int get(int i) {
return getRaw(checkedByteOffset(i));
}
public final int getAndSet(int i, int newValue) {
return unsafe.getAndSetInt(array, checkedByteOffset(i), newValue);
}
public final boolean compareAndSet(int i, int expect, int update) {
return compareAndSetRaw(checkedByteOffset(i), expect, update);
}
public final int getAndIncrement(int i) {
return getAndAdd(i, 1);
}
//将第i个下标元素减1
public final int getAndDecrement(int i) {
return getAndAdd(i, -1);
}
//将第i个下标增加delta
public final int getAndAdd(int i, int delta) {
return unsafe.getAndAddInt(array, checkedByteOffset(i), delta);
}
使普通变量拥有原子操作:AtomicIntegerFieldUpdater
用途:可以让你在不改动(或者极少改动)原有代码的基础上,让普通的变量也享受CAS操作带来的线程安全。
根据数据类型不同,这个Updater有三种,分别是AtomicIntegerFieldUpdater、AtomicLongFieldUpdater和
AtomicReferenceFieldUpdater.
//Candidate是更改变量所在的对象,"score"是要更改的变量
public final static AtomicIntegerFieldUpdater<Candidate> scoreUpdater = AtomicIntegerFieldUpdater.newUpdater(Candidate.class, "score")
注意:1、Updater只能修改它可见范围内的变量。因为Updater使用反射得到这个变量。如果变量不可见,就会出错。比如如果score申明为private,就是不可行的。
2、为了确保变量被正确的读取,他必须是volatile类型的。如果我们原有代码中未申明这个类型,那么简单的申明一下就行。
3、由于CAS操作会通过对象实例中的偏移量直接进行赋值,因此,它不支持static字段(Unsafe.objectFieldOffset()不支持静态变量)