关于线程和线程并发的一些问题

来源:互联网 发布:mac重装系统全盘格式化 编辑:程序博客网 时间:2024/05/17 09:07

一、概念性问题

第一题:线程的基本概念和线程的基本状态以及他们之间的关系?

 线程:也称为轻量级进程,是程序执行流的最小单元,一个标准线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,比如在每个线程中都应具有一个用于控制线程运行的线程控制块TCB,用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。但它与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤销另一个线程,同一进程中的多个线程之间可以并发执行。

由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程有就绪、阻塞和执行三种基本状态。

 

 

 

 

每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。线程是程序中一个单一的顺序控制流程,在单个程序中同时运行多个线程完成不同的工作,称为多线程。在多线程OS中,通常是在一个进程中包括多个线程,每个线程都作为利用CPU的基本单位,是花费开销最小的实体。

 

第二题:线程与进程的区别?(常考)

子进程和父进程有相同的代码段,不同的数据段,而多个进程则共享数据空间,每个线程都有自己的执行堆栈和程序计数器为其执行上下文。多线程主要是为了节约CPU时发挥作用,线程的运行中需要用计算机的内存资源。 通常一个进程中可以包含若干个线程,它们可以利用进程所拥有的资源。在引入线程的操作系统中,通常把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。由于线程比进程更小,基本不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统内多个程序间并发执行的程度,从而显著提高系统资源的利用率和吞吐量。

主要区别:

1.地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。

2.通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。

3.调度和切换:线程上下文切换比进程上下文切换要快得多。

4.在多线程OS中,进程不是一个可执行的实体。

另:

1、 线程是进程的一部分,所以线程有的时候被称为是轻权进程或者轻量级进程。
2、 一个没有线程的进程是可以被看作单线程的,如果一个进程内拥有多个线程,进程的执行过程不是一条线(线程)的,而是多条线(线程)共同完成的。
3、 系统在运行的时候会为每个进程分配不同的内存区域,但是不会为线程分配内存(线程所使用的资源是它所属的进程的资源),线程组只能共享资源。那就是说,除了CPU之外(线程在运行的时候要占用CPU资源),计算机内部的软硬件资源的分配与线程无关,线程只能共享它所属进程的资源。
4、 与进程的控制表PCB相似,线程也有自己的控制表TCB,但是TCB中所保存的线程状态比PCB表中少多了。
5、 进程是系统所有资源分配时候的一个基本单位,拥有一个完整的虚拟空间地址,并不依赖线程而独立存在

 

 

 

 

第三题:多线程有几种实现方法,都是什么?

多线程编程的目的,就是"最大限度地利用CPU资源",当某一线程的处理不需要占用CPU而只和I/O,OEMBIOS等资源打交道时,让需要占用CPU资源的其它线程有机会获得CPU资源。从根本上说,这就是多线程编程的最终目的。

如同程序和进程的区别,要掌握多线程编程,第一要弄清的问题是:线程对象和线程的区别。

线程对象是可以产生线程的对象。比如在java平台中Thread对象,Runnable对象。

线程,是指正在执行的一个指令序列。在java平台上是指从一个线程对象的start()开始,运行run方法体中的那一段相对独立的过程。 

两种实现方法:

1.继承Thread类

2.实现Runnable接口再new Thread(YourRunnableOjbect) 

多线程有3种方法:1.class mt implements Runnable{    public void run(){...}//run()由Runnable定义,就是你要执行的代码    ...}...new Thread(new mt()).start();2.class mt extends Thread{    public void run(){...}    ...}...new mt().start();3.class mt extends TimerTask{    public void run(){...}    ...}...new Timer().schedule(new mt(),1000,200);//等待1秒钟后每2毫秒执行一次后两种方法在本质上和第一种一样,因为Thread 和 TimerTask 都implements Runnable但显然第一种方法最好,因为mt 还可以extends其他类至于同步,只要在你要同步的方法前加synchronized,然后适当用一下wait() 和 notify()

 

让我们先从最简单的"单线程"来入手:

(1)带引号说明只是相对而言的单线程    (2)基于java。

class BeginClass

{
 public static void main(String[] args)

    {
  for(int i=0;i<100;i++)
   System.out.println("Hello,World!");
 }
}

  如果我们成功编译了该java文件,然后在命令行上敲入:

  java BeginClass

  JVM进程被启动,在同一个JVM进程中,有且只有一个进程,就是它自己。然后在这个JVM环境中,所有程序的运行都是以线程来运行。JVM最先会产生一个主线程,由它来运行指定程序的入口点。在这个程序中,就是主线程从main方法开始运行。当main方法结束后,主线程运行完成。JVM进程也随之退出。

  我们看到的是一个主线程在运行main方法,这样的只有一个线程执行程序逻辑的流程我们称之为单线程。这是JVM提供给我们的单线程环境,事实上,JVM底层还至少有垃圾回收这样的后台线程以及其它非java线程,但这些线程对我们而言不可访问,我们只认为它是单线程的。主线程是JVM自己启动的,在这里它不是从线程对象产生的。在这个线程中,它运行了main方法这个指令序列。

  [JAVA线程对象]

  现在我们来开始考察JAVA中线程对象。

  在JAVA中,要开始一个线程,有两种方式。一是直接调用Thread实例的start()方法,二是将Runable实例传给一个Thread实例然后调用它的start()方法。

  线程对象和线程是两个完全不同的概念。生成一个线程的实例,并不代表启动了线程。而启动线程是说在某个线程对象上启动了该实例对应的线程,当该线程结束后,并不会就立即消失。

class MyThread extends Thread{ public int x = 0; public void run(){  for(int i=0;i<100;i++)   {    try      {     Thread.sleep(10);    }catch(Exception e){}    System.out.println(x++);  } }}   如果我们生成MyThread的一个实例,然后调用它的start()方法,那么就产生了这个实例对应的线程:public class Test { public static void main(String[] args) throws Exception  {  MyThread mt = new MyThread();  mt.start(); }}   不用说,最终会打印出0到99,现在我们稍微玩一点花样:public class Test { public static void main(String[] args) throws Exception  {  MyThread mt = new MyThread();  mt.start();  System.out.println(101); }}  由于单CPU的原因,一般会先打印101,然后打印0到99。不过我们可以控制线程让它按我们的意思来运行:public class Test { public static void main(String[] args) throws Exception {  MyThread mt = new MyThread();  mt.start();  mt.join();  System.out.println(101); }} 

mt实例对应的线程,在运行完成后,主线程才打印101。因为我们让当前线程(这里是主线程)等待mt线程的运行结束。"在线程对象a上调用join()方法,就是让当前正在执行的线程等待线程对象a对应的线程运行完成后才继续运行。"

public class Test{ public static void main(String[] args) throws Exception  {  MyThread mt = new MyThread();  mt.start();  mt.join();  Thread.sleep(3000);  mt.start(); }} 

  当线程对象mt运行完成后,让主线程休息一下,然后再次在这个线程对象上启动线程。结果我们看到:

Exception inthread "main"java.lang.IllegalThreadStateException 也就是这种线程对象一时运行一次完成后,它就再也不能运行第二次了

我们可以看一下它有具体实现:public synchronized void start() { if (started)  throw new IllegalThreadStateException();  started = true;  group.add(this);  start0(); } 


一个Thread的实例一旦调用start()方法,这个实例的started标记就标记为true,事实中不管这个线程后来有没有执行到底,只要调用了一次start()就再也没有机会运行了,这意味着:[通过Thread实例的start(),一个Thread的实例只能产生一个线程。

那么如果要在一个实例上产生多个线程(也就是我们常说的线程池),我们应该如何做呢?这就Runnable接口给我们带来的伟大的功能。


 

class R implements Runnable{ private int x = 0; public void run(){  for(int i=0;i<100;i++)   {    try{        Thread.sleep(10);    }catch(Exception e){}    System.out.println(x++);            } }} 
Runnable的实例是可运行的,但它自己并不能直接运行,它需要被Thread对象来包装才行运行:public class Test { public static void main(String[] args) throws Exception    {      new Thread(new R()).start();   }} 
当然这个结果和mt.start()没有什么区别。但如果我们把一个Runnable实例给Thread对象多次包装,我们就可以看到它们实际是在同一实例上启动线程:public class Test {   public static void main(String[] args) throws Exception     {  R r = new R();  for(int i=0;i<10;i++)    new Thread(r).start();     }} x是实例对象,但结果是x被加到了999,说明这10个线程是在同一个r对象上运行的。请大家注意,因为这个例子是在单CPU上运行的,所以没有对多个线程同时操作共同的对象进行同步。这里是为了说明的方便而简化了同步,而真正的环境中你无法预知程序会在什么环境下运行,所以一定要考虑同步
一个完整的例子来说明线程产生的方式不同而生成的线程的区别:package debug;import java.io.*;import java.lang.Thread;class MyThread extends Thread{ public int x = 0; public void run()  {   System.out.println(++x); }}class R implements Runnable{ private int x = 0; public void run()  {   System.out.println(++x); }}public class Test { public static void main(String[] args) throws Exception{  for(int i=0;i<10;i++)   {    Thread t = new MyThread();    t.start();  }  Thread.sleep(10000);//让上面的线程运行完成  R r = new R();  for(int i=0;i<10;i++)   {    Thread t = new Thread(r);    t.start();  }}  }   上面10个线程对象产生的10个线程运行时打印了10次1。下面10个线程对象产生的10个线程运行时打印了1到10。我们把下面的10个线程称为同一实例(Runnable实例)的多个线程。





 

第四题:多线程同步和互斥有几种实现方法,都是什么?

线程间的同步方法大体可分为两类:用户模式和内核模式。顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。内核模式下的方法有:事件,信号量,互斥量。

同步实现有两种方法:synchronized,wait与notity

wait():使一个线程处于等待状态,并且释放所持有的对象的lock

sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法需要捕捉InterruptedException异常

notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待的线程,而是由于JVM确定唤醒哪一个线程,而不是按优先级

Allnotify():唤醒所有处于等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。

注意:启动一个线程是调用start()方法,使线程就绪状态,以后可以被调度为运行状态,一个线程必须关联一些具体的执行代码,run()方法是该线程所关联的执行代码。

第五题:多线程同步和互斥有何异同,在什么情况下分别使用它们?举例说明

 线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。

 

[线程的并发与并行]

  在单CPU系统中,系统调度在某一时刻只能让一个线程运行,虽然这种调试机制有多种形式(大多数是时间片轮巡为主),但无论如何,要通过不断切换需要运行的线程让其运行的方式就叫并发(concurrent)。而在多CPU系统中,可以让两个以上的线程同时运行,这种可以同时让两个以上线程同时运行的方式叫做并行(parallel)。

 

二、选择题

第一题:一下多线程对int型变量的操作,哪几个不需要进行同步:(D )  ——百度笔试题

A. x = y;         B. x++;     C.  ++x;     D.  x = 1;

第二题:多线程中栈与堆是公有的还是私有的(C ) ——阿里巴巴笔试题

A:栈公有, 堆私有

B:栈公有,堆公有

C:栈私有, 堆公有

D:栈私有,堆私有

三、综合题

第一题:在windows编程中互斥量与临界区比较类似,请分析一下二者的主要区别。 ——台湾杀毒软件公司面试题

 

第二题:一个全局变量tally,两个线程并发执行(代码段都是Threadproc),问两个线程都结束后,tally取值范围。

 

inttally= 0;//glablevoidThreadproc( ){   for(inti= 1;i<= 50;i++)    Tally+= 1;}

[50, 100]

第三题(某培训机构的练习题):

子线程循环10次,接着主线程循环100次,接着又回到子线程循环10次,接着再回到主线程又循环100次,如此循环50次,试写出代码。

第四题(迅雷笔试题):

编写一个程序,开启3个线程,这3个线程的ID分别为ABC,每个线程将自己的ID在屏幕上打印10遍,要求输出结果必须按ABC的顺序显示;如:ABCABC.依次递推。

第五题(Google面试题)

有四个线程1、234。线程1的功能就是输出1,线程2的功能就是输出2,以此类推.........现在有四个文件ABCD。初始都为空。现要让四个文件呈如下格式:

A1 2 3 4 1 2....

B2 3 4 1 2 3....

C3 4 1 2 3 4....

D4 1 2 3 4 1....

请设计程序。

下面的第六题与第七题也是在考研中或是程序员和软件设计师认证考试中的热门试题。

第六题

生产者消费者问题

这是一个非常经典的多线程题目,题目大意如下:有一个生产者在生产产品,这些产品将提供给若干个消费者去消费,为了使生产者和消费者能并发执行,在两者之间设置一个有多个缓冲区的缓冲池,生产者将它生产的产品放入一个缓冲区中,消费者可以从缓冲区中取走产品进行消费,所有生产者和消费者都是异步方式运行的,但它们必须保持同步,即不允许消费者到一个空的缓冲区中取产品,也不允许生产者向一个已经装满产品且尚未被取走的缓冲区中投放产品。

第七题

读者写者问题

这也是一个非常经典的多线程题目,题目大意如下:有一个写者很多读者,多个读者可以同时读文件,但写者在写文件时不允许有读者在读文件,同样有读者读时写者也不能写。