面试阿里,字节跳动99%会被问到的java线程和线程池,看完这篇你就懂了!

前言:

最近也是在后台收到不少小伙伴私信问我线程和线程池这一块的问题,说本身在面试的时候总是被问到这一块的问题,被问的很头疼。前几天看到后帮几个小伙伴解决了问题,可是问的人有点多我一个个回答也回答不过来,干脆花了一个上午时间写了这篇文章分享给你们。话很少说,满满的干货都在下面了!java

并发与并行

并发:指两个或多个事件在同一个时间段内发生。
在操做系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每 一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感受是同时运行,那是由于分 时交替运行的时间是很是短的。面试

并行:指两个或多个事件在同一时刻发生(同时发生)。
在多个 CPU 系统中,这些能够并发执行的程序即可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每一个处理器来处理一个能够并发执行的程序,这样多个程序即可以同时执行。目前电脑市场上说的多核 CPU,即是多核处理器,核越多,可以并行处理的程序数量越多,这能大大的提升电脑运行的效率。安全

注意:单核处理器的计算机确定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是同样的,从宏观角度上理解线程是并行运行的,可是从微观角度上分析倒是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,咱们把这种状况称之为线程调度。多线程

线程与进程

进程:是指一个内存中运行的应用程序,每一个进程都有一个独立的内存空间,一个应用程序能够同时运行多 个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序便是一个进程从创 建、运行到消亡的过程。
线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程 中是能够有多个线程的,这个应用程序也能够称之为多线程程序并发

建立线程类

Java使用 java.lang.Thread 类表明线程,全部的线程对象都必须是Thread类或其子类的实例。每一个线程的做用是完成必定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来表明这段程序流。
Java中经过继承Thread类来建立并启动多线程的步骤以下:ide

定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就表明了线程须要完成的任务,所以把 run()方法称为线程执行体。
建立Thread子类的实例,即建立了线程对象。
调用线程对象的start()方法来启动该线程。
首先自定义一个线程类工具

public class ThreadClass extends Thread {
    //重写run方法
    @Override
    public void run()
    {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+"正在执行"+i);
            try {
                //休眠500毫秒
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

主线程:操作系统

public class DemoTest {
    public static void main(String[] args) {
        //建立一个线程对象
        ThreadClass mythread = new ThreadClass();
        //开启线程
        mythread.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("主线程正在执行" + i);
            try {
                //休眠500毫秒
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

其实实际上咱们通常不会去继承线程类,因为java的单继承特性,当咱们继承了线程类就没法继承别的父类了,通常咱们是经过重写接口来开启线程的。线程

重写Runnable接口

步骤以下:code

定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体一样是该线程的线程执行体。
建立Runnable实现类的实例,并以此实例做为Thread的target来建立Thread对象,该Thread对象才是真正 的线程对象。
调用线程对象的start()方法来启动线程。
首先重写接口

public class Runnableimp implements Runnable {
    //重写run方法
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+"正在执行"+i);
            try {
                //休眠500毫秒
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

主线程:

public class DemoTest {
    public static void main(String[] args) {
        //建立一个线程对象,传入重写了run方法的接口对象
        Thread mythread = new Thread(new Runnableimp());
        //开启线程
        mythread.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("主线程正在执行" + i);
            try {
                //休眠500毫秒
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

执行的结果和刚才相同。

匿名内部类方式实现线程的建立

public class DemoTest {
    public static void main(String[] args) {
        //建立一个线程对象,使用匿名内部类重写run方法
        Thread mythread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName()+"正在执行"+i);
                    try {
                        //休眠500毫秒
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        //开启线程
        mythread.start();
    }
}

使用lambda表达式

public class DemoTest {
    public static void main(String[] args) {
        //建立一个线程对象,使用lambda表达式重写run方法
        Thread mythread = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName()+"正在执行"+i);
                try {
                    //休眠500毫秒
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        //开启线程
        mythread.start();
    }
}

线程安全

线程安全问题都是由全局变量及静态变量引发的。若每一个线程中对全局变量、静态变量只有读操做,而无写操做,通常来讲,这个全局变量是线程安全的;如有多个线程同时执行写操做,通常都须要考虑线程同步, 不然的话就可能影响线程安全。

线程同步

当咱们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操做,就容易出现线程安全问题。
要解决上述多线程并发访问一个资源的安全性问题,Java中提供了同步机制 (synchronized)来解决。

同步代码块

同步代码块: synchronized 关键字能够用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
格式:

synchronized(同步锁){
      须要同步操做的代码 
      }

示例

private int num = 100;
private Object lock = new Object();
synchronized (lock)
{
      num--;
}

同步方法

同步方法:使用synchronized修饰的方法,就叫作同步方法,保证A线程执行该方法的时候,其余线程只能在方法外 等着。

public synchronized void method()
{     
    可能会产生线程安全问题的代码   
}

Lock锁

java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更普遍的锁定操做, 同步代码块/同步方法具备的功能Lock都有,除此以外更强大,更体现面向对象。
Lock锁也称同步锁,加锁与释放锁以下:

public void lock() :加同步锁。
public void unlock() :释放同步锁。

Lock lock = new ReentrantLock();    
//加锁
lock.lock();
可能会产生线程安全问题的代码  
//释放锁
lock.unlock();

咱们使用线程的时候就去建立一个线程,这样实现起来很是简便,可是就会有一个问题:
若是并发的线程数量不少,而且每一个线程都是执行一个时间很短的任务就结束了,这样频繁建立线程就会大大下降系统的效率,由于频繁建立线程和销毁线程须要时间。
那么有没有一种办法使得线程能够复用,就是执行完一个任务,并不被销毁,而是能够继续执行其余的任务?
在Java中能够经过线程池来达到这样的效果。今天咱们就来详细讲解一下Java的线程池。

线程池

线程池:其实就是一个容纳多个线程的容器,其中的线程能够反复使用,省去了频繁建立线程对象的操做, 无需反复建立线程而消耗过多资源。

Java里面线程池的顶级接口是 java.util.concurrent.Executor ,可是严格意义上讲 Executor 并非一个线程 池,而只是一个执行线程的工具。真正的线程池接口是 java.util.concurrent.ExecutorService 。
要配置一个线程池是比较复杂的,尤为是对于线程池的原理不是很清楚的状况下,颇有可能配置的线程池不是较优 的,所以在 java.util.concurrent.Executors线程工厂类里面提供了一些静态工厂,生成一些经常使用的线程池。官方建议使用Executors工程类来建立线程池对象。
newFixedThreadPool方法

public static ExecutorService newFixedThreadPool(int nThreads)

建立一个可重用固定线程数的线程池,以共享的***队列方式来运行这些线程。在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。若是在全部线程处于活动状态时提交附加任务,则在有可用线程以前,附加任务将在队列中等待。若是在关闭前的执行期间因为失败而致使任何线程终止,那么一个新线程将代替它执行后续的任务(若是须要)。在某个线程被显式地关闭以前,池中的线程将一直存在。

参数:

nThreads - 池中的线程数

返回:

新建立的线程池

抛出:

IllegalArgumentException - 若是 nThreads <= 0
获取到了一个线程池ExecutorService 对象,那么怎么使用呢,在这里定义了一个使用线程池对象的方法以下: public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行。

下面的代码经过四种方式向线程池中提交任务执行

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Demo01 {
    public static void main(String[] args) throws InterruptedException {
        //建立线程池,线程数量为2
        ExecutorService es = Executors.newFixedThreadPool(2);
        //将任务扔到线程池的四种方式
        //使用匿名内部类,
        es.submit(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName()+"正在执行"+i);
                    try {
                        //休眠500毫秒
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        //使用lambda表达式
        es.submit(()->{
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName()+"正在执行"+i);
                    try {
                        //休眠500毫秒
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        //使用重写的接口
        es.submit(new Runnableimp());
        //使用重写的线程类
        es.submit(new ThreadClass());
        //启动一次顺序关闭,执行之前提交的任务,但不接受新任务
        es.shutdown();
        //主线程等待全部线程将任务执行完毕
        while (!es.isTerminated());
        System.out.println("线程执行完毕!");
    }
}

除此以外java还提供了:

newScheduledThreadPool:建立一个线程池,它可安排在给定延迟后运行命令或者按期地执行。
newSingleThreadExecutor:建立一个使用单个 worker 线程的 Executor,以***队列方式来运行该线程。
newSingleThreadScheduledExecutor: 建立一个单线程执行程序,它可安排在给定延迟后运行命令或者按期
地执行。

小结:

今天的分享就到这里了,你们看完有什么不懂的话能够发私信问我,我看到都会回复的。

相关文章
相关标签/搜索