多线程

发布时间 2023-09-25 21:38:09作者: zhanggangde

学习多线程我们要先明白进程与线程

  进程就是在内存中正在运行的程序,就跟我们手机上一个个正在运行的软件一样.

  线程:线程是进程的最小执行单元,一个进程中最少拥有一个线程,线程就相当于手机软件中的一个个软件

线程创建的方式(共四种)

  第一种是继承Thread类,重写run方法

  第二种是实现Runnable接口,实现run方法

  第三种是Callable接口实现call方法结合FutrueTask来创建线程,可以返回结果

  第四种是线程池创建线程:线程词共有七大参数,分别是核心线程数,最大线程数,阻塞队列,临时线程存活时间,临时线程存活时间单位,线程工厂,拒绝策略

拒绝策略又有四种,分别是

  AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 (默认)

  DiscardPolicy:丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。

  DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。

  CallerRunsPolicy:由调用线程处理该任务

线程池创建设置这些参数一般是根据实际情况来设置的,创建时里面并没有线程,任务提交过来后才会创建线程.

这四种创建方式究其底层都是实现Runnable接口实现的。

继承Thread 类和实现Runnable 接口的区别

  因为java是单继承的因此如果是采用的是继承Thread的方法,那么在以后进行代码重构的时候可能会遇到问题,因为你无法继承别的类了。其次,如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享

实现Runable接口和实现Callable接口有什么区别 

  实现Runable接口没有返回值,实现Callable接口有返回值,在Runnable中,我们无法对run()方法抛出的异常进行任何处理。但在Callable中,自定义的call()方法可以抛出一个checked Exception,并由其执行者Handler进行捕获并处理。Runnable适用于那些不需要返回值,且不会抛出checked Exception的情况,比如简单的打印输出或者修改一些共享的变量。Callable适用于那些需要返回值或者需要抛出checked Exception的情况,比如对某个任务的计算结果进行处理,或者需要进行网络或IO操作等。在Java中,常常使用Callable来实现异步任务的处理,以提高系统的吞吐量和响应速度。

线程池的工作原理

  当有任务提交时,会先判断是否达到核心线程数量,如果没达到会创建一个工作线程来执行任务,达到了会去查看工作队列是否已满,没满,则会将提交的任务存储在工作队列里,工作队列满了,查看是否达到最大线程数,没达到创建一个新的工作线程来执行任务,达到了,会执行拒绝策略来处理任务.

线程的状态共有五种分别时,

新建(new):新创建了一个线程对象,但还没有调用start()方法。

就绪(runnable):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。

运行(running):就绪状态的线程在获得CPU时间片后变为运行中状态

阻塞(blocker):进入该状态的线程需要等待其他线程做出一些特定动作,或者是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态

死亡(dead):表示线程已经结束或者异常退出

多线程的数据安全问题:

  多线程下共享数据不安全,解决安全问题我们一般会给共享数据加锁.一般我们使用synchronized关键字和Lock接口来保证共享数据的安全

  synchronized关键字:synchronized关键字可以加在类上,也可以加在代码快上,普通同步方法,锁是当前实例对象 this,静态同步方法,锁是当前类的class对象,同步代码块,锁是括号里面的对象【必须共享】

  lock:一般使用ReentrantLock类做为锁。在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。

Synchronized 与 Lock 的区别

  Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现,Lock是代码层面的实现

  synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。)

 

  lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断;

  Lock可以通过trylock来知道有没有获取锁,而synchronized不能;

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

  性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择

  synchronized使用Object对象本身的wait 、notify、notifyAll调度机制,而Lock可以使用Condition进行线程之间的调度

  synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。

  而Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作

死锁产生的原因:

  1、系统资源不足;2、进程运行推进的次序不合适;3、资源分配不当。4、如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。其次,进程运行推进顺序与速度不同,也可能产生死锁。

死锁的四个必要条件:

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

一、避免死锁

避免死锁的方法是,在进程运行之前就预测可能出现的死锁,并采取措施避免其发生。具体方法如下:

  1、破坏互斥条件:使某些资源不排他地分配给进程,即允许多个进程同时访问某些资源。

  2、破坏不可抢占条件:若进程获得了某些资源,但其又请求其他资源而无法满足时,可以释放已获得的资源以避免死锁。

  3、破坏占有且申请条件:当进程请求资源无法满足时,可以释放该进程已占有的资源。

  4、破坏循环等待条件:规定所有进程对资源的请求必须按照一定的次序进行。

二、检测死锁

  如果无法避免死锁,就需要检测死锁。检测死锁的方法是通过检查系统的状态来判断是否存在死锁。有两种基本方法:

  1、死锁预防算法:在进行资源分配时,可以通过动态规划算法等手段,判断资源分配的合理性,从而避免死锁的发生。

  2、死锁避免算法:在进行资源分配时,可以根据预测的数据,通过安全线算法等手段避免死锁的发生。

三、解除死锁

  如果检测到死锁已经发生,就需要立即解除死锁。常用的解除死锁的方法有:

  1、抢占资源:即操作系统强制释放某些进程所占用的资源,从而打破死锁。

  2、回滚进程:回滚是指将某些进程运行的时间推回到某个时间点,释放其所占用的资源,从而打破死锁。

  3、杀死进程:采用杀死进程的方法来打破死锁。

线程通信:

  同步通信:wait、notify、notifyAll,jion方法,通过 volatile 关键字

  异步通信:消息中间件

  进程间的通信:http、feign,socket,mq

多线程并发:

  线程安全的特性:

    原子性:一个线程操作是不能被其他线程打断

    有序性:线程在执行程序是有序的

    可见性:一个线程修改数据后,对其他线程是可见的

我们一般用volatile关键字多线程并发的线程安全问题:

  保证可见性,可以在一个线程修改了共享数据时对其他线程可见,其原理就是修改了变量副本值后及时同步到主内存,其他线程从主内存中获取

  保证有序性,会禁止指令重排:指令重排:Java代码翻译成class文件后,最终在JVM执行时,是把class翻译一个个的指令进行执行的。而CPU为了程序的执行性能,会对指令进行重新排序也就是说万一翻译后的指令是123,那么重排后的指令可能就是213。在多线程情况下,就会出现变量的可见性问题

volatile是根据内存屏障来保证可见性和有序性的,内存屏障

其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性,但volatile无法保证原子性。内存屏障之前的所有写操作都要回写到主内存,内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)。 因此重排序时,不允许把内存屏障之后的指令重排序到内存屏障之前因为,volatile 变量的写操作和读操作之间是可以被中断的,这意味着在读取或者修改 volatile 变量的过程中,其他线程可能会对这个变量进行修改。因此,使用 volatile 变量并不能保证对变量的操作是原子性的