Java多线程下——各类锁的详解

Java多线程:锁的底层实现 最近准备招聘,在网上搜集了很多关于JVM获取的锁流程的文章,基本上所有关于偏向锁获取的部分都是错的,所以专门花时间自己看了看代码,整理出来供大家参考。 一、 基本概念 CAS(compare and swap) 参考资料: https://blog.youkuaiyun.com/ls5718/article/details/52563959 缓存一致性协议 1. 什么是... 阅读详情

这里写目录标题

各类锁的详解

常见的锁策略

乐观锁 vs 悲观锁

  • 悲观锁:

    总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。

例: 同学 A 认为 "老师是比较忙的, 我来问问题, 老师不一定有空解答". 因此同学 A 会先给老师发消息: "老师 你忙嘛? 我下午两点能来找你问个问题嘛?" (相当于加锁操作) 得到肯定的答复之后, 才会真的来问问题. 如果得到了否定的答复, 那就等一段时间, 下次再来和老师确定时间. 这个是悲观锁.

  • 乐观锁:

    假设数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并

    发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。

例: 同学 B 认为 "老师是比较闲的, 我来问问题, 老师大概率是有空解答的". 因此同学 B 直接就来找老师.(没加 锁, 直接访问资源) 如果老师确实比较闲, 那么直接问题就解决了. 如果老师这会确实很忙, 那么同学 B 也 不会打扰老师, 就下次再来(虽然没加锁, 但是能识别出数据访问冲突). 这个是乐观锁.

还记得我们之前经常提到的Synchronized吗?

Synchronized 初始使用乐观锁策略. 当发现锁竞争比较频繁的时候, 就会自动切换成悲观锁策略

首先说明一下我们熟知的Synchronized 不是读写锁.

Java 标准库提供ReentrantReadWriteLock 类, 实现了读写锁。

ReentrantReadWriteLock.ReadLock 类表示一个读锁. 这个对象提供了 lock / unlock 方法进行加锁解锁。

ReentrantReadWriteLock.WriteLock 类表示一个写锁. 这个对象也提供了 lock / unlock 方法进行加锁解锁。

读加锁和读加锁之间, 不互斥;

写加锁和写加锁之间, 互斥;

读加锁和写加锁之间, 互斥;

只要是涉及到 “互斥”, 就会产生线程的挂起等待. 一旦线程挂起, 再次被唤醒就不知道隔了多久了,因此尽可能减少 “互斥” 的机会, 就是提高效率的重要途径.

所以很明显了吧,读写锁更适合于 "频繁读, 不频繁写" 的场景中

重量级锁 vs 轻量级锁

synchronized 并不仅仅是对 mutex 进行封装, 在 synchronized 内部还做了很多其他的 工作

嗯?不知道什么意思?那肯定不知道啊因为没说来着,下面我们来理解一下:

重量级锁: 加锁机制重度依赖了 OS 提供了 mutex

  • 大量的内核态用户态切换
  • 很容易引发线程的调度
    这两个操作, 成本比较高. 一旦涉及到用户态和内核态的切换, 就意味着 "沧海桑田".

轻量级锁: 加锁机制尽可能不使用 mutex, 而是尽量在用户态代码完成. 实在搞不定了, 再使用 mutex.

  • 少量的内核态用户态切换.
  • 不太容易引发线程调度.

嗯。。。又来了我们熟悉的synchronized 开始是一个轻量级锁. 如果锁冲突比较严重, 就会变成重量级锁.

自旋锁(Spin Lock)

首先我们提出问题为什么是自旋锁,干啥的?

解:按之前的方式,线程在抢锁失败后进入阻塞状态,放弃 CPU,需要过很久才能再次被调度.但实际上, 大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。没必要就放弃 CPU. 这个时候就可以使用自旋锁来处理这样的问题.

如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会在极短的时间内到来.一旦锁被其他线程释放, 就能第一时间获取到锁.

自旋锁是一种典型的 轻量级锁 的实现方式.

  • 优点: 没有放弃 CPU, 不涉及线程阻塞和调度, 一旦锁被释放, 就能第一时间获取到锁.
  • 缺点: 如果锁被其他线程持有的时间比较久, 那么就会持续的消耗 CPU 资源. (而挂起等待的时候是不消耗 CPU 的)

公平锁 vs 非公平锁

  • 公平锁: 遵守 “先来后到”. B 比 C 先来的. 当 A 释放锁的之后, B 就能先于 C 获取到锁.
  • 非公平锁: 不遵守 “先来后到”. B 和 C 都有可能获取到锁.

操作系统内部的线程调度就可以视为是随机的. 如果不做任何额外的限制, 锁就是非公平锁. 如果要想实现公平锁, 就需要依赖额外的数据结构, 来记录线程们的先后顺序.

synchronized 是非公平锁.

可重入锁 vs 不可重入锁

可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也叫做递归锁)。

Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的。
Linux 系统提供的 mutex 是不可重入锁.

Compare and swap

CAS 是直接读写内存的, 而不是操作寄存器.

CAS 的读内存, 比较, 写内存操作是一条硬件指令, 是原子的.

CAS 有哪些应用:

  1. 实现原子类

    标准库中提供了 java.util.concurrent.atomic 包, 里面的类都是基于这种方式来实现

    典型的就是 AtomicInteger 类其中的 getAndIncrement ()相当于 i++ 操作.

通过形如上述代码就可以实现一个原子类. 不需要使用重量级锁, 就可以高效的完成多线程的自增操作.

本来 check and set 这样的操作在代码角度不是原子的. 但是在硬件层面上可以让一条指令完成这个操作, 也就变成原子的了.

  1. 实现自旋锁

    基于 CAS 实现更灵活的锁, 获取到更多的控制权

  1. CAS 的 ABA 问题
  • ABA问题描述:

    假设存在两个线程 t1 和 t2. 有一个共享变量 num, 初始值为 A,接下来, 线程 t1 想使用 CAS 把 num 值改成 Z, 那么就需要先读取 num 的值, 记录到 oldNum 变量中.使用 CAS 判定当前 num 的值是否为 A, 如果为 A, 就修改成 Z.但是, 在 t1 执行这两个操作之间, t2 线程可能把 num 的值从 A 改成了 B, 又从 B 改成了 A。

    线程 t1 的 CAS 是期望 num 不变就修改. 但是 num 的值已经被 t2 给改了. 只不过又改成 A 了. 这个时候 t1 究竟是否要更新 num 的值为 Z 呢?

    就像娶个新婚媳妇,怎么判断她在你之前是否有过无数个老公?

嗯?你说无所谓?都可以不介意?但是计算机不一样哦,来看一个Bug:

假设 滑稽老哥 有 100 存款. 滑稽想从 ATM 取 50 块钱. 取款机创建了两个线程 并发的来执行 -50操作:

我们期望一个线程执行 -50 成功, 另一个线程 -50 失败.如果使用 CAS 的方式来完成这个扣款过程就可能出现问题.

正常的过程:

  1. 存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期
    望更新为 50.
  2. 线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中.
  3. 轮到线程2 执行了, 发现当前存款为 50, 和之前读到的 100 不相同, 执行失败.

异常的过程:

  1. 存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期望更新为 50.
  1. 线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中.
  2. 在线程2 执行之前, 滑稽的朋友正好给滑稽转账 50, 账户余额变成 100 !!
  3. 轮到线程2 执行了, 发现当前存款为 100, 和之前读到的 100 相同, 再次执行扣款操作

你可以不介意媳妇,嗯。。理解理解但是钱。。。你不介意?

所以着急问我解决方案是什么了吧:

其实我们之前提到过就是引入版本号

CAS 操作在读取旧值的同时, 也要读取版本号.

真正修改的时候:

  1. 如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1.
  2. 如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了

在 Java 标准库中提供了 AtomicStampedReference<E> 类. 这个类可以对某个类进行包装, 在内部就提供了上面描述的版本管理功能.

Synchronized 原理

截止目前,本文前段我们已经了解了各种锁策略,期间也多次提到Synchronized,所以我们可以知道其具备以下特性:

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

JVM 将 synchronized 锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。会根据情况,进行依次升级

没有听过偏向锁?

  1. 偏向锁不是真的 “加锁”, 只是给对象头中做一个 “偏向锁的标记”, 记录这个锁属于哪个线程
  2. 如果后续没有其他线程来竞争该锁, 那么就不用进行其他同步操作了(避免了加锁解锁的开销)
  3. 如果后续有其他线程来竞争该锁(刚才已经在锁对象中记录了当前锁属于哪个线程了, 很容易识别当前申请锁的线程是不是之前记录的线程), 那就取消原来的偏向锁状态, 进入一般的轻量级锁状态

举个栗子理解偏向锁:

假设男主是一个锁, 女主是一个线程. 如果只有这一个线程来使用这个锁, 那么男主女主即使不领证结婚(避免了高成本操作), 也可以一直幸福的生活下去.但是,但是,但是,女配出现了, 也尝试竞争男主, 此时不管领证结婚这个操作成本多高, 女主也势必要把这个动作完成了, 让女配死心.

编译器+JVM 判断锁是否可消除. 如果可以, 就直接消除
有些应用程序的代码中, 用到了 synchronized, 但其实没有在多线程环境下. (例如 StringBuffer)

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

 

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

一段逻辑中如果出现多次加锁解锁, 编译器 + JVM 会自动进行锁的粗化.

实际开发过程中, 使用细粒度锁, 是期望释放锁的时候其他线程能使用锁,但是实际上可能并没有其他线程来抢占这个锁. 这种情况 JVM 就会自动把锁粗化, 避免频繁申请释放锁.

举个栗子:

1、打开铅笔盒 ; 2、拿出橡皮 ; 3、关闭铅笔盒。

4、打开铅笔盒 ;5、拿出尺子 ; 6、关闭铅笔盒。

7、打开铅笔盒 ;8、拿出橡皮 ; 9、关闭铅笔盒。

然鹅。。人类会这样吗?当然不会,我们会:

1、打开铅笔盒 ; 2、拿出橡皮 ; 3、拿出尺子;4、拿出铅笔;5、关闭铅笔盒。

这就是Synchronized,不愧出自JVM对咱们Java程序猿够良心吧。。。

Callable 接口

Callable 是一个 interface . 相当于把线程封装了一个 "返回值". 方便程序猿借助多线程的方式计算结果.

我们举个栗子:

创建线程计算 1 + 2 + 3 + … + 1000

不使用 Callable 版本:

package demo2;

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

    public static void main(String[] args) throws InterruptedException {
 
        Result result = new Result();
        Thread t = new Thread() {
 
            @Override
            public void run() {
 
                int sum = 0;
                for (int i = 1; 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);
        }
    }
}
可以看到, 上述代码需要一个辅助类 Result, 还需要使用一系列的加锁和 wait notify 操作, 代码复杂, 容易出错;所以我们看看使用 Callable 后。

使用 Callable 版本:

package demo2;

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

public class Test {
 
    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<>(callable);
        Thread t = new Thread(futureTask); t.start();
        int result = futureTask.get(); System.out.println(result);
    }
    
}

 

可以看到, 使用 Callable 和 FutureTask 之后, 代码简化了很多, 也不必手动写线程同步代码了.

Callable 和 Runnable 相对, 都是描述一个 "任务". Callable 描述的是带有返回值的任务, Runnable描述的是不带返回值的任务.

说明: Callable 通常需要搭配 FutureTask 来使用. FutureTask 用来保存 Callable 的返回结果. 因为

Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定.FutureTask 就可以负责这个等待结果出来的工作

ReentrantLock

JUC类下的可重入互斥锁. 和 synchronized 定位类似, 都是用来实现互斥效果, 保证线程安全

ReentrantLock 的用法:

  1. lock(): 加锁, 如果获取不到锁就死等.
  2. trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁.
  3. unlock(): 解锁

ReentrantLock 和 synchronized 的区别:

  • synchronized 是一个关键字, 是 JVM 内部实现的(大概率是基于 C++ 实现).
  • ReentrantLock 是标准库的一个类, 在 JVM 外实现的(基于 Java 实现).
  • synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活, 但是也容易遗漏 unlock.
  • synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃.
  • synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个 true 开启公平锁模式.
  • 更强大的唤醒机制. synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程.

如何选择使用哪个锁?

  • 锁竞争不激烈的时候, 使用 synchronized, 效率更高, 自动释放更方便.
  • 锁竞争激烈的时候, 使用 ReentrantLock, 搭配 trylock 更灵活控制加锁的行为, 而不是死等.
  • 如果需要使用公平锁, 使用 ReentrantLock.
原子类内部用的是 CAS 实现,所以性能要比加锁实现 i++ 高很多。原子类有以下几个:

AtomicBoolean; AtomicInteger;AtomicIntegerArray;AtomicLong; AtomicReference; AtomicStampedReference

以 AtomicInteger 举例,常见方法有:

addAndGet(int delta); //i +=delta;

    decrementAndGet(); //--i;

    getAndDecrement(); //i--;

    incrementAndGet(); //++i;

    getAndIncrement(); //i++;

虽然创建销毁线程比创建销毁进程更轻量, 但是在频繁创建销毁线程的时候还是会比较低效所以线程池就是为了解决这个问题. 如果某个线程不再使用了, 并不是真正把线程释放, 而是放到一个 “池子” 中, 下次如果需要用到线程就直接从池子中取, 不必通过系统来创建了。

ExecutorService 和 Executors

ExecutorService 表示一个线程池实例.

Executors 是一个工厂类, 能够创建出几种不同风格的线程池.

ExecutorService 的 submit 方法能够向线程池中提交若干个任务.

Executors 创建线程池的几种方式

  • newFixedThreadPool: 创建固定线程数的线程池
  • newCachedThreadPool: 创建线程数目动态增长的线程池.
  • newSingleThreadExecutor: 创建只包含单个线程的线程池.
  • newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.

Executors 本质上是 ThreadPoolExecutor 类的封装.

举个栗子:

package demo2;

import java.util.concurrent.*;

public class Test {
 
    public static void main(String[] args) {
 
        ExecutorService pool = Executors.newFixedThreadPool(10);
        pool.submit(new Runnable() {
 
            @Override
            public void run() {
 
                System.out.println("hello");
            }
        });
    }
}

ThreadPoolExecutor

理解 ThreadPoolExecutor 构造方法的参数:
把创建一个线程池想象成开个公司. 每个员工相当于一个线程.

  1. corePoolSize: 正式员工的数量. (正式员工, 一旦录用, 永不辞退)maximumPoolSize: 正式员工 + 临时工的数目. (临时工: 一段时间不干活, 就被辞退).
  2. keepAliveTime: 临时工允许的空闲时间.
  3. unit: keepaliveTime 的时间单位, 是秒, 分钟, 还是其他值.
  4. workQueue: 传递任务的阻塞队列
  5. threadFactory: 创建线程的工厂, 参与具体的创建线程工作.
  6. RejectedExecutionHandler: 拒绝策略, 如果任务量超出公司的负荷了接下来怎么处理.
  • AbortPolicy(): 超过负荷, 直接抛出异常.
  • CallerRunsPolicy(): 调用者负责处理
  • DiscardOldestPolicy(): 丢弃队列中最老的任务.
  • DiscardPolicy(): 丢弃新来的任务.

举个栗子:

package demo2;

import java.util.concurrent.*;

public class Test {
 
    public static void main(String[] args) {
 
        ExecutorService pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, new SynchronousQueue<Runnable>(), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
        for (int i = 0; i < 3; i++) {
 
            pool.submit(new Runnable() {
 
                @Override
                public void run() {
 
                    System.out.println("hello");
                }
            });
        }
    }
}

线程池的工作流程

信号量 Semaphore

信号量, 用来表示 "可用资源的个数". 本质上就是一个计数器.
就象是停车场的展示牌: 当前有车位 100 个. 表示有 100 个可用资源.

  1. 当有车开进去的时候, 就相当于申请一个可用资源, 可用车位就 -1 (这个称为信号量的 P 操作)
  2. 当有车开出来的时候, 就相当于释放一个可用资源, 可用车位就 +1 (这个称为信号量的 V 操作)
  3. 如果计数器的值已经为 0 了, 还尝试申请资源, 就会阻塞等待, 直到有其他线程释放资源.

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

举个栗子:

  • 创建 Semaphore 示例, 初始化为 4, 表示有 4 个可用资源.
  • acquire 方法表示申请资源(P操作), release 方法表示释放资源(V操作)
  • 创建 20 个线程, 每个线程都尝试申请资源, sleep 1秒之后, 释放资源. 观察程序的执行效果

 

package demo2;

import java.util.concurrent.*;

public class Test {
 
    public static void main(String[] args) {
 
        Semaphore semaphore = new Semaphore(4);
        Runnable runnable = new Runnable() {
 
            @Override
            public void run() {
 
                try {
 
                    System.out.println("申请资源");
                    semaphore.acquire();
                    System.out.println("我获取到资源了");
                    Thread.sleep(1000);
                    System.out.println("我释放资源了");
                    semaphore.release();
                } catch (InterruptedException e) {
 
                    e.printStackTrace();
                }
            }
        };
        for (int i = 0; i < 20; i++) {
 
            Thread t = new Thread(runnable);
            t.start();
        }
    }
}

使用场景:

使用信号量可以实现 “共享锁”, 比如某个资源允许 3 个线程同时使用, 那么就可以使用 P 操作作为加锁, V 操作作为解锁, 前三个线程的 P 操作都能顺利返回, 后续线程再进行 P 操作就会阻塞等待,直到前面的线程执行了 V 操作.

多线程环境使用哈希表

Hashtable

只是简单的把关键方法加上了 synchronized 关键字

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

ConcurrentHashMap

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

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

ConcurrentHashMap.每个哈希桶都有一把锁.只有两个线程访问的恰好是同一个哈希桶上的数据才出现锁冲突

死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

  1. 互斥:至少有一个资源必须处于非共享模式,即一次只有一个进程可使用。如果另一进程申请该资源,那么申请进程应等到该资源释放为止。
  2. 占有并等待:—个进程应占有至少一个资源,并等待另一个资源,而该资源为其他进程所占有。
  3. 非抢占:资源不能被抢占,即资源只能被进程在完成任务后自愿释放。
  4. 循环等待:有一组等待进程 {P0,P1,…,Pn},P0 等待的资源为 P1 占有,P1 等待的资源为 P2 占有,……,Pn-1 等待的资源为 Pn 占有,Pn 等待的资源为 P0 占有。

举个栗子:

如何避免死锁

复习一下:

死锁产生的四个必要条件:

  1. 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
  2. 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
  3. 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
  4. 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。

当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。

众所周知其中最容易破坏的就是 "循环等待".

破坏循环等待

最常用的一种死锁阻止技术就是锁排序. 假设有 N 个线程尝试获取 M 把锁, 就可以针对 M 把锁进行编号(1, 2, 3…M).

N 个线程尝试获取锁的时候, 都按照固定的按编号由小到大顺序来获取锁. 这样就可以避免环路等待.

可能环路等待示例:

package demo2;

import java.util.Hashtable;
import java.util.concurrent.*;

public class Test {
 
    public static void main(String[] args) {
 
        Object lock1 = new Object();
        Object lock2 = new Object();
        Thread t1 = new Thread() {
 
            @Override
            public void run() {
 
                synchronized (lock1) {
 
                    synchronized (lock2) {
 
                        // do something...
                    }
                }
            }
        };
        t1.start();
        Thread t2 = new Thread() {
 
            @Override
            public void run() {
 
                synchronized (lock2) {
 
                    synchronized (lock1) {
 
                        // do something...
                    }
                }
            }
        };
        t2.start();
    }
}

不会产生环路等待的代码:(对比一下)

约定好先获取 lock1, 再获取 lock2 , 就不会环路等待.

package demo2;

import java.util.Hashtable;
import java.util.concurrent.*;

public class Test {
 
    public static void main(String[] args) {
 
        Object lock1 = new Object();
        Object lock2 = new Object();
        Thread t1 = new Thread() {
 
            @Override
            public void run() {
 
                synchronized (lock1) {
 
                    synchronized (lock2) {
 
                        // do something...
                    }
                }
            }
        };
        t1.start();
        Thread t2 = new Thread() {
 
            @Override
            public void run() {
 
                synchronized (lock1) {
 
                    synchronized (lock2) {
 
                        // do something...
                    }
                }
            }
        };
        t2.start();
    }
}

相关问题

Java多线程是如何实现数据共享的?

JVM 把内存分成了这几个区域:

方法区, 堆区, 栈区, 程序计数器.

其中堆区这个内存区域是多个线程之间共享的.

只要把某个数据放到堆内存中, 就可以让多个线程都能访问到.

Java线程共有几种状态?状态之间怎么切换的?

  • NEW: 安排了工作, 还未开始行动. 新创建的线程, 还没有调用 start 方法时处在这个状态.
  • RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作. 调用 start 方法之后, 并正在CPU 上运行/在即将准备运行 的状态.
  • BLOCKED: 使用 synchronized 的时候, 如果锁被其他线程占用, 就会阻塞等待, 从而进入该状态.
  • WAITING: 调用 wait 方法会进入该状态.
  • TIMED_WAITING: 调用 sleep 方法或者 wait(超时时间) 会进入该状态.
  • TERMINATED: 工作完成了. 当线程 run 方法执行完毕后, 会处于这个状态.

Thread和Runnable的区别和联系?

Thread 类描述了一个线程.

Runnable 接口描述了一个任务.

在创建线程的时候需要指定线程完成的任务, 可以直接重写 Thread 的 run 方法, 也可以使用Runnable 来描述这个任务.

多次start一个线程会怎么样?

第一次调用 start 可以成功调用.后续再调用 start 会抛出java.lang.IllegalThreadStateException 异常

有synchronized两个方法,两个线程分别同时用这个方法,请问会发生什么?

synchronized 加在非静态方法上, 相当于针对当前对象加锁.

  • 如果这两个方法属于同一个实例:
    线程1 能够获取到锁, 并执行方法. 线程2 会阻塞等待, 直到线程1 执行完毕, 释放锁, 线程2 获取到锁之后才能执行方法内容.
  • 如果这两个方法属于不同实例:
    两者能并发执行, 互不干扰.

进程和线程的区别?

  • 进程是包含线程的. 每个进程至少有一个线程存在,即主线程。
  • 进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.
  • 进程是系统分配资源的最小单位,线程是系统调度的最小单位。

 

java多线程——synchronized的偏向锁、轻量级锁和重量级锁 偏向锁:适用于只有一个线程访问同步块的场景,通过存储锁偏向的线程ID来优化性能。轻量级锁:适用于多线程竞争不激烈的情况,通过CAS机制和自旋操作来竞争锁。重量级锁:适用于多线程竞争激烈的情况,通过监视器来保证线程同步,但会导致较大的性能损耗。 阅读详情

相关推荐

java多线程(二)锁对象

在上一篇博客中,我们讨论了Race Condition现象以及它产生的原因,现在我们知道它是不好的一种现象了,那么我们有什么方法避免它呢。最直接有效的方式就是放弃多线程,直接改为使用单线程但操作数据,但是这是不优雅的,因为我们知道有时候,多线程有它自己的优势。在这里我们讨论两种其他的方法——锁对象和条件对象。 锁对象 java SE5.0之后为实现多线程的互斥引入了ReentrantLock类

xingjiarong的专栏 3191

Java锁的种类

概述 锁大概有以下名词: 自旋锁、自旋锁的其他种类、阻塞锁、可重入锁、读写锁、互斥锁、悲观锁、乐观锁、公平锁、偏向锁、对象锁、线程锁、锁粗化、锁消除、轻量级锁、重量级锁、信号量、独享锁、共享锁、分段锁。 我们所说锁的分类其实应该按照锁的特性和设计来划分。 其实从并发的角度来讲,按照线程安全的三种策略看,主要内容都集中在互斥同步里,我们所讨论的锁也集中在这个部分,这个部分的锁都是悲观锁;第二个部分是非阻塞同步,这个部分也就一种通过CAS进行原子类操作,这个部分可以看成乐观锁,其实也就是不加锁;第三个部分是无同

抽象的螺旋 6228

【Java多线程】成神之路中必须要了解的锁分类

文章目录什么是串行、并发、并行浅谈锁的作用1.锁有什么作用呢2.Java中的锁有什么作用呢3.为什么要用锁一.Java锁分类1.公平锁/非公平锁2.可重入锁/不可重入锁3.独享锁/共享锁4.互斥锁/读写锁5.乐观锁/悲观锁6.分段锁7.偏向锁/轻量级锁/重量级锁8.自旋锁8.1.简单了解一下CAS算法?CAS的问题8.2.什么是自旋锁?8.3.Java如何实现自旋锁?8.4.自旋锁存在的问题8.5...

墩墩分墩 1602

【老生常谈】一文理解Java中的各种锁

本文Java中常用的锁以及常见的锁的概念进行了基本介绍,限于篇幅以及个人水平,没有在本篇文章中对所有内容进行深层次的讲解。其实Java本身已经对锁本身进行了良好的封装,降低了小伙伴们在平时工作中的使用难度。在编写多线程程序时,需要特别注意共享资源的访问和操作,避免出现竞态条件等问题,确保程序的正确性和稳定性。同时,也需要注意多线程的性能问题,合理使用锁机制,避免过多的锁竞争导致程序的性能下降。参考资料Java中的锁。

独泪了无痕 2180

arduino esp8266_Arduino传感器教程 第22章ESP8266 控制 5V 继电器

ESP8266控制5V继电器目录一、NodeMCU二、工作原理本文将使用亚马逊 AlexaEcho 和 ESP8266 控制 5V 继电器。NodeMCUNodeMCU 是一个开源的 IoT 物联网硬件开发板,由于它支持 WIFI 功能且使用方法十分类似 Arduino 开发板,所以近些年获得了越来越多来自全世界的创客朋友们的青睐。NodeMCU 尺寸与 Arduino Nano 类似。...

weixin_39611722的博客 3457

静态与动态LACP链路聚合:原理、配置与排错实战指南

链路聚合技术是提升网络带宽与可靠性的核心方案,通过将多个物理端口逻辑捆绑,实现带宽叠加与链路冗余。其原理基于IEEE 802.3ad标准,通过负载均衡算法将数据流分布到不同物理链路,既避免了数据包乱序,又提升了整体吞吐量。该技术的核心价值在于消除单端口瓶颈、提供毫秒级故障切换,并能简化网络拓扑管理。在企业服务器接入、数据中心互联及虚拟化平台等场景中,链路聚合已成为构建高可用网络的基础。实践中,动态LACP模式通过协议报文协商,能主动检测链路故障并防止配置错误导致的网络环路,相比静态聚合更为可靠。本文将深入解

weixin_31412781的博客 268

Java中的锁有哪些?

1、悲观锁和乐观锁 悲观锁:当前线程去操作数据的时候,总是认为别的线程会去修改数据,所以每次操作数据的时候都会上锁,别的线程去操作数据的时候就会阻塞,比如synchronized; 乐观锁:当前线程每次去操作数据的时候都认为别人不会修改,更新的时候会判断别人是否会去更新数据,通过版本来判断,如果数据被修改了就拒绝更新,例如cas是乐观锁,但是严格来说并不是锁,通过原子性来保证数据的同步,例如数据库的乐观锁,通过版本控制来实现,cas不会保证线程同步,乐观的认为在数据更新期间没有其他线程影响 2、公平锁

mazadazhangjia的博客 1万+

转录组测序(RNA-seq)的数据如何分析?

经过富集分析后,我们就可以从富集到的GO term或者Pathway中寻找关键基因,通过一个GO term和Pathway里面的基因往往小于100个,至此我们就又将上千条基因的范围缩小到了100条以内。对GO term或者Pathway里面的基因进行范围的缩小也有两个方法,一种方法是根据基因的注释或者文献来找到GO term或者Pathway里面的关键基因(2-3个),另一种方法需要有相应的表型数据,与GO term或者Pathway里面基因进行相关性分析,找出与表型高度相关的基因。

weixin_55372631的博客 1万+

Java中有哪些锁?

是 Java 最基础的锁机制,用于实现方法或代码块的同步,保证多线程环境下的原子性、可见性和有序性。竞争激烈时,线程进入阻塞状态,依赖操作系统的互斥量(Mutex)管理。提供更灵活的读写锁控制,支持乐观读、悲观读、写锁三种模式,性能优于。允许多个线程同时读,但写线程独占资源,适用于读多写少的场景。读多写少且对性能要求极高的场景,但需谨慎处理锁转换逻辑。:默认抢占式获取锁,不保证等待时间长的线程优先获取。需要复杂同步控制的场景,如多条件等待、锁超时处理。:写线程获取锁时,禁止其他读/写操作。

qq_45906596的博客 2826

从静态到动态:如何用Deformable 3DGS和4DGS实现高效动态场景建模

本文深入探讨了Deformable 3DGS和4DGS在动态场景建模中的技术突破,详细解析了这两种方法如何高效处理动态内容。Deformable 3DGS通过引入时间维度的变形场扩展传统3DGS,而4DGS则采用四维时空体素分解方法,两者均显著提升了动态场景建模的效率和质量,适用于虚拟现实、增强现实等应用场景。

weixin_29063035的博客 435

从零构建智慧农业监测终端:STM32数据采集与LoRa组网实战

本文详细介绍了如何从零构建智慧农业监测终端,涵盖STM32数据采集与LoRa组网实战。通过DHT22温湿度传感器和YL-69土壤湿度传感器实现精准环境监测,结合LoRa无线传输技术解决偏远农田信号问题。文章还分享了硬件选型、电路设计、低功耗优化及Flutter跨平台应用开发等关键技巧,助力农业智能化升级。

weixin_29172963的博客 357

java多线程 —— 面试题集合(最全集合)

文章目录导读1. 多线程基本概念多线程有什么用?线程、进程、协程的区别什么是多线程上下文切换?什么是线程安全2. java多线程创建创建线程的方式Runnable和Callable的区别Thread类中的start()和run()方法有什么区别?什么导致线程阻塞?3. 多线程同步和锁怎么检测一个线程是否持有对象监视器Condition?4. 线程池CyclicBarrier和CountDownLa...

Charles Ren's Tech Blog 3万+

Python爬虫教程:抓取纽约时报文章存档

爬虫技术主要用于从网站上抓取并提取数据。HTTP请求:使用requests库发送HTTP请求以获取网页。网页解析:使用或lxml库解析HTML网页。数据存储:将抓取的数据保存为CSV、JSON等格式,或者存储在数据库中。防反爬虫技术:模拟正常的用户请求,避免被反爬虫机制封锁。多线程与异步爬取:提高抓取效率。通过这篇博客,您已经学习了如何使用Python爬取纽约时报的历史文章存档。本文介绍了如何模拟请求头、抓取网页内容、解析HTML并提取数据,最后存储到CSV文件或数据库中。

2201_76125261的博客 593

多线程中的锁分类及其作用与原理详解

在并发编程中,线程间的同步与互斥是保证数据一致性和线程安全的核心问题。当多个线程并发地访问共享资源时,必须引入一定的机制来避免竞态条件。锁(Lock)是最常用的同步机制之一,它可以确保在同一时刻只有一个线程可以访问共享资源,从而避免数据的不一致。本文将详细讲解多线程中各种锁的分类、作用以及其工作原理。

lssffy的博客 1690

【2026年华为暑期实习(AI)-4月22日-第三题- 网络异常流量传播链路溯源】(题目+思路+Java&C++&Python解析+在线测试)

本文提出了一种基于空间距离和流量负载的网络异常传播链检测方法。首先通过曼哈顿距离建立基站间的直接关联关系,形成无向图;然后计算各连通块的总负载,筛选出关键节点;最后在关键节点构成的有向无环图(DAG)中寻找用户数最大的传播路径。算法采用DFS/BFS识别连通块,并通过动态规划求解最长路径,时间复杂度为O(N^2)。该方法适用于监控网络中异常流量的定向传播分析,能有效识别高负载区域和最大影响路径。

m0_73659489的博客 187

Python开发MCP Client

全部例子只是MCP Client访问MCP Server的过程,那么这样其实就失去了MCP协议的创建的初衷,和大模型集成后才能发挥MCP最大的作用。

Criss@陈磊 2973

Java锁详解

文章目录什么是锁锁的实现方式锁涉及的几个重要概念类锁和对象锁(重要)synchronized实现原理 什么是锁 计算机还是单线程的时代,下面代码中的count,始终只会被一个线程累加,调用addOne()10次,count的值一定就累加了10。 public class Test { // 计数器 private Integer count = 0; // 累加.........

爱雨轩 2万+

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

Java中的锁各类解析

锁类型优点缺点常见应用场景典型使用结构自旋锁减少线程上下文切换开销浪费 CPU 资源(自旋时间长时)持有锁时间短的场景,如短时间内获取锁的操作无特定知名结构广泛应用,可自定义实现可重入锁支持同一线程多次获取锁,避免死锁,简化复杂同步逻辑无明显特定缺点,相比简单锁实现稍复杂递归调用且涉及共享资源访问,复杂同步逻辑实现类乐观锁提高多读少写场景的并发读性能,高性能且低冲突场景表现佳冲突处理不当可能导致重试或异常处理复杂数据库多读少写查询,分布式缓存数据操作。

weixin_69477306的博客 2051

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

Java多线程 各种锁(一篇全搞懂)

Java多线程 锁 文章目录Java多线程 锁1、乐观锁与悲观锁2、公平锁与非公平锁3、可重入锁与不可重入锁4、独享锁与共享锁5、自旋锁 VS 适应性自旋锁6、无锁 、 偏向锁、量级锁 和 重量级锁(难点) 1、乐观锁与悲观锁 (1)悲观锁 对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,syn...

烟雨gps 5953

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

Java【多线程】(7)常见的锁策略

哈喽大家好,今天来给大家分享Java多线程中常见的锁策略,锁策略不是和Java强相关,但凡涉及到并发编程涉及到锁都会涉及锁策略,概念较多但都很重要,废话不多说让我们开始吧。

2301_81073317的博客 3253

Java多线程、锁、线程池详解

通过本文的介绍,我们深入了解了Java中的多线程、锁机制、线程池的原理和应用。多线程可以提高程序的执行效率,但同时也带来了线程安全的问题。锁机制可以解决线程安全问题,而线程池则可以提高线程的复用率,减少线程创建和销毁的开销。理解这些概念的原理和应用可以帮助开发者编写高效、可靠的并发程序。希望本文能够帮助大家更好地理解和掌握Java中的多线程、锁和线程池,提升编程水平。

weixin_67825126的博客 2136

Java 中的各种锁详解

自旋锁的定义:当一个线程尝试去获取某一把锁的时候,如果这个锁此时已经被别人获取(占用),那么此线程就无法获取到这把锁,该线程将会等待,间隔一段时间后会再次尝试获取。这种采用循环加锁 -> 等待的机制被称为自旋锁(spinlock)。ReentrantLock 是一把可重入锁,也是一把互斥锁,它具有与相同的方法和监视器锁的语义,但是它比 synchronized 有更多可扩展的功能。ReentrantLock 的可重入性是指它可以由上次成功锁定但还未解锁的线程拥有。当只有一个线程尝试加锁时,该线程调用。

在技术的广袤天地里,本博客如精准罗盘。剖析前沿科技,深掘代码奥秘,以精炼笔触,带您穿越复杂技术迷宫,速达知识彼岸。 1648

Java多线程中 的各种锁

学习 java 多线程时,最头疼的知识点之一就是 java 中的锁了,什么互斥锁、排它锁、自旋锁、死锁、活锁等等,细分的话可以罗列出 20 种左右的锁,光是看着这些名字就足以让人望而却步了,更别说一个个去理解它们的含义了。其实我要在这里告诉大家,我们看到的其实只是假象,其实根本没有这么多锁,或者这样说,这里边有很多锁其实就是一个东西,当我们从不同的侧重点去看的时候,它们就会衍生出不同的名字。 ...

程序员世杰 2580

详解Java多线程锁之synchronized

synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。 synchronized的四种使用方式 修饰代码块:被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用于调用对象 修饰方法:被修饰的方法称为同步方法,其作用的范围是整个方法,作用于调用对象 注意:synchronized修饰方法时必须是显式调用,如果没有显式调用...

全菜工程师小辉的博客 6362
上一篇: 多线程与高并发基础
下一篇: SpringBoot中使用注解来实现 Redis 分布式锁
java_lujj
博客等级 码龄4年 18粉丝 152原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值