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


    前言:

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

    并发与并行

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

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

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

    线程与进程

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

    创建线程类

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

    定义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接口

    步骤如下:

    定义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: 创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期
    地执行。

    小结:

    今天的分享就到这里了,大家看完有什么不懂的话可以发私信问我,我看到都会回复的。

  • 相关阅读:
    luogu P3801 红色的幻想乡
    [USACO07OPEN]吃饭Dining
    [SCOI2007]蜥蜴
    [SDOI2011]染色
    [USACO07DEC]Sightseeing Cows
    [HNOI2007]紧急疏散EVACUATE
    [ZJOI2009]假期的宿舍
    [SDOI2013]费用流
    [CQOI2009]跳舞
    使用chrony安装chrony
  • 原文地址:https://www.cnblogs.com/lwh1019/p/13130966.html
Copyright © 2020-2023  润新知