java 内存锁_Java 高并发的几种锁优化方案

Java 语言自身实现的一些并发容器和工具,你了解的越多,越觉得设计的精妙。本文主要讲并行优化的几种方式,其结构如下:

锁优化

减少锁的持有时间

例如避免给整个方法加锁。

public synchronized void syncMethod(){

othercode1();

mutextMethod();

othercode2();

}

改进后:

public void syncMethod2(){

othercode1();

synchronized(this){

mutextMethod();

}

othercode2();

}

减小锁的粒度

将大对象,拆成小对象,大大增加并行度,降低锁竞争。如此一来偏向锁,轻量级锁成功率提高.。

一个简单的例子就是 jdk 内置的 ConcurrentHashMap 与 SynchronizedMap。

Collections.synchronizedMap 其本质是在读写 map 操作上都加了锁,在高并发下性能一般。

ConcurrentHashMap 内部使用分区 Segment 来表示不同的部分,每个分区其实就是一个小的 hashtable,各自有自己的锁。

只要多个修改发生在不同的分区,他们就可以并发的进行。把一个整体分成了 16 个 Segment,最高支持 16 个线程并发修改。

代码中运用了很多 volatile 声明共享变量,第一时间获取修改的内容,性能较好。

读写分离锁替代独占锁

顾名思义,用 ReadWriteLock 将读写的锁分离开来,尤其在读多写少的场合,可以有效提升系统的并发能力。读-读不互斥:读读之间不阻塞。

读-写互斥:读阻塞写,写也会阻塞读。

写-写互斥:写写阻塞。

锁分离

在读写锁的思想上做进一步的延伸,根据不同的功能拆分不同的锁,进行有效的锁分离。

一个典型的示例便是 LinkedBlockingQueue,在它内部,take 和 put 操作本身是隔离的。有若干个元素的时候,一个在 queue 的头部操作,一个在 queue 的尾部操作,因此分别持有一把独立的锁。

/** Lock held by take, poll, etc */

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();

锁粗化

通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短。

即在使用完公共资源后,应该立即释放锁。只有这样,等待在这个锁上的其他线程才能尽早的获得资源执行任务。

而凡事都有一个度,如果对同一个锁不停的进行请求同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化。

一个极端的例子如下,在一个循环中不停的请求同一个锁。

for(int i = 0; i < 1000; i++){

synchronized(lock){

}

}

// 优化后

synchronized(lock){

for(int i = 0;i < 1000; i++){

}

}

锁粗化与减少锁的持有时间,两者是截然相反的,需要在实际应用中根据不同的场合权衡使用。

ThreadLocal

除了控制有限资源访问外,我们还可以增加资源来保证对象线程安全。

对于一些线程不安全的对象,例如 SimpleDateFormat,与其加锁让 100 个线程来竞争获取。

不如准备 100 个 SimpleDateFormat,每个线程各自为营,很快的完成 format 工作。

public class ThreadLocalDemo {

public static ThreadLocal threadLocal = new ThreadLocal();

public static void main(String[] args){

ExecutorService service = Executors.newFixedThreadPool(10);

for (int i = 0; i < 100; i++) {

service.submit(new Runnable() {

@Override

public void run() {

if (threadLocal.get() == null) {

threadLocal.set(new SimpleDateFormat("yyyy-MM-dd"));

}

System.out.println(threadLocal.get().format(new Date()));

}

});

}

}

}

原理

对于 set 方法,先获取当前线程对象,然后 getMap() 获取线程的 ThreadLocalMap,并将值放入 map 中。

该 map 是线程 Thread 的内部变量,其 key 为 threadlocal,vaule 为我们 set 进去的值。

public void set(T value) {

Thread t = Thread.currentThread();

ThreadLocalMap map = getMap(t);

if (map != null)

map.set(this, value);

else

createMap(t, value);

}

对于 get 方法,自然是先拿到 map,然后从 map 中获取数据。

public T get() {

Thread t = Thread.currentThread();

ThreadLocalMap map = getMap(t);

if (map != null) {

ThreadLocalMap.Entry e = map.getEntry(this);

if (e != null)

return (T)e.value;

}

return setInitialValue();

}

内存释放手动释放: 调用 threadlocal.set(null) 或者 threadlocal.remove() 即可

自动释放: 关闭线程池,线程结束后,自动释放 threadlocalmap。

public class StaticThreadLocalTest {

private static ThreadLocal tt = new ThreadLocal();

public static void main(String[] args) throws InterruptedException {

ExecutorService service = Executors.newFixedThreadPool(1);

for (int i = 0; i < 3; i++) {

service.submit(new Runnable() {

@Override

public void run() {

BigMemoryObject oo = new BigMemoryObject();

tt.set(oo);

// 做些其他事情

// 释放方式一: 手动置null

tt.set(null);

// 释放方式二: 手动remove

tt.remove();

}

});

}

// 释放方式三: 关闭线程或者线程池

// 直接new Thread().start()的场景, 会在run结束后自动销毁线程

service.shutdown();

while (true) {

Thread.sleep(24 * 3600 * 1000);

}

}

}

// 构建一个大内存对象, 便于观察内存波动.

class BigMemoryObject{

List list = new ArrayList<>();

BigMemoryObject() {

for (int i = 0; i < 10000000; i++) {

list.add(i);

}

}

}

内存泄露

内存泄露主要出现在无法关闭的线程中,例如 web 容器提供的并发线程池,线程都是复用的。

由于 ThreadLocalMap 生命周期和线程生命周期一样长。对于一些被强引用持有的 ThreadLocal,如定义为 static。

如果在使用结束后,没有手动释放 ThreadLocal,由于线程会被重复使用,那么会出现之前的线程对象残留问题。

造成内存泄露,甚至业务逻辑紊乱。

对于没有强引用持有的 ThreadLocal,如方法内变量,是不是就万事大吉了呢? 答案是否定的。

虽然 ThreadLocalMap 会在 get 和 set 等操作里删除 key 为 null 的对象,但是这个方法并不是 100% 会执行到。

看 ThreadLocalMap 源码即可发现,只有调用了 getEntryAfterMiss 后才会执行清除操作。

如果后续线程没满足条件或者都没执行 get set 操作,那么依然存在内存残留问题。

private ThreadLocal.ThreadLocalMap.Entry getEntry(ThreadLocal key) {

int i = key.threadLocalHashCode & (table.length - 1);

ThreadLocal.ThreadLocalMap.Entry e = table[i];

if (e != null && e.get() == key)

return e;

else

// 并不是一定会执行

return getEntryAfterMiss(key, i, e);

}

private ThreadLocal.ThreadLocalMap.Entry getEntryAfterMiss(ThreadLocal key, int i, ThreadLocal.ThreadLocalMap.Entry e) {

ThreadLocal.ThreadLocalMap.Entry[] tab = table;

int len = tab.length;

while (e != null) {

ThreadLocal k = e.get();

if (k == key)

return e;

// 删除key为null的value

if (k == null)

expungeStaleEntry(i);

else

i = nextIndex(i, len);

e = tab[i];

}

return null;

}

最佳实践

不管 threadlocal 是 static 还是非 static 的,都要像加锁解锁一样,每次用完后, 手动清理,释放对象。

无锁

与锁相比,使用 CAS 操作,由于其非阻塞性,因此不存在死锁问题,同时线程之间的相互影响,也远小于锁的方式。使用无锁的方案,可以减少锁竞争以及线程频繁调度带来的系统开销。❝例如生产消费者模型中,可以使用BlockingQueue来作为内存缓冲区,但他是基于锁和阻塞实现的线程同步。如果想要在高并发场合下获取更好的性能,则可以使用基于 CAS 的 ConcurrentLinkedQueue。同理,如果可以使用 CAS 方式实现整个生产消费者模型,那么也将获得可观的性能提升,如 Disruptor 框架。

关于无锁,Java 中与 Atomic 相关的工具,前面已经有不少文章讨论过,后面等我抽出时间再一一细说!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值