《Java-SE-第二十九章》之Synchronized原理与JUC常用类

前言

在你立足处深挖下去,就会有泉水涌出!别管蒙昧者们叫嚷:“下边永远是地狱!”

博客主页:KC老衲爱尼姑的博客主页

博主的github,平常所写代码皆在于此

共勉:talk is cheap, show me the code

作者是爪哇岛的新手,水平很有限,如果发现错误,一定要及时告知作者哦!感谢感谢!


文章目录

  • Synchronized原理
    • 偏向锁
    • 自旋锁
    • 重量级锁
    • 其他的优化操作
      • 锁消除
      • 锁粗化
  • Callable接口
    • Callable的用法
  • **JUC(java.util.concurrent)** **的常见类**
    • **ReentrantLock**
    • **信号量** **Semaphore**
    • CountDownLatch
  • 线程安全的集合类
    • 多线程环境使用ArrayList
    • **多线程环境使用哈希表**

Synchronized原理

Synchronized即是轻量级锁又是重量级锁,它会根据实际情况自适应加锁。
《Java-SE-第二十九章》之Synchronized原理与JUC常用类_第1张图片

偏向锁

(1)第一次加锁的时候线程,会进入偏向锁 的状态,偏向锁并不是真的加锁,只是给对象头做了一个偏向锁的标记,记录该锁属于哪个线程,如果后续没有其他的线程加锁,就可以不进行加锁操作。如果后续有其他的线程来竞争该锁,那么刚才的锁对象已经记录了当前时锁属于那个线程,很容易知道当前的线程是不是之前记录的线程,那么就取消偏向锁的状态,进入一般的轻量级锁状态,偏向锁是本质是延迟加锁,能不加锁就不加锁,尽量来避免不必要的加锁开销。但是该做的标志还的做,否则无法区分什么时候需要真正加锁。

自旋锁

(2)当其他的线程进入竞争的时候,偏向锁状态消除会进行轻量级锁,也就是自旋锁。

此处的轻量级锁是通过的CAS实现的,具体操作如下

  1. 通过 CAS 检查并更新一块内存 (比如 null => 该线程引用)
  2. 如果更新成功, 则认为加锁成功
  3. 如果更新失败, 则认为锁被占用, 继续自旋式的等待(并不放弃 CPU).

自旋操作是一直让 CPU 空转, 比较浪费 CPU 资源.。因此此处的自旋不会一直持续进行, 而是达到一定的时间/重试次数, 就不再自旋了. 也就是所谓的 “自适应”。

重量级锁

(3)重量级锁

如果竞争进一步激烈, 自旋不能快速获取到锁状态, 就会膨胀为重量级锁

此处的重量级锁就是指用到内核提供的 mutex .,具体操作如下

  1. 执行加锁操作, 先进入内核态.
  2. 在内核态判定当前锁是否已经被占用
  3. 如果该锁没有占用, 则加锁成功, 并切换回用户态.
  4. 如果该锁被占用, 则加锁失败. 此时线程进入锁的等待队列, 挂起. 等待被操作系统唤醒.
  5. 经历了一系列的沧海桑田, 这个锁被其他线程释放了, 操作系统也想起了这个挂起的线程, 于是唤醒这个线程, 尝试重新获取锁.

其他的优化操作

锁消除

JVM自动判定,发现这个地方的代码,不必加锁,如果你写了Synchronized,就会自动的把锁去掉。比如,只有一个线程,或者多个线程不涉及修改同一个变量,如果代码中写Synchronized,此时Synchronized加锁操作,就会被JVM给干掉。Synchronized加锁是先偏向锁的,只是改 了个标记位,按理说这个操作开销也不大?即是如此,能消除的时候,也不是连这一点开销都不想承担。锁消除也是一种编译器优化的行为,编译器的判定,不一定非常准,因此,如果代码的锁百分之100能消除,就给你消除了。如果这个代码的锁,判断的准,就还是不消除了,锁消除只是在编译器/JVM有十足的把握的时候才进行。

示例代码

StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
sb.append("d");

此时每个 append 的调用都会涉及加锁和解锁. 但如果只是在单线程中执行这个代码, 那么这些加锁解锁操作是没有必要的, 白白浪费了一些资源开销。

锁粗化

锁的粒度,Synchronized对应的代码块中包含多少代码,包含的代码少,粒度细,包含的代码多,粒度粗,锁粗化,就是把细粒度的加锁->粗粒度的加锁。粗的前提是保证代码的逻辑不变,细化的时候代码是正确的,粗化之后还是正确的。

举个栗子理解锁粗化,张三给下交代任务,方式一:张三给下属打电话,交代任务1,挂断电话,再打电话,交代任务2,挂断电话,再打电话,交代任务三,方式二:张三大电话,一次性交代了三个任务,再挂断电话。这就是一个锁细化–>锁粗化的过程。

Callable接口

由于Runnable不提供返回值,而时候需要得到返回值,此时就可以使用Callable。

Callable的用法

Callable 是一个 interface ,描述了一个带返回值的任务,相当于把线程封装了一个 “返回值”. 方便程序猿借助多线程的方式计算结果.

代码示例

创建线程计算 1 + 2 + 3 + … + 1000, 不使用 Callable 版本

public class Demo {
    static class Result{
        public int sum  =0;
        private Object lock = new Object();
    }

    public static void main(String[] args) throws InterruptedException {
        Result result = new Result();
        Thread t = new Thread(() -> {
            int sum = 0;
            for (int i = 0; i <=1000;i++) {
                sum+=i;
            }
            synchronized (result.lock) {
                result.sum=sum;
                result.lock.notify();
            }
        });
        t.start();
        synchronized (result.lock) {
            while (result.sum==0) {
                result.lock.wait();
            }
            System.out.println(result.sum);
        }

    }
}

运行结果:
《Java-SE-第二十九章》之Synchronized原理与JUC常用类_第2张图片

上述代码需要借助一个辅助类,还需要使用到一系列的加锁和wait/notify,相对而言代码是比较复杂的。

代码示例:创建线程计算 1 + 2 + 3 + … + 1000, 使用 Callable 版本

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class CallableDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> callable = new Callable<Integer>(){
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i=1;i<=1000;i++) {
                    sum+=i;
                }
                return sum;
            }
        };
        FutureTask<Integer> futureTask = new FutureTask<Integer>(callable);
        Thread t = new Thread(futureTask);
        t.start();
        int result = futureTask.get();
        System.out.println(result);
    }
}

运行结果:

《Java-SE-第二十九章》之Synchronized原理与JUC常用类_第3张图片

Callable 通常需要搭配 FutureTask 来使用,FutureTask 用来保存 Callable 的返回结果. 因为allable 往往是在另一个线程中执行的, 啥时候执行完并不确定,FutureTask 就可以负责这个等待结果出来的工作。

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

ReentrantLock

ReentrantLock是可重入锁和synchronized类似都是实现互斥效果,保证线程安全。

ReentrantLock 的基础使用

  • lock(): 加锁, 如果获取不到锁就死等.
  • trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁.
  • unlock(): 解锁
ReentrantLock lock = new ReentrantLock(); 
-----------------------------------------
lock.lock();   
try {    
 // working    
} finally {    
 lock.unlock()    
}  

示例代码


import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo3 {
    private static Lock lock = new ReentrantLock();
    private static Condition waitCigaretteQueue = lock.newCondition();
    private static Condition waitbreakfastQueue = lock.newCondition();
    private static volatile boolean hasCigrette = false;
    private static volatile boolean hasBreakFast = false;
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            lock.lock();
            try {
                while (!hasCigrette) {
                    try {
                        waitCigaretteQueue.await();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println("等到了它的烟");
                }

            }finally {
                lock.unlock();
            }
        }).start();

        new Thread(() -> {
            lock.lock();
            try {
                while (!hasBreakFast) {
                    try {
                        waitbreakfastQueue.await();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println("等到了它的早餐");
                }

            }finally {
                lock.unlock();
            }
        }).start();
        Thread.sleep(1000);
        sendBreakFast();
        Thread.sleep(1000);
        sendCigarette();
    }

    private static void sendCigarette() {
        lock.lock();
        try {
            System.out.println("送烟来了");
            hasCigrette = true;
            waitCigaretteQueue.signal();
        }finally {
            lock.unlock();
        }
    }

    private static void sendBreakFast() {
        lock.lock();
        try {
            System.out.println("送早餐来了");
            hasBreakFast = true;
            waitbreakfastQueue.signal();
        }finally {
            lock.unlock();
        }
        
    }
}

运行结果:
《Java-SE-第二十九章》之Synchronized原理与JUC常用类_第4张图片

ReentrantLock 和 synchronized 的区别:

  1. synchronized 是一个关键字, 是 JVM 内部实现的(大概率是基于 C++ 实现). ReentrantLock 是标准库的一个类, 在 JVM 外实现的(基于 Java 实现).

  2. synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活, 但是也容易遗漏 unlock.

  3. synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃.

  4. synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个 true 开启公平锁模式.

  5. 更强大的唤醒机制. synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一个随机等待的线程. ReentrantLock搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程.

如何选择使用哪个锁?

  • 锁竞争不激烈的时候, 使用 synchronized, 效率更高, 自动释放更方便.
  • 锁竞争激烈的时候, 使用 ReentrantLock, 搭配 trylock 更灵活控制加锁的行为, 而不是死等.
  • 如果需要使用公平锁, 使用 ReentrantLock.

信号量 Semaphore

信号量, 用来表示 “可用资源的个数”. 本质上就是一个计数器。举个栗子,可以把信号量想象成是停车场的展示牌: 当前有车位 100 个. 表示有 100 个可用资源。当有车开进去的时候, 就相当于申请一个可用资源, 可用车位就 -1 (这个称为信号量的 P 操作),当有车开出来的时候, 就相当于释放一个可用资源, 可用车位就 +1 (这个称为信号量的 V 操作),如果计数器的值已经为 0 了, 还尝试申请资源, 就会阻塞等待, 直到有其他线程释放资源.。

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

代码示例


import java.util.concurrent.Semaphore;

public class SemaphoreDemo {
    //可用资源设置为1
    private static Semaphore semaphore = new Semaphore(1);

    public static void main(String[] args) {
        Runnable runnable = () -> {
            try {
                System.out.println("申请资源");
                semaphore.acquire();
                System.out.println("我获取到资源");
                Thread.sleep(1000);
                System.out.println("我释放资源了");
                semaphore.release();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        };
        for (int i = 0; i <2;i++) {
            Thread t = new Thread(runnable);
            t.start();
        }
    }
}

运行结果:
《Java-SE-第二十九章》之Synchronized原理与JUC常用类_第5张图片

CountDownLatch

同时等待 N 个任务执行结束。举个栗子,号称地表最强的下载器IDM,下载文件的时候,会将一个文件分配给多个线程下载,只有当所有的线程下载好了,才是整个文件下载好。

代码示例

假设有十名运动员参加跑步比赛,当所有的运功员通过终点的时候,比赛才结束。


import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo {

    public static void main(String[] args) throws InterruptedException {
        //构造 CountDownLatch 实例, 初始化 10 表示有 10 个任务需要完成
        CountDownLatch latch = new CountDownLatch(10);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"已经到了");
                latch.countDown();
            }
        };
        for (int i = 0; i <10;i++) {
            new Thread(runnable).start();
        }
        latch.await();
        System.out.println("比赛结束");
    }
}

运行结果:
《Java-SE-第二十九章》之Synchronized原理与JUC常用类_第6张图片

线程安全的集合类

多线程环境使用ArrayList

(1)自己使用同步机制 (synchronized 或者 ReentrantLock)

(2)Collections.synchronizedList(new ArrayList);

synchronizedList 是标准库提供的一个基于 synchronized 进行线程同步的 List,synchronizedList 的关键操作上都带有 synchronized。

(3)使用 CopyOnWriteArrayList

CopyOnWrite容器即写时复制的容器。所谓的写时拷贝,就是当我们往一个容器中添加元素的时候,不直接往当前容器添加,而是先将当前的容器进行copy复制出一个新的容器,然后新的容器里添加元素。添加完元素之后,再将 原来容器的引用指向新容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

多线程环境使用哈希表

HashMap 本身不是线程安全的,在多线程环境下使用哈希表可以使用:Hashtable和ConcurrentHashMap

(1)Hashtable

Hashtable只是简单的在一些关键的方法如get/put上加了synchronized。
在这里插入图片描述

在这里插入图片描述

这相当于直接针对 Hashtable 对象本身加锁.

  • 如果多线程访问同一个 Hashtable 就会直接造成锁冲突.
  • size 属性也是通过 synchronized 来控制同步, 也是比较慢的.
  • 一旦触发扩容, 就由该线程完成整个扩容过程. 这个过程会涉及到大量的元素拷贝, 效率会非常低.

《Java-SE-第二十九章》之Synchronized原理与JUC常用类_第7张图片

一个Hashtable只有一把锁,两个线程访问的Hashtable中的任意数据都会出现锁竞争。

(2) ConcurrentHashMap

相比于 Hashtable 做出了一系列的改进和优化. 以 Java1.8 为例

  • 读操作没有加锁(但是使用了 volatile 保证从内存读取结果), 只对写操作进行加锁. 加锁的方式仍然是用 synchronized, 但是不是锁整个对象, 而是 “锁桶” (用每个链表的头结点作为锁对象), 大大降低了锁冲突的概率.
  • 充分利用 CAS 特性. 比如 size 属性通过 CAS 来更新. 避免出现重量级锁的情况.
  • 优化了扩容方式: 化整为零 , 发现需要扩容的线程, 只需要创建一个新的数组, 同时只搬几个元素过去.,扩容期间, 新老数组同时存在.,后续每个来操作 ConcurrentHashMap 的线程, 都会参与搬家的过程. 每个操作负责搬运一小部分元素.,搬完最后一个元素再把老数组删掉. 这个期间, 插入只往新数组加,这个期间, 查找需要同时查新数组和老数组。如果是要插入元素,直接在新的数组上添加,如果是删除元素,直接删 了。

《Java-SE-第二十九章》之Synchronized原理与JUC常用类_第8张图片


各位看官如果觉得文章写得不错,点赞评论关注走一波!谢谢啦!。

《Java-SE-第二十九章》之Synchronized原理与JUC常用类_第9张图片

你可能感兴趣的:(java,java,开发语言)