Java多线程基础(面试向)

Posted liangyueyuan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java多线程基础(面试向)相关的知识,希望对你有一定的参考价值。

----?为什么要用到多线程

    CPU是以时间片的方式为进程分配CUP处理时间的,如果当一个进程同时要完成几件事的时候,如当从网上下载文件的时候,需要一边下载一边显示进度而且还要一边保存,如果按照单线程的思想,那么这种情况就只能一个完成之后才能继续下一个,如果将CPU看成有100个时间片的话,那么完成一件事可能只占了10%,这样剩下的就白白浪费了,没有充分的利用到CPU的资源。如果同时处理3件事的话,那CPU的利用率就提升到了30%,这样就大幅地提升了CPU的利用率。

   总的一句话就是:CPU还是要花同样多的时间去完成所有的事情,但多线程可以让CPU掺插地同时做多件事情,在视觉上让用户觉得计算机在同时帮他处理多件事情,更好地改善用户体验。

 

----? 程序、进程和线程

  程序:一段静态代码的集合。为完成特定的功能,编写的一组指令的集合。即指一段静态的代码,静态对象。

  进程:程序的一次运行,或者是在运行一个程序。动态进程:有它自身的产生、存亡和销毁过程

  程序是静态的,进程是动态的

  线程:每一个进程都包含有多个线程,相当于进程的细化,每一个Java程序都隐含一个进程,即main方法

注意:多线程不是为了提高执行速度,而是提高应用程序的使用率.,线程和线程共享”堆内存和方法区内存”.栈内存是独立的,一个线程一个栈.

 

----? 创建多线程的方式

    继承Thread类,实现Runnable接口,实现Callable接口,开辟线程池(线程池提供了线程队列,队列包含多个等待状态线程)

--->继承Thread类

public class ThreadTest01 {

    public static void main(String[] args) {
        //设置线程名字
        Thread.currentThread().setName("main thread");
        MyThread myThread = new MyThread();
        myThread.setName("子线程:");
        //开启线程
        myThread.start();
        for(int i=0; i<5; i++){
        System.out.println(Thread.currentThread().getName()+i);
        }        
    }
}

class MyThread extends Thread{
    @Override
    public void run() {
        for(int i=0; i<10; i++){
            System.out.println(Thread.currentThread().getName()+i);
        }
    }
}

--->实现Runnable接口

public class ThreadTest02 {
    public static void main(String[] args) {
        Thread.currentThread().setName("main thread");
        Thread myThread = new Thread(new MyRunnable());
        myThread.setName("子线程:");
        //开启线程
        myThread.start();
        for(int i=0; i<5; i++){
            System.out.println(Thread.currentThread().getName()+i);
        }            
    }
}

class MyRunnable implements Runnable{
    @Override
    public void run() {
        for(int i=0; i<10; i++){
            System.out.println(Thread.currentThread().getName()+i);
        }        
    }
    
}

以上两段程序输出内容相同,只是实现的方式不同

main thread0
子线程:0
子线程:1
子线程:2
子线程:3
子线程:4
子线程:5
子线程:6
子线程:7
子线程:8
子线程:9
main thread1
main thread2
main thread3
main thread4

 

!!!!Thread和Runnable区别

如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。

总结:

实现Runnable接口比继承Thread类所具有的优势:

1):适合多个相同的程序代码的线程去处理同一个资源

2):可以避免java中的单继承的限制

3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立

4):线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类

main方法其实也是一个线程。在java中所以的线程都是同时启动的,至于什么时候,哪个先执行,完全看谁先得到CPU的资源。

在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个jVM实习在就是在操作系统中启动了一个进程。

--->实现Callable接口

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadTest03 {

    public static void main(String[] args) {
        //执行Callable 方式,需要FutureTask 实现实现,用于接收运算结果
        FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyCallable());
        new Thread(futureTask).start();
        //接收线程运算后的结果
        Integer sum;
        try {
            sum = futureTask.get();
            System.out.println(sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }        
    }
}

class MyCallable implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for(int i=0; i<100; i++){
            sum += i;
        }
        return sum;
    }
    
}

相较于实现Runnable 接口的实现,方法可以有返回值,并且抛出异常

--->线程池

  线程池提供了一个线程队列,队列中保存着所有等待状态的线程。避免了创建与销毁额外开销,提交了响应速度

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
//线程池实现
public class ThreadPoolExecutorTest {
    public static void main(String[] args) {
  //创建线程池
  ExecutorService executorService = Executors.newFixedThreadPool(10);
  ThreadPool threadPool = new ThreadPool();
  for(int i =0;i<5;i++){
  //为线程池分配任务
  executorService.submit(threadPool);
  }
  //关闭线程池
 executorService.shutdown();
  }
 }
class ThreadPool implements Runnable {
    @Override
  public void run() {  for(int i = 0 ;i<10;i++){
  System.out.println(Thread.currentThread().getName() + ":" + i);
  }
 }
}

 

----? 线程状态

技术分享图片

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

 ----? 线程的调度与控制

    线程的调度模型分为: 分时调度模型抢占式调度模型,Java使用抢占式调度模型
    通常我们的计算机只有一个 CPU,CPU 在某一个时刻只能执行一条指令,线程只有得到 CPU时间片,也就是使用权,才可以执行指令。在单 CPU 的机器上线程不是并行运行的,只有在多个 CPU 上线程才可以并行运行。Java 虚拟机要负责线程的调度,取得 CPU 的使用权,目前有两种调度模型:分时调度模型和抢占式调度模型,Java 使用抢占式调度模型。

    分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片

    抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些

    那么CPU的抢占式调度是如何实现?

    CPU是通过设置时间中断来实现抢占机制的,在进入保护模式之前,先初始化中断向量表,在时钟中断入口处放置任务切换代码,然后设置好时钟中断的时间。

    当某一线程的执行用光了时间片时,时钟中断产生,CPU转去执行中断处的任务切换代码,保存当前线程状态,得到并恢复下一个线程的状态,然后转去执行那个线程,以此类推。

   为了尽可能地提高CPU的利用率,当某一线程必须进入等待状态,如果要获取某一低速外设的数据时,这里的读操作程序会主动放弃当前的没有用光的时间片,主动调用任务切换代码,把CPU的时间让给其他已准备好可以继续的线程。

 

----? 线程的优先级

1、调整线程优先级:Java线程有优先级,优先级高的线程会获得较多的运行机会。
 
Java线程的优先级用整数表示,取值范围是1~10,Thread类有以下三个静态常量:
static int MAX_PRIORITY
          线程可以具有的最高优先级,取值为10。
static int MIN_PRIORITY
          线程可以具有的最低优先级,取值为1。
static int NORM_PRIORITY
          分配给线程的默认优先级,取值为5。
 
    Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。
    每个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY。
    线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。
 
2、线程睡眠:Thread.sleep(long millis)方法:sleep 设置休眠的时间,单位毫秒,当一个线程遇到 sleep 的时候,就会睡眠,进入到阻塞状态,放弃 CPU,腾出 cpu 时间片,给其他线程用,所以在开发中通常我们会这样做,使其他的线程能够取得 CPU 时间片,当睡眠时间到达了,线程会进入可运行状态,得到 CPU 时间片继续执行,如果线程在睡眠状态被中断了,将会抛出 IterruptedException
public class ThreadTest04 {

    public static void main(String[] args) {
        Runnable runnable = new Processor();
        Thread thread_1 = new Thread(runnable,"r1");
        thread_1.start();
        Thread thread_2 = new Thread(runnable,"r2");
        thread_2.start();
    }

}

class Processor implements Runnable{

    @Override
    public void run() {
        for(int i=0; i<100; i++){
         System.out.println(Thread.currentThread().getName() + "," + i);
         
         if(i%10 == 0){
             try {
//睡眠100毫秒,将CPU时间片交给其他线程使用 Thread.sleep(
100); } catch (InterruptedException e) { e.printStackTrace(); } } } } }

结果:当哪个线程运行时自己的i的值能被10整除的,那么这个线程就进入休眠状态,让另外一个线程运行。多次运行发现,也有可能依旧是当前的线程休眠好后仍然占用资源继续运行,估计是睡眠时间比较短的问题

 

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

为什么要用join()方法

在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。

public class ThreadTest05 {
    
       public static void main(String[] args) {  
            System.out.println(Thread.currentThread().getName()+"主线程运行开始!");  
            Thread1 mTh1=new Thread1("A");  
            Thread1 mTh2=new Thread1("B");  
            mTh1.start();  
            mTh2.start();  
            try {
                mTh1.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            try {
                mTh2.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+ "主线程运行结束!");  
      
        }  

}
class Thread1 extends Thread{  
    private String name;  
    public Thread1(String name) {  
        super(name);  
       this.name=name;  
    }  
    public void run() {  
        System.out.println(Thread.currentThread().getName() + " 线程运行开始!");  
        for (int i = 0; i < 5; i++) {  
            System.out.println("子线程"+name + "运行 : " + i);  
            try {  
                sleep((int) Math.random() * 10);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
        System.out.println(Thread.currentThread().getName() + " 线程运行结束!");  
    }  
}  

结果:每次运行,主线程都要等子线程运行结束后自己才结束运行

main主线程运行开始!
B 线程运行开始!
A 线程运行开始!
子线程B运行 : 0
子线程A运行 : 0
子线程B运行 : 1
子线程A运行 : 1
子线程B运行 : 2
子线程A运行 : 2
子线程B运行 : 3
子线程A运行 : 3
子线程B运行 : 4
子线程A运行 : 4
B 线程运行结束!
A 线程运行结束!
main主线程运行结束!

4、线程让步:Thread.yield() 方法

      yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中

      结论:大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。

public class ThreadTest06 {

    public static void main(String[] args) {
        MyThread_2 yt1 = new MyThread_2("张三");  
        MyThread_2 yt2 = new MyThread_2("李四");  
        yt1.start();  
        yt2.start();  
    }

}

class MyThread_2 extends Thread{
    
    MyThread_2(String s){
        super(s);
    }
    @Override
    public void run() {
        for(int i=0; i<=50; i++){
            System.out.println(Thread.currentThread().getName()+":"+i);
            if(i==30){
          // 当i为30时,该线程就会把CPU时间让掉,让其他或者自己的线程执行(也就是谁先抢到谁执行)  
                this.yield();
            }
        }
    }
}

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

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

 

~~~各种区别

   sleep()、wait()?

  • . wait()和sleep()都可以通过interrupt()方法 打断线程的暂停状态 ,从而使线程立刻抛出InterruptedException。 
      如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B正在wait/sleep /join,则线程B会立刻抛InterruptedException,在catch() {} 中直接return即可安全地结束线程。 
        需要注意的是,InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛出的。对某一线程调用 interrupt()时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。但是,一旦该线程进入到 wait()/sleep()/join()后,就会立刻抛出InterruptedException
  • sleep()是Thread类的方法,wait()是Object类的方法
  • sleep()方法正在执行的线程主动让出CPU(然后CPU就可以去执行其他任务),在sleep指定时间后CPU再回到该线程继续往下执行(注意:sleep方法只让出了CPU,而并不会释放同步资源锁!!!);wait()方法则是指当前线程让自己暂时退让出同步资源锁,以便其他正在等待该资源的线程得到该资源进而运行,只有调用了notify()方法,之前调用wait()的线程才会解除wait状态,可以去参与竞争同步资源锁,进而得到执行。(注意:notify的作用相当于叫醒睡着的人,而并不会给他分配任务,就是说notify只是让之前调用wait的线程有权利重新参与线程的调度)
  • sleep()方法可以在任何地方使用;wait()方法则只能在同步方法或同步块中使用
  • 在调用sleep()方法的过程中,线程不会释放对象锁。

    而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备,获取对象锁进入运行状态

举个栗子 

package com.xxg.thread;

public class ThreadTest07 {

    public static void main(String[] args) {
        new Thread(new Thread01()).start();
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        new Thread(new Thread02()).start();
    }

}

class Thread01 implements Runnable {

    @Override
    public void run() {
        synchronized (ThreadTest07.class) {
            System.out.println("enter thread1...");
            System.out.println("thread1 is waiting...");

            try {
                // 调用wait()方法,线程会放弃对象锁,进入等待此对象的等待锁定池
                ThreadTest07.class.wait();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println("thread1 is going on ....");
            System.out.println("thread1 is over!!!");
        }
    }
}

class Thread02 implements Runnable {

    @Override
    public void run() {
        synchronized (ThreadTest07.class) {
            System.out.println("enter thread2...");
            System.out.println("thread2 is sleeping...");
            // 只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
            ThreadTest07.class.notify();

            // 区别
            // 如果我们把代码:TestD.class.notify();给注释掉,即TestD.class调用了wait()方法,但是没有调用notify()
            // 方法,则线程永远处于挂起状态。
            try {
                // sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,
                // 但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。
                // 在调用sleep()方法的过程中,线程不会释放对象锁。
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println("thread2 is going on ....");
            System.out.println("thread2 is over!!!");
        }
    }
}

结果:

技术分享图片

注释掉  ThreadTest07.class.notify();,运行结果如下,且程序出于挂起状态

技术分享图片

   sleep()、yield()?

  • sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行
  • sleep 方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU  的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程
  •   另外,sleep 方法允许较低优先级的线程获得运行机会,但 yield()  方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程些时获得 CPU 占有权。在一个运行系统中,如果较高优先级的线程没有调用 sleep 方法,又没有受到 IO 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。
  • sleep()方法比yield()方法有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行。

 ----? 线程常见名词

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

 线程类的一些常用方法:
  sleep(): 强迫一个线程睡眠N毫秒。 
  isAlive(): 判断一个线程是否存活。 
  join(): 等待线程终止。 
  activeCount(): 程序中活跃的线程数。 
  enumerate(): 枚举程序中的线程。 
       currentThread(): 得到当前线程。 
  isDaemon(): 一个线程是否为守护线程。 
  setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束) 
  setName(): 为线程设置一个名称。 
  wait(): 强迫一个线程等待。 
  notify(): 通知一个线程继续运行。 
  setPriority(): 设置一个线程的优先级。

----? 线程同步

     多线程并发时,多个线程同时请求同一个资源,必然导致此资源的数据不安全,A线程修改了B线程的处理的数据,而B线程又修改了A线程处理的数理。显然这是由于全局资源造成的,有时为了解决此问题,优先考虑使用局部变量,退而求其次使用同步代码块,出于这样的安全考虑就必须牺牲系统处理性能,加在多线程并发时资源挣夺最激烈的地方,这就实现了线程的同步机制

要跨线程维护正确的可见性,只要在几个线程之间共享非 final 变量,就必须使用synchronized(或 volatile)以确保一个线程可以看见另一个线程做的更改

为了在线程之间进行可靠的通信,也为了互斥访问,同步是必须的。这归因于java语言规范的内存模型,它规定了:一个线程所做的变化何时以及如何变成对其它线程可见。

同步和多线程关系:没多线程环境就不需要同步;有多线程环境也不一定需要同步。

锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)。

互斥 即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据。

可见性 要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 —— 如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题

小结:为了防止多个线程并发对同一数据的修改,所以需要同步,否则会造成数据不一致(就是所谓的:线程安全。如java集合框架中Hashtable和Vector是线程安全的。我们的大部分程序都不是线程安全的,因为没有进行同步,而且我们没有必要,因为大部分情况根本没有多线程环境)。

--->同步机制实现方法

 1)synchronized

    synchronized的作用域有两个,在对象实例内,如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法。这时,不同的对象实例的synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法;
   在某个类的范围,synchronized static aStaticMethod{}防止多个线程同时访问这个类中的synchronized static 方法。它可以对类的所有对象实例起作用。
   synchronized关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块。如果再细的分类,synchronized可作用于instance变量、object reference(对象引用)、static函数和class literals(类名称字面常量)身上。

   synchronized关键字是不能继承的,也就是说,基类的方法synchronized f(){} 在继承类中并不自动是synchronized f(){},而是变成了f(){}。继承类需要你显式的指定它的某个方法为synchronized方法。

 

 

 

 
























以上是关于Java多线程基础(面试向)的主要内容,如果未能解决你的问题,请参考以下文章

JAVA多线程和并发基础面试问答

java基础--31.多线程常见的面试题

JAVA多线程和并发基础面试问答

JAVA多线程和并发基础面试问答

JAVA多线程和并发基础面试问答

JAVA多线程和并发基础面试问答(转载)