Java面试准备三:进程、线程

来源:互联网 发布:华融新兴产业投资 知乎 编辑:程序博客网 时间:2024/05/22 05:20

该系列的Java面试复习内容全参考自近一个月的面试总结,在此谢谢博主的分享。
下面主要是我根据博主列出的Java基础部分需要了解的内容进行复习所做的笔记。这里只是为了记录,由于自身水平实在不怎么样,难免错误百出,有错的地方还望大家多多指出,谢谢。

  1. 线程、进程简单介绍
  2. 简单总结
  3. 线程的创建
  4. 线程的状态
  5. 线程调度
  6. wait()、notify()和notifyAll()
  7. Thread.sleep(long millis)和Thread.yeild()
  8. interrupt()
  9. sleep()、wait()
  10. 常见线程名词解释
  11. 线程同步

1. 线程、进程简单介绍
参考进程与线程的一个简单解释
计算机的核心是CPU,它承担了所有的计算任务。把CPU比作一个工厂,它时刻都在运行。单个CPU一次只能运行一个任务。进程就好比工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。一个车间里,可以有很多工人。他们协同完成一个任务。线程就好比车间里的工人。一个线程可以包括多个线程。车间的空间是工人们共享的,比如许多房间是每个工人都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。可是,每间房间的大小不同,有些房间最多只能容纳一个人,比如厕所。里面有人的时候,其他人就不能进去了。这代表一个线程使用某些共享内存是,其他线程必须等它结束,才能使用这一块内存。一个放置他人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫“互斥锁”,防止多个线程同时读写某一块内存区域。
操作系统的设计,因此可以归结为三点:
1)以多线程形式,允许多个任务同时运行;
2)以多线程形式,允许单个任务分成不同的部分运行;
3)提供协调机制,一方面防止进程之间和线程之间发生冲突,另一方面允许进程之间和线程之间共享资源。

以下均参考 Java多线程学习(吐血超详细总结)
2. 简单总结
一个进程是一个独立的运行环境,它可以被看作是一个程序或者一个应用。而线程是在进程中执行的一个任务。每个进程中,代码和数据空间是相互独立的,进程间的切换开销相对大些(进程是资源分配的最小单位)。而在同一进程不同线程中,它们的代码和数据空间则是共享的、相同的,但每一个线程都会拥有独立的运行栈和程序计数器,线程间的切换开销相对小些(线程是cpu调度的最小单位)。

3. 线程的创建
一种是继承Thread类,另一种是实现Runnable接口。比较基础,这里不详述。
在Java中,每次程序运行至少启动2个线程,一个是main线程,一个是垃圾收集线程。
4. 线程的状态
线程状态转换

新建状态(new):新创建了一个线程对象
就绪状态:线程创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获得CPU的使用权。
运行状态:就绪状态的线程获取了CPU,这行程序代码。
阻塞状态:线程放弃了CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。(注意线程进入到运行状态只有一个入口,就是就绪状态)
1)等待阻塞:运行的程序执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
2)同步阻塞:运行的程序在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
3)其他阻塞:运行的程序执行sleep()或join()方法,或者发出了I/O请求时,JVM会把线程置为阻塞状态。直到sleep()设定的时间到了、join()线程终止或者超时、或者I/O处理完毕时,线程重新进入就绪状态。(sleep是不会释放持有的锁)
死亡状态:线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

5. 线程调度
优先级:整数表示,取值范围1~10,优先级越高获得CPU执行的记录越大。
Thread类有三个静态常量:
static int MAX_PRIORITY 线程可以具有的最高优先级,取值为10。
static int MIN_PRIORITY 线程可以具有的最低优先级,取值为1。
static int NORM_PRIORITY 分配给线程的默认优先级,取值为5。
每个线程都由默认优先级。主线程的默认优先级为static int NORM_PRIORITY。

Thread类通过setPriority()和 getPriority()分别用来设置和获取线程的优先级。

线程睡眠:Thread.sleep(long millis),使线程转到阻塞状态。当睡眠结束后,就转为就绪状态。

线程等待:Object类中的wait()方法,当值当前额线程等待,直到其他线程调用此对象的notify()或notifyAll()唤醒方法,这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。

线程让步:Thread.yield(),暂停当前正在执行的线程对象,线程进入到就绪状态,把执行机会让给相同或者更高优先级的线程。

线程加入:join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程进入阻塞状态,知道另一个线程运行结束,当前线程才有阻塞转换到就绪状态。(还是那句,线程进入到运行状态只有一个入口,就是就绪状态)。

线程唤醒:Object类中的nofity()方法,唤醒在此监视器对象上等待的单个线程。如果有一个以上的线程都在此监视器对象上等待唤醒,则只会选择其中一个唤醒。选择是任意的。Object类中还有一个方法notifyAll(),唤醒在此监视器对象上等待的所有线程。

6. wait()、notify()和notifyAll()
参考线程通信
java.lang.Object类定义了三个方法,wait(),notify()和notifyAll()来允许线程在等待信号的时候变为非运行状态,提高了CPU的利用率。

一个线程一旦调用了任意对象的wait()方法,就会变为非运行状态,知道另一个线程调用了同一个对象的notify()方法。为了调用wait()或者notify(),线程必须先获得那个对象的锁。也就是说,线程必须在同步块里调用wait()或者notify()。当一个线程调用一个对象的notify()方法,正在等待该对象的所有线程中将有一个线程被唤醒并允许执行(校注:这个将被唤醒的线程是随机的,不可以指定唤醒哪个线程)。同时也提供了一个notifyAll()方法来唤醒正在等待一个给定对象的所有线程。一旦线程调用了wait()方法,它就释放了所持有的监视器对象上的锁。这将允许其他线程可以调用wait()和notify()。wait()也可以指定阻塞的毫秒数。

示例:
类Thread1继承Thread

class A{}public class Thread1 extends Thread{    Object lock = null;    public Thread1(Object lock){        this.lock = lock;    }  @Override    public void run() {       synchronized (lock) {            System.out.println("进入到doWait同步代码块");            try {                lock.wait();            } catch (InterruptedException e) {                e.printStackTrace();            }            System.out.println("继续执行...");       }    }}

类Thread2继承Thread

public class Thread2 extends Thread{    Object lock = null;    public Thread2(Object lock){        this.lock = lock;    }    @Override    public void run() {        synchronized (lock) {            System.out.println("进入到doNotify同步代码块");            lock.notify();        }    }}

测试类

public class Test_ {    public static void main(String[] args) throws InterruptedException {        A a = new A();        Thread1 thread1 = new Thread1(a);        Thread2 thread2 = new Thread2(a);        thread1.start();        Thread.sleep(100);        thread2.start();    }}

执行结果:
进入到doWait同步代码块
进入到doNotify同步代码块
继续执行…

一旦一个线程被唤醒,线程只是回到可执行状态,并不能立刻就执行wait()后面的代码,直到调用notify()的线程退出了它自己的同步块,释放了监视器对象的锁,而且被唤醒的线程重新获得监视器对象的锁,才能继续执行。

另一经典的面试题,三线程打印ABC,题目要求:建立三个线程,A线程打印10次A,B线程打印10次B,C线程打印10次C,要求线程同时运行,交替打印10次ABC。这个问题用Object的wait(),notify()就可以很方便的解决。代码如下:

/**  * wait用法  * @author DreamSea   * @time 2015.3.9   */  package com.multithread.wait;  public class MyThreadPrinter2 implements Runnable {         private String name;         private Object prev;         private Object self;         private MyThreadPrinter2(String name, Object prev, Object self) {             this.name = name;             this.prev = prev;             this.self = self;         }         @Override        public void run() {             int count = 10;             while (count > 0) {                 synchronized (prev) {                     synchronized (self) {                         System.out.print(name);                         count--;                        self.notify();                     }                     try {                         prev.wait();                     } catch (InterruptedException e) {                         e.printStackTrace();                     }                 }             }         }         public static void main(String[] args) throws Exception {             Object a = new Object();             Object b = new Object();             Object c = new Object();             MyThreadPrinter2 pa = new MyThreadPrinter2("A", c, a);             MyThreadPrinter2 pb = new MyThreadPrinter2("B", a, b);             MyThreadPrinter2 pc = new MyThreadPrinter2("C", b, c);             new Thread(pa).start();          Thread.sleep(100);  //确保按顺序A、B、C执行          new Thread(pb).start();          Thread.sleep(100);            new Thread(pc).start();             Thread.sleep(100);            }     }    

输出结果:
ABCABCABCABCABCABCABCABCABCABC

7. Thread.sleep()和Thread.yield()
sleep:使线程转到阻塞状态。sleep()没有释放锁。当睡眠结束后,就转换成就绪状态。线程休眠时间到后,该线程不一定会立即执行,这时因为其他线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级。

yield:让当前运行线程转换到就绪状态,以允许具有相同优先级或更高优先级的线程获得运行机会。但是,无法保证获得CPU执行权的会是相同优先级或更高优先级的线程,也可能还是原来的线程。

区别:sleep()使当前线程进入阻塞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;yield()只是使当前线程回到就绪状态,所以执行yield()的线程有可能马上又被执行。另外,sleep允许较低优先级的线程获得运行机会,因为它的睡眠时间没到是无法得到CPU运行的。而yield()方法不可能让较低优先级的线程运行,实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级或更高优先级的线程处于可运行的状态,如有,则把CPU的占有权让给此线程,否则,继续运行原来的线程。

8. interrupt()
不要以为它是中断某个程序!它只是向线程发送一个中断信号,让线程在无限等待时(如死锁时)能抛出异常,从而结束线程,但是如果你吃掉了这个异常,那么线程还是不会中断的!

9. sleep()和 wait()
都可以在程序的调用处阻塞指定的毫秒数,并返回。

都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException(但是不建议使用该方法 ?)。如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B正在wait/sleep /join,则线程B会立刻抛出InterruptedException,在catch() {} 中直接return即可安全地结束线程。

需要注意的是,InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛出的。对某一线程调用 interrupt()时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。但是,一旦该线程进入到 wait()/sleep()/join()后,就会立刻抛出InterruptedException 。

不同点
sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。

wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用

10. 常见线程名词解释
主线程:JVM调用程序main()所产生的线程。
当前线程:这个是容易混淆的概念。一般指通过Thread.currentThread()来获取的进程。
后台线程:指为其他线程提供服务的线程,也成为守护线程。JVMd的垃圾回收线程就是一个后台线程。用户线程和守护线程区别在于,是否依赖于主线程结束而结束。
前台线程:是指接收后台线程服务的线程。
线程类的一些常见方法
isAlive():判断一个线程是否存活。
currentThread(): 得到当前线程。
isDaemon(): 一个线程是否为守护线程。
setDaemon(): 设置一个线程为守护线程。

11. 线程同步
参考Java同步块
Java同步块,用来标记方法或者代码块是同步的。Java同步块用来避免竞争。下面将介绍以下内容:

  • Java同步关键字
  • 实例方法同步
  • 静态方法同步
  • 实例方法中同步块
  • 静态方法中同步块
  • Java同步示例

Java同步关键字
Java中的同步块用synchronized标记。同步块在Java中是同步在某个对象上。所有同步在一个对象上的同步块在同时只能被一个线程进入并执行操作。所有其他等待进入该同步的线程将被阻塞,知道执行该同步块中的线程退出。有四种不同的同步块:
实例方法
静态方法
实例方法中的同步块
静态方法中的同步块
上述同步块都同步在不同对象中。实际需要哪种同步块视具体情况而定。

实例方法同步
实例方法示例:

public synchronized void add(int value){    this.count += value; }

Java实例方法同步是同步在拥有该方法所属的实例。

静态方法同步
静态方法示例:

public static synchronized void add(int value){ count += value; }

静态方法的同步是同步在该方法所在的类对象上。

实例方法中同步块
有时你不需要同步整个方法,而是同步方法中的一部分。
在非同步的Java方法中的同步块的示例:

public void add(int value){    synchronized(this){       this.count += value;    }  }

在上例中,使用了“this”,即为调用add方法的实例本身。
在同步构造器中在括号中的对象叫做监视器对象。实例方法中的同步块是同步在监视器对象上的。
下面两个例子都同步他们所调用的实例对象上,因此他们在同步的执行效果上是等效的。

public class MyClass {   public synchronized void log1(String msg1, String msg2){                log.writeln(msg1);      log.writeln(msg2);   }   public void log2(String msg1, String msg2){      synchronized(this){         log.writeln(msg1);         log.writeln(msg2);      }   } }

在上可以例中,每次只有一个线程能够在两个同步块中任意一个方法内执行。
如果第二个同步块不是同步在this实例对象上,那么两个方法被线程同时执行。

静态方法中同步块
和上面类似,下面是两个静态方法同步的例子。这些方法同步在该方法所属的类对象上。

public class MyClass {    public static synchronized void log1(String msg1, String msg2){       log.writeln(msg1);       log.writeln(msg2);    }    public static void log2(String msg1, String msg2){       synchronized(MyClass.class){          log.writeln(msg1);          log.writeln(msg2);       }    }  }

这两个方法不允许同时被线程访问。
如果第二个同步块不是同步在MyClass.class这个对象上。那么这两个方法可以同时被线程访问。

Java同步示例
在下面例子中,启动了两个线程,都调用Counter类同一个实例的add()方法。因为同步在该方法所属的实例上,所以同时只能有一个线程访问该方法。

public class Counter{     long count = 0;         public synchronized void add(long value){       this.count += value;     }  }  public class CounterThread extends Thread{     protected Counter counter = null;     public CounterThread(Counter counter){        this.counter = counter;     }       public void run() {    for(int i=0; i<10; i++){           counter.add(i);        }     }  }  public class Example {    public static void main(String[] args){      Counter counter = new Counter();      Thread  threadA = new CounterThread(counter);      Thread  threadB = new CounterThread(counter);      threadA.start();      threadB.start();    }  }

两个线程threadA,threadB的构造器引用同一个Counter实例。Counter.add()方法是同步在实例上,因此只允许一个线程调用该方法。另外一个线程必须要等到第一个获得同步锁的线程退出add()方法时,才能继续执行方法。

如果两个线程引用了两个不同的Counter实例,那么他们可以同时add()方法。这些方法分别同步在两个不同的Counter实例上。

最后贴上一些线程相关的面试题
JAVA多线程和并发基础面试问答

0 0
原创粉丝点击