四、多线程和锁

 

目录

进程和线程

线程和进程的区别?

创建线程有哪几种方式?

线程模型

线程有哪些状态?

什么是线程安全?如何保证线程安全?

创建线程池有哪几种方式?

线程池参数和各个参数的意义

线程池都有哪些状态?

run 方法和 start 方法区别

sleep 和 wait 方法区别

notify() 和 notifyAll() 的区别

锁模块

什么是死锁?产生的条件是什么?

乐观锁与悲观锁

多线程锁的升级原理是什么(锁膨胀)?

ThreadLocal 是什么?有哪些使用场景?

volatile 有什么用?

synchronized 和 volatile 的区别是什么?

synchronized 底层实现原理

synchronized 在静态方法和普通方法的区别

synchronized和ReentrantLock的区别

synchronized和lock的区别

Lock 实现锁的底层原理?

说一下 atomic 的原理?


进程和线程

线程和进程的区别?

  1. 进程是资源分配的最小单位,线程是 CPU 调度的最小单位。
  2. 进程有自己的独立地址空间,线程没有
  3. 进程和线程通信方式不同 。 (线程之间的通信比较方便。同一进程下的线程共享数据,比如全局变量,静态变量。wait();是调用其的线程进入阻塞状态。并释放锁。notify();唤醒被阻塞的线程。如果有多个线程,就唤醒优先级高的那个。notifyAll();唤醒所有被阻塞的线程。)
  4. 进程上下文切换开销大,线程开销小;对进程进程操作一般开销都比较大,对线程开销就小了
  5. 一个进程挂掉了不会影响其他进程,而线程挂掉了会影响其他线程。

创建线程有哪几种方式?

  1. 继承 java.lang.Thread 类
  2. 实现Runnable接口来重写run()方法实现线程。
  3. 实现Callable接口并重写call()方法。

实现Runnable的方式更好一点。避免单继承局限性,灵活方便,方便同一个对象被多个线程使用。

解实现Callable接口创建多线程比实现Runnable接口创建多线程的方式更强大:

① call()方法可以有返回值
② call()方法可以抛出异常,被外面的操作捕获,然后处理异常
③ call()方法支持泛型

线程模型

用户线程:线程是由其依托的平台(APP)来创建、调度和管理线程,不需要用户态/内核态的切换,速度快

内核线程:所有线程的创建和管理、以及线程的上下文切换中间状态的信息,都是依赖操作系统去管理的

线程有哪些状态?

什么是线程安全?如何保证线程安全?

线程安全:就是多线程访问同一代码,不会产生不确定结果。(比如死锁)
如何保证呢:

1使用线程安全的类
2使用synchronized同步代码块,或者用Lock锁(实现线程安全的控制中,比较常用的是ReentrantLock)
3多线程并发情况下,线程共享的变量改为方法局部级变量

创建线程池有哪几种方式?

  • 快捷创建线程池很简单,只需要调用 Executors 中相应的静态工厂方法即可
  • 使用 ThreadPoolExecutor 创建线程池

Java通过Executors提供四种线程池

  • newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  • newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  • newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
  • newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

线程池参数和各个参数的意义

  • corePoolSize: 线程池中核心线程数量(经常干活的线程的数量),后续简称为coreSize

  • maximumPoolSize当前线程数量等于coreSize并且queue也满了,这时候就得额外创建线程了,创建之后线程池中的最大线程数就是由这个值决定的,后续简称为maxSize

  • keepAliveTime空闲线程能存活的时间数值

  • workQueue当前线程数量等于coreSize的时候,新来的任务保存的地方,等着有空闲线程的时候再执行,后续简称为queue

  • unit空闲线程能存活的时间单位

  • threadFactory:创建线程的工厂,可以指定线程的名字啊等等

  • handler当前线程数等于maxSize,并且queue也满了,对于新来的任务的处理策略(是丢掉呢还是抛出异常呢)

线程池都有哪些状态?

RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。线程池的初始化状态是 RUNNING。线程池被一旦被创建,就处于 RUNNING 状态,并且线程池中的任务数为 0。
SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。调用线程池的 shutdown() 方法时,线程池由 RUNNING->SHUTDOWN。
STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。调用线程池的 shutdownNow() 方法时,线程池由 (RUNNING or SHUTDOWN)->STOP。
TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行构造方法 terminated()。因为 terminated() 在 ThreadPoolExecutor 类中是空的,所以用户想在线程池变为 TIDYING 时进行相应的处理;可以通过重载 terminated() 函数来实现。
TERMINATED:线程池处在 TIDYING 状态时,执行完 terminated() 之后,就会由 TIDYING ->TERMINATED。
注意:
当线程池在 SHUTDOWN 状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN->TIDYING。
当线程池在 STOP 状态下,线程池中执行的任务为空时,就会由 STOP->TIDYING。

run 方法和 start 方法区别

start 方法是启动线程的方法,调用线程的 start 方法之后,线程就会进入就绪状态

run 方法并不会启动线程,是线程实现逻辑的方法,只是相当于普通的方法调用

sleep 和 wait 方法区别

sleep 是 Thread 中的方法,wait 是 Object 中的方法

sleep 必须指定时间,wait 可以不指定时间,不指定代表当前线程阻塞到持有锁的线程完成任务

sleep 可以写在任何地方,wait 只能写在同步代码块中

sleep 释放 CPU 的使用权,但是不释放锁;wait 释放 CPU 的使用权,同时也释放锁。

notify() 和 notifyAll() 的区别

notify()方法不能唤醒某个具体的线程,所以只有一个线程在等待的时候它才有用武之地。

notifyAll()唤醒所有线程并允许他们争夺锁,确保了至少有一个线程能继续运行。

锁模块

什么是死锁?产生的条件是什么?

多个线程因竞争资源而造成的互相等待,没有外力作用则一直等待下去。

死锁产生的四个条件(有一个条件不成立,则不会产生死锁)

  • 互斥条件:一个资源一次只能被一个进程使用
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得资源保持不放
  • 不剥夺条件:进程获得的资源,在未完全使用完之前,不能强行剥夺
  • 循环等待条件:若干进程之间形成一种头尾相接的环形等待资源关系

如何避免:避免死锁最简单的方法就是阻止循环等待条件,将系统中所有的资源设置标志位、排序,规定所有的进程申请资源必须以一定的顺序(升序或降序)做操作来避免死锁。

乐观锁与悲观锁

悲观锁,顾名思义,悲观的认为在数据处理过程中极有可能存在修改数据的并发事务,于是将处理的数据设置为锁定状态。

实现方式:Java synchronized就属于悲观锁的一种实现, 每次线程要修改数据时都先获得锁,保证同时刻只有一个线程能操作数据, 其他线程则会被block。(先获取锁,再进行业务操作

乐观锁,顾名思义,对并发事务持乐观态度(认为对数据的并发操作不会经常性的发生),只有在数据进行提交更新时候才会对数据冲突与否进行检测,如果发现冲突了,则返回错误信息,让用户决定如何去做。它通过更加宽松的锁机制,来解决由于悲观锁排他性的数据访问对系统性能造成的严重影响。

实现方式:使用版本号实现,它是为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的“version” 字段来实现。当读取数据时,将version字段的值一同读出, 数据每更新一次,对此version值+1。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果相等,则予以更新,否则认为是过期数据。

CAS:乐观锁的一种实现

包括三种操作数,内存位置(V),预期原值(A)和新值(B)。

当V的值和A相匹配时,才会将V的值设为B,如果不匹配就不做任何操作。

存在的问题:

ABA问题:线程1把A取出来,线程2把A改成B又改成A,线程1认为A还是A。
解决方法:版本号

多线程锁的升级原理是什么(锁膨胀)?

锁的级别从低到高:无锁—偏向锁—轻量级锁—重量级锁

无锁:不加锁,所有线程都可以对资源进行访问,失败则重试
偏向锁:适用于必须同步但是只有一个线程执行的代码块,虚拟机检测到同步的代码块只有一个线程持续执行,就会加上偏向锁。减少非竞争条件的同步代码,提升性能。
轻量级锁:只有一个线程执行同步代码块的时候会加上偏向锁,当第二个线程加入竞争, 偏向锁就会升级会轻量级锁,第二个线程不会阻塞,而是通过自旋等待第一个线程释放锁。第二个线程自旋(也就是 CPU 空转)可以避免用户态和内核态的转换,提升性能。
重量级锁:当第三个线程加入竞争,轻量级锁就会升级为重量级锁,线程会阻塞等待, 直到线程释放锁

ThreadLocal 是什么?有哪些使用场景?

ThreadLocal 是线程的局部变量, 是每一个线程所单独持有的,其他线程不能对其进行访问。

当使用 ThreadLocal 维护变量的时候,为每一个使用该变量的线程提供一个独立的变量副本,因此他们使用的都是自己从内存中拷贝过来的变量的副本,这样就不存在线程安全问题,也不会影响程序的执行性能。

最常见的 ThreadLocal 使用场景为: 数据库连接、 Session 管理等

synchronized 对线程共享的数据加锁,让线程以有序的方式访问该数据。ThreadLocal 是为每一个线程在内存中开辟一份私有空间,保证数据安全。synchronized 是以时间换空间,ThreadLocal 是以空间换时间

volatile 有什么用?

volatile 关键字的作用:

  • 保证被修饰的变量对所有线程是可见的 
  • 禁止指令重排序(被修饰的变量不会被缓存在寄存器中或者对其他处理器不可见的地方,因此在读取volatile修饰的变量时总是会返回最新写入的值)

synchronized 和 volatile 的区别是什么?

  • volatile只能修饰实例变量和类变量,而synchronized可以修饰方法以及代码块。
  • volatile只能保证数据的可见性,但是不保证原子性,synchronized是一种排它机制,可以保证原子性。
  • volatile是一种轻量级的同步机制,在访问volatile修饰的变量时并不会执行加锁操作,线程不会阻塞,使用synchronized加锁会阻塞线程。

synchronized 底层实现原理

synchronized 是由一对 monitorenter/monitorexit 指令实现的,monitor 对象是同步的基本实现单元。在 JVM 处理字节码会出现相关指令。

代码块的同步:利用 monitorenter 和 monitorexit 这两个字节码指令,它们分别位于同步代码块的开始和结束位置。当 jvm 执行到 monitorenter 指令时,当前线程试图获取 monitor 对象的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器 + 1;当执行 monitorexit 指令时,锁计数器 -1;当锁计数器为 0 时,该锁就被释放了。 如果获取 monitor 对象失败,该线程则会进入阻塞状态,直到其他线程释放锁。

方法级的同步:是隐式的,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM 可以从方法常量池中的方法表结构 (method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有 monitor(虚拟机规范中用的是管程一词),然后再执行方法,最后再方法完成 (无论是正常完成还是非正常完成) 时释放 monitor。

对象锁
1、同步代码块
synchronized(this |Object)
2、同步非静态方法
synchronized method

类锁
1、同步代码块
synchronizd(class)
2、同步静态方法
synchronized static method

synchronized 在静态方法和普通方法的区别

synchronized修饰不加static的方法,锁是加在单个对象上,不同的对象没有竞争关系;

修饰加了static的方法,锁是加载类上,这个类所有的对象竞争一把锁。

synchronized和ReentrantLock的区别

  • synchronized是隐式锁,ReentrantLock是显示锁,使用时必须在finally代码块中进行释放锁操作
  • synchronized是非公平锁,ReentrantLock可以实现公平锁
  • ReentrantLock可响应中断,可轮回,为处理锁提高了更多灵活性
  • synchronized是关键字,ReentrantLock是类
  • synchronized是采用悲观并发策略,ReentrantLock是采用乐观并发策略,会先尝试以CAS方式获取锁

synchronized和lock的区别

  1. Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放
  2. Lock只有代码块锁,synchronized有代码块锁和方法锁
  3. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

使用顺序:Lock>同步代码块(已经进入了方法体,分配了相应资源)>同步方法(在方法体之外)

synchronized 有什么不足之处?

  • 如果临界区是只读操作,其实可以多线程一起执行,但使用 synchronized 的话,同一时间只能有一个线程执行
  • synchronized 无法知道线程有没有成功获取到锁
  • 使用 synchronized,如果临界区因为 IO 或者 sleep 方法等原因阻塞了,而当前线程又没有释放锁,就会导致所有线程等待

Lock 接口有什么优势?

  • 可以使锁更公平
  • 可以使线程在等待锁的时候响应中断
  • 可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
  • 可以在不同的范围,以不同的顺序获取和释放锁

整体上来说 Lock 是 synchronized 的扩展版,Lock 提供了无条件的、可轮询的(tryLock 方法)、定时的(tryLock 带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition 方法)锁操作。另外 Lock 的实现类基本都支持非公平锁(默认)和公平锁,synchronized 只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择。

Lock 实现锁的底层原理?

  • Lock 锁的底层实现是 AQS,AQS(AbstractQuenedSynchronizer ),抽象的队列式同步器,除了 java 自带的 synchronized 关键字之外的锁机制。
  • AQS 是将每一条请求共享资源的线程封装成一个 CLH 锁队列(该队列是一个双向链表,没有实现)的一个结点(Node),将暂时获取不到锁的线程加入到队列中来实现锁的分配。AQS 基于 CLH 队列,用 volatile 修饰共享变量 state,线程通过 CAS 去改 变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。

Lock 的实现过程

  • lock 的存储结构:一个 int 类型状态值(用于锁的状态变更),一个双向链表(用于 存储等待中的线程)
  • lock 获取锁的过程:本质上是通过 CAS 来获取状态值修改,如果当场没获取到,会将该线程放在线程等待链表中。
  • lock 释放锁的过程:修改状态值,调整等待链表。

可以看到在整个实现过程中,lock 大量使用 CAS+ 自旋。因此根据 CAS 特性,lock 建议 使用在低锁冲突的情况下。目前 java1.6 以后,官方对 synchronized 做了大量的锁优化(偏 向锁、自旋、轻量级锁)。因此在非必要的情况下,建议使用 synchronized 做同步操作。

说一下 atomic 的原理?

  • Atomic 包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基 本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。
  • Atomic 系列的类中的核心方法都会调用 unsafe 类中的几个本地方法。因此 atomic 证原子性就是通过:自旋 +CAS(乐观锁)
  • Lock 类和 Atomic 包底层实现都是通过 CAS + 自旋的方式解决多线程同步问题。 Atomic 在竞争激烈时能维持常态,比 lock 性能好,但是只能同步一个变量。
     
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值