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++){ }
}
锁粗化与减少锁的持有时间, 两者是截然相反的, 需要在实际应用中根据不同的场合权衡使用.
JDK中各种涉及锁优化的并发类可以看之前的博文: 并发包总结
ThreadLocal
除了控制有限资源访问外, 我们还可以增加资源来保证对象线程安全.
对于一些线程不安全的对象, 例如SimpleDateFormat, 与其加锁让100个线程来竞争获取,
不如准备100个SimpleDateFormat, 每个线程各自为营, 很快的完成format工作.
示例
public class ThreadLocalDemo { public static ThreadLocal<SimpleDateFormat> 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<Integer> 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源码分析
Java高并发之锁优化的更多相关文章
- java高并发之锁的使用以及原理浅析
锁像synchronized同步块一样,是一种线程同步机制.让自Java 5开始,java.util.concurrent.locks包提供了另一种方式实现线程同步机制——Lock.那么问题来了既然都 ...
- java高并发之线程池
Java高并发之线程池详解 线程池优势 在业务场景中, 如果一个对象创建销毁开销比较大, 那么此时建议池化对象进行管理. 例如线程, jdbc连接等等, 在高并发场景中, 如果可以复用之前销毁的对 ...
- Java线程安全与锁优化,锁消除,锁粗化,锁升级
线程安全的定义 来自<Java高并发实战>"当多个线程访问一个对象的时候,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方法的时候进行任何 ...
- Java线程安全与锁优化
线程安全的严谨定义: 当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交题执行,也不需要进行额外的同步,或者调用方法进行其他任何操作,调用这个对象的行为都可以或者正确的结果,那么这 ...
- Java 多线程编程(锁优化)
转:https://mp.weixin.qq.com/s/lDuguEhuWiLY8ofBRy3tZA 并发环境下进行编程时,需要使用锁机制来同步多线程间的操作,保证共享资源的互斥访问. 加锁会带来性 ...
- Java高并发之无锁与Atomic源码分析
目录 CAS原理 AtomicInteger Unsafe AtomicReference AtomicStampedReference AtomicIntegerArray AtomicIntege ...
- Java高并发之设计模式
本文主要讲解几种常见并行模式, 具体目录结构如下图. 单例 单例是最常见的一种设计模式, 一般用于全局对象管理, 比如xml配置读写之类的. 一般分为懒汉式, 饿汉式. 懒汉式: 方法上加synchr ...
- 1.6 JAVA高并发之线程池
一.JAVA高级并发 1.5JDK之后引入高级并发特性,大多数的特性在java.util.concurrent 包中,是专门用于多线程发编程的,充分利用了现代多处理器和多核心系统的功能以编写大规模并发 ...
- Java高并发之从零到放弃
前言 本篇主要讲解如何去优化锁机制或者克服多线程因为锁可导致性能下降的问题 ThreadLocal线程变量 有这样一个场景,前面是一大桶水,10个人去喝水,为了保证线程安全,我们要在杯子上加锁导致大家 ...
随机推荐
- 百度地图api描绘车辆历史轨迹图
最近公司在做项目需需求:车辆定位后在地图显示历史轨迹的功能 一开始使用了google的地图api,但是发现会一直关闭,索性支持下国产,使用了百度地图api search方法把两个点连接成线后,会出现起 ...
- [美国代购] Nexus 6 与 Moto X 询价聊天记录整理
目前手上使用的是 Mi 3,使用了根本还不到一年,但是发现非常多的问题. 官方 APP 不能卸载: 手机的顶部(摄像头)处经常出现高温度现象,如果你长时间讲电话,那么这个温度真的可以烫到你的耳朵无法承 ...
- opengl教程推荐
非常不错的教程! OpenGL教程
- LeeDUT个人WEB作品
*****目前大三前端狗一只,听说博客里写点记点能求OFFER***** 1.微云盘upan.oureda.cn 2013.10 微云盘是基于分布式系统.提供文件分享的校园存储站点,上传文件之后随即 ...
- Volo.Abp.EntityFrameworkCore.MySQL 使用
创建新项目 打开 https://cn.abp.io/Templates ,任意选择一个项目类型,然后创建项目,我这里创建了一个Web Api 解压项目,还原Nuget,项目目录如下: 首先我们来查看 ...
- yum 下载RPM包而不进行安装
yum命令本身就可以用来下载一个RPM包,标准的yum命令提供了--downloadonly(只下载)的选项来达到这个目的. $ sudo yum install --downloadonly < ...
- Application Loader上传app,一直卡在“正在通过 App Store 进行鉴定”
1.问题现象描述 上传iOS应用,卡在 Authenticating with the iTunes Store 2.解决办法 2.1 打开终端输入代码即可 cd ~ mv .itmstranspor ...
- Aircrack-ng无线破解总结
过年回来家,奈何没网,实属无奈,只好看破解教程,看能否破出来.于是总结如下 测试环境在linux平台下,我用的是ubuntu环境.ubuntu安装可以直接用sudo apt-get install a ...
- “全栈2019”Java多线程第七章:等待线程死亡join()方法详解
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...
- kinect 2(ubuntu16.04)
安装libfreenect2 参考 https://github.com/OpenKinect/libfreenect2/blob/master/README.md#linux 如果安装后找不到有关库 ...