多线程、进程、线程五种状态、synchronized、volatile、Lock、CAS、死锁、ThreadLocal

发布于:2024-05-24 ⋅ 阅读:(140) ⋅ 点赞:(0)

1、并发编程

并发编程三要素

  • 原子性:只一个操作要么全部成功,要么全部失败
  • 可见性:一个线程对共享变量的修改,其他线程能够立刻看到
  • 有序性:程序执行的顺序按照代码的先后顺序执行

synchronized,Lock解决原子性问题
volatile,,Lock解决可见性和有序性问题

2、线程和进程的区别

  • 根本区别:进程是操作系统资源分配的最小单位;线程是处理任务调度和执行的最小单位
  • 资源开销:每个进程都有单位的空间,进程间的切换有较大的开销;线程是轻量级的进程,同一类线程共享数据空间,每个线程有独立的运行栈和程序计数器,线程之间的切换开销较小
  • 包含关系:进程包含线程
  • 内存分配:线程共享地址空间和资源,进程之间的地址空间和资源相互独立
  • 影响关系:一个进程崩溃,在保护模式下其他进程不受影响;一个线程崩溃,其他线程也会受影响,整个进程都会崩溃。多进程要比多线程更加健壮
  • 执行关系:进程有独立的执行入口,线程不能独立执行,必须依赖进程

上下文切换:任务从保存到再加载的过程就是一次上下文切换。

3、创建线程的四种方式

  • 继承Thread
  • 实现Runnable接口
  • 实现Callable接口
  • 使用Executors工具类创建线程池

Runnable和Callable区别

相同点:

  • 都是接口;
  • 都可以编写多线程程序;
  • 都采用Thread.start()启动线程

不同点

  • Runnable接口的run()方法没有返回值 ;Callable()接口的call()方法有返回值,是个泛型
  • Runnable接口run()方法只能抛出运行时异常,且无法捕捉处理;Callable接口call()方法允许抛出异常,可以获取异常信息

线程的run()和start()有何区别

  • start()用于启动线程,run()用于执行线程运行时的代码。run()方法可以重复调用,start()只能调用一次
  • start()方法用于启动线程,真正实现了多线程的运行。调用start()时无需等待run()方法方法体代码执行完毕就可以执行其他代码;此时线程是就绪态,并没有开始运行,然后通过Thread类调用run()方法完成其运行状态,run()方法运行结束,此线程就终止了
  • run()方法是在线程里的,直接调用run()方法,相当于调用了一个普通的函数,必须等待run()方法执行完毕才可以执行下面的代码,所以执行路径还是一条,没有多线程的特征,所以在线程启动时,要调用start()方法而不是run()方法。

Future和FutureTask

  • Callable接口的call()方法有返回值,Future可以拿到异步执行任务(这个任务也许并没有完成)的返回值,并且可以抛出异常信息。
  • FutureTask是Future的具体实现。FutureTask实现了RunnableFuture接口。RunnableFuture接口又同时继承了Future和 Runnable接口。所以FutureTask既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。

4、线程的状态和基本操作

线程的五种状态和生命周期

在这里插入图片描述

  • 新建(new)
  • 可运行/就绪态(runnable):调用start()方法后就处于Runnable态
  • 运行(running):runnable态获取到时间片,就进入running态;就绪态是进入运行态的唯一入口,线程要进入运行态就必须要进入就绪态
  • 阻塞(block)
  • 死亡(dead):死亡的线程不可复生

线程调度的方法

1、wait():使一个线程处于阻塞等待状态,并且释放所持有的对象锁
2、sleep():使役个正在运行的线程处于睡眠状态,是一个静态方法
3、notify():唤醒一个处于等待队列的线程,再调用此方法时,并不能确切的唤醒某个等待的线程,由JVM确定唤醒哪个线程,并且与优先级无关
4、notifyAll():唤醒所有处于等待队列的线程,然后重新竞争锁

wait()和sleep()区别

  • 所在类不同:sleep()是Thread类的静态方法;wait()是Object类的方法
  • 锁:sleep()不释放锁,wait()释放锁
  • 用途:wait()用于线程之间通信;sleep()用于暂停线程执行
  • 用法不同:wait()在结束后,不会自动苏醒,需要别的线程调用同一个对象上的notify()或者notifyAll()去唤醒;而sleep()方法在结束后,自动苏醒。

interrupt()、interrupted()、isInterrupted()

  • interrupt()、isInterrupted()是通过Thread对象调用,是实例方法;interrupted()是通过Thread类调用,是静态方法
  • interrupt()方法,只是通知该线程停止运行,只是通知,并没有直接中断,而是由程序自己决定是否中断

线程类的构造方法、静态代码块是被new这个线程的类的线程所调用的(谁new谁调用),run()方法是自身线程调用

5、synchronized

作用:用来控制线程同步的,被synchronized修饰的代码不能被多个线程同时执行,可以修饰类、方法、变量

synchronized底层原理

synchronized修饰的代码在反编译为字节码文件时,前后都出现了monitor字样,前面出现的是monitorenter,后面出现的是monitorexit,就是释放锁。当执行monitorenter时,当前线程试图获取对象锁所持有的monitor,当计数器为0时,就可以成功获取,当获取到时,计数器+1。并且就算当前线程已经拥有对象锁的monitor的持有权,那么就可以重入这个monitor,重入计数器也会加一。如果其他线程占有monitor持有权,那么当前线程就会阻塞,知道其他线程执行monitorexit,执行后锁释放,计数器设置为0。

自旋

即其他线程不进入阻塞态,而是在synchronized边界循环等待,不断尝试获取锁,这就是自旋

synchronized锁升级

synchronized涉及到用户态和内核态的切换,在1.6之前,锁都是重量级锁,即我们不管什么线程来操作资源,都要进行加锁释放锁,如果有多线程,还要等待之类的,很浪费资源,1.6之后引入了偏向锁与轻量锁来减小获取和释放锁所带来的性能消耗。

锁升级其实就是对synchronized的优化,以前用synchronized修饰一个对象或者是方法,方法也等于是锁住对象,直接用一把操作系统层面的大锁,万一只有少量线程的话会大题小作了,如果大量线程的话又会特别消耗时间,划不来,所以要将以前的二话不说用一把大锁进行优化。

无锁->偏向锁->轻量级锁->重量级锁

原理:在锁对象的对象头有一个threadid字段,第一次访问时threadid为空,JVM让其持有偏向锁,并把threadid设置为线程id,再次进入时只需要判断两个id是否相等,相等就直接进入,不相等就升级为轻量级锁;自旋一段时间后还没有获取到就升级到重量级锁。

synchronized、volatile、CAS区别

  • synchronized是悲观锁,属于抢占式,会引起其他线程阻塞
  • volatile提供多个线程共享变量可见性和禁止指令重排序
  • CAS是基于冲突监测的乐观锁(非阻塞)

synchronized、Lock、ReentrantLock区别

  • synchronized、ReentrantLock都是可重入锁
  • synchronized是关键字,Lock是接口,ReentrantLock是实现了Lock接口的一个类
  • synchronized可以给类、方法、代码块加锁,Lock和ReentrantLock只能给代码块加锁
  • synchronized不用手动获取和释放,发生异常会自动释放锁,不会造成死锁;Lock和ReentrantLock需要手动,没有unLock()就会死锁
  • Lock可以知道是否成功获取到锁,synchronized不行

6、volatile

保证可见性和禁止指令重排序,提供happens-before的保证,确保一个线程的修改对于其他线程是可见的。被volatile修饰的共享变量,当它被修改时,可以将修改的值立即更新到主内存中,其他线程需要读取时,重新去主内存中读取新值
volatile可以保证可见性和禁止重排序,但不能保证原子性;atomic方法可以让这种方法具有原子性

7、Lock体系

Lock是synchronized的扩展版本,Lock提供了无条件的、可轮询的(tryLock方法)、定时的(tryLock带参方法)、可中断的(lockInterruptibly)、可多条件队列的锁操作。Lock的实现基本都支持公平锁和非公平锁,synchronized只支持非公平锁

  • 悲观锁:悲伤的假设最坏的情况,每次拿数据都认为别人会修改,所以在拿的时候就会加锁,别人想拿就阻塞(共享资源每次只给一个线程使用,其他线程阻塞,用完再把资源转让给其他线程)
  • 乐观锁:每次拿数据,不会上锁,直到提交数据时才会证实数据是否被修改(产生并发冲突),多用于多读场景。一般用版本号或者CAS实现

8、CAS

CompareAndSweep——比较并交换
CAS包含三个操作数——内存位置(V)、预期值(A)、拟写入的新值(B)

  • 第一步:比较V和A是否相等
  • 第二步:相等,就把B写入V
  • 第三步:返回boolean类型,表示操作成功

多个线程进行CAS操作时,只有一个线程可以操作成功,其他线程自旋等待

CAS产生的问题

  • ABA问题:从A变到B,再从B变到A,过程不知道;解决办法:引入版本号
  • 循环开销时间大:资源竞争严重时,CAS自旋概率大,浪费CPU
  • 只能保证一个共享变量的原子性操作

9、线程死锁

两个或以上的线程互相持有对方资源并且不主动释放造成的恶性循环

死锁的四个条件

  • 互斥条件:一个资源只能被一个线程占用
  • 请求与保持条件:请求被占用资源而阻塞,不放弃已经获得的资源
  • 不剥夺条件:资源未使用前不能被其他线程强行剥夺
  • 循环等待条件:等待的线程形成了一个死循环

避免死锁

破坏造成死锁四个条件中的一个就行

  • 互斥条件无法破坏
  • 破坏请求与保持条件:一次性申请所有资源
  • 破坏不剥夺条件:申请不到被占用的资源,就主动释放
  • 破坏循坏等待条件:

活锁

没有被阻塞,只是某些条件没满足,导致一直重复尝试、失败、尝试、失败这个过程
活锁有可能自己解开,死锁不能

饥饿

因为种种原因无法获取到所需要的资源,导致一直无法执行

10、ThreadLocal

为线程提供局部变量,保证各个线程里的变量独立于其他线程的变量,也就是说ThreadLocal为每个线程创建一个单独的副本,线程之间不相关,
同步机制是为了保证多线程环境下数据的统一性,而ThreadLocal则是保证多线程环境下数据的独立性

ThreadLocal底层原理

Thread类中有一个ThreadHashMap的数据结构,用来保存线程对象的变量
每个线程的ThreadHashMap都是属于线程自己的,这就保证了每个线程都是独立的,多个操作不会互相影响