• 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源码分析

  • 相关阅读:
    获取docx文件中表格的内容
    从指定地址获取文件进行正则匹配,输出至指定表格
    遍历ID从数据库获得需要的数据
    python实现将txt文件内容存入mysql数据库中
    Dockerfile 文件学习(二)
    Dockerfile 文件学习(二)
    Docker数据卷
    Docker最详细的命令记载
    docker学习
    Vm 热添加<在不重新启动虚拟机,就可以为虚拟机添加硬盘>
  • 原文地址:https://www.cnblogs.com/xdecode/p/9137804.html
Copyright © 2020-2023  润新知