1.什么是线程?什么是进程?线程和进程的关系。
2.线程创建与运行。创建一个线程有那几种方式?有何区别?
3.线程通知与等待,多线程同步的基础设施,等待通知模型。
4.线程的虚假唤醒是什么?如何避免?
5.Object中finalize()是什么?该如何使用?
6.让线程睡眠的 sleep ()、yield()和join(),sleep 的线程会释放持有的锁?
7.线程中断。什么是协作式中断?什么是抢占式中断?
8.理解线程上下文切换。线程多了一定好?
什么是线程?什么是进程?线程和进程的关系。
进程是具有一定独立功能的,关于某个数据集合的一次运行活动,进程是计算机操作系统分配的最小单元;线程是计算机操作系统执行的最小单元。进程的特点是切换代价大,一个JVM就是一个进程,线程的切换代价相比于进程要小。
在资源方面来看,进程拥有的资源更多,线程拥有的资源更少。以JVM为例,堆区和栈区都是属于进程的,而线程只拥有栈区的资源,话句话说,栈是线程私有的,堆是线程共享的。
线程创建与运行。创建一个线程有那几种方式?有何区别?
java语言中提供了三种创建线程的方法:
1.继承至Thread类并重写run();
2.实现Runnalbe接口;
3.实现Callable接口,这种方式实现的线程是能够获得返回值的。
线程的启动都是通过调用Thread.start()方法来启动线程,如果直接调用run()方法并不会启动一个线程,而只是单纯调用一个run()方法。
线程通知与等待,多线程同步的基础设施,等待通知模型。
首先我们来了解一下线程的一个运行状态:
在Object类中提供了wait(),notify(),notifyAll()三个方法,通过这三个方法何以实现线程之间的协作
wait():执行该方法的线程会进入阻塞状态,只有等待到notify()或者notifyAll()方法被调用是才能将wait从阻塞状态转换为就绪状态。
notify():唤醒执行该方法的对象,例如test.notify()方法调用时会唤醒因执行test.wait()方法而进入阻塞状态的线程,但是只会唤醒一个线程。
notifyAll():唤醒所有等待中的线程。
需要注意的是:这三个方法必须放在同步代码块中使用,否则会抛出异常。
等待通知的经典范式
等待方伪代码:
synchronized(Object){
while(条件){
Object.wait(); //执行该方法的线程将进入阻塞状态
. . . 业务逻辑....
}
}
解释:
1.线程先获取Object对象的锁;
2.当执行到Object.wait()代码是,当前线程会被阻塞,只有当其他线程调用了该对象的notify()或者notifyAll()方法时,当前线程才会被释放。
3.执行自定义的业务逻辑
通知方伪代码:
synchronized(Object){
while(条件){
Object.notify(); //执行该方法将唤醒因为执行Object.wait()而进入阻塞状态的线程,notify()只会唤醒一个线程
. . . 业务逻辑....
}
}
解释:
1.线程先获取Object对象的锁;
2.当执行到Object.wait()代码是,执行该方法将唤醒因为执行Object.wait()而进入阻塞状态的线程,notify()只会唤醒一个线程,如果使用notifyAll()方法的话则会唤醒所有因为执行wait()而阻塞的线程。
3.执行自定义的业务逻辑
下面是一个具体的实例:
场景描述:如果进行一场面试,只有一个面试官,面试官一个时间段内只会面试一个求职者,如果面试官正在面试的话,其他求职者必须等待,当面试官面试完一个求职者后,会通知下一个求职者进行面试,使用等待通知模型模拟这个场景。
代码如下:
package demo.ConcurrentDemo.waitnotify;
import java.util.ArrayList;
/**
* 类说明:如果进行一场面试,只有一个面试官,面试官一个时间段内只会面试一个求职者,
* 如果面试官正在面试的话,其他求职者必须等待,当面试官面试完一个求职者后,
* 会通知下一个求职者进行面试,使用等待通知模型模拟这个场景。
*
* @author zp
*/
public class InterviewDemo {
public void test() throws InterruptedException {
Interviewer interviewer=new Interviewer();
//创建三个面试者
JobSeeker jobSeekerA=new JobSeeker("张三");
JobSeeker jobSeekerB=new JobSeeker("李四");
JobSeeker jobSeekerC=new JobSeeker("王五");
interviewer.jobSeelerWait(jobSeekerA);
interviewer.jobSeelerWait(jobSeekerB);
interviewer.jobSeelerWait(jobSeekerC);
}
public static void main(String[] args) throws InterruptedException {
InterviewDemo demo=new InterviewDemo();
demo.test();
}
class Interviewer{
private ArrayList<JobSeeker> jobSeekers=new ArrayList<>();
private Object lock=new Object();
/**
* 当前面试官是否在面试中,默认为false
*/
private boolean isInterview=false;
/**
* 求职者等待
*/
public void jobSeelerWait(JobSeeker jobSeeker) throws InterruptedException {
if(!isInterview){
isInterview=true;
interview(jobSeeker);
}else {
//等待,进入等待列表
jobSeekers.add(jobSeeker);
//进入阻塞状态
synchronized (lock){
jobSeeker.wait();
}
interview(jobSeeker);
}
}
/**
* 从等待队列中获唤醒第一个求职者
*/
public void jobSeelerNotify(){
if(jobSeekers.isEmpty()){
System.out.println("面试结束....");
isInterview=false;
}else {
JobSeeker jobSeeker = jobSeekers.get(0);
//从等待队列中移除
jobSeekers.remove(jobSeeker);
synchronized (lock){
jobSeeker.notify(); //需要注意jobSeeker一定要确保与执行wait方法的是同一个
}
}
}
/**
* 面试过程
* @param jobSeeker
* @throws InterruptedException
*/
public void interview(JobSeeker jobSeeker) throws InterruptedException {
System.out.println(jobSeeker.name+"开始面试...");
//模拟面试过程 1秒钟
Thread.sleep(1000);
isInterview=false;
//唤醒一个求职者
jobSeelerNotify();
}
}
class JobSeeker{
private String name;
public JobSeeker(String name){
this.name=name;
}
}
}
需要注意的是,该实例中并没有模拟并发过程,只是展示了等待通知模型,想要模拟并发的话可以对这个实例进行改造。上述代码并不是那么的完美,因为wait()应该总是在一个循环中被调用,挂起当前线程来等待一个条件的成立。 Wait调用会一直等到其他线程调用notifyAll()时才返回。
线程的虚假唤醒是什么?如何避免?
虚假唤醒:当同时有多个生产者消费者线程时,notifyAll()唤醒多个wait继续执行,可能会导致数据的不安全。因为唤醒的线程重新获取锁后, 会继续执行wait后面的操作 之前的代码不会执行 这个时候其他线程已经修改了判断条件。
JDK推荐通过while循环判断造成wait()等待的条件,来避免虚假唤醒。
public class ProductAndConsumer {
public static void main(String[] args) {
Pen pen = new Pen();
Consumer cus = new Consumer(pen);
Product pro = new Product(pen);
new Thread(pro,"pro1").start();
new Thread(pro,"pro2").start();
new Thread(cus,"cus1").start();
new Thread(cus,"cus2").start();
}
}
class Pen{
private int count = 10;
public synchronized void getCount() {
//当铅笔数量小于10时才会触发等待
while(count < 10) {
System.out.println(Thread.currentThread().getName()+":"+ "缺货");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+":"+ --count);
//当铅笔数量大于或等于10的时候,唤醒所有阻塞的线程,这些线程唤醒后会去抢占锁并减少count的值
//如果此时不使用循环判断的话,如果此时count=10,当前线程准备去获取资源,但此时其他线程已经将count--,
//当前线程本来应该阻塞,但却执行了notifyAll(),这就造成了一个虚假唤醒.
this.notifyAll();
}
public synchronized void setCount() {
while(count >= 10) {
System.out.println(Thread.currentThread().getName()+":"+ "仓库满了");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+":"+ ++count);
this.notifyAll();
}
}
class Product implements Runnable{
private Pen pen = null;
public Product(Pen pen) {
this.pen = pen;
}
@Override
public void run() {
for(int i=0;i<2000;i++) {
pen.setCount();
}
}
}
class Consumer implements Runnable{
private Pen pen = null;
public Consumer(Pen pen) {
this.pen = pen;
}
@Override
public void run() {
try {
Thread.sleep(40);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
for(int i=0;i<2000;i++) {
pen.getCount();
}
}
}
Object中finalize()是什么?该如何使用?
在C++语言中,有一种和构造函数相对应的是析构函数,析构函数会在对象销毁之前做最后的处理,在java中同样也提供了类似的方法,那就是finalize()方法,实际上,只有在垃圾回收器释放对象的内存时才会调用finalize(),而且还不保证这个方法一定能够完全执行,但是实际上java并不推荐我们使用finalize(),因为finalize的代价太大,而且会导致对象不稳定,,并且try语句块能够实现和finalize()一样的功能,而且还更高效。
让线程睡眠的 sleep ()、yield()和join(),sleep 的线程会释放持有的锁吗?
sleep() :
sleep()方法需要指定等待的时间,它可以让当前正在执行的线程在指定的时间内暂停执行,进入等待状态,该方法既可以让其他同优先级或者高优先级的线程得到执行的机会,也可以让低优先级的线程得到执行机会。但是sleep()方法不会释放“锁标志”,也就是说如果有synchronized同步块,其他线程仍然不能访问共享数据。
wait() :
wait()方法与sleep()方法的不同之处在于,wait()方法会释放对象的“锁标志”。当调用某一对象的wait()方法后,会使当前线程暂停执行,并将当前线程放入对象等待池中,直到调用了notify()方法后,将从对象等待池中移出任意一个线程并放入锁标志等待池中,只有锁标志等待池中的线程可以获取锁标志,它们随时准备争夺锁的拥有权。当调用了某个对象的notifyAll()方法,会将对象等待池中的所有线程都移动到该对象的锁标志等待池。
除了使用notify()和notifyAll()方法,还可以使用带毫秒参数的wait(long timeout)方法,效果是在延迟timeout毫秒后,被暂停的线程将被恢复到锁标志等待池。
yield()
yield()方法和sleep()方法类似,也不会释放“锁标志”,区别在于,它没有参数,即yield()方法只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行,另外yield()方法只能使同优先级或者高优先级的线程得到执行机会,这也和sleep()方法不同。
join()
join()方法会使当前线程等待调用join()方法的线程结束后才能继续执行,如果是join(long time),那么当前线程会等待time时间,给执行join()的线程执行。
线程中断。什么是协作式中断?什么是抢占式中断?
通过stop()方法暂停线程,是典型的抢占式中断,调用stop()方法后,会强制线程停止执行,这会导致所有已锁的检同期被解锁,任何之前被这些监听器保护的对象都会变为不可预知的状态,导致线程不安全问题。
java中通过协作式中断来实现中断功能。所谓协作式中断,实际上就是通过一个状态值来表示当前线程是否中断,但是真正的中断操作需要在代码中提前写好,通过这种约定的形式来实现中断功能就是协作式中断。
通过isInterrupted(),interrupt()和interrupted()来实现中断,中断的原理是:通过一个标识位来表示当前线程是否被中断,但是线程进不进行具体的中断取决于程序是否设计了针对中断标识的操作。
线程中断的伪代码:
public void run(){
try{
....
//线程退出条件
while(!Thread.currentThread().isInterrupted()&& more work to do){
// do more work;
}
}catch(InterruptedException e){
// thread was interrupted during sleep or wait
}
finally{
// cleanup, if required
}
}
理解线程上下文切换。线程多了一定好?
在多线程编程中,线程个数一般都大于 CPU 个数,而每个 CPU 同一时刻只能被一个线程使用,为了让用户感觉多个线程是在同时执行,CPU 资源的分配采用了时间片轮转的策略,也就是给每个线程分配一个时间片,在时间片内占用 CPU 执行任务。当前线程的时间片使用完毕后当前就会处于就绪状态并让出 CPU 让其它线程占用,这就是上下文切换,从当前线程的上下文切换到了其它线程。
那么就有一个问题让出 CPU 的线程等下次轮到自己占有 CPU 时候如何知道之前运行到哪里了?所以在切换线程上下文时候需要保存当前线程的执行现场,当再次执行时候根据保存的执行现场信息恢复执行现场。
线程上下文切换时机:
当前线程的 CPU 时间片使用完毕处于就绪状态时候;
当前线程被其它线程中断时候。
注:由于线程切换是有开销的,所以并不是开的线程越多越好,比如如果机器是4核心的,你开启了100个线程,那么同时执行的只有4个线程,这100个线程会来回切换线程上下文来共享这四个 CPU。