首页 > 技术文章 > 线程问题

liangxr 2020-10-24 16:18 原文

多线程的优缺点
优点:
充分利用多核多cpu的资源,提高cpu的使用率,提高了程序的运行效率。
缺点:
线程数过多会影响性能,操作系统会在线程切换之间增加内存的开销。
存在线程同步和安全问题
可能产生死锁
增加了开发人员的技术难度


线程有几种状态?

一共五种状态:分别是新建,就绪,运行,阻塞和死亡状态。详细见下图:

新建状态:当用new创建一个线程时,线程还没有开始运行,此时线程处于新建状态。处于新建状态的线程还没有开始运行。
就绪状态:一个新建的线程并不会自动运行,要执行线程,要手动调用线程的start()方法,当start()方法返回后,线程就处于就绪状态,等待处理器的调度。
运行状态:当线程获取了CPU的时间后,它才进入运行状态,真正的执行run()方法里的内容。
阻塞状态:线程运行过程中,可能因为各种原因进入阻塞状态:比如调用sleep()进入休眠状态;调用一个在IO上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者;等待获取锁被阻塞;线程在等待其他的触发条件。所谓的阻塞状态就是正在运行的线程没有运行结束,暂时让出CPU资源。
死亡状态:有两个原因会导致线程死亡:run()方法正常结束;一个未捕获的异常终止了run()方法而导致线程猝死。


创建线程的几种方法
1、继承Thread类,重写run()方法,利用Thread.start()启动线程。
2、实现Runnable接口,重写run()方法,通过new Thread(Runnable a)创建线程,并调用start()方法启动线程。
3、通过callable和futuretask创建线程,实现callable接口,重写call方法,利用future对象包装callable实例,通过new Thread方法创建线程。
4、通过线程池创建线程。


sleep和wait方法的区别
1、wait只能在synchronized中调用,属于对象级别的方法,sleep不需要,属于Thread的方法
2、调用wait方法会释放锁,sleep不会释放锁
3、wait超时之后线程进入就绪状态,等待获取cpu继续执行。


yield和join区别
1、yield会释放cpu资源,不会释放锁,让当前线程进入就绪状态,只能使同优先级或更高优先级的线程有执行的机会。
2、join会释放cpu资源和锁,底层是wait()方法实现的,join会等待调用join方法的线程执行完成之后再继续执行。


start 和 run 方法解释 

  1. start():用start()方法来启动线程,真正实现了多线程运行,这时无需等待run()方法体代码执行完毕而直接继续执行下面的代码。通过调用Thread类的start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里方法 run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。
  2. run(): run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。总结:调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里执行。这两个方法应该都比较熟悉,把需要并行处理的代码放在run()方法中,start()方法启动线程将自动调用 run()方法,这是由jvm的内存机制规定的。并且run()方法必须是public访问权限,返回值类型为void。
    多线程就是分时利用CPU,宏观上让所有线程一起执行,也叫并发。

线程池
1、简单理解,就是一个管理线程的池子。
2、它帮我们管理线程,避免增加创建线程和销毁线程的资源损耗。因为线程本身也是一个对象,创建需要
经过类加载,回收也要GC回收。
3、它提高了响应速度。如果任务到达了,相对于从线程池拿线程,重新创建线程执行要慢很多。
4、重复利用。线程用完,再返回池子,可以达到重复利用的效果,节省资源。

线程池可以通过ThreadPoolExecutor来创建,看下构造函数:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)

几个核心参数的作用:
corePoolSize:核心线程的数量
maximumPoolSize:线程池中最大的线程数量
keepAliveTime:线程池中非核心线程空闲的存活时间
TimeUnit:线程空闲存活时间的时间单位
workQueue:存放任务的阻塞队列
threadFactory:用于创建核心线程的线程工厂,可以给创建的线程自定义名字,方便查日志
handler:线程池的饱和策略(拒绝策略),有四种类型。


什么是死锁
当线程A持有独占锁a,并尝试去获取独占锁b的同时,线程B持有独占锁b,并尝试获取独占锁a的情况下,就会发生AB两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。


怎么防止死锁
尽量使用 tryLock(long timeout, TimeUnit unit)的方法(ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁
尽量使用 Java. util. concurrent 并发类代替自己手写锁
尽量降低锁的使用粒度,尽量不要几个功能用同一把锁
尽量减少同步的代码块


说一下乐观锁和悲观锁
乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据。
悲观锁:每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻止,直到这个锁被释放。
数据库的乐观锁需要自己实现,在表里面添加一个 version 字段,每次修改成功值加 1,这样每次修改的时候先对比一下,自己拥有的 version 和数据库现在的 version 是否一致,如果不一致就不修改,这样就实现了乐观锁。

推荐阅读