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

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

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

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

JVM 将 synchronized 锁分为四种情况.

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

锁粗化涉及到锁的粒度

  • 锁的粒度: 加锁代码涉及到的范围.(不是整体多个加锁总和的范围,而是单个锁涉及的范围)

  • 如果两次加锁之间隔的代码比较多,不会影响整体运行状态的情况下,一般不会优化.

  • 如果之间间隔比较小(中间间隔的代码少),间隔过少的时候可能涉及多次释放锁后,又马上申请锁的操作.就很可能触发这个优化(JVM 就会自动把锁粗化)

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

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

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

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;

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;
            }
        };

        FutureTask<Integer> task = new FutureTask<>(callable);

        Thread t = new Thread(task);

        t.start();

        try {

            System.out.println(task.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

2.2.Callable 和 Runnable 比较

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

  • Runnable 描述的是不带返回值的任务.

3.1.ReentrantLock:可重入锁

有三个主要的方法

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

import java.util.concurrent.locks.ReentrantLock;

public class Test2 {
    public static void main(String[] args) {
        ReentrantLock locker = new ReentrantLock(true);

        locker.lock();

        try {

        } finally {
            locker.unlock();
        }
    }
}

3.2.Semaphore:信号量

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

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

类的参数为信号量个数

import java.util.concurrent.Semaphore;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(4);

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

        semaphore.release();

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

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

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

  • countDown()方法.任务调用此方法表示任务结束.此时CountDownLatch内部的计数器减1.

  • await()方法.调用此方法的线程阻塞等待所有任务执行完毕. 就是计数器减为0的时候.

import java.util.concurrent.CountDownLatch;

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 的线程, 都会参与搬家的过程. 每个操作负责搬运一小 部分元素.

  • 搬完最后一个元素再把老数组删掉.

  • 这个期间, 插入只往新数组加.

  • 这个期间, 查找需要同时查新数组和老数组.

Original: https://blog.csdn.net/m0_58154870/article/details/127813542
Author: 魚小飛
Title: synchronized的优化机制和一些多线程的常见类

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/652427/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球