JDK内部锁优化策略概要
图片过大导致字体较小,可下载后查看。
1.ThreadLocal
- 为每一个线程提供所需要的资源
- 需要在应用层面保证。如果在应用上为每一个线程分配了相同的对象实例,依旧无法保证线程安全
1.1例子
package threadLocal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* ThreadLocal:
* 1.判断当前线程是否持有使用的对象实例
* 2.如果不持有则new新建一个并设置到当前线程中进行使用
* 3.该功能的实现需要在应用层面进行保证
* @author wsz
* @date 2017年12月20日
*/
/*
1.获取当前线程
2.获取线程的ThreadLocalMap
3.设值到ThreadLocalMap中
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
1.获取当前线程对象
2.获取线程的ThreadLocalMap
3.线程作为key获取实际的对象等数据
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();
}
*/
public class ThreadLocalDemo {
//SimpleDateFormat,parse()不是线程安全的
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
static ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<SimpleDateFormat>();
public class Demo2 implements Runnable{
private int i;
public Demo2(int i) {
this.i = i;
}
@Override
public void run() {
if(tl.get() == null)//如果当前线程不持有该对象实例,则新建一个并设置到当前线程中
tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
try {
Date date = tl.get().parse("2017-12-20 20:12:"+i%60);
System.out.println(date);
} catch (ParseException e) {
e.printStackTrace();
}
}
}
public class Demo1 implements Runnable{
private int i;
public Demo1(int i) {
this.i = i;
}
@Override
public void run() {
try {
Date date = sdf.parse("2017-12-20 20:12:"+i%60);
System.out.println(date);
} catch (ParseException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ExecutorService ftp = Executors.newFixedThreadPool(10);
for(int i = 0 ; i< 5000 ; i++) {
// ftp.execute(new ThreadLocalDemo().new Demo1(i));
ftp.execute(new ThreadLocalDemo().new Demo2(i));
}
}
}
1.2set()
- 获取当前线程
- 获取线程的ThreadLocalMap
- 设值到ThreadLocalMap中
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
1.3get()
- 获取当前线程对象
- 获取线程的ThreadLocalMap
- 线程作为key获取实际的对象等数据
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();
}
1.4exit()
ThreadLocal内部的变量只要线程不退出,将一直存在与内存中。在使用线程池时,这些线程不一定会退出,如果使用的对象占用的内存较大,可能出现内存泄漏的风险。
可以使用ThreadLocal.remove()方法将这些变量移除,JVM回收这些不再需要的对象。为了方便JVM的回收,在exit()方法中将某些变量都设置为了null。
/**
* This method is called by the system to give a Thread
* a chance to clean up before it actually exits.
*/
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;
}
1.5性能测试
线程利用两种不同资源类型来产生200W个随机数测试性能。
在多线程共享一个Random实例情况下,5个线程的总耗时时间为3924ms;在ThreadLocal模式下,耗时仅为198ms。性能差异很明显。
package threadLocal;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/**
* 测试ThreadLocal对性能的影响
* @author wsz
* @date 2017年12月20日
*/
public class ThreadLocalEffic {
public static final int NUM = 2000000;//每个线程要产生的随机数数量
public static final int SIZE = 5; //参与工作的线程数量
static ExecutorService exe = Executors.newFixedThreadPool(SIZE);//固定数量的线程池
//多线程共享对象
public static Random rnd = new Random(123);
//ThreadLocal对象封装的Random
public static ThreadLocal<Random> trnd = new ThreadLocal<Random>() {
@Override
protected Random initialValue() {
return new Random(123);
}
};
public class RndTest implements Callable<Long>{
private int model = 0;
public RndTest(int model) {
this.model = model;
}
public Random getRandom() {//分类进行返回不同的随机数对象
if(model == 0) {
return rnd;
}else if(model == 1) {
return trnd.get();
}else {
return null;
}
}
//消耗的时间
@Override
public Long call() throws Exception {
long b = System.currentTimeMillis();
for(int i= 0 ;i < NUM ; i++) {
getRandom().nextInt();
}
long e = System.currentTimeMillis();
System.out.println(Thread.currentThread().getClass()+" take "+(e-b)+"ms");
return e-b;
}
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
Future<Long>[] futs = new Future[SIZE];
//普通Random
for (int i = 0; i < SIZE; i++) {
futs[i] = exe.submit(new ThreadLocalEffic().new RndTest(0));
}
int totaltime = 0;
for(int i =0; i< SIZE; i++) {
totaltime += futs[i].get();
}
System.out.println("多线程访问同一个Random实例:"+totaltime+"ms");
//ThreadLocal的Random
for (int i = 0; i < SIZE; i++) {
futs[i] = exe.submit(new ThreadLocalEffic().new RndTest(1));
}
totaltime = 0;
for(int i =0; i< SIZE; i++) {
totaltime += futs[i].get();
}
System.out.println("多线程访问同一个Random实例:"+totaltime+"ms");
exe.shutdown();
}
}
class java.lang.Thread take 729ms
class java.lang.Thread take 787ms
class java.lang.Thread take 801ms
class java.lang.Thread take 804ms
class java.lang.Thread take 803ms
多线程访问同一个Random实例:3924ms
class java.lang.Thread take 39ms
class java.lang.Thread take 39ms
class java.lang.Thread take 39ms
class java.lang.Thread take 40ms
class java.lang.Thread take 41ms
多线程访问同一个Random实例:198ms
2.悲观锁+无锁
- 也即普通的加锁模式,总是假设每一次的临界区操作会产生冲突;在并发多线程下若多个线程同时需要访问临界区资源,便牺牲时间进行等待。
- 无锁是一种乐观的策略,它会假设对资源的访问没有冲突,也不会进行等待。但是当遇到冲突时,将采用CAS比较交换技术鉴别线程冲突,一旦检测到冲突产生,就重试当前操作直到没有冲突为止。
2.1比较交换CAS
3.无锁的开发工具
3.1线程安全整数AtomicInteger
- private volatile int value;代表AtomicInteger的当前实际取值。
- valueOffset:保存value字段在AtomicInteger对象中的偏移量。
- incrementAdnGet():使用CAS操作将自己加1,并返回当前值。
- Unsafe类:采用指针技术。
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
3.2对象引用AtomicReference
- 封装对普通对象的引用,保证在修改对象引用时的线程安全
- 可能出现CAS技术中逻辑不严谨问题
package atomic;
import java.util.concurrent.atomic.AtomicReference;
/**
* 初始账户19元;当小于20元时,每次充值20元,且只能最多充值一次;多于10元时,每次消费10元。
* 使用AtomicReference将会被重复充值,不符合实际情况
* @author wsz
* @date 2017年12月21日
*/
public class AtomicReferenceDemo {
//定义账户
static AtomicReference<Integer> money = new AtomicReference<Integer>();
public static void main(String[] args) {
//初始账户有19元
money.set(19);
for(int i = 0; i <20; i++) {//线程进行充值
new Thread() {
public void run() {
while(true) {
Integer m = money.get();
if(m < 20) {
if(money.compareAndSet(m, m+20)) {//每次充值新增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("小于10元");
break;
}
}
}
}
}.start();
}
}
3.3带有时间戳的AtomicStampedReference
- 记录修改状态,便能解决对象被反复修改导致线程无法判断对象状态的问题
- 不仅维护对象值;还维护时间戳。
- 数值被修改时,还更新时间戳,当时间戳不一致时,即可表示对象已被其他线程修改
package atomic;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* AtomicStampedReference带有时间戳,由此可判断是否被其他线程修改
* 此案例将只能新增一次数据
* @author wsz
* @date 2017年12月21日
*/
public class AtomicStampedReferenceDemo {
static AtomicStampedReference<Integer> money = new AtomicStampedReference<Integer>(19, 0);
public static void main(String[] args) {
for(int i = 0; i <3; i++) {//线程进行充值
final int stamp = money.getStamp();//充值前,final时间戳,后面充值将修改一次时间戳(+1);
//后续操作的期望时间戳仍为第一次设置的值,但时间戳已被修改,无法满足。
new Thread() {
public void run() {
while(true) {
while(true) {
Integer m = money.getReference();
if(m < 20) {
if(money.compareAndSet(m, m+20, stamp, stamp+1)) {//期望值、写入新值、期望时间戳、新时间戳
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 stamp = money.getStamp();//获取时间戳
Integer m = money.getReference();//获取值
if(m > 10) {
System.out.println("大于10");
if(money.compareAndSet(m, m-10, stamp, stamp+1)) {
System.out.println("消费10元,剩余:"+money.getReference());
break;
}
}else {
System.out.println("小于10元");
break;
}
}
}
}
}.start();
}
}