Java高并发之锁优化

本文主要讲并行优化的几种方式, 其结构如下:

锁优化

减少锁的持有时间

例如避免给整个方法加锁

1     public synchronized void syncMethod(){
2         othercode1();
3         mutextMethod();
4         othercode2();
5     }

改进后

1     public void syncMethod2(){
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 = new ReentrantLock();
 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 = new ReentrantLock();
 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 class ThreadLocalDemo {
 2
 3     public static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal();
 4
 5     public static void main(String[] args){
 6         ExecutorService service = Executors.newFixedThreadPool(10);
 7         for (int i = 0; i < 100; i++) {
 8             service.submit(new Runnable() {
 9                 @Override
10                 public void run() {
11                     if (threadLocal.get() == null) {
12                         threadLocal.set(new SimpleDateFormat("yyyy-MM-dd"));
13                     }
14
15                     System.out.println(threadLocal.get().format(new Date()));
16                 }
17             });
18         }
19     }
20 }

原理

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

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

1     public void set(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     public T 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         return setInitialValue();
10     }

内存释放

  • 手动释放: 调用threadlocal.set(null)或者threadlocal.remove()即可
  • 自动释放: 关闭线程池, 线程结束后, 自动释放threadlocalmap.
 1 public class StaticThreadLocalTest {
 2
 3     private static ThreadLocal tt = new ThreadLocal();
 4     public static void main(String[] args) throws InterruptedException {
 5         ExecutorService service = Executors.newFixedThreadPool(1);
 6         for (int i = 0; i < 3; i++) {
 7             service.submit(new Runnable() {
 8                 @Override
 9                 public void run() {
10                     BigMemoryObject oo = new BigMemoryObject();
11                     tt.set(oo);
12                     // 做些其他事情
13                     // 释放方式一: 手动置null
14 //                    tt.set(null);
15                     // 释放方式二: 手动remove
16 //                    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 class BigMemoryObject{
36
37     List<Integer> 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     private ThreadLocal.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             return e;
 6         else
 7             // 并不是一定会执行
 8             return getEntryAfterMiss(key, i, e);
 9     }
10
11     private ThreadLocal.ThreadLocalMap.Entry getEntryAfterMiss(ThreadLocal key, int i, 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                 return e;
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源码分析

原文地址:https://www.cnblogs.com/xdecode/p/9137804.html

时间: 2024-11-05 14:43:19

Java高并发之锁优化的相关文章

java高并发之synchronized

java高并发之synchronized synchronized可以保证代码块或者方法在运行时,同一时刻只有一个方法可以进入临界区域,同时也可以保证共享变量的内存可见性 synchronized可以使用在三种情况: 1. 普通同步方法,锁是当前实例对象 2. 静态同步方法,锁是当前类的class对象 3. 同步方法块,锁是括号里面的对象 package com.tianmaying.crawler.impl; public class SynchronizedTest { public syn

java线程安全和锁优化

面向对象的编程思想是站在现实世界的角度去抽象和解决问题,他把数据和行为都看作是对象的一部分,这样可以让程序员能以符合现实世界的思维方式来编写和组织程序. 线程安全的一个恰当的定义:当多个线程访问一个对象时,如果不用考虑这些线程在运行环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的 . 按照线程安全的安全程度由强至弱来排序,可以将java语言中各种操作共享的数据分为以下5类:不可变.绝对线程安全 相对

Java高并发之无锁与Atomic源码分析

目录 CAS原理 AtomicInteger Unsafe AtomicReference AtomicStampedReference AtomicIntegerArray AtomicIntegerFieldUpdater 无锁的Vector 无锁即无障碍的运行, 所有线程都可以到达临界区, 接近于无等待. 无锁采用CAS(compare and swap)算法来处理线程冲突, 其原理如下 CAS原理 CAS包含3个参数CAS(V,E,N).V表示要更新的变量, E表示预期值, N表示新值.

Java高并发之设计模式.

本文主要讲解几种常见并行模式, 具体目录结构如下图. 单例 单例是最常见的一种设计模式, 一般用于全局对象管理, 比如xml配置读写之类的. 一般分为懒汉式, 饿汉式. 懒汉式: 方法上加synchronized 1 public static synchronized Singleton getInstance() { 2 if (single == null) { 3 single = new Singleton(); 4 } 5 return single; 6 } 这种方式, 由于每次获

Java高并发-无锁

一.无锁类的原理 1.1 CAS CAS算法的过程是这样:它包含3个参数CAS(V,E,N).V表示要更新的变量,E表示预期值,N表示新值.仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做.最后,CAS返回当前V的真实值 .CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作.当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败.失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许

Java高并发之同步异步

1.概念理解: 2.同步的解决方案: 1).基于代码 synchronized 关键字 修饰普通方法:作用于当前实例加锁,进入同步代码前要获得当前实例的锁. 修饰静态方法:作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁. 修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁. code1 package com.thread; import java.util.concurrent.ExecutorService; import java.util.concur

Java高并发之线程池详解

线程池优势 在业务场景中, 如果一个对象创建销毁开销比较大, 那么此时建议池化对象进行管理. 例如线程, jdbc连接等等, 在高并发场景中, 如果可以复用之前销毁的对象, 那么系统效率将大大提升. 另外一个好处是可以设定池化对象的上限, 例如预防创建线程数量过多导致系统崩溃的场景. jdk中的线程池 下文主要从以下几个角度讲解: 创建线程池 提交任务 潜在宕机风险 线程池大小配置 自定义阻塞队列BlockingQueue 回调接口 自定义拒绝策略 自定义ThreadFactory 关闭线程池

高并发之数据库优化

数据库缓存 常见的缓存形式:内存缓存,文件缓存 mysql查询缓存(了解即可) query-cache-type 查询缓存类型,有0.1.2三个取值.0则不使用查询缓存.1表示 始终使用查询缓存.2表示按需使用查询缓存. query_cache_type为1时,亦可关闭查询缓存 SELECT SQL_NO_CACHE * FROM my_table WHERE condition; query-cache-type为2时,可按需使用查询缓存 SELECT SQL_CACHE * FROM my_

高并发之流量优化

工作原理 1.通过Referer或者签名,网站可以检测目标网页访问的来源网页,如果是资源文件,则可以跟踪到显示它的网页地址.一旦检测到来源不是本站即进行阻止或者返回指定的页面. 2.通过计算签名的方式,判断请求是否合法,如果合法则显示,否则返回错误信息. Referer Nginx模块ngx-http-referer module用于当来源非法的域名请求. Nginx指令valid_referers,全局变量$invalid_referer valid_referers none|blocked