- 问 创建线程有哪几种方法?
答: 三种:
- 继承Thread类型
继承Thread类来创建并启动线程的步骤如下
1.1. 定义Thread类的子类,并重写该类的run()方法,该run()方法将作为线程执行体
1.2. 创建Thread子类的实例,即创建了线程对象
1.3. 调用线程对象的start()方法来启动该线程 - 实现Runnable接口
2.1. 定义Runnable接口的实现类,并实现该接口的run()方法,该run()方法将作为线程执行体
2.2. 创建Runnable实现类的实例,并将其作为Thread的target来创建Thread对象,Thread对象为线程对象
2.3. 调用线程对象的start()方法来启动线程 - 实现Callable接口
3.1. 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值. 然后再创建Callable实现类的实例
3.2. 使用Future Task类来包装Callable对象,该Future Task对象封装了该Callable对象的call()方法的返回值 - 使用Future Task对象作为Thread对象的target创建并启动新线程
- 调用Future Task对象的get()方法来获得子线程执行结束后的返回值
拓展阅读
通过继承Thread类,实现Runnable接口,实现Callable接口都可以实现多线程,不过实现Runnable接口与实现Callable接口的方式基本相同,只是Callable接口里定义的方法有返回值,可以声明抛出异常而已. 因此可以将实现Runnable接口和实现Callable接口归为一种方式.
采用实现Runnable,Callable接口的方式创建多线程的优缺点:
- 线程类只是实现了Runnable,Callable接口,还可以继承其他类
- 在这种方式下,多个线程可以共享一个target对象,所以适合多个线程来处理同一份资源的情况,从而可以将CPU,代码和数据分开,形成清晰的模型,较好地体现面向对象的思想.
- 劣势: 变成略复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法.
采用继承Thread类的方式创建多线程的优缺点:
劣势: 因为线程继承了Thread类,所以不能再继承其他父类
优势: 编程简单,如果需要访问当前线程,直接使用this即可获得当前线程.
- 问: 说说Thread类的常用方法
答: Thread类常用的构造方法:
Thread()
Thread(String name)
Thread(Runnable target)
Thread(Runnable target,String name)
其中,参数name为线程名,参数target为包含线程体的目标对象
Thread类常用静态方法:
currentThread(): 返回当前正在执行的线程
interrupted(): 返回当前执行的线程是否已经被中断
sleep(long millis): 使当前执行的线程睡眠多少毫秒数
yield(): 使当前执行的线程自愿暂时放弃对处理器的使用权并允许其他线程执行;
Thread类常用实例方法:
getId(): 返回该线程的id
getName(): 返回该线程的名字
getPriority(): 返回该线程的优先级
interrupt(): 使该线程中断
isInterrupted(): 返回该线程是否被中断
isAlive(): 返回该线程是否处于活动状态
isDaemon(): 返回该线程是否是守护线程
setDeamon(boolean on): 将该线程标记为守护线程或用户线程,不标记默认是非守护线程
setName(String name): 设置该线程的名字
setPriority(int newPriority): 改变该线程的优先级
join(): 等待该线程终止
join(long millis): 等待该线程终止,至多等待多少毫秒
问: run()和start()有什么区别?
答: run()方法被称为线程执行体,它的方法体代表了线程需要完成的任务,而start()方法用来启动线程.
调用start()方法启动线程时,系统会把该run()方法当成线程执行体来处理.但如果直接调用线程对象的run()方法,则run()方法立即就会被执行,而且在run()方法返回之前其他线程无法并发执行. 也就是说,如果直接调用线程对象的run()方法,系统把线程对象当成一个普通对象,而run()方法也是一个普通方法,而不是线程执行体.介绍下线程的生命周期
答: 五种状态: 新建(New),就绪(Ready),运行(Running),阻塞(Blocked)和死亡(Dead)
- 调用new关键字后,创建一个线程,处于新建状态. 由Java虚拟机分配内存,并初始化成员变量的值.
- 当线程对象调用start()方法后,线程处于就绪状态(Ready),Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始,只是表示该线程可以运行了.运行情况取决于JVM里线程调度器的调度.
- 就绪状态的线程获得CPU后,就开始执行run()方法的线程执行体,则线程处于运行状态.如果计算器只有一个CPU,那么在任何时刻只有一个线程处于运行状态. 当然,在一个多处理器的机器上,将会有多个线程并行执行;当线程数大于处理器数时,依然会存在多个线程在同一个CPU上轮换的现象.
- 当线程开始运行后,它不可能一直处于运行状态,线程在运行过程中需要被中断,目的是使其他线程获得执行的机会,线程调度的细节取决于底层平台所采用的策略.对于采用抢占式策略的系统而言,系统会给每个可执行的线程一个小时间段来处理任务. 当该时间段用完之后,系统就会剥夺该线程所占用的资源,让其他线程获得执行的机会.
当发生如下情况时,线程将会进入阻塞状态:
调用sleep()方法主动放弃所占用的处理器资源
线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞.
线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有.
线程在等待某个通知(notify)
程序调用了线程的suspend()方法将该线程挂起.但这个方法容易导致死锁,所以应该尽量避免该方法.
针对上面几种情况,当发生如下情况时可解除上面的阻塞,让线程重新进入就绪状态
调用sleep()方法的线程经过了指定时间
线程调用的阻塞式IO方法已经返回
线程成功地获得了试图取得的同步监视器
线程正在等待某个通知时,其他线程发出了一个通知
处于挂起状态的线程被调用了resume()恢复方法
线程会以如下三种方式结束,结束后就处于死亡状态:
run()或call()方法执行完成,线程正常结束
线程抛出一个未捕获的Exception或Error
直接调用该线程的stop()方法来结束进程,该方法容易导致死锁,通常不推荐使用.
拓展阅读
线程5种状态的转换关系,如下图
- 问: 如何实现线程同步?
答:
- 同步方法 synchronized关键字
用synchronized关键字修饰方法,使方法执行前需要获得内置锁,否则处于阻塞状态. 另外若synchronized修饰静态方法,则执行时会锁住整个类 - 同步代码块
用synchronized关键字修饰语句块.因为同步锁是一个高开销操作,若不必要同步整个方法,则用synchronized同步关键代码块即可. - ReentrantLock
Java 5新增的java.util.concurrent包来支持同步,其中ReentrantLock类是可重入,互斥,实现了Lock接口的锁,与使用synchronized方法和块具体相同的基本行为和语义,并拓展了其能力. 但会大幅度降低程序运行效率,因此不推荐使用. - volatile
violatile为域变量的访问提供了一种免锁机制,使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,因此每次使用该域就要重新计算.而不是使用寄存器中的值. 需要注意的是,volatile不会提供任何原子操作,它不能用来修饰final类型的变量 - 原子变量
在Java的util.concurrent.atomic包提供了创建原子类变量的工具类,使用该类可以简化线程同步. 例如AtomicInteger表可以用原子方式更新int值. 可以在应用程序中(如以原子方式增加的计数器),但不能用于替换Integer. 可扩展Number,允许那些处理机遇数字类的工具和实用工具进行统一访问.
- 问 说一说Java多线程之间的通信方式
在Java中线程通信主要有以下三种方法:
wait(),notify(),notifyAll()
如果线程之间采用synchronized来保证线程安全,则可以利用wait(),notify(),notifyAll()来实现线程通信.
这三个方法都不是Thread类中所声明的方法,而是object类中声明的方法. 原因是每个对象都拥有锁,所以让当前线程等待某个对象的锁,就应该通过这个对象来操作. 不气人因为当前线程可能会等待多个线程的锁,如果通过线程来操作,就非常复杂了. 另外,这三个方法都是本地方法,并且被final修饰,无法被重写.
1.1. wait()方法可以让当前线程释放对象锁并进入阻塞状态. notify()方法用于唤醒一个正在等待相应对象锁的线程,使其进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行. notifyAll()用于唤醒所有正在等待相应对象锁的线程,使他们进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行.
1.2. 每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列. 就绪队列存储了已就绪(将要竞争锁)的线程,阻塞队列存储了被阻塞的线程. 当一个阻塞线程被唤醒后,才会进入就绪队列,进而等待CPU的调度. 反之,当一个线程被wait后,就会进入阻塞队列,等待被唤醒.await(),signal(),signAll()
如果线程之间采用Lock来保证线程安全,则可以利用await(),signal(),signalAll()来实现线程通信.这三个方法都是Condition接口中的方法. 在Java1.5中出现的,用于替代传统的wait+notify实现线程间的协作,它依赖于Lock. 相比于使用wait+notify,使用Condition的await+signal这种方式能够更加安全和高效地实现线程间协作.
Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition. 但要注意,Condition的await()/signal()/signalAll()使用都必须在lock保护之内,也就是说,必须在lock.lock()和lock.unlock()之间才可以使用.BlockingQueue
Java5提供了一个BlockingQueue接口,虽然BlockingQueue也是Queue的子接口,但它的主要用途不是作为容器,而是作为线程通信的工具. BlockingQueue具有一个特征: 当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞.
程序的两个线程通过交替向BlockingQueue中放入元素,取出元素,即可很好地控制线程的通信. 线程之间需要通信,最经典的场景就是生产者与消费者模型,而BlockingQueue就是针对该模型提供的解决方案.
- 问 说一说sleep()和wait()的区别
答:
- sleep()是Thread类中的静态方法,而wait()是Object类中的成员方法;
- sleep()可以在任何地方使用,而wait()只能在同步方法或同步代码块中使用.
- sleep()不会释放锁,而wait()会释放锁,并需要通过notify()/notifyAll()重新获得锁
- 问 说一说notify()和notifyAll()的区别
答:
notify():用于唤醒一个正在等待相应对象锁的线程,使其进入就绪队列,以便当前线程释放锁后竞争锁,进而得到CPU执行
notifyAll(): 用于唤醒所有正在等待相应对象锁的线程.......