草庐IT

Java高并发编程实战1,那些年学过的锁

哪 吒 2023-05-05 原文

目录

一、进程与线程

程序本身是静态的,是众多代码的组合产物,代码保存在文件中。如果程序要运行,则需要将程序加载到内存中,通过编译器将其编译成计算机能够理解的方式运行。
如果想启动一个Java程序,先要创建一个JVM进程。
进程是操作系统进行资源分配的最小单位,在一个进程中可以创建多个线程。多个线程各自拥有独立的局部变量、线程堆栈和程序计数器,能够访问共享的资源。

  1. 进程是操作系统分配资源的最小单位,线程是CPU调度的最小单位;
  2. 一个进程中可以包含多个线程;
  3. 进程与进程之间是相对独立的,进程中的线程之间并不完全独立,可以共享进程中的堆内存、方法区内存、系统资源等;
  4. 进程上下文的切换要比线程的上下文切换慢很多;
  5. 某个进程发生异常,不会对其它进程造成影响,但,某个线程发生异常,可能会对此进程中的其它线程造成影响;

二、线程组与线程池

1、线程组

线程组可以管理多个线程,顾名思义,线程组,就是把功能相似的线程放到一个组里,方便管理。

package com.guor.test;

public class ThreadGroupTest {
    public static void main(String[] args) {
        // 创建线程组
        ThreadGroup threadGroup = new ThreadGroup("nezha");

        Thread thread = new Thread(threadGroup,()->{
            // 线程组名称
            String groupName = Thread.currentThread().getThreadGroup().getName();
            // 线程名称
            String threadName = Thread.currentThread().getName();
            System.out.println("groupName -- "+groupName);//groupName -- nezha
            System.out.println("threadName -- "+threadName);//threadName -- thread
        },"thread");

        thread.start();
    }
}

2、线程组和线程池有啥区别?

  1. 线程组中的线程可以跨线程修改数据,而线程组和线程组之间不可以跨线程修改数据;
  2. 线程池就是创建一定数量的线程,批量处理任务,当前任务执行完毕后,线程又可以去执行其它任务,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
  3. 线程池可以有效的管理线程的数量,避免线程的无限制创建,线程是很耗费系统资源的,动不动就会产生OOM,并且会造成cpu过度切换,也有强大的拓展功能,比如延时定时线程池。

三、用户线程与守护线程

在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程) 。

用户线程是最常见的线程,比如通过main方法启动,就会创建一个用户线程。

Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者。

JVM中的垃圾回收、JIT编译器线程就是最常见的守护线程。

只要有一个用户线程在运行,守护线程就会一直运行。只有所有的用户线程都结束的时候,守护线程才会退出。

编写代码时,也可以通过thread.setDaemon(true)指定线程为守护线程。

Thread daemonTread = new Thread();
 
  // 设定 daemonThread 为 守护线程,默认false
 daemonThread.setDaemon(true);
 
 // 验证当前线程是否为守护线程,返回 true 则为守护线程
 daemonThread.isDaemon();

守护线程的注意事项:

  1. thread.setDaemon(true)要在thread.start()之前设置,否则会抛出IllegalThreadStateException异常。你不能把正在运行的线程设置为守护线程;
  2. 在守护线程中产生的新线程也是守护线程;
  3. 读写操作或者计算逻辑不可以设置为守护线程;

四、并行与并发

并行指当多核CPU中的一个CPU执行一个线程时,其它CPU能够同时执行另一个线程,两个线程之间不会抢占CPU资源,可以同时运行。

并发指在一段时间内CPU处理多个线程,这些线程会抢占CPU资源,CPU资源根据时间片周期在多个线程之间来回切换,多个线程在一段时间内同时运行,而在同一时刻不是同时运行的。

并行和并发的区别?

  1. 并行指多个线程在一段时间的每个时刻都同时运行,并发指多个线程在一段时间内同时运行(不是同一时刻,一段时间内交叉执行)
  2. 并行的多个线程不会抢占系统资源,并发的多个线程会抢占系统资源;
  3. 并行是多CPU的产物,单核CPU中只有并发,没有并行;

五、悲观锁与乐观锁

1、悲观锁

悲观锁在一个线程进行加锁操作后使得该对象变为该线程的独有对象,其它的线程都会被悲观锁阻拦在外,无法操作。

悲观锁的缺陷:

  1. 一个线程获得悲观锁后其它线程必须阻塞。
  2. 线程切换时要不停的释放锁和获取锁,开销巨大。
  3. 当一个低优先级的线程获得悲观锁后,高优先级的线程必须等待,导致线程优先级倒置,synchronized锁是一种典型的悲观锁。

2、乐观锁

乐观锁认为对一个对象的操作不会引发冲突,所以每次操作都不进行加锁,只是在最后提交更改时验证是否发生冲突,如果冲突则再试一遍直到成功为止,这个尝试的过程被称为自旋。乐观锁其实并没有加锁,但乐观锁也引入了诸如ABA、自旋次数过多等问题。

乐观锁一般会采用版本号机制,先读取数据的版本号,在写数据时比较版本号是否一致,如果一致,则更新数据,否则再次读取版本号,直到版本号一致。

Java中的乐观锁都是基于CAS自旋实现的。

六、CAS

1、什么是CAS?

Compare And Swap。

CAS(V, A, B) ,内存值V,期待值A, 修改值B(V 是否等于 A, 等于执行, 不等于将B赋给V)

2、CAS带来的问题

(1)ABA问题

CAS操作的流程为:

  1. 读取原值。
  2. 通过原子操作比较和替换。
  3. 虽然比较和替换是原子性的,但是读取原值和比较替换这两步不是原子性的,期间原值可能被其它线程修改。

ABA问题有些时候对系统不会产生问题,但是有些时候却也是致命的。

ABA问题的解决方法是对该变量增加一个版本号,每次修改都会更新其版本号。JUC包中提供了一个类AtomicStampedReference,这个类中维护了一个版本号,每次对值的修改都会改动版本号。

(2)自旋次数过多

CAS操作在不成功时会重新读取内存值并自旋尝试,当系统的并发量非常高时即每次读取新值之后该值又被改动,导致CAS操作失败并不断的自旋重试,此时使用CAS并不能提高效率,反而会因为自旋次数过多还不如直接加锁进行操作的效率高。

(3)只能保证一个变量的原子性

当对一个变量操作时,CAS可以保证原子性,但同时操作多个变量时CAS就无能为力了。

可以封装成对象,再对对象进行CAS操作,或者直接加锁。

七、那些年学过的锁

1、公平锁与非公平锁

  • 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
  • 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的

2、独占锁与共享锁

  • 独占锁:当多个线程则争抢锁的过程中,无论是读操作还是写操作,只能有一个线程获取到锁,其他线程阻塞等待。
  • 共享锁:允许多个线程同时获取共享资源,采取的是乐观锁的机制,共享锁限制写写操作、读写操作,但不会限制读读操作。

3、可重入锁与不可重入锁

  • 可重入锁:一个线程可以多次占用同一个锁,但是解锁时,需要执行相同次数的解锁操作;
  • 不可重入锁:一个线程不能多次占用同一个锁;

八、死锁、活锁、饿死

1、死锁

多个线程互相持有对方需要的资源,导致多个线程相互等待,无法继续执行后续任务。

2、产生死锁的4个必要条件

  1. 互斥条件:指进程对所分配到的资源进行排它性使用,在一段时间内某资源只由一个进程占用,如果此时还有其他进程请求资源,则请求者只能等待,直至占有资源的进程被释放;
  2. 请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
  3. 不可剥夺:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
  4. 循环等待:一个等待一个,产生了一个闭环。

3、饥饿

饥饿指的是线程由于无法获取需要的资源而无法继续执行。

4、产生饥饿的主要原因

  1. 高优先级的线程不断抢占资源,低优先级的线程抢不到;
  2. 某个线程一直不释放资源,导致其他线程无法获取资源;

5、如何避免饥饿

  1. 使用公平锁分配资源;
  2. 为程序分配足够的系统资源;
  3. 避免持有锁的线程长时间占用锁;

6、活锁

活锁指的是多个线程同时抢占同一个资源时,都主动将资源让给其他线程使用,导致这个资源在多个线程之间来回切换,导致线程因无法获取相应资源而无法继续执行的现象。

7、如何避免活锁

可以让多个线程随机等待一段时间后再次抢占资源,这样会大大减少线程抢占资源的冲突次数,有效避免活锁的产生。

九、多线程锁的升级原理是什么?

锁的状态总共有四种,无锁状态、偏向锁、轻量级锁、重量级锁。

随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。但是锁的升级是单向的,只能升级不能降级。

1、无锁

没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其它修改失败的线程会不断重试直到修改成功。

无锁总是假设对共享资源的访问没有冲突,线程可以不停执行,无需加锁,无需等待,一旦发现冲突,无锁策略则采用一种称为CAS的技术来保证线程执行的安全性,CAS是无锁技术的关键。

2、偏向锁

对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续执行中自动获取锁,降低获取锁带来的性能开销。偏向锁,指的是偏向第一个加锁线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争偏向锁才会被释放。

偏向锁的撤销,需要在某个时间点上没有字节码正在执行时,先暂停偏向锁的线程,然后判断锁对象是否处于被锁定状态,如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁。

如果线程处于活动状态,升级为轻量级锁的状态

3、轻量级锁

轻量级锁是指当锁是偏向锁的时候,被第二个线程B访问,此时偏向锁就会升级为轻量级锁,线程B会通过自旋的形式尝试获取锁,线程不会阻塞,从er提升性能。

当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定次数时,轻量级锁便会升级为重量级锁,当一个线程已持有锁,另一个线程在自旋,而此时第三个线程来访时,轻量级锁也会升级为重量级锁。

注:自旋是什么?

自旋(spinlock)是指当一个线程获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

4、重量级锁

指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。

重量级锁通过对象内部的监听器(monitor)实现,而其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。

5、锁状态对比

偏向锁轻量级锁重量级锁
使用场景只有一个线程进入同步块虽然很多线程,但没有冲突,线程进入时间错开因而并未争抢锁发生了锁争抢的情况,多条线程进入同步块争用锁
本质取消同步操作CAS操作代替互斥同步互斥同步
优点不阻塞,执行效率高(只有第一次获取偏向锁时需要CAS操作,后面只是比对ThreadId)不会阻塞不会空耗CPU
缺点适用场景太局限。若竞争产生,会有额外的偏向锁撤销的消耗长时间获取不到锁空耗CPU阻塞,上下文切换,重量级操作,消耗操作系统资源

6、锁消除

消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,别切不会被其它线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。

十、Java多线程思维导图



Java高并发编程实战系列文章

Java高并发编程实战1,那些年学过的锁

Java高并发编程实战2,原子性、可见性、有序性,傻傻分不清

Java高并发编程实战3,Java内存模型与Java对象结构

Java高并发编程实战4,synchronized与Lock底层原理

Java高并发编程实战5,异步注解@Async自定义线程池

Java高并发编程实战6,通过AQS源码分析lock()锁机制

Java高并发编程实战7,ConcurrentHashMap详解

有关Java高并发编程实战1,那些年学过的锁的更多相关文章

  1. java - 等价于 Java 中的 Ruby Hash - 2

    我真的很习惯使用Ruby编写以下代码:my_hash={}my_hash['test']=1Java中对应的数据结构是什么? 最佳答案 HashMapmap=newHashMap();map.put("test",1);我假设? 关于java-等价于Java中的RubyHash,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/22737685/

  2. ruby - 寻找通过阅读代码确定编程语言的ruby gem? - 2

    几个月前,我读了一篇关于ruby​​gem的博客文章,它可以通过阅读代码本身来确定编程语言。对于我的生活,我不记得博客或gem的名称。谷歌搜索“ruby编程语言猜测”及其变体也无济于事。有人碰巧知道相关gem的名称吗? 最佳答案 是这个吗:http://github.com/chrislo/sourceclassifier/tree/master 关于ruby-寻找通过阅读代码确定编程语言的rubygem?,我们在StackOverflow上找到一个类似的问题:

  3. java - 从 JRuby 调用 Java 类的问题 - 2

    我正在尝试使用boilerpipe来自JRuby。我看过guide从JRuby调用Java,并成功地将它与另一个Java包一起使用,但无法弄清楚为什么同样的东西不能用于boilerpipe。我正在尝试基本上从JRuby中执行与此Java等效的操作:URLurl=newURL("http://www.example.com/some-location/index.html");Stringtext=ArticleExtractor.INSTANCE.getText(url);在JRuby中试过这个:require'java'url=java.net.URL.new("http://www

  4. java - 我的模型类或其他类中应该有逻辑吗 - 2

    我只想对我一直在思考的这个问题有其他意见,例如我有classuser_controller和classuserclassUserattr_accessor:name,:usernameendclassUserController//dosomethingaboutanythingaboutusersend问题是我的User类中是否应该有逻辑user=User.newuser.do_something(user1)oritshouldbeuser_controller=UserController.newuser_controller.do_something(user1,user2)我

  5. java - 什么相当于 ruby​​ 的 rack 或 python 的 Java wsgi? - 2

    什么是ruby​​的rack或python的Java的wsgi?还有一个路由库。 最佳答案 来自Python标准PEP333:Bycontrast,althoughJavahasjustasmanywebapplicationframeworksavailable,Java's"servlet"APImakesitpossibleforapplicationswrittenwithanyJavawebapplicationframeworktoruninanywebserverthatsupportstheservletAPI.ht

  6. Observability:从零开始创建 Java 微服务并监控它 (二) - 2

    这篇文章是继上一篇文章“Observability:从零开始创建Java微服务并监控它(一)”的续篇。在上一篇文章中,我们讲述了如何创建一个Javaweb应用,并使用Filebeat来收集应用所生成的日志。在今天的文章中,我来详述如何收集应用的指标,使用APM来监控应用并监督web服务的在线情况。源码可以在地址 https://github.com/liu-xiao-guo/java_observability 进行下载。摄入指标指标被视为可以随时更改的时间点值。当前请求的数量可以改变任何毫秒。你可能有1000个请求的峰值,然后一切都回到一个请求。这也意味着这些指标可能不准确,你还想提取最小/

  7. 【Java 面试合集】HashMap中为什么引入红黑树,而不是AVL树呢 - 2

    HashMap中为什么引入红黑树,而不是AVL树呢1.概述开始学习这个知识点之前我们需要知道,在JDK1.8以及之前,针对HashMap有什么不同。JDK1.7的时候,HashMap的底层实现是数组+链表JDK1.8的时候,HashMap的底层实现是数组+链表+红黑树我们要思考一个问题,为什么要从链表转为红黑树呢。首先先让我们了解下链表有什么不好???2.链表上述的截图其实就是链表的结构,我们来看下链表的增删改查的时间复杂度增:因为链表不是线性结构,所以每次添加的时候,只需要移动一个节点,所以可以理解为复杂度是N(1)删:算法时间复杂度跟增保持一致查:既然是非线性结构,所以查询某一个节点的时候

  8. 网络编程套接字 - 2

    网络编程套接字网络编程基础知识理解源`IP`地址和目的`IP`地址理解源MAC地址和目的MAC地址认识端口号理解端口号和进程ID理解源端口号和目的端口号认识`TCP`协议认识`UDP`协议网络字节序socket编程接口`sockaddr``UDP`网络程序服务器端代码逻辑:需要用到的接口服务器端代码`udp`客户端代码逻辑`udp`客户端代码`TCP`网络程序服务器代码逻辑多个版本服务器单进程版本多进程版本多线程版本线程池版本服务器端代码客户端代码逻辑客户端代码TCP协议通讯流程TCP协议的客户端/服务器程序流程三次握手(建立连接)数据传输四次挥手(断开连接)TCP和UDP对比网络编程基础知识

  9. 微信小程序开发入门与实战(Behaviors使用) - 2

    @作者:SYFStrive @博客首页:HomePage📜:微信小程序📌:个人社区(欢迎大佬们加入)👉:社区链接🔗📌:觉得文章不错可以点点关注👉:专栏连接🔗💃:感谢支持,学累了可以先看小段由小胖给大家带来的街舞👉微信小程序(🔥)目录自定义组件-behaviors    1、什么是behaviors    2、behaviors的工作方式    3、创建behavior    4、导入并使用behavior    5、behavior中所有可用的节点    6、同名字段的覆盖和组合规则总结最后自定义组件-behaviors    1、什么是behaviorsbehaviors是小程序中,用于实现

  10. 【Java入门】使用Java实现文件夹的遍历 - 2

    遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg

随机推荐