黑马程序员技术交流社区

标题: 【西安校区】多线程知识点整理 [打印本页]

作者: 逆风TO    时间: 2020-4-22 10:37
标题: 【西安校区】多线程知识点整理
写个博客记录一下知识,方便以后复习用到。大多数来自网上
(https://www.cnblogs.com/toria/p/11234323.html),并对其中一些忘了的东西做了补充,
进程:在操作系统中能够独立运行,并且作为资源分配的基本单位。它表示运行中的程序。系统运行一个程序就是一个进程从创建、运行到消亡的过程。
线程:是一个比进程更小的执行单位,能够完成进程中的一个功能,也被称为轻量级进程。一个进程在其执行的过程中可以产生多个线程。
2.上下文切换
即使单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程是同时执行的。(时间片一般是几十毫秒)
  CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到加载的过程就是一次上下文切换。上下文切换会影响多线程的执行速度。
3、并发与并行?
并发指的是多个任务交替进行,并行则是指真正意义上的“同时进行”。
  实际上,如果系统内只有一个CPU,使用多线程时,在真实系统环境下不能并行,只能通过切换时间片的方式交替进行,从而并发执行任务。真正的并行只能出现在拥有多个CPU的系统中。
4 什么是线程死锁?如何避免死锁,死锁产生?
 多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
避免死锁的几个常见方法:
避免一个线程同时获取多个锁
避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
尝试使用定时锁,使用 lock.tryLock(timeout) 来代替使用内部锁机制。
对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
5. sleep() 方法和 wait() 方法区别和共同点?
相同点:
  两者都可以暂停线程的执行,都会让线程进入等待状态。
不同点:
sleep()方法没有释放锁,而 wait()方法释放了锁。
sleep()方法属于Thread类的静态方法,作用于当前线程;而wait()方法是Object类的实例方法,作用于对象本身。
执行sleep()方法后,可以通过超时或者调用interrupt()方法唤醒休眠中的线程;执行wait()方法后,通过调用notify()或notifyAll()方法唤醒等待线程。

为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
new 一个 Thread,线程进入初始状态;调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结: 调用 start 方法可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。

多线程原子性,可见性,有序性
原子性:
是指一个操作是不可中断的。即使是多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
比如,对于一个静态全局变量int i,两个线程同时对它赋值,线程A给他赋值为1,线程B给他赋值为-1。那么不管这两个线程以何种方式。何种步调工作,i的值要么是1,要么是-1.线程A和线程B之间是没有干扰的。这就是原子性的一个特点,不可被中断。
可见性:
是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。显然,对于串行来说,可见性问题是不存在的。
有序性:
在并发时,程序的执行可能会出现乱序。给人的直观感觉就是:写在前面的代码,会在后面执行。有序性问题的原因是因为程序在执行时,可能会进行指令重排,重排后的指令与原指令的顺序未必一致。
synchronized 能够实现代码的原子性(同步)
volatile 保证变量的可见性,不能保证变量的符合操作原子性。

8、多线程开发带来的问题与解决方法?(重要)
使用多线程主要会带来以下几个问题:
(一)线程安全问题
  线程安全问题指的是在某一线程从开始访问到结束访问某一数据期间,该数据被其他的线程所修改,那么对于当前线程而言,该线程就发生了线程安全问题,表现形式为数据的缺失,数据不一致等。
  线程安全问题发生的条件:
    1)多线程环境下,即存在包括自己在内存在有多个线程。
    2)多线程环境下存在共享资源,且多线程操作该共享资源。
    3)多个线程必须对该共享资源有非原子性操作。

线程安全问题的解决思路:

1)尽量不使用共享变量,将不必要的共享变量变成局部变量来使用。
    2)使用synchronized关键字同步代码块,或者使用jdk包中提供的Lock为操作进行加锁。
    3)使用ThreadLocal为每一个线程建立一个变量的副本,各个线程间独立操作,互不影响。
(二)性能问题
  线程的生命周期开销是非常大的,一个线程的创建到销毁都会占用大量的内存。同时如果不合理的创建了多个线程,cup的处理器数量小于了线程数量,那么将会有很多的线程被闲置,闲置的线程将会占用大量的内存,为垃圾回收带来很大压力,同时cup在分配线程时还会消耗其性能。
  解决思路:
  利用线程池,模拟一个池,预先创建有限合理个数的线程放入池中,当需要执行任务时从池中取出空闲的先去执行任务,执行完成后将线程归还到池中,这样就减少了线程的频繁创建和销毁,节省内存开销和减小了垃圾回收的压力。同时因为任务到来时本身线程已经存在,减少了创建线程时间,提高了执行效率,而且合理的创建线程池数量还会使各个线程都处于忙碌状态,提高任务执行效率,线程池还提供了拒绝策略,当任务数量到达某一临界区时,线程池将拒绝任务的进入,保持现有任务的顺利执行,减少池的压力。
(三)活跃性问题
  1)死锁,假如线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。多个线程环形占用资源也是一样的会产生死锁问题。
  解决方法:
避免一个线程同时获取多个锁
避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
尝试使用定时锁,使用 lock.tryLock(timeout) 来代替使用内部锁机制。
  想要避免死锁,可以使用无锁函数(cas)或者使用重入锁(ReentrantLock),通过重入锁使线程中断或限时等待可以有效的规避死锁问题。
  2)饥饿,饥饿指的是某一线程或多个线程因为某些原因一直获取不到资源,导致程序一直无法执行。如某一线程优先级太低导致一直分配不到资源,或者是某一线程一直占着某种资源不放,导致该线程无法执行等。
  解决方法:
  与死锁相比,饥饿现象还是有可能在一段时间之后恢复执行的。可以设置合适的线程优先级来尽量避免饥饿的产生。
  3)活锁,活锁体现了一种谦让的美德,每个线程都想把资源让给对方,但是由于机器“智商”不够,可能会产生一直将资源让来让去,导致资源在两个线程间跳动而无法使某一线程真正的到资源并执行,这就是活锁的问题。
四)阻塞
  阻塞是用来形容多线程的问题,几个线程之间共享临界区资源,那么当一个线程占用了临界区资源后,所有需要使用该资源的线程都需要进入该临界区等待,等待会导致线程挂起,一直不能工作,这种情况就是阻塞,如果某一线程一直都不释放资源,将会导致其他所有等待在这个临界区的线程都不能工作。当我们使用synchronized或重入锁时,我们得到的就是阻塞线程,如论是synchronized或者重入锁,都会在试图执行代码前,得到临界区的锁,如果得不到锁,线程将会被挂起等待,知道其他线程执行完成并释放锁且拿到锁为止。
  解决方法:
  可以通过减少锁持有时间,读写锁分离,减小锁的粒度,锁分离,锁粗化等方式来优化锁的性能。
临界区:
  临界区是用来表示一种公共的资源(共享数据),它可以被多个线程使用,但是在每次只能有一个线程能够使用它,当临界区资源正在被一个线程使用时,其他的线程就只能等待当前线程执行完之后才能使用该临界区资源。
读写锁设计模式:同一时间对资源只能读或者写,读取可以多线程,写只能一个线程

2 阻塞和非阻塞
通常用来形容多线程之间的相互影响.比如一个线程占用了临界区资源,那么其他所有需要这个资源的线程就必须在这个临界区进行等待,导致线程挂起,这就是阻塞.
非阻塞与之相反,强调没有一个线程可以妨碍其他线程执行,所有线程都会尝试不断地前向执行.

9、 synchronized 关键字
synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
synchronized关键字最主要的三种使用方式:修饰实例方法:、修饰静态方法、修饰代码块。
对于普通同步方法,锁是当前实例对象。
对于静态同步方法,锁是当前类的Class对象。
对于同步代码块,锁是synchronized括号里配置的对象。

10、synchronized与lock
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;

2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

4)通过Lock可以知道有没有成功获取锁(tryLock()方法:如果获取锁成功,则返回true),而synchronized却无法办到。

5)Lock可以提高多个线程进行读操作的效率。

在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
synchronized 关键字和 volatile 关键字的区别
volatile关键字是线程同步的轻量级实现,所以volatile性能比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。
多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞。、
volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。
volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。

15、使用线程池的好处?
降低资源消耗。通过重复利用已创建的线程,降低线程创建和销毁造成的消耗。
提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

乐观锁与悲观锁
悲观锁:
当我们要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制在修改数据之前锁定,再修改的方式被称为悲观并发控制(PCC)
之所以叫做悲观锁,是因为抱有悲观的态度去修改数据的并发控制方式,认为数据并发修改的概率比较大,所以需要在修改之前先加锁。
悲观并发控制实际上是 “先取锁,再访问” 的保守策略,为数据处理的安全提供了保证。
悲观锁的实现方式:悲观锁的实现,依靠数据库提供的锁机制。在数据库中,悲观锁的流程如下:
在对数据修改前,尝试增加排他锁。
加锁失败,意味着数据正在被修改,进行等待或者抛出异常。
加锁成功,对数据进行修改,提交事务,锁释放。
如果我们加锁成功,有其他线程对该数据进操作或者加排他锁的操作,只能等待或者抛出异常。

乐观锁
乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测。
相对于悲观锁,在数据库进行处理的时候,乐观锁不会使用数据库提供的锁机制,一般是增加 version 参数,记录数据版本
数据版本,为数据增加的一个版本标识。当读取数据时,将版本标识的值一同读出,数据每更新一次,同时对版本标识进行更新。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的版本标识进行比对,如果数据库表当前版本号与第一次取出来的版本标识值相等,则予以更新,否则认为是过期数据。
乐观并发控制相信事务之间的数据竞争概率非常小,因此尽可能直接操作,提交的时候才去锁定,不会产生任何锁和死锁。

多线程锁机制:
锁相关概念:
多线程:指的是这个程序(一个进程)运行时产生了不止一个线程。
并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。
并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力。
线程安全:经常用来描绘一段代码。指在并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响任何结果。这个时候使用多线程,我们只需要关注系统的内存,cpu是不是够用即可。反过来,线程不安全就意味着线程的调度顺序会影响最终结果,如不加事务的转账代码:
同步:Java中的同步指的是通过人为的控制和调度,保证共享资源的多线程访问成为线程安全,来保证结果的准确。如上面的代码简单加入@synchronized关键字。在保证结果准确的同时,提高性能,才是优秀的程序。线程安全的优先级高于性能。

转载自CSDN






欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) 黑马程序员IT技术论坛 X3.2