一、名词解释
1.线程的五种状态
新建(new):用new关键字和Thread类(包含其子类),可以创建一个线程对象,并开启一块独立的栈内存空间,此时线程便处于新建状态。
就绪(Runnable):新建状态下的线程通过调用start方法可以进入就绪状态,此时的线程已具备在CPU上运行条件,处在线程就绪的队列,当获得CPU分配的时间片后开始运行
运行(Running):就绪队列中的线程,如获得CPU调度,便进入运行状态。运行状态可变为就绪、死亡、阻塞状态。当运行中的线程失去CPU的资源后,会回到就绪状态重新等待分配资源。
阻塞(Blocked):运行中的线程,当被执行某些特定操作后,让出了自己的CPU资源并终端了自己的执行,此时的线程便进入了阻塞状态。当且仅当其重回到就绪状态,才有机会被再次执行。具体分为以下三种阻塞
①等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态。当调用notify()或notifyAll()等方法,则该线程就会重新转入就绪状态
②同步阻塞:线程在获取同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
当获取同步锁成功,则该线程就会重新转入就绪状态。
③其他阻塞:通过调用线程sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,则该线程就会重新转入就绪状态。
死亡(Dead):当Run方法执行完毕,或因异常退出任务执行,线程便结束生命周期,进入死亡状态。如果线程执行了interrupt()或stop()方法,也会以异常退出的方式结束生命周期。
2.线程状态的控制
此处重点关注Thread类的以下几种方法
直接控制方法:start()、interrupt()、join()、sleep()、yield()
间接控制方法:setDaemon()、setPriority()
线程睡眠-sleep():让当前正在执行的线程暂停一段时间,并进入阻塞状态。在指定时间后重回就绪状态。使用时需要捕获异常
线程优先级-getPriority()、setPriority(int newPriority): 线程优先级越高获得CPU分配资源的概率越大,但依然无法保证具体的执行顺序。可通过方法,设置和返回一个指定线程的优先级,其中set方法的参数范围在1-10之间的整数,也可用类内部提供的三个常量。
线程让步-yield():同sleep方法类似,也是让当前正在执行的线程暂停,但不同的是yied方法不会让线程进入阻塞状态,而是直接回到就绪状态,等待CPU重新调度。
线程合并-join():线程的合并就是:线程A在运行期间,可以调用线程B的join()方法,这样线程A就必须等待线程B执行完毕后,才能继续执行。(谁调join方法,谁就先执行)
守护线程-setDaemon(true):调用本方法后,就可以把该线程标记为守护线程,守护线程通常用于执行一些后台作业,例如在你的应用程序运行时播放背景音乐,在文字编辑器里做自动语法检查、自动保存等功能。Java的垃圾回收也是一个守护线程。
3.线程同步
问题:什么是线程安全?
答:根本原因是多个线程对同一成员变量进行操作修改导致(银行取钱案例),生的后果就是“脏读”,也就是取到的数据其实是被更改过的的结果。
问题:如何解决线程安全问题?
答:让并发执行的多个线程在某个时间内只允许一个线程在执行并访问共享数据(排队取钱),java提供了线程同步机制,它能够解决上述的线程安全问题,线程同步的方式有两种:同步代码块和同步方法。如下
同步代码块:编写的方法体比较大,且存在一些比较耗时的操作,而需要同步的代码又只有一小部分,此时可选择同步代码块。(此处同步监视器就是一个“对象”,可理解为一把锁,一般使用this作为同步监视器,也可以专门创建一个对象来作为同步监视器。)
| synchronized (同步监视器) { // 可能会产生线程安全问题的代码 } |
同步方法:当某一个方法中的所有代码都需要同步的时候,此时可选择同步方法。具体区分为静态同步方法和非静态同步方法。
非静态同步方法默认监视器为this,实际是对调用该方法的对象(this)加锁,俗称对象锁
| public synchronized void method() { // 可能会产生线程安全问题的代码 } |
静态同步方法默认同步监视器为“类名.class”。实际上是对该类对象加锁,俗称“类锁”。
| public static synchronized void method() { // 可能会产生线程安全问题的代码 } |
问题:一个线程取得了同步锁,那么在什么时候才会释放掉呢?
答:①当同步代码块或同步方法正常执行完毕后释放。②使用return或 break终止执行或抛出了未处理的异常。③当线程执行同步方法或代码块时,程序执行了同步锁对象的wait()方法。
问题:死锁是啥?会导致什么问题?如何解决?
答:简单的说就是:当A线程等待B线程释放资源,而同时B又在等待A线程释放资源,直接导致关联线程阻塞锁死。唯一的解决方案就是优化代码的逻辑,减少不必要的同步操作。
问题:synchronized有何缺陷?解决方案有什么?
答:当一个线程获取了对应的锁,并执行该代码块时,其它线程便只能一直等待,等待获取锁的线程释放锁,(具体什么时候释放锁,参考问题一)。在多生产者多消费者问题中,我们通过while判断和notifyAll()全唤醒方案来解决问题,但是notifyAll()同时也带来了弊端,它要唤醒所有的被等待的线程,意味着既唤醒了对方,也唤醒了本方,在唤醒本方线程后还要不断判断标记,就降低了程序的效率。如果我们希望只唤醒对方的线程,而不唤醒本方线程,那么我们可以使用JDK5以后提供的Lock锁。
关于Lock锁:
概述:
1.诞生于JDK5以后,不是java内置关键字,而是外部团队为解决当时synchronized缺陷而设计的一个接口,位于java.util.concurrent.locks包下,使用Lock加锁更加灵活。
2.synchronized不需手动释放锁,达到指定条件后会自动释放锁;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,当出现异常后,就会导致出现死锁现象。(此处需要注意:必须在try{}catch{}块中使用Lock,并且将释放锁的操作(unlock())写在finally中,保证锁一定会被释放)
3.synchronized锁对象默认为Object类型对象,Lock则用Condition接口替代了Object,利用Lock实现类中重写的new Condition()方法则可以获取到对象锁。
Lock接口方法:
Condition接口方法:用await()替换wait(),用signal()替换notify(),用signalAll()替换notifyAll()
4.线程通讯
关于线程通讯,最经典的例子就是生产者消费者模式(等待唤醒机制)。
等待唤醒机制:所谓等待唤醒机制,其实就是让线程之间进入等待和判断等待的循环中,所涉及的方法如下(以方法均未定义在Thread类中,而是定义在Object类中)
wait():让调用方法的线程进入阻塞状态,并释放同步监视器(释放锁)。
notify():唤醒其中一个在等待状态的线程(唤醒其中一个通过wait()方法进入阻塞的线程)
notifyAll():唤醒所有的在等待状态的线程(唤醒全部通过wait()方法进入阻塞的线程)。
问题:为何这些线程操作方法要定义在Object?
答:这些方法的使用,必须要标明所属的锁(格式:锁对象.方法名()),而锁又可以是任意对象,能被任意对象调用的方法一定是定义在Object类中。
问题:sleep()和 wait()方法的区别
答:①sleep方法是Thread类的方法,而wait方法是Object类的方法。
②sleep方法可以在任何地方使用,而wait方法必须在同步代码中使用。(否则报错)
③sleep在休眠的时间内,不能唤醒,而wait在等待的时间内,能被唤醒。
④sleep不释放同步锁,会一直持有锁,而wait方法会释放同步锁。



