[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 是一个支持"写时拷贝" 的集合类.

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

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

 

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
优化是指在多线程编程中,通过改进机制和使用方式来提高程序的性能和并发能力。synchronized关键字是Java中最常用的机制之一,它可以保证同一时间只有一个线程可以进入被synchronized修饰的代码块。下面是一些synchronized优化的方法: 1. 减小的粒度:如果在一个方法中有多个synchronized代码块,可以考虑将这些代码块拆分成多个方法,以减小的粒度。这样可以使得多个线程可以并发执行不同的代码块,提高程序的并发性能。 2. 使用局部变量替代成员变量:在使用synchronized关键字时,尽量使用局部变量而不是成员变量。因为成员变量的访问需要通过对象实例来进行,而局部变量的访问是线程私有的,不需要加。 3. 使用同步代码块代替同步方法:在某些情况下,使用同步代码块比使用同步方法更加灵活。同步代码块可以指定的粒度,只对需要同步的代码进行加,而不是整个方法。 4. 使用volatile关键字:volatile关键字可以保证变量的可见性和禁止指令重排序,可以在一定程度上替代synchronized关键字。但是需要注意,volatile关键字只能保证单个变量的原子性,不能保证多个操作的原子性。 5. 使用Lock接口:Java提供了Lock接口及其实现类ReentrantLock,相比于synchronized关键字,Lock接口提供了更加灵活的机制。可以手动控制的获取和释放,可以实现公平和非公平,并且支持多个条件变量。 6. 使用读写:如果在多线程环境下,读操作远远多于写操作,可以考虑使用读写ReadWriteLock来提高程序的并发性能。读写允许多个线程同时读取共享数据,但只允许一个线程写入共享数据。 7. 使用并发集合类:Java提供了一些并发集合类,ConcurrentHashMap、ConcurrentLinkedQueue等,它们内部使用了一些优化的技术,可以提高多线程环境下的并发性能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值