1. 提高锁性能的几点建议
1.1 减小锁持有时间
// 减小锁持有时间可以降低锁冲突的可能性,提升系统并发能力。
public synchronized void syncMethod(){
otherMethod1(); //不需要同步
mutextMethod(); //需要同步
otherMethod2() //不需要同步
}
public void syncMethod(){
otherMethod1(); //不需要同步
sychronized(this){
mutextMethod(); //需要同步
}
otherMethod2() //不需要同步
}
1.2 减小锁粒度
- 例如ConcurrentHashMap,将map划分为了很多段SEGMENT(默认16),需要put时,计算出属于哪个段,然后只对这个段加锁。其他段不受同步影响,幸运的话,可以同时接受16个线程的插入。
- 但是当获取一些全局信息,如size()时,可能需要获取全部16个锁。ConcurrentHashMap会尝试无锁方法求size(),失败再请求全局锁。
1.3 锁分离
- 读写锁
- LinkedBlockingQueue:put和take方法分别作用于链表的两端,所以采用了两把锁takeLock和putLock。
private final ReentrantLock takeLock = new ReentrantLock();
/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();
/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();
/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();
1.4 锁粗化
- 线程请求、同步、释放锁,本身也会消耗一定的资源。当锁细化到一定程度时,反而不利于系统性能。
//减少锁请求的次数
public void demoMethod(){
sychronized(lock){
//do sth
}
//other thing which need no lock
sychronized(lock){
//do sth
}
}
public void demoMethod(){
sychronized(lock){
//do sth
//other thing which need no lock
//do sth
}
}
//更极端
for(100次循环){
sychronized(lock){
}
}
sychronized(lock){
for(100次循环){
}
}
2. 虚拟机对锁优化所做的努力
2.1 锁偏向
- 针对加锁操作的优化手段,核心思想是:如果一个线程获得了锁,那么锁就进入偏向模式。当这个线程再次请求锁时,无需做任何同步操作,节省了大量有关锁的申请操作。
- 对于几乎没有锁竞争的场合,偏向锁有比较好的优化效果,因为连续多次有可能时同一线程请求相同的锁。
- 对于锁竞争比较激烈的场合,其效果不佳。
2.2 轻量级锁
- 如果偏向锁失败,虚拟机不会立即挂起线程,而是使用轻量级锁。
- 轻量级锁只是简单的将对象头部作为指针,指向持有锁的线程堆栈内部,来判断一个线程是否持有对象锁。如果获得轻量级锁,则可以进入临界区,如果失败,当前线程的锁请求就会膨胀为重量级锁。
2.3 自旋锁
- 锁膨胀后,为了避免线程真实的在操作系统层面挂起,虚拟机会尝试自旋锁。
- 简单粗暴的挂起锁消耗较大,因此假设该线程可以在不久的将来获得锁,虚拟机将该线程做几个空循环等待,如果可以获得锁,就进入临界区,还不行就GG,在操作系统层面挂起。
2.4 锁消除
- 因为一些编码原因,可能会在产生一些根本不可能存在竞争资源的锁。虚拟机在JIT编译时会消除这些锁。
//vector内部含有锁,但是此处的vector是局部变量,分配在虚拟机栈空间,线程私有,不存在竞争。虚拟机会消除这个锁。
//此处涉及逃逸分析。
public String[] createStrings() {
Vector<String> v = new Vector<>();
for (int i = 0; i < 100; i++) {
v.add(Integer.toString(i));
}
return v.toArray(new String[] {});
}
3. ThreadLocal
- ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
- 线程本地变量是和线程相关的变量,一个线程则一份数据。我们通过ThreadLocal保存的数据最终是保存在Thread类的ThreadLocalMap threadLocals变量中。ThreadlocalMap是一个Map结构,其中key为我们声明的ThreadLocal对象,value即为我们使用ThreadLocal保存的线程本地变量。
- ThreadLocal本身并不存储value值,只是作为key在ThreadLocalMap中索引value值。
- 当我们调用ThreadLocal变量set方法时,那么为将TheadLocal作为key,set方法的参数做为value保存在当前线程的threadLocals中.调用get方法时类似,调用get方法时,会去Thread的threadLocals中去寻找key为ThreadLocal 变量的值。
- 值得注意的是,ThreadLocal随着当前线程的销毁而销毁,如果程序中采用线程池,在上一次任务运行完之后,记得清掉之前ThreadLocal数据。
- 作用:提供一个线程内公共变量(比如本次请求的用户信息)
//如果get()方法先于set()方法之前调用,则调用setInitialValue()方法返回初始值。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);// getMap方法即去获取当前线程的ThreadLocalMap变量。
if (map != null)
map.set(this, value);//以this(ThreadLocal本身)为Key,参数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();
}
【ThreadLocal是否会造成内存泄露?】
- 每个Thread含有的ThreadLocalMap中的Key为ThreadLocal变量的弱引用,如果一个ThreadLocal变量没有外部强引用来引用它,那么它在JVM下一次GC的时候会被垃圾回收掉,这时候,Map中就存在了key为NULL的value,这个value无法被访问。如果当前线程再迟迟不结束的话(例如当前线程在一个线程池中),那么value所指向的对象可能永远无法释放,也即不能被回收,造成内存泄露。
- ThreadLocalMap的设计者很显然也想到了这个问题,所以其在每一次对ThreadLocalMap的set,get,remove等操作中,都会清除Map中key为null的Entry。因此,ThreadLocal一般是不会存在内存泄露风险的。
- 但是,将清除NULL对象的工作交给别人,并不是一个明智的选择,所以聪明的你,在Thread中使用完ThreadLocal对象后,一定要记得调用ThreadLocal的remove方法,进行手动清除。
4. 无锁
- 锁:悲观策略,假设每一次对临界区操作都会产生冲突,宁愿牺牲性能让线程等待,也要阻塞线程。
- 无锁:乐观策略,假设每一次对临界区操作都不会产生冲突,采用CAS(Compare and Swap)来鉴别线程冲突,一旦检测到冲突,就重试当前操作直到没有冲突为止。它不会阻塞线程。
4.1 CAS算法
- 它包含三个参数CAS(V,E,N)。V表示要更新的变量,E表示预期值,N表示新值。仅当V==E时,才会将V的值设为N,如果V!=E,说明其它线程已经做了更新,当前线程什么都不做。最后返回V的真实值。
- 多个线程操作时,只有一个会胜出,并且成功更新,其余均会失败。失败的线程不会被挂起,仅仅是被告知失败,并且允许再次尝试,也可以放弃操作。
4.2 无锁的线程安全整数:AtomicInteger
- 与Integer相比,AtomicInteger是可变的,线程安全的。
就内部实现而言,AtomicInteger保存着
private volatile int value;//当前实际取值
private static final long valueOffset;//value在AtomicInteger对象中的偏移量
public class AtomicIntegerDemo {
static AtomicInteger i = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Runnable r = ()->{
for (int k = 0; k < 10000; k++) {
i.incrementAndGet();
}
};
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(r);
}
for (int i = 0; i < threads.length; i++) {
threads[i].start();
}
for (int i = 0; i < threads.length; i++) {
threads[i].join();
}
System.out.println(i);
}
}
//输出100000,说明线程安全的。
incrementAndGet()的实现:无限循环就是CAS
public final int incrementAndGet() {
for(;;) {
int current = get();
int next = current + 1;
if(compareAndSet(current, next)) {
return next;
}
}
}
4.3 无锁的对象引用:AtomicReference
class Person {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
// 普通引用,多线程下会导致数据不一致,name和age不是一个人
private static Person person;
// 原子性引用
private static AtomicReference<Person> aRperson;
4.4 带时间戳的对象引用:AtomicStampedReference
- 当你获得对象的数据后,在准备修改为新值前,对象的值被其它线程连续修改了两次,对象的值又恢复到了旧值。如果修改的对象没有过程的状态信息,那还是ok的。如果对象的过程变化很重要,那么就需要AtomicStampedReference了。
- AtomicStampedReference内部维护了一个时间戳。
4.5 数组无锁:AtomicIntegerArray
4.6 让普通变量享受原子操作:AtomicIntegerFieldUpdater
- 可以将普通变量提升为线程安全的。
- 有三个:AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,AtomicReferenceFieldUpdater
class Candidate{
int id;
volatile int score;
}
AtomicIntegerFieldUpdater<Candidate> updater = AtomicIntegerFieldUpdater.newUpdater(Candidate.class, "score");
【注意】
- Updater只能修改它可见范围内的变量(因为updater使用反射得到这个变量)。如果score为private,就不可行。
- 变量必须是volatile的。
- CAS操作通过对象实例中的偏移量直接进行赋值,因此,不支持static。
4.7 SynchronousQueue
http://blog.csdn.net/yanyan19880509/article/details/52562039