概述
本文主要讲并行优化的几种方式, 其结构如下:
锁优化
减少锁的持有时间
例如避免给整个方法加锁
1 public synchronized voidsyncMethod(){2 othercode1();3 mutextMethod();4 othercode2();5 }
改进后
1 public voidsyncMethod2(){2 othercode1();3 synchronized(this){4 mutextMethod();5 }6 othercode2();7 }
减小锁的粒度
将大对象,拆成小对象,大大增加并行度,降低锁竞争. 如此一来偏向锁,轻量级锁成功率提高.
一个简单的例子就是jdk内置的ConcurrentHashMap与SynchronizedMap.
Collections.synchronizedMap
其本质是在读写map操作上都加了锁, 在高并发下性能一般.
ConcurrentHashMap
内部使用分区Segment来表示不同的部分, 每个分区其实就是一个小的hashtable. 各自有自己的锁.
只要多个修改发生在不同的分区, 他们就可以并发的进行. 把一个整体分成了16个Segment, 最高支持16个线程并发修改.
代码中运用了很多volatile声明共享变量, 第一时间获取修改的内容, 性能较好.
读写分离锁替代独占锁
顾名思义, 用ReadWriteLock将读写的锁分离开来, 尤其在读多写少的场合, 可以有效提升系统的并发能力.
读-读不互斥:读读之间不阻塞。
读-写互斥:读阻塞写,写也会阻塞读。
写-写互斥:写写阻塞。
锁分离
在读写锁的思想上做进一步的延伸, 根据不同的功能拆分不同的锁, 进行有效的锁分离.
一个典型的示例便是LinkedBlockingQueue,在它内部, take和put操作本身是隔离的,
有若干个元素的时候, 一个在queue的头部操作, 一个在queue的尾部操作, 因此分别持有一把独立的锁.
1 /**Lock held by take, poll, etc*/
2 private final ReentrantLock takeLock = newReentrantLock();3
4 /**Wait queue for waiting takes*/
5 private final Condition notEmpty =takeLock.newCondition();6
7 /**Lock held by put, offer, etc*/
8 private final ReentrantLock putLock = newReentrantLock();9
10 /**Wait queue for waiting puts*/
11 private final Condition notFull = putLock.newCondition();
锁粗化
通常情况下, 为了保证多线程间的有效并发, 会要求每个线程持有锁的时间尽量短,
即在使用完公共资源后, 应该立即释放锁. 只有这样, 等待在这个锁上的其他线程才能尽早的获得资源执行任务.
而凡事都有一个度, 如果对同一个锁不停的进行请求 同步和释放, 其本身也会消耗系统宝贵的资源, 反而不利于性能的优化
一个极端的例子如下, 在一个循环中不停的请求同一个锁.
1 for(int i = 0; i < 1000; i++){2 synchronized(lock){3
4 }5 }6
7 //优化后
8 synchronized(lock){9 for(int i = 0;i < 1000; i++){10
11 }12 }
锁粗化与减少锁的持有时间, 两者是截然相反的, 需要在实际应用中根据不同的场合权衡使用.
JDK中各种涉及锁优化的并发类可以看之前的博文: 并发包总结
ThreadLocal
除了控制有限资源访问外, 我们还可以增加资源来保证对象线程安全.
对于一些线程不安全的对象, 例如SimpleDateFormat, 与其加锁让100个线程来竞争获取,
不如准备100个SimpleDateFormat, 每个线程各自为营, 很快的完成format工作.
示例
1 public classThreadLocalDemo {2
3 public static ThreadLocal threadLocal = newThreadLocal();4
5 public static voidmain(String[] args){6 ExecutorService service = Executors.newFixedThreadPool(10);7 for (int i = 0; i < 100; i++) {8 service.submit(newRunnable() {9 @Override10 public voidrun() {11 if (threadLocal.get() == null) {12 threadLocal.set(new SimpleDateFormat("yyyy-MM-dd"));13 }14
15 System.out.println(threadLocal.get().format(newDate()));16 }17 });18 }19 }20 }
原理
对于set方法, 先获取当前线程对象, 然后getMap()获取线程的ThreadLocalMap, 并将值放入map中.
该map是线程Thread的内部变量, 其key为threadlocal, vaule为我们set进去的值.
1 public voidset(T value) {2 Thread t =Thread.currentThread();3 ThreadLocalMap map =getMap(t);4 if (map != null)5 map.set(this, value);6 else
7 createMap(t, value);8 }
对于get方法, 自然是先拿到map, 然后从map中获取数据.
1 publicT get() {2 Thread t =Thread.currentThread();3 ThreadLocalMap map =getMap(t);4 if (map != null) {5 ThreadLocalMap.Entry e = map.getEntry(this);6 if (e != null)7 return(T)e.value;8 }9 returnsetInitialValue();10 }
内存释放
手动释放: 调用threadlocal.set(null)或者threadlocal.remove()即可
自动释放: 关闭线程池, 线程结束后, 自动释放threadlocalmap.
1 public classStaticThreadLocalTest {2
3 private static ThreadLocal tt = newThreadLocal();4 public static void main(String[] args) throwsInterruptedException {5 ExecutorService service = Executors.newFixedThreadPool(1);6 for (int i = 0; i < 3; i++) {7 service.submit(newRunnable() {8 @Override9 public voidrun() {10 BigMemoryObject oo = newBigMemoryObject();11 tt.set(oo);12 //做些其他事情13 //释放方式一: 手动置null14 //tt.set(null);15 //释放方式二: 手动remove16 //tt.remove();
17 }18 });19 }
24 //释放方式三: 关闭线程或者线程池25 //直接new Thread().start()的场景, 会在run结束后自动销毁线程26 //service.shutdown();
27
28 while (true) {29 Thread.sleep(24 * 3600 * 1000);30 }31 }32
33 }34 //构建一个大内存对象, 便于观察内存波动.
35 classBigMemoryObject{36
37 List list = new ArrayList<>();38
39 BigMemoryObject() {40 for (int i = 0; i < 10000000; i++) {41 list.add(i);42 }43 }44 }
内存泄露
内存泄露主要出现在无法关闭的线程中, 例如web容器提供的并发线程池, 线程都是复用的.
由于ThreadLocalMap生命周期和线程生命周期一样长. 对于一些被强引用持有的ThreadLocal, 如定义为static.
如果在使用结束后, 没有手动释放ThreadLocal, 由于线程会被重复使用, 那么会出现之前的线程对象残留问题,
造成内存泄露, 甚至业务逻辑紊乱.
对于没有强引用持有的ThreadLocal, 如方法内变量, 是不是就万事大吉了呢? 答案是否定的.
虽然ThreadLocalMap会在get和set等操作里删除key 为 null的对象, 但是这个方法并不是100%会执行到.
看ThreadLocalMap源码即可发现, 只有调用了getEntryAfterMiss后才会执行清除操作,
如果后续线程没满足条件或者都没执行get set操作, 那么依然存在内存残留问题.
1 privateThreadLocal.ThreadLocalMap.Entry getEntry(ThreadLocal key) {2 int i = key.threadLocalHashCode & (table.length - 1);3 ThreadLocal.ThreadLocalMap.Entry e =table[i];4 if (e != null && e.get() ==key)5 returne;6 else
7 //并不是一定会执行
8 returngetEntryAfterMiss(key, i, e);9 }10
11 private ThreadLocal.ThreadLocalMap.Entry getEntryAfterMiss(ThreadLocal key, inti, ThreadLocal.ThreadLocalMap.Entry e) {12 ThreadLocal.ThreadLocalMap.Entry[] tab =table;13 int len =tab.length;14
15 while (e != null) {16 ThreadLocal k =e.get();17 if (k ==key)18 returne;19 //删除key为null的value
20 if (k == null)21 expungeStaleEntry(i);22 else
23 i =nextIndex(i, len);24 e =tab[i];25 }26 return null;27 }
最佳实践
不管threadlocal是static还是非static的, 都要像加锁解锁一样, 每次用完后, 手动清理, 释放对象.
无锁
与锁相比, 使用CAS操作, 由于其非阻塞性, 因此不存在死锁问题, 同时线程之间的相互影响,
也远小于锁的方式. 使用无锁的方案, 可以减少锁竞争以及线程频繁调度带来的系统开销.
例如生产消费者模型中, 可以使用BlockingQueue来作为内存缓冲区, 但他是基于锁和阻塞实现的线程同步.
如果想要在高并发场合下获取更好的性能, 则可以使用基于CAS的ConcurrentLinkedQueue.
同理, 如果可以使用CAS方式实现整个生产消费者模型, 那么也将获得可观的性能提升, 如Disruptor框架.
关于无锁, 这边不再赘述, 之前博文已经有所介绍, 具体见: Java高并发之无锁与Atomic源码分析
最后
以上就是爱笑白开水为你收集整理的java高并发优化_Java高并发之锁优化的全部内容,希望文章能够帮你解决java高并发优化_Java高并发之锁优化所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复