《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 的线程, 都会参与搬家的过程. 每个操作负责搬运一小部分元素.,搬完最后一个元素再把老数组删掉. 这个期间, 插入只往新数组加,这个期间, 查找需要同时查新数组和老数组。如果是要插入元素,直接在新的数组上添加,如果是删除元素,直接删 了。

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

相关文章
|
2天前
|
Java
java的类详解
在 Java 中,类是面向对象编程的核心概念,用于定义具有相似特性和行为的对象模板。以下是类的关键特性:唯一且遵循命名规则的类名;描述对象状态的私有属性;描述对象行为的方法,包括实例方法和静态方法;用于初始化对象的构造方法;通过封装保护内部属性;通过继承扩展其他类的功能;以及通过多态增强代码灵活性。下面是一个简单的 `Person` 类示例,展示了属性、构造方法、getter 和 setter 方法及行为方法的使用。
|
2天前
|
缓存 Java 编译器
JAVA并发编程synchronized全能王的原理
本文详细介绍了Java并发编程中的三大特性:原子性、可见性和有序性,并探讨了多线程环境下可能出现的安全问题。文章通过示例解释了指令重排、可见性及原子性问题,并介绍了`synchronized`如何全面解决这些问题。最后,通过一个多窗口售票示例展示了`synchronized`的具体应用。
|
4天前
|
Java
Java 对象和类
在Java中,**类**(Class)和**对象**(Object)是面向对象编程的基础。类是创建对象的模板,定义了属性和方法;对象是类的实例,通过`new`关键字创建,具有类定义的属性和行为。例如,`Animal`类定义了`name`和`age`属性及`eat()`、`sleep()`方法;通过`new Animal()`创建的`myAnimal`对象即可调用这些方法。面向对象编程通过类和对象模拟现实世界的实体及其关系,实现问题的结构化解决。
|
3天前
|
Java API 开发者
【Java字节码的掌控者】JDK 22类文件API:解锁Java深层次的奥秘,赋能开发者无限可能!
【9月更文挑战第8天】JDK 22类文件API的引入,为Java开发者们打开了一扇通往Java字节码操控新世界的大门。通过这个API,我们可以更加深入地理解Java程序的底层行为,实现更加高效、可靠和创新的Java应用。虽然目前它还处于预览版阶段,但我们已经可以预见其在未来Java开发中的重要地位。让我们共同期待Java字节码操控新篇章的到来,并积极探索类文件API带来的无限可能!
|
2天前
|
Java 程序员
Java编程中的对象和类: 初学者指南
【9月更文挑战第9天】在Java的世界中,对象和类构成了编程的基石。本文将引导你理解这两个概念的本质,并展示如何通过它们来构建你的程序。我们将一起探索类的定义,对象的创建,以及它们如何互动。准备好了吗?让我们开始这段Java的旅程吧!
|
30天前
|
Java 开发者
奇迹时刻!探索 Java 多线程的奇幻之旅:Thread 类和 Runnable 接口的惊人对决
【8月更文挑战第13天】Java的多线程特性能显著提升程序性能与响应性。本文通过示例代码详细解析了两种核心实现方式:Thread类与Runnable接口。Thread类适用于简单场景,直接定义线程行为;Runnable接口则更适合复杂的项目结构,尤其在需要继承其他类时,能保持代码的清晰与模块化。理解两者差异有助于开发者在实际应用中做出合理选择,构建高效稳定的多线程程序。
43 7
|
27天前
|
Oracle 安全 Java
JDK8到JDK28版本升级的新特性问题之在Java 15及以后的版本中,密封类和密封接口是怎么工作的
JDK8到JDK28版本升级的新特性问题之在Java 15及以后的版本中,密封类和密封接口是怎么工作的
|
22天前
|
安全 Java
【Java集合类面试三】、Map接口有哪些实现类?
这篇文章介绍了Java中Map接口的几种常用实现类:HashMap、LinkedHashMap、TreeMap和ConcurrentHashMap,以及它们适用的不同场景和线程安全性。
|
3月前
|
Java
Java中,有两种主要的方式来创建和管理线程:`Thread`类和`Runnable`接口。
【6月更文挑战第24天】Java创建线程有两种方式:`Thread`类和`Runnable`接口。`Thread`直接继承受限于单继承,适合简单情况;`Runnable`实现接口可多继承,利于资源共享和任务复用。推荐使用`Runnable`以提高灵活性。启动线程需调用`start()`,`Thread`直接启动,`Runnable`需通过`Thread`实例启动。根据项目需求选择适当方式。
45 2
|
2月前
|
Java 开发者