synchronized的优化机制和一些多线程的常见类

  • Post author:
  • Post category:其他




1.synchronized



1.1.我们现在知道常用的锁策略

  1. 乐观锁 vs 悲观锁
  2. 读写锁 vs 普通的互斥锁
  3. 重量级锁 vs 轻量级锁
  4. 挂起等待锁 vs 自旋锁
  5. 公平锁 vs 非公平锁
  6. 可重入锁 vs 不可重入锁


那synchronized基于这些策略有哪些特性呢?

  1. synchronized 开始时是乐观锁,如果锁冲突频繁,就转换为悲观锁.
  2. synchronized 不是读写锁,是普通互斥锁
  3. synchronized 开始是轻量级锁实现,如果锁被持有的时间较长,就转换成重量级锁.
  4. synchronized 实现轻量级锁的时候大概率用到的自旋锁策略,重量级锁的部分基于挂起等待实现
  5. synchronized 是一种非公平锁
  6. synchronized 是一种可重入锁



1.2.synchronized 的一些锁优化机制(jdk 1.8)



1.2.1.锁膨胀

JVM 将 synchronized 锁分为四种情况.

  1. 无锁
  2. 偏向锁
  3. 轻量级锁
  4. 重量级锁

四种锁状态也会根据实际情况依次进行升级.

  1. 没有加锁的状态
  2. 首个线程加锁,就会进入偏向锁状态(只是给对象头中做一个 “偏向锁的标记”, 记录这个锁属于哪个线程,并没有真的加锁)
  3. 如果偏向锁后续有其他线程来竞争该锁,那就取消原来的偏向锁状态, 进入一般的轻量级锁状态.(自适应的自旋锁)
  4. 如果竞争进一步激烈,自旋不能快速获取到锁状态,就会膨胀为重量级锁(用到内核提供的mutex.)



1.2.2.锁粗化

锁粗化涉及到锁的粒度

  • 锁的粒度: 加锁代码涉及到的范围.(不是整体多个加锁总和的范围,而是单个锁涉及的范围)
  1. 加锁代码的范围越大,认为锁的粒度越粗.
  2. 加锁代码的范围越小,则认为粒度越细
  • 如果两次加锁之间隔的代码比较多,不会影响整体运行状态的情况下,一般不会优化.
  • 如果之间间隔比较小(中间间隔的代码少),间隔过少的时候可能涉及多次释放锁后,又马上申请锁的操作.就很可能触发这个优化(JVM 就会自动把锁粗化)



1.2.3.锁消除

有这么一种情况: 有些代码,明明不用加锁,结果你给加上锁了.编译器就会发现这个加锁好像没啥必要,就直接把锁给去掉了(锁消除).

当然你可能说,我不会乱加锁的.不过,有的时候加锁操作并不是那么明显,稍不留神就会做出了这种错误的决定.

例如: StringBuffer, Vector…这些类,在标准库中进行了加锁操作,而我们在单个线程中用到这些类的时候,就是单线程进行了加锁解锁.而我们并不会发觉,不过我们也不用担心.因为编译器会发现并处理这一类情况,也就是上述的锁消除.



2.Callable接口



2.1.Callable接口的使用


Callable

通常需要搭配

FutureTask

来使用.

FutureTask

用来保存

Callable

的返回结果. 因为

Callable

往往是在另一个线程中执行的, 啥时候执行完并不确定.所以

FutureTask

就可以负责这个

等待结果出来

的工作.

  • 创建一个匿名内部类, 实现 Callable 接口. Callable 带有泛型参数. 泛型参数表示返回值的类型.
  • 重写 Callable 的 call 方法, 完成一个任务. 直接通过返回值返回计算结果.
  • 把 callable 实例使用 FutureTask 包装一下.
  • 创建线程, 线程的构造方法传入 FutureTask .此时新线程就会执行 FutureTask 内部的 Callable 的 call 方法, 完成计算. 计算结果就放到了 FutureTask 对象中.
  • 在主线程中调用 task.get() 能够阻塞等待新线程计算完毕. 并获取到 FutureTask 中的结果.
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * Callable 接口
 * java.util.concurrent
 * <p>
 * Callable 解决了 Runnable 不方便返回结果的问题
 */
public class Test {
    public static void main(String[] args) {
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 0; i <= 1000; i++) {
                    sum += i;
                }
                return sum;
            }
        };
        //创建线程,完成任务
        //为了让线程执行 callable 中的任务,光使用构造方法还不够,还需要一个辅助的类
        //FutureTask 运行结果会保存在这个类中
        FutureTask<Integer> task = new FutureTask<>(callable);//任务

        Thread t = new Thread(task);

        t.start();

        try {
            //如果线程的任务没有执行完,get就会阻塞,一直阻塞到,有return 的结果
            System.out.println(task.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}



2.2.Callable 和 Runnable 比较

  • 都是描述一个任务
  • Callable 描述的是带有返回值的任务.
  • Runnable 描述的是不带返回值的任务.



3.JUC(java.util.concurrent) 中的常见类



3.1.ReentrantLock:可重入锁

有三个主要的方法

  1. lock()方法 : 加锁,如果获取不到锁就死等.
  2. unlock()方法 : 解锁
  3. trylock(超时时间)方法 : 也是加锁,不同于lock()方法的是这个方法懂得放弃加锁失败,不会死等,而是等待一定的时间之后就放弃加锁

相对于习惯使用 synchronized 的我来说,这里加锁和解锁 分开的做法,是不太友好的,因为很容易遗漏 unlock(),出现锁死.



3.1.1.ReentrantLock与synchronized的区别

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

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

  3. synchronized 如果竞争锁的时候失败,就会阻塞等待.但是ReentrantLock除了阻塞等待这一手之外,还有一手, trylock()超时失败了会返回.

  4. synchronized是一个非公平锁.ReentrantLock 提供了非公平和公平锁两个版本!在构造方法中,通过参数就可以来指定当前是公平锁还是非公平锁.

    • ReentrantLock locker = new ReentrantLock(true);
    • 参数为true 时该类为公平锁
    • 参数默认是false 默认该类为非公平锁



3.1.1.ReentrantLock类使用

import java.util.concurrent.locks.ReentrantLock;

/**
 * ReentrantLock : 可重入锁   
 */
public class Test2 {
    public static void main(String[] args) {
        ReentrantLock locker = new ReentrantLock(true);
        //参数为true 为公平锁  默认为false 非公平锁

        locker.lock();//加锁

        try {

        } finally {
            locker.unlock();//解锁
        }
    }
}



3.2.Semaphore:信号量

锁就相当于一个二元信号量,可用资源只有一个.计数器非0,即1

Semaphore 的 PV 操作中的加减计数器操作都是原子的, 可以在多线程环境下直接使用.

类的参数为信号量个数



3.2.1.PV操作

  1. acquire()方法 P操作(申请资源):无资源的时候获取会阻塞等待资源释放
  2. release()方法 V操作(释放资源)



3.2.2.Semaphore类使用

import java.util.concurrent.Semaphore;

/**
 * Semaphore 信号量
 * 锁:二元信号量, 可用资源就一个,计数器 非0,即1
 */
public class Test {
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(4);
        //参数为4 代表可用资源有 4 个

        //申请资源 P操作
        semaphore.acquire();//1
        System.out.println("申请成功");
        semaphore.acquire();//2
        System.out.println("申请成功");
        semaphore.acquire();//3
        System.out.println("申请成功");
        semaphore.acquire();//4
        System.out.println("申请成功");
//        semaphore.acquire();//5 阻塞等待
//        System.out.println("申请成功");

        semaphore.release();//释放资源 V操作

        semaphore.acquire();
        System.out.println("申请成功");
    }
}



3.3.CountDownLatch 同时等待N个任务执行结束.

类的参数.表示需要等待任务的个数.



3.3.1.方法使用

  • countDown()方法.任务调用此方法表示任务结束.此时CountDownLatch内部的计数器减1.
  • await()方法.调用此方法的线程阻塞等待所有任务执行完毕. 就是计数器减为0的时候.



3.3.2.CountDownLatch类使用

import java.util.concurrent.CountDownLatch;

/**
 * CountDownLatch 同时等待 N 个任务执行结束
 */
public class Test {
    public static void main(String[] args) throws InterruptedException {
        //构造方法的参数表示 有多少个选手参赛
        CountDownLatch countDownLatch = new CountDownLatch(10);

        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(() -> {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "到达终点");
                countDownLatch.countDown();
            });
            t.start();
        }

        countDownLatch.await();//等待所有线程到达
        System.out.println("比赛结束");
    }
}



3.4.CopyOnWriteArrayList 写时拷贝

当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy.复制出一个新的容器后往新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器.

这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为只读的时候不会有线程安全问题.



3.5.ConcurrentHashMap 多线程环境下可以使用的哈希表

ConcurrentHashMap 相比于 Hashtable 又做出了一系列的改进和优化

  • 读操作没有加锁(但是使用了 volatile 保证从内存读取结果),只对写操作进行加锁.加锁的方式仍然是用 synchronized,但不是对整个对象加锁, 而是

    “锁桶”

    (用每个链表的头结点作为锁对象,让锁加到每个链表的头结点上), 大大降低了锁冲突的概率.
  • 充分利用 CAS 特性. 比如 size 属性通过 CAS 来更新. 避免出现重量级锁的情况.
  • 优化了扩容方式: 化整为零

    • 发现需要扩容的线程, 只需要创建一个新的数组, 同时只搬几个元素过去.
    • 扩容期间, 新老数组同时存在.
    • 后续每个来操作 ConcurrentHashMap 的线程, 都会参与搬家的过程. 每个操作负责搬运一小 部分元素.
    • 搬完最后一个元素再把老数组删掉.
    • 这个期间, 插入只往新数组加.
    • 这个期间, 查找需要同时查新数组和老数组.



版权声明:本文为m0_58154870原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。