1.锁优化的思路和方法
减少锁持有时间
方法一,方法二的区别:方法二对锁持有时间比价短,因为在操作共享数据时才需要加锁;而ohtercode1(),othercode2()方法并不涉及对共享数据的操作,所以不需要阻塞其它线程。
减小锁粒度
将大对象,拆成小对象,大大增加并行度,降低锁竞争。
偏向锁,轻量级锁成功率提高。
HashMap的同步实现
– Collections.synchronizedMap(Map<K,V> m)
– 返回SynchronizedMap对象
//并没有整个对象加锁,而只是对一小块变量加锁
public V get(Object key) {
synchronized (mutex) {return m.get(key);}
}
public V put(K key, V value) {
synchronized (mutex) {return m.put(key, value);}
}
ConcurrentHashMap
– 若干个Segment :Segment<K,V>[] segments
– Segment中维护HashEntry<K,V>
– put操作时
• 先定位到Segment,锁定一个Segment,执行put
在减小锁粒度后, ConcurrentHashMap允许若干个线程同时进入。
锁分离
根据功能进行锁分离
ReadWriteLock
读多写少的情况,可以提高性能
读写分离思想可以延伸,只要操作互不影响,锁就可以分离
LinkedBlockingQueue
锁粗化
通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完
公共资源后,应该立即释放锁。只有这样,等待在这个锁上的其他线程才能尽早的获得资源执行
任务。但是,凡事都有一个度,如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗
系统宝贵的资源,反而不利于性能的优化。
锁消除
在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作。
2.虚拟机内的锁优化
对象头Mark
Mark Word,对象头的标记,32位
描述对象的hash、锁信息,垃圾回收标记,年龄
– 指向锁记录的指针
– 指向monitor的指针
– GC标记
– 偏向锁线程ID
偏向锁
大部分情况是没有竞争的,所以可以通过偏向来提高性能
所谓的偏向,就是偏心,即锁会偏向于当前已经占有锁的线程
将对象头Mark的标记设置为偏向,并将线程ID写入对象头Mark
只要没有竞争,获得偏向锁的线程,在将来进入同步块,不需要做同步
当其他线程请求相同的锁时,偏向模式结束
-XX:+UseBiasedLocking
– 默认启用
在竞争激烈的场合,偏向锁会增加系统负担
轻量级锁
普通的锁处理性能不够理想,轻量级锁是一种快速的锁定方法。
如果对象没有被锁定
– 将对象头的Mark指针保存到锁对象中
– 将对象头设置为指向锁的指针(在线程栈空间中)
lock->set_displaced_header(mark);
if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)){
TEVENT (slow_enter: release stacklock) ;
return ;
}
如果轻量级锁失败,表示存在竞争,升级为重量级锁(常规锁)。
在没有锁竞争的前提下,减少传统锁使用OS互斥量产生的性能损耗。
在竞争激烈时,轻量级锁会多做很多额外操作,导致性能下降。
自旋锁
当竞争存在时,如果线程可以很快获得锁,那么可以不在OS层挂起线程,让线程做几个空操作(
自旋)
JDK1.6中-XX:+UseSpinning开启
JDK1.7中,去掉此参数,改为内置实现
如果同步块很长,自旋失败,会降低系统性能
如果同步块很短,自旋成功,节省线程挂起切换时间,提升系统性能
偏向锁,轻量级锁,自旋锁总结
不是Java语言层面的锁优化方法
内置于JVM中的获取锁的优化方法和获取锁的步骤
– 偏向锁可用会先尝试偏向锁
– 轻量级锁可用会先尝试轻量级锁
– 以上都失败,尝试自旋锁
– 再失败,尝试普通锁,使用OS互斥量在操作系统层挂起
3.一个错误使用锁的案例
public class IntegerLock {
static Integer i=0;
public static class AddThread extends Thread{
public void run(){
for(int k=0;k<100000;k++){
//此处对每次循环都加锁,错误。应在循环体外加锁。
synchronized(i){
i++;
}
}
}
}
public static void main(String[] args) throws InterruptedException {
AddThread t1=new AddThread();
AddThread t2=new AddThread();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
4.ThreadLocal及其源码分析
使用场景:
Synchronized,锁,并发控制用于多个线程间的数据共享。
ThreadLocal则用于线程间的数据隔离。
每个线程会维护自己的ThreadLocalMap。
比如:多个用户都在同时登陆,会给每个用户分配一个Token,Token是每个用户独立的,相互隔离。
package com.thread.chapter07.threadlocal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 使用ThreadLocal,每个线程都有自己的SimpleDateFormat实例
* Created by chenbin on 2019\8\22 0022.
*/
public class ThreadLocalDemo {
private static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>();
public static class ParseDate implements Runnable {
int i = 0;
public ParseDate(int i) {
this.i = i;
}
public void run() {
try {
if (threadLocal.get() == null) {
//为每个线程分配一个实例
threadLocal.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
Date t = threadLocal.get().parse("2019-08-22 17:29:"+i%60);
System.out.println(i+":"+t);
} catch (ParseException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(10);
for (int i = 0;i < 1000;i++) {
es.execute(new ParseDate(i));
}
}
}
为了解释ThreadLocal类的工作原理,必须同时介绍与其工作甚密的其他几个类
ThreadLocalMap(内部类)
Thread
首先,在Thread类中有一行:
/* ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
其中ThreadLocalMap类的定义是在ThreadLocal类中,真正的引用却是在Thread类中。同时,ThreadLocalMap中用于存储数据的entry定义:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
从中我们可以发现这个Map的key是ThreadLocal类的实例对象,value为用户的值。
ThreadLocal的set和get方法代码:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, 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();
}
其中的getMap方法:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
给当前Thread类对象初始化ThreadlocalMap属性:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
到这里,我们就可以理解ThreadLocal究竟是如何工作的了
- Thread类中有一个成员变量属于ThreadLocalMap类(一个定义在ThreadLocal类中的内部类),它是一个Map,他的key是ThreadLocal实例对象。
- 当为ThreadLocal类的对象set值时,首先获得当前线程的ThreadLocalMap类属性,然后以ThreadLocal类的对象为key,设定value。get值时则类似。
- ThreadLocal变量的活动范围为某线程,是该线程“专有的,独自霸占”的,对该变量的所有操作均由该线程完成!也就是说,ThreadLocal 不是用来解决共享对象的多线程访问的竞争问题的,因为ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。当线程终止后,这些值会作为垃圾回收。
- 由ThreadLocal的工作原理决定了:每个线程独自拥有一个变量,并非是共享的。
备注:本文为JAVA高并发程序设计(葛一鸣著)读书笔记,以及自身的整理和实践。