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

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

文章目录

Synchronized原理

偏向锁

自旋锁

重量级锁

其他的优化操作

锁消除

锁粗化

Callable接口

Callable的用法

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

**ReentrantLock**

**信号量** **Semaphore**CountDownLatch

线程安全的集合类

多线程环境使用ArrayList

**多线程环境使用哈希表**

Synchronized原理

Synchronized即是轻量级锁又是重量级锁,它会根据实际情况自适应加锁。

偏向锁

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

运行结果:

上述代码需要借助一个辅助类,还需要使用到一系列的加锁和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);
    }
}

运行结果:

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

运行结果:

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

运行结果:

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("比赛结束");
    }
}

运行结果:

线程安全的集合类

多线程环境使用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 对象本身加锁.

如果多线程访问同一个 Hashtable 就会直接造成锁冲突.

size 属性也是通过 synchronized 来控制同步, 也是比较慢的.

一旦触发扩容, 就由该线程完成整个扩容过程. 这个过程会涉及到大量的元素拷贝, 效率会非常低.

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

(2) ConcurrentHashMap

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

读操作没有加锁(但是使用了 volatile 保证从内存读取结果), 只对写操作进行加锁. 加锁的方式仍然是用 synchronized, 但是不是锁整个对象, 而是 “锁桶” (用每个链表的头结点作为锁对象), 大大降低了锁冲突的概率.

充分利用 CAS 特性. 比如 size 属性通过 CAS 来更新. 避免出现重量级锁的情况.

优化了扩容方式: 化整为零 , 发现需要扩容的线程, 只需要创建一个新的数组, 同时只搬几个元素过去.,扩容期间, 新老数组同时存在.,后续每个来操作 ConcurrentHashMap 的线程, 都会参与搬家的过程. 每个操作负责搬运一小部分元素.,搬完最后一个元素再把老数组删掉. 这个期间, 插入只往新数组加,这个期间, 查找需要同时查新数组和老数组。如果是要插入元素,直接在新的数组上添加,如果是删除元素,直接删 了。

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

相关文章
|
1天前
|
安全 算法 Java
Java一分钟:线程同步:synchronized关键字
【5月更文挑战第11天】Java中的`synchronized`关键字用于线程同步,防止竞态条件,确保数据一致性。本文介绍了其工作原理、常见问题及避免策略。同步方法和同步代码块是两种使用形式,需注意避免死锁、过度使用导致的性能影响以及理解锁的可重入性和升级降级机制。示例展示了同步方法和代码块的运用,以及如何避免死锁。正确使用`synchronized`是编写多线程安全代码的核心。
10 2
|
1天前
|
安全 Java 调度
Java一分钟:多线程编程初步:Thread类与Runnable接口
【5月更文挑战第11天】本文介绍了Java中创建线程的两种方式:继承Thread类和实现Runnable接口,并讨论了多线程编程中的常见问题,如资源浪费、线程安全、死锁和优先级问题,提出了解决策略。示例展示了线程通信的生产者-消费者模型,强调理解和掌握线程操作对编写高效并发程序的重要性。
10 3
|
2天前
|
Java
【JAVA基础篇教学】第五篇:Java面向对象编程:类、对象、继承、多态
【JAVA基础篇教学】第五篇:Java面向对象编程:类、对象、继承、多态
|
2天前
|
存储 安全 Java
Java容器类List、ArrayList、Vector及map、HashTable、HashMap
Java容器类List、ArrayList、Vector及map、HashTable、HashMap
|
3天前
|
Java 编译器 开发者
Java一分钟之-继承:复用与扩展类的特性
【5月更文挑战第9天】本文探讨了Java中的继承机制,通过实例展示了如何使用`extends`创建子类继承父类的属性和方法。文章列举了常见问题和易错点,如构造器调用、方法覆盖、访问权限和类型转换,并提供了解决方案。建议深入理解继承原理,谨慎设计类结构,利用抽象类和接口以提高代码复用和扩展性。正确应用继承能构建更清晰、灵活的代码结构,提升面向对象设计能力。
9 0
|
3天前
|
Java
【Java多线程】面试常考 —— JUC(java.util.concurrent) 的常见类
【Java多线程】面试常考 —— JUC(java.util.concurrent) 的常见类
13 0
|
5月前
|
Java
多线程与并发,Java中介绍一下Thread类和Runnable接口的区别。
多线程与并发,Java中介绍一下Thread类和Runnable接口的区别。
33 1
|
5月前
|
Java Unix 程序员
java 8 新特性讲解Optional类--Fork/Join 框架--新时间日期API--以及接口的新特性和注解
java 8 新特性讲解Optional类--Fork/Join 框架--新时间日期API--以及接口的新特性和注解
63 1
|
18天前
|
Java
一文搞清楚Java中的包、类、接口
包、类、接口、方法、变量、参数、代码块,这些都是构成Java程序的核心部分,即便最简单的一段代码里都至少要包含里面的三四个内容,这两天花点时间梳理了一下,理解又深刻了几分。
33 10
|
1月前
|
Java
Java中的多线程实现:使用Thread类与Runnable接口
【4月更文挑战第8天】本文将详细介绍Java中实现多线程的两种方法:使用Thread类和实现Runnable接口。我们将通过实例代码展示如何创建和管理线程,以及如何处理线程同步问题。最后,我们将比较这两种方法的优缺点,以帮助读者在实际开发中选择合适的多线程实现方式。
24 4