【Java面试】Java 多线程入门学习

  操作系统的发展使得多个程序能够同时运行,程序在各自的进程(processes)中运行,相互分离,各自独立执行,由操作系统来分配资源,如内存、文件句柄、安全证书等。
  
  进程是资源分配的最小单位,每个进程都有独立的代码和数据空间(进程上下文),且都可以包含1~n个线程,进程间的切换会有较大的开销。如果需要的话,进程会通过一些原始的机制相互通信:Socket、信号处理(signal handlers)、共享内存(shared memory),信号量(semaphores),和文件;
  
  线程有些时候被称为轻量级进程(lightweight processes),并且大多数现代操作系统把线程作为时序调度的基本单元,而不是进程。线程允许程序控制流的多重分支同时存在于一个进程,它们共享进程范围内的资源,如内存和文件句柄,但是每一个线程有其自己的程序计数器(program counter),栈和本地变量。线程也为多处理器系统中并行地使用硬件提供了一个自然而然的分解,同一程序内的多个线程可以在多CPU的情况下同时调度;


两种实现多线程的方式

  Java中实现多线程,一般有两种方式,一种是继承Thread类,另一种是实现Runnable接口。(有些博客中看到,应该有三种,还有一种是实现Callable接口,并与Future、线程池结合使用);
  
  第一种:扩展java.lang.Thread类
  继承Thread类的方法是比较常用的一种,如果说只是想起一个线程,没有什么其它特殊的要求,那么可以使用Thread。(推荐使用Runnable,后头会说使用Runnable的优势有什么)。下面来看一个简单的继承Thread类的实例:

public class ExtendThreadTest {

    public static void main(String[] args) {
        MyThread thread1 = new MyThread("A");
        thread1.start();
        MyThread thread2 = new MyThread("B");
        thread2.start();
    }

    static class MyThread extends Thread {

        private String name;

        public MyThread(String name) {
            super();
            this.name = name;
        }

        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.println(name + " Thread:" + i);
                try {
                    Thread.sleep(new Random().nextInt(500));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            super.run();
        }
    }
}

运行示例:

这里写图片描述

  程序启动运行main函数的时候,Java虚拟机(JVM)启动一个进程,主线程main在main()调用时候被创建。随着调用main()函数中两个对象的start()方法,这两个线程也启动了,这样,整个应用就在多线程下运行。从程序运行的结果可以发现,多线程程序是乱序执行。所有的多线程代码执行顺序都是不确定的,每次执行的结果都是随机的,可以执行试一下;

注意: start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行是由操作系统决定的。
另外: start()方法不可重复调用,不然会出现java.lang.IllegalThreadStateException异常;


  第二种:实现java.lang.Runnable接口

  采用实现Runnable接口,只需重写run()方法,下面来看一个简单的实现Runnable接口的实例:

public class ImplementRunnable implements Runnable {

    private String name;

    public ImplementRunnable(String name) {
        super();
        this.name = name;
    }

    public static void main(String[] args) {
        ImplementRunnable thread1 = new ImplementRunnable("A");
        new Thread(thread1).start();
        ImplementRunnable thread2 = new ImplementRunnable("B");
        new Thread(thread2).start();
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(name + " Thread:" + i);
            try {
                Thread.sleep(new Random().nextInt(500));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

  运行结果同第一个示例,ImplementRunnable 类通过实现Runnable接口,使得该类有了多线程类的特征,所有的多线程代码都在run()方法里面。Thread类实际上也是实现了Runnable接口的类。
  在启动多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。
  实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是扩展Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。
  
总结:实现Runnable接口比继承Thread类所具有的优势:
1. 适合多个有相同的程序代码的线程去处理同一个资源;
2. 可以避免Java中的单继承的限制;
3. 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立;
4. 线程池只能放入实现Runable或callable接口的线程,不能直接放入继承Thread类的线程;

注意: main()方法其实也是一个线程,在Java中所有的线程都是同时启动的,至于什么时候,哪个先执行,完全看谁先拿到CPU的资源。


线程状态转换

  这里写图片描述
  
1. 新建状态(New):新创建了一个线程对象,还没有调用start()方法时,线程处于此状态;
2. 就绪状态(Runnable):线程对象创建后,其它线程(如主线程中)调用了该对象的start()方法后,线程进入就绪状态,该状态的线程位于可运行线程池中,变的可运行,等待Java运行时系统的线程调度程序(thread scheduler)来获取CPU的使用权;
3. 运行状态(Running):就绪状态的线程获取了CPU后,执行程序代码(run()方法中的代码);
4. 阻塞状态(Blocked):线程因为某种原因放弃CPU使用权,暂时停止运行,直到线程进入就绪状态,才有机会转到运行状态;阻塞的情况分三种:
  (1)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁);
  (2)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中;
  (3)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程设置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(sleep不会释放持有的锁);
5. 死亡状态(Dead):线程执行结束或者因异常退出了run()方法,线程进入死亡状态,结束生命周期;  


线程调度

线程优先级: Java线程的优先级用整数表示,取值范围为1~10,优先级高的线程会获得更多的机会获取CPU资源,Thread类有以下三个静态常量:
1. static int MAX_PRIORITY:线程可以具有的最高优先级,取值为10;
2. static int MIN_PRIORITY:线程可以具有的最低优先级,取值为1;
3. static int NORM_PRIORITY:分配给线程的默认优先级,取值为5;
Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。

  每个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY。线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread类的三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。

线程睡眠: Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间长短,以毫秒为单位,当睡眠结束后,就转为就绪状态,sleep()平台移植性好。

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

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

线程加入: join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个线程运行结束,当前线程再由阻塞转为就绪状态。

线程唤醒: Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait ()方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。


常用函数

sleep(long millis): 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),让出CUP的使用、目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会;

  sleep()是Thread类的Static(静态)方法;因此他不能改变对象的机锁,所以当在一个Synchronized块中调用Sleep()方法是,线程虽然休眠了,但是对象的机锁并木有被释放,其他线程无法访问这个对象(即使睡着也持有对象锁)。
  在sleep()休眠时间期满后,该线程不一定会立即执行,这是因为其它线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级。

yield(): 暂停当前正在执行的线程对象,并执行其他线程。

  yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
  
  结论: yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。
  
  sleep()和yield()的区别 
  
  sleep()和yield()的区别:sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。
  sleep 方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于可运行状态,如有,则把 CPU 的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程。
  另外,sleep()方法允许较低优先级的线程获得运行机会,但 yield()方法执行时,当前线程仍处在可运行状态,所以,较低优先级的线程不可能获得CPU占有权。在一个运行系统中,如果较高优先级的线程没有调用 sleep 方法,又没有受到 I\O 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。

join(): 等待该线程终止。这里需要理解的就是该线程是指的主线程等待子线程的终止。也就是在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行。

  为什么要用join()方法?

  在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量耗时的运算或网络请求操作,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。可以写个小程序试一试加不加join()方法的区别。

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

wait(): 该方法的作用是将当前运行的线程挂起(即让其进入阻塞状态),直到其他线程调用notify()或notifyAll()方法来唤醒该线程。
  
  注意: wait()方法的使用必须在同步的范围内,Obj.wait(),与Obj.notify()必须要与synchronized(Obj)一起使用,也就是wait,与notify是针对已经获取了Obj锁进行操作,从语法角度来说就是Obj.wait(),Obj.notify必须在synchronized(Obj){…}语句块内,并且必须先调用notify()后调用wait(),否则就会抛出IllegalMonitorStateException异常,wait()方法的作用就是阻塞当前线程等待notify/notifyAll方法,或等待超时后自动唤醒。但有一点需要注意,notify()调用后,并不是马上就释放对象锁的,而是在相应的synchronized(Obj){…}语句块执行结束,自动释放锁。
  
  下面是Object.wait(),Object.notify()的应用小示例(多线程交替输出奇偶数问题),为了控制线程执行的顺序,那么就必须要确定唤醒、等待的顺序。如下:

public class SynchronizedThread {
    public static void main(String args[]) {
        Num num = new Num(0);
        new Thread(new ThEdd(num)).start();
        new Thread(new ThOdd(num)).start();
    }
}

class Num {
    public int num;
    public Num(int num) {
        this.num = num;
    }
    // 打印奇数,此方法加锁
    public synchronized void printOdd() {
        System.out.println("OddNum :" + (num++));
        try {
            // 唤醒下一个等待线程
            this.notifyAll();
            // 释放方法自身对象锁
            this.wait();
            Thread.sleep(500);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    // 打印偶数,此方法加锁
    public synchronized void printEdd() {
        System.out.println("EvenNum:" + (num++));
        try {
            this.notifyAll();
            this.wait();
            Thread.sleep(500);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
//打印 奇数的线程
class ThOdd implements Runnable {
    private Num num;
    public ThOdd(Num num) {
        this.num = num;
    }
    public void run() {
        while (true) {
            num.printOdd();
        }
    }
}
// 打印偶数的线程
class ThEdd implements Runnable {
    private Num num;
    public ThEdd(Num num) {
        this.num = num;
    }
    public void run() {
        while (true) {
            num.printEdd();
        }
    }
}

运行示例:

这里写图片描述
  
  wait()和sleep()的区别:
  两者最简单的区别是,wait()方法是Object的方法,依赖于同步,而sleep()方法是Thread类的方法,可以直接调用,更深层次的区别在于sleep()方法只是暂时让出CPU的执行权,并不释放锁,而wait()方法则需要释放锁。下面示例所示:

public class SleepAndWait {

    public synchronized void sleepMethod() {
        System.out.println("Sleep start-----");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Sleep end-----");
    }

    public synchronized void waitMethod() {
        System.out.println("Wait start-----");
        synchronized (this) {
            try {
                wait(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("Wait end-----");
    }

    public static void main(String[] args) {
        final SleepAndWait test1 = new SleepAndWait();

        for (int i = 0; i < 3; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    test1.sleepMethod();
                }
            }).start();
        }

        try {
            Thread.sleep(5000);// 暂停五秒,等上面程序执行完成
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("-----分割线-----");

        final SleepAndWait test2 = new SleepAndWait();

        for (int i = 0; i < 3; i++) {
            new Thread(new Runnable() {

                @Override

                public void run() {
                    test2.waitMethod();
                }
            }).start();
        }
    }
}

运行示例:

这里写图片描述

  结果的区别很明显,通过sleep()方法实现的暂停,程序是顺序进入同步块的,只有当上一个线程执行完成的时候,下一个线程才能进入同步方法,sleep()暂停期间一直持有monitor对象锁,其他线程是不能进入的。而wait方法则不同,当调用wait方法后,当前线程会释放持有的monitor对象锁,因此,其他线程还可以进入到同步方法,线程被唤醒后,需要竞争锁,获取到锁之后再继续执行。


常见线程名词

  主线程: JVM调用程序main()所产生的线程。
  
  当前线程: 这个是容易混淆的概念。一般指通过Thread.currentThread()来获取的进程。
  
  后台线程: 指为其他线程提供服务的线程,也称为守护线程。JVM的垃圾回收线程就是一个后台线程。可以通过Thread类的isDaemon()和setDaemon()方法来判断和设置一个线程是否为后台线程。
  
  前台线程: 是指接受后台线程服务的线程,其实前台与后台线程是联系在一起,就像傀儡和幕后操纵者一样的关系。傀儡是前台线程、幕后操纵者是后台线程。由前台线程创建的线程默认也是前台线程。


线程同步

  首先,我们要知道为什么要线程同步?
  
  因为当我们有多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常。
  
  线程同步的使用方法有:
  1、同步方法: 指使用synchronized关键字修饰的方法。 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法。此时,不同的对象实例的synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法。

    // synchronized关键字修饰的方法,它锁定的是调用这个同步方法的对象
    public  synchronized void method(Parm parm){  
        ...... 
    }  

  另外,synchronized还可用与同步static方法,如下:

    public class Test{
        // 同步的static方法
        public synchronized static void methodA() {  
            // ......
        }
        public void methodB() {
            synchronized(Test.class)   //  class literal(类名称字面常量)  
        }
    }  

  代码中的methodBBB()方法是把class literal作为锁的情况,它和同步的static函数产生的效果是一样的,所取到的锁很特别,是当前调用这个方法的对象所属的类(Class,而不再是由这个Class产生的某个具体对象了)。

  可以推断:如果一个类中定义了一个synchronized的static方法A,也定义了一个synchronized 的instance方法B,那么这个类的同一对象Obj在多线程中分别访问A和B两个方法时,不会构成同步,因为它们的锁是不一样。A方法的锁是Obj这个对象,而B的锁是Obj所属的那个Class类。

  2、同步代码块 :指使用synchronized关键字修饰的语句块(synchronized修饰的大括号内的语句块)。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。

    public void method(Parm parm){  
        // synchronized关键字修饰的代码块,它锁定的是parm这个对象,只有拿到这个锁才能运行此代码块
        synchronized(parm){
             ...... 
        }
    }

  注: 同步是一种高开销的操作,因此应该尽量减少同步的内容和无畏的同步控制。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
  
  同步方法和同步语句块,如果再细的分类的话,synchronized可作用于instance变量、object reference(对象引用)、static函数和class literals(类名称字面常量)身上。

  3、使用特殊域变量(Volatile)实现线程同步
  
  volatile不能保证原子操作,因此volatile不能代替synchronized。此外volatile会组织编译器对代码优化,因此能不使用它就不适用它。它的原理是每次线程要访问volatile修饰的变量时都是从内存中读取,而不是从缓存当中读取,因此每个线程访问到的变量值都是一样的,这样就保证了同步。
  
  a、volatile关键字为域变量的访问提供了一种免锁机制。
  b、使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新。
  c、因此每次使用该域就要重新计算,而不是使用寄存器中的值。
  d、volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。
  
  注意:
  1、编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和安全做出正确的判断,对“原子”操作做出分析,并保证原子操作期间别的线程无法访问竞争资源。
  
  2、线程同步方法是通过锁来实现,每个对象都有切仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的其他非同步方法。
  

线程数据传递

  在同步开发模式下,当我们调用一个函数时,通过这个函数的参数将数据传入,并通过这个函数的返回值来返回最终的计算结果。但在多线程的异步开发模式下,数据的传递和返回与同步开发模式有很大的区别。由于线程的运行和结束是不可预料的,因此,在传递和返回数据时就无法象函数一样通过函数参数和return语句来返回数据。
  
  1、通过构造方法传递数据
  
  在创建线程时,必须要建立一个Thread类的或其子类的实例。因此,我们不难想到在调用start方法之前通过线程类的构造方法将数据传入线程。并将传入的数据使用类变量保存起来,以便线程使用(其实就是在run方法中使用)。
  
  2、通过变量和方法传递数据
  
  向对象中传入数据一般有两次机会,第一次机会是在建立对象时通过构造方法将数据传入,另外一次机会就是在类中定义一系列的public的方法或变量(也可称之为字段)。然后在建立完对象后,通过对象实例逐个赋值。上述两种都比较好理解,不在赘述。
  
  3、通过回调函数传递数据
  
  上面讨论的两种向线程中传递数据的方法是最常用的。但这两种方法都是main方法中主动将数据传入线程类的。这对于线程来说,是被动接收这些数据的。然而,在有些应用中需要在线程运行的过程中动态地获取数据,如在下面代码的run方法中产生了3个随机数,然后通过Work类的process方法求这三个随机数的和,并通过Data类的value将结果返回。  

public class CallBackThread extends Thread {
    private Work work;

    public CallBackThread(Work work) {
        this.work = work;
    }

    public void run() {
        java.util.Random random = new java.util.Random();
        Data data = new Data();
        int n1 = random.nextInt(1000);
        int n2 = random.nextInt(2000);
        int n3 = random.nextInt(3000);
        int[] num = { n1, n2, n3 };
        work.process(data, num); // 使用回调函数
        System.out.println(String.valueOf(n1) + " + " + 
                String.valueOf(n2) + " + " + String.valueOf(n3)
                + " = " + data.value);
    }

    public static void main(String[] args) {
        Thread thread = new CallBackThread(new Work());
        thread.start();
    }
}

class Data {
    public int value = 0;
}

class Work {
    public void process(Data data, int[] num) {
        for (int n : num) {
            data.value += n;
        }
    }
}

运行示例:

这里写图片描述

参考文献:http://blog.csdn.net/evankaka/article/details/44153709

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值