当前位置: 首页 > article >正文

[Java] synchronized的锁优化机制

目录

一 . 锁膨胀(锁升级)

二 . 锁消除

三 . 锁粗化

附加 : 

Callable 接口

ReentrantLock 

 ReentrantLock 与 synchronized 的区别

Semaphore (信号量)

CountDownLatch

多线程下使用哈希表

1. HashTable

2 .ConcurrentHashMap

ConcurrentHashMap 优点 

CopyOnWriteArrayList


一 . 锁膨胀(锁升级)

主要经过四个阶段 : 无锁 -> 偏向锁 -> 自旋锁 -> 重量级锁 , 根据锁竞争的激烈程度 , 逐步提升 , synchronized 能够 "自适应" , 自动转变.

好处 : 既保证了效率 , 又保证了线程安全

偏向锁是synchronized 内部的工作 , synchronized 会针对某个对象进行加锁. "偏向锁" 就是给这个对象头里做个标记. 如果有其他的线程也要进行加锁操作, 发现锁已经被标记过了 , "偏向锁" 就会升级成为 (自旋锁)轻量级锁.

自旋锁(轻量级锁) : 拿到锁的速度会很快 , 但是消耗大量的CPU , 自选的时候 , CPU 是快速空转的 , 如果竞争锁的线程较少 ,  短时间内就会拿到锁 , 让CPU空转一会也没事 , 但是如果当前的锁竞争非常激烈 , 自旋锁就会升级为重量级锁.在内核中进行阻塞等待 . 意味着线程要暂时放弃CPU , 由内核进行后续调度.

二 . 锁消除

编译器的一种优化手段, 它会自动检测当前的代码是否有必要加锁 , 如果没有必要 , 但是程序猿写了 , 就会在编译过程中自动把锁去掉. (非必要 , 不加锁)

例如 : StringBuffer 它是线程安全的 , 其中的关键方法都加上了synchronized , 但是如果是单线程使用 , 是不会涉及到线程安全的 , 加锁操作是不会被编译的.

三 . 锁粗化

: 指的是加锁代码涉及到的范围 , 加锁的代码的范围小则认为粒度越细 , 范围越大 , 认为锁的粒度越粗.

那么粒度粗好还是细好 ?

答案是不一定的.

如果粒度比较细 , 那么多线程之间的并发性就更高.

如果粒度比较粗 , 那么加锁解锁的开销就更小.

编译器的优化 : 如果加锁之间间隔比较小(中间隔的代码较少) , 就会触发锁粗化的优化机制.

粒度比较细 , 确实会提高线程之间的并发性 , 但是频繁的加锁解锁这样的开销也是很大的.

举个栗子 : 在做一份数学练习题时 , 遇到一道不会 , 就跑到了老师的办公室 , 问她说 : "老师,A这道题我不会 , 你给我讲讲".  问完之后回到教室, 没过一会 , 又有一道不会 , 这时又跑到老师的办公室 , 问她说 : "老师 , B这道题我不会, 你给我讲讲". ........... 这样来来回回把时间都浪费在路上了. 

经过优化后 , 先把这份题中不会的先圈出来 , 这样我们只用去一趟办公室 , 问一次就Ok了.

附加 : 

Callable 接口

JUC(java.util.concurrent)   - >  Callable 就在这个包下.

Callable  跟Runnable 非常类似 , 都是来描述一个任务.

区别在于 : Callable 会有一个返回值. Runnable 是没有返回值的.

import java.util.concurrent.*;

public class Demo1 {
    public static void main(String[] args) {
        // 通过 callable 来描述一个这样的任务
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int count = 0;
                for (int i = 0; i <= 100; i++) {
                    count += i;
                }
                return count;
            }
        };
        // 为了让线程执行 callable 中的任务, 直接放到Thread()的构造方法中是不行的
        // 还需要一个辅助类. FutureTask<>
        FutureTask<Integer> task = new FutureTask<>(callable);
        Thread t = new Thread(task);
        t.start();
        try {
            // get 获取到的就是上述任务call 方法返回值的结果.
            // get 和 join 类似 , 都是会阻塞等待 call方法执行完毕.
            System.out.println(task.get());
        } catch (InterruptedException e) { // 这个异常是在get阻塞的时候被打断了(任务还是继续正常执行)
            e.printStackTrace();
        } catch (ExecutionException e) { // 这个异常是任务执行过程中出现异常
            e.printStackTrace();
        }

    }
}

ReentrantLock 

ReentrantLock 可重入锁 , synchronized 也是可重入锁.

但 ReentrantLock 是把 加锁和解锁两个操作分开了.

lock() : 加锁 , unlock() : 解锁 .

这样的做法缺点就是很容易遗漏 unlock() , 一旦遗忘就会导致死锁.

更好的办法就是把unlock() 放到 finally 中. 保证不管是否出现任何异常都可以执行到unlock().

import java.util.concurrent.locks.ReentrantLock;

public class Demo2 {
    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();
        int count = 0;
        try {
            reentrantLock.lock();
            for (int i = 1; i <= 50; i++) {
                count += i;
            }
        }finally {
            reentrantLock.unlock();
        }
        System.out.println(count);
    }
}

 ReentrantLock 与 synchronized 的区别

ReentrantLock 与 synchronized 的区别 :

synchronized 关键字, 是对代码块进行加锁.

Reentrantlock 是将加锁解锁操作分开.

1. synchronized 是一个关键字(背后的逻辑是JVM 内部实现的) , ReentrantLock 是一个标准库中的类 (背后的逻辑是Java代码写的)

2. synchronized 不需要手动释放锁 , 出了代码块 , 锁自动释放 , ReentrantLock 必须要手动释放锁.

3. synchronized 如果出现了锁竞争, 就会阻塞等待 , 但是ReentrantLock 除了阻塞等待 , 还可以 trylock , 出现了锁竞争, 竞争失败就会直接返回.

4. synchronized 是一个公平锁 , RenntrantLock 提供了非公平和公平锁两个版本. 可以在构造方法中 , 通过参数来指定当前是公平锁还是非公平锁.(公平 : 遵循先来后到)

true : 表示是公平锁 .  false : 表示是非公平锁. 

5. 基于synchronized 衍生出来的等待机制 是 wait notify 

    基于ReentrantLock 衍生出来的等待机制 , 是 Condition 类 , 功能比起synchronized 更丰富

Semaphore (信号量)

是一个更广义的锁 , 锁是信号量里第一种特殊情况 , 叫做 "二元信号量" 

P 操作 , 申请资源 , 计数器 -1

V 操作 , 释放资源 , 计数器 +1

acquire  申请

release 释放

import java.util.concurrent.Semaphore;

public class Demo3 {
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(4);
        semaphore.acquire(3);
        System.out.println("申请成功");
        semaphore.acquire(1);
        System.out.println("申请成功");
        semaphore.release(4);
        System.out.println("释放成功");
        System.out.println("---");
    }
}

如果计数器已经是0了 , 继续申请资源, 就会阻塞等待!

acquire会产生阻塞等待 , 但是release 不会

import java.util.concurrent.Semaphore;

public class Demo3 {
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(4);
        semaphore.acquire(3);
        System.out.println("申请成功");
        semaphore.acquire(2);
        System.out.println("申请成功");
        System.out.println("---");
    }
}

CountDownLatch

通过 

控制了10个线程 ,

 表示其中一个线程执行完毕.

主线程使用CountDownLatch.await() 方法 , 来阻塞等待所有任务完成.

 10个线程每个线程执行完 , 都调用一个CountDownLatch.countDown方法 (表示当前线程执行完毕)

import java.util.concurrent.CountDownLatch;

public class Demo4 {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(() -> {
                try {
                    Thread.sleep(3000);
                    System.out.println(Thread.currentThread().getName() + "到达终点!");
                    latch.countDown(); // 表示当前这个线程执行完毕

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        // 需要等待10个线程全部都执行完了, await 才执行 , 否则就处于阻塞状态
        latch.await();
    }
}

多线程下使用哈希表

在多线程下 : HashMap 本身是线程不安全的

Java 提供了另外两个类 : 

1. HashTable

2.ConcurrentHashMap

1. HashTable

 

如果元素多了 , 每个链表下的节点就会很多, 就需要进行扩容操作 , 从而增加数组的长度.

这时就要创建一个更大的数组 , 然后把之前的元素都给搬运过去 , 这是一个非常耗时的操作.

2 .ConcurrentHashMap

ConcurrentHashMap 是对每个数组下标的链表头进行加锁操作.

在使用ConcurrentHashMap时, 如果你两个线程操作是针对两个不同的链表上的元素, 这时它们之间是没有锁冲突的 , 就不存在线程安全问题.

由于hash 表中 , 链表的数目非常多 , 每个链表的长度是相对短的 , 因此锁冲突的概率就是非常小的.

ConcurrentHashMap 优点 

1. ConcurrentHashMap 减少了锁冲突 , 就让锁加到每个链表的头结点上 (锁桶)

2. ConncurrentHashMap 只是针对 写操作加锁了 , 读操作没有加锁 , 而只是使用 .

3. ConcurrentHashMap 中更广泛的使用CAS, 进一步提高效率 (比如获取/更新元素个数)

4. ConcurrentHashMap 针对扩容, 不像HashTable 那样 一次直接全部搬运过去 ,

对于ConcurrentHashMap , 每次操作只搬运一点点 , 通过多次操作完成整个搬运的过程.

同时维护了旧的HashMap 和 新的 HashMap .这时如果要进行get 查找操作时 , 既需要查找新的, 也要查找旧的 , 如果要进行put 操作 , 就插入要新的HashMap中. 直到搬运完全部再销毁旧的.

CopyOnWriteArrayList

CopyOnWriteArrayList 是一个支持"写时拷贝" 的集合类.

如果是多个线程读 , 由于读本身就是线程安全 , 它是安全的.

如果此时有一个线程在尝试修改, 就会触发  写时拷贝 操作.

 

虽然不加锁 , 但是如果元素很多 , 拷贝的开销也就很大. 适用于 元素个数小 , 读多写少的情景.


http://www.kler.cn/a/5558.html

相关文章:

  • uniapp 文本转语音
  • CS 144 check7: putting it all together
  • linux---awk命令详细教程
  • 118.【C语言】数据结构之排序(堆排序和冒泡排序)
  • 【C语言】库函数常见的陷阱与缺陷(二):字符串转化函数[3]--strtol
  • Docker数据库的主从复制
  • Java 8 - Lambda 表达式
  • 【gitlab部署】centos8安装gitlab(搭建属于自己的代码服务器)
  • 小驰私房菜_09_应用集成Raw域算法流程
  • 我的天哪, 什么是SOA架构
  • 数据库+加密算法参考材料-2023.3.29
  • 【Linux】进程控制(创建、终止、等待)
  • Android FrameWork——SystemServer
  • 提高java反射效率
  • 网络编程socket(下)
  • 非科班应届生培训Java能就业吗?
  • GPT-4老板:AI可能会杀死人类,已经出现我们无法解释的推理能力
  • 正则表达式
  • 如何同时处理多个聊天
  • 第03章 用户与权限管理
  • 用python如何实现智能合约?
  • ​【java】蓝桥杯——小蓝与钥匙_全错排列​
  • 我用Python写的一个操控玩具车的程序
  • 微服务_微服务的架构演进之路
  • STL容器之<multiset>
  • 【计算机视觉 | 目标检测】Object query的理解