BAT面试官被我干懵了,Java多线程你真的理解透彻了吗?带你玩转一次多线程!Let's go!别再out了!

在这里插入图片描述


神标题引入

Java语言提供了非常优秀的多线程支持,程序可以通过非常简单,简单不能再简单的方式来创建和启动多线程!!!接下来,带你玩转一次多线程!!!走起!!!


线程和进程

一定要分清楚线程和进程的区别,几乎所有的操作系统都有“进程”这一概念!一任务,一程序。每一个运行中的程序就是一个进程!当程序运行时,其内部包含了多个顺序执行流,每一个顺序执行流就是一个线程!

进程有以下三种特征:

  • 独立性:进程是系统中独立存在的实体,可拥有自己的独立资源,每个进程都有自己私有的地址空间。在没有经过进程本身允许的情况下,用户不能够直接访问其它进程的地址空间!

  • 动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中动的指令集合。在进程中加入了时间的概念。进程具有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的。

  • 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响。

多线程则扩展了多进程的概念,使得同一个进程可以同时并发处理多个任务。线程(Thread)也被称作轻量级进程( Lightweight Process),线程是进程的执行单元。就像进程在操作系统中的地位一样,线程在程序中是独立的、并发的执行流。当进程被初始化后,主线程就被创建了。对于绝大多数的应用程序来说,通常仅要求有一个主线程,但也可以在该进程内创建多条顺序执行流,这些顺序执行流就是线程,每个线程也是互相独立的。

线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源,它与父进程的其他线程共享该进程所拥有的全部资源。

最简单的话阐述,就是 一个程序运行后至少有一个进程,一个进程里可以包含多个线程,但至少要包含一个线程。


多线程的优势

  • 进程之间不能共享内存,但线程之间共享内存非常容易。
  • 系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程的效率高。
  • Java语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了Java的多线程编程。

线程创建方式

Java使用Thread类代表线程,所有的线程对象都必须是 Thread类 或其 子类的实例

继承Thread类来创建和启动

  1. 定义Thread类的子类,并写该类的run()方法,该run()方法的方法体就代表了线程需要完成
    的任务。因此把run()方法称为线程执行体。
  2. 创建Thread子类的实例,即创建了线程对象。
  3. 调用线程对象的start()方法来启动该线程。
public class Main extends Thread {

    /**
     * 程忆难
     * https://myhub.blog.csdn.net/
     *
     * @param args
     */
    public static void main(String[] args) {

        //获取线程的名字
        System.out.println("Thread name:" + Thread.currentThread().getName());

        //实例化后调用start启动
        new Main().start();

		//实例化后调用start启动
        new Main().start();
    }


    /**
     * 继承Thread重写run方法
     */
    @Override
    public void run() {
        System.out.println("Thread start!Thread name:" + Thread.currentThread().getName());
    }
}

运行共启动三个线程,一个主线程,两个子线程!
在这里插入图片描述
主线程的执行体不是由run()方法确定的,而是由main方法确定的,main方法的方法体代表主线程的线程执行体。

  • Thread.currentThread()是Thread类的静态方法,该方法返回正在执行的线程对象!
  • getName(): 该方法是Thread类的实例方法,该方法返回调用该方法的线程名字。
  • setName(): 设置线程的名字!

特别注意:
使用继承Thread类的方法来创建线程类时,多个线程之间无法共享线程类的实例变量。


实现Runnable接口重写run方法创建线程类

  1. 定义Runnable接口的实现类,并重写该接口的run0方法,该run(方法的方法体同样是该线程的线程执行体;
  2. 创建Runnable实现类的实例,并以此实例作为Thread的target 来创建Thread对象,该Thread对象才是真正的线程对象;
  3. 调用线程对象的start(方法来启动该线程。

Runnable 对象仅仅作为 Thread 对象的target, Runnable 实现类里包含的 run() 方法仅作为线程执行体。而实际的线程对象依然是Thread实例,只是该Thread 线程负责执行其 targetrun() 方法。

public class Main implements Runnable {

    /**
     * 程忆难
     * https://myhub.blog.csdn.net/
     *
     * @param args
     */
    public static void main(String[] args) {

        //获取线程的名字
        System.out.println("Thread name:" + Thread.currentThread().getName());

        Main main = new Main();
        new Thread(main, "Thread ONE").start();
        new Thread(main, "Thread TWO").start();

    }

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

开启两个线程,各for循环100次打印Thread Name。

在这里插入图片描述


使用 Callable 和 Future 创建线程

从Java5开始,Java提供了Callable 接口,Callable接口提供了一个 call() 方法可以作为线程执行体,但 call() 方法比 run() 方法功能更强大。

  • call() 方法可以有返回值;
  • call() 方法可以声明抛出异常。

因此完全可以提供一个 Callable 对象作为Thread的target,而该线程的线程执行体就是该Callable对象的call()方法。问题是: Callable 接口是Java 5新增的接口,而且它不是Runnable接口的子接口,所以Callable对象不能直接作为Thread的target。
Java 5提供了Future 接口来代表Callable接口里 call() 方法的返回值,并为Future 接口提供了一个FutureTask实现类,该实现类实现了Future 接口,并实现了Runnable 接口一可以作为Thread类的target。值得注意的是:Callable接口有泛型限制,Callable接口里的泛型形参类型与call() 方法返回值类型相同。而且Callable接口是函数式接口,因此可使用Lambda表达式创建Callable对象。

创建并启动有返回值的线程的步骤:

  1. 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,且该 call() 方法有返回值,再创建 Callable 实现类的实例。从Java 8开始,可以直接使用 Lambda 表达式创建 Callable 对象;
  2. 使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该Callable 对象的 call() 方法的返回值;
  3. 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程;
  4. 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Main {

    /**
     * 程忆难
     * https://myhub.blog.csdn.net/
     *
     * @param args
     */
    public static void main(String[] args) {

        //获取线程的名字
        System.out.println("Thread name:" + Thread.currentThread().getName());

        //创建Main对象
        Main main = new Main();

        //使用Lambda表达式创建Callable<Integer>对象
        //FutureTask包装Callable<Integer>对象
        FutureTask<Integer> task = new FutureTask<Integer>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {

                int temp = 0;
                while (temp < 10) {
                    temp++;
                    System.out.println("Thread name:" + Thread.currentThread().getName());

                }

                return temp;
            }
        });

        //启动线程
        new Thread(task).start();

        try {

            //线程返回值
            Integer integer = task.get();

            System.out.println("Thread return:" + integer);

        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
        
    }
}

当线程顺序执行完毕后,然后返回!
在这里插入图片描述

上面程序中使用 Lambda 表达式直接创建了 Callable 对象,这样就无须先创建 Callable 实现类,再创建 Callable 对象了。实现 Callable 接口与实现 Runnable 接口并没有太大的差别,只是 Callablecall() 方法允许声明抛出异常,而且允许带返回值。


三种创建线程方式做出对比

通过继承 Thread 类或实现 RunnableCallable 接口都可以实现多线程,不过实现 Runnable 接口与实现 Callable 接口的方式基本相同,只是 Callable 接口里定义的方法有返回值,可以声明抛出异常而已。因此可以将实现 Runnable 接口和实现 Callable 接口可以理解为同一种方式!

采用实现Runnable、Callable 接口的方式创建多线程的优缺点:

  • 线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。
  • 在这种方式下,多个线程可以共享同一个 target 对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
  • 劣势是,编程稍稍复杂,如果需要访问当前线程,则必须使 Thread.currentThread() 方法。采用继承 Thread 类的方式创建多线程的优缺点:
  • 劣势是,因为线程类已经继承了 Thread 类,所以不能再继承其他父类。
  • 优势是,编写简单,如果需要访问当前线程,则无须使用 Thread.currentThread() 方法, 直接使用 this 即可获得当前线程。

所以推荐采用实现Runnable接口、Callable 接口的方式来创建多线程。


线程生命周期

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态, 在线程的生命周期中,它要经过 新建(New)、就绪( Runnable)、运行( Running)、阻塞( Blocked)和死亡(Dead) 5种状态。尤其是当线程启动以后,它不可能一直“霸占”着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。


新建状态

当使用new关键字创建的线程之后,此时此刻线程就处于新建状态!不会执行线程的执行体!

就绪状态

当线程对象调用了 start() 方法之后,该线程处于就绪状态,Java 虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度。

运行状态
如果线程处于就绪状态,并获得了CPU,开始执行 run() 内方法体,此时线程就处于运行状态!

阻塞状态
当线程处于运行状态时候,如果需要在线程运行的过程中被中断,使其它线程获得执行的机会。所有现代的桌面和服务器操作系统都采用抢占式调度策略,但一些小型设备如手机则可能采用协作式调度策略,在这样的系统中,只有当一个线程调用了它的 sleep()yield() 方法后才会放弃所占用的资源一也就是必须由该线程主动放弃所占用的资源。

当发生下列情况时,线程就处于阻塞状态:

  • 线程调用 sleep() 方法主动放弃所占用的处理器资源。
  • 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。
  • 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。
  • 线程在等待某个通知。
  • 程序调用了线程的 suspend() 方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法。

被阻塞的线程在合适的时候会重新进入就绪状态

当处于以下情况时,线程会重新处于就需状态:

  • 调用 sleep() 方法的线程经过了指定时间;
  • 线程调用的阻塞式IO方法已经返回;
  • 线程成功地获得了试图取得的同步监视器;
  • 线程正在等待某个通知时,其他线程发出了一个通知;
  • 处于挂起状态的线程被调用了 resume() 恢复方法。

线程死亡
线程会以如下三种方式结束,结束后就处于死亡状态。

  • run()call() 方法执行完成,线程正常结束;
  • 线程抛出一个未捕获的ExceptionError
  • 直接调用该线程的 stop() 方法来结束该线程一该方法容 易导致死锁,通常不推荐使用。

生命周期流程图如下:

在这里插入图片描述


*值得注意的是:只能对处于新建状态的线程调用start()方法, 否则将引发IllegalThreadStateException 异常。线程死亡后请不要试图再次调用start()再次启动,他已经死亡了! 程序只能对新建状态的线程调用 start() 方法,对新建状态的线程两次调用 start() 方法也是错误的。这都会引发 IllegalThreadStateException 异常。


线程控制

Java的线程支持提供了一些便捷的工具方法,可以很好地控制线程的执行。

join线程

Thread提供了让一个线程等待另一个线程完成的方法 join() 方法。 当在某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到被 join() 方法加入的join线程执行完为止。

join() 方法通常由使用线程的程序调用,以将大问题划分成许多小问题,每个小问题分配一个线程。当所有的小问题都得到处理后,再调用主线程来进一步操作。

public class JoinThread extends Thread {

    /**
     * 程忆难
     * https://myhub.blog.csdn.net/
     *
     * @param args
     */
    public static void main(String[] args) {

        new JoinThread("threadA").start();

        for (int i = 0; i < 100; i++) {
            if (i == 20) {
                JoinThread joinThread = new JoinThread("被join的线程");
                joinThread.start();
                try {
                    joinThread.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        System.out.println("Main thread");

    }

    /**
     * 构造设置线程名字
     *
     * @param threadName
     */
    public JoinThread(String threadName) {
        super(threadName);
    }

    @Override
    public void run() {
        System.out.println("Thread name:" + getName());
    }
}

在这里插入图片描述

从上方代码和运行结果不难看出,最后打印的主线程,也就意味着主线程必须等待“被join 的线程”这个线程执行完毕后才会执行!

join 线程有三种重载的方式:

  • join( ):等待被join的线程执行完成。
  • join(long millis) : 等待被join的线程的时间最长为
  • join(long millis, int nanos):等待被join的线程的时间最长为 millis 毫秒加 nanos 毫微秒。

后台线程

后台线程,从字面上就可以看出,它是运行在后台的线程,后台线程有一个特征:如果所有的前台线程都死亡,后台线程会自动死亡。 可以调用Thread对象中的setDaemon(true) 方法可将指定线程设置成后台线程。

代码模拟:

public class ThreadTest extends Thread {

    /**
     * 程忆难
     * https://myhub.blog.csdn.net/
     *
     * @param args
     */
    public static void main(String[] args) {
        ThreadTest threadTest = new ThreadTest();

        //设置为后台线程
        threadTest.setDaemon(true);

        threadTest.start();

        for (int i = 0; i < 10; i++) {
            System.out.println("Main thread");
        }
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("thread run " + i);
        }
    }
}

在这里插入图片描述
当Main - 主线程执行完毕后,子线程也随之结束,而没有继续循环下去!另外,Thread还提供了 isDaemon( ) 方法,用于判断指定线程是否是后台线程!

一定要注意,前台线程死亡后,JVM会通知后台线程死亡,但从它接收指令到做出响应,需要一定时间。而且要将某个线程设置为后台线程,必须在该线程启动之前设置,也就是说,setDaemon(true) 必须在 start( ) 方法之前调用,否则会引发IllegalThreadStateException异常。


线程睡眠

这个很好理解,就是让线程进入暂停状态,Thread 提供了 sleep( ) 方法,可以指定线程睡眠多长时间,单位:毫秒。

public class ThreadTest extends Thread {

    /**
     * 程忆难
     * https://myhub.blog.csdn.net/
     *
     * @param args
     */
    public static void main(String[] args) {

        //创建线程
        ThreadTest threadTest = new ThreadTest();

        //启动县城
        threadTest.start();


    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("thread run " + i);
            
            try {
                //睡眠一秒后继续顺序执行
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


线程让步yieId

yield( ) 方法是一个和 sleep( ) 方法有点相似的方法,它也是Thread类提供的一个静态方法,它也可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程转入就绪状态。yield( ) 只是让当前线程暂停一下,让系统的线程调度器重新调度一次, 完全可能的情况是:当某个线程调用了 yield( ) 方法暂停之后,线程调度器又将其调度出来重新执行。

案例代码

public class ThreadTest extends Thread {

    /**
     * 程忆难
     * https://myhub.blog.csdn.net/
     *
     * @param args
     */
    public static void main(String[] args) {

        ThreadTest threadTest1 = new ThreadTest("线程ONE");
        threadTest1.start();

        ThreadTest threadTest2 = new ThreadTest("线程TWO");
        threadTest2.start();
    }

    public ThreadTest(String threadName) {
        super(threadName);
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {

            System.out.println(getName() + i);

            //当i等于20的时候,做出线程让步
            if (i == 20) {
                Thread.yield();
            }
        }
    }
}


线程优先级控制

Thread 类提供了 setPriority(int newPriority)getPriority( ) 方法来设置和返回指定线程的优先级,其中 setPriority( ) 方法的参数可以是一个整数,范围是1~10之间,也可以使用 Thread 类的如下三个静态常量。

public class ThreadTest extends Thread {

    /**
     * 程忆难
     * https://myhub.blog.csdn.net/
     *
     * @param args
     */
    public static void main(String[] args) {

        ThreadTest threadTest1 = new ThreadTest("高优先级");

        //设置优先级
        threadTest1.setPriority(MAX_PRIORITY);
        threadTest1.start();

        ThreadTest threadTest2 = new ThreadTest("低优先级");

        //设置优先级
        threadTest1.setPriority(NORM_PRIORITY);
        threadTest2.start();
    }

    public ThreadTest(String threadName) {
        super(threadName);
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {

            System.out.println(getName() + i);

            //当i等于20的时候,做出线程让步
            if (i == 20) {
                Thread.yield();
            }
        }
    }
}

  • MAX PRIORITY: 其值是10;
  • MIN PRIORITY: 其值是1;
  • NORM PRIORITY: 其值是5。

简而言之,setPriority 值越高,线程获得的执行机会也就越多!不推荐直接设置数值
,Windows 2000仅提供了7个优先级。因此应该尽量避免直接为线程指定优先级,而应该使用MAX_ PRIORITY、 MIN_ PRIORITY和NORM PRIORITY三个静态常量来设置优先级,这样才可以保证程序具有最好的可移植性。


线程同步的“必要性”

现在来假设一种场景,我们都知道12306每逢春运,购票压力都很大,有多个窗口都在卖票,这就相当于多线程,现在考虑,会不会遇到这种情况,假如还剩下最后一张票没有卖,现在同时有两个窗口同时查询,都显示为最后一张票,也就意味着两个窗口都可以操作把这张票卖出去,当都操作成功后,那后台数据库中的一张票就是卖出去两张,最后为-1张。又或者是同时查询同时卖票,卖出的与剩余的数量对应不上!当然,12306不可能出现这个问题,我们只是举例发挥想象!来解决这个算法问题!

现在来写卖票算法!

定义票剩余数量(模拟12306票务数据库)

public class TiketAdmin {

    private int tiketNum;

    public TiketAdmin(int tiketNum) {
        this.tiketNum = tiketNum;
    }

    public int getTiketNum() {
        return tiketNum;
    }

    public void setTiketNum(int tiketNum) {
        this.tiketNum = tiketNum;
    }
}

初始化票务,模拟两个窗口取票

/**
 * @author CSDN程忆难
 * @link https://myhub.blog.csdn.net
 */
public class ThreadTest extends Thread {

    //总票数
    private static TiketAdmin tiketAdmin;


    public static void main(String[] args) {

        //初始化剩余票数
        tiketAdmin = new TiketAdmin(100);

        ThreadTest threadTest1 = new ThreadTest("窗口1");
        threadTest1.start();

        ThreadTest threadTest2 = new ThreadTest("窗口2");
        threadTest2.start();
    }

    public ThreadTest(String threadName) {
        super(threadName);
    }

    @Override
    public void run() {

        if (tiketAdmin.getTiketNum() > 0) {
            try {
                sleep(300 + tiketAdmin.getTiketNum());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //如果票数大于0,那就卖一张票
            if (tiketAdmin.getTiketNum() > 0) {

                //模拟卖票
                tiketAdmin.setTiketNum(tiketAdmin.getTiketNum() - 1);

                //打印剩余票数
                System.out.println(getName() + "卖了一张,剩余票数:" + tiketAdmin.getTiketNum());
            }
        }

    }
}

两个线程(两个窗口),同时卖出票,剩余99张!但这卖出了两张票,跟总余票不对应,这样的线程是不安全的!
在这里插入图片描述

我们发现,这样的逻辑本身就存在很大的问题,票数根本对不上!怎么解决呢,所以要实现线程同步!

为了解决这个问题,Java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块。


synchronized线程同步

现在加上synchronized

/**
 * @author CSDN程忆难
 * @link https://myhub.blog.csdn.net
 */
public class ThreadTest extends Thread {

    //总票数
    private static TiketAdmin tiketAdmin;


    public static void main(String[] args) {

        //初始化剩余票数
        tiketAdmin = new TiketAdmin(100);

        ThreadTest threadTest1 = new ThreadTest("窗口1");
        threadTest1.start();

        ThreadTest threadTest2 = new ThreadTest("窗口2");
        threadTest2.start();


    }

    public ThreadTest(String threadName) {
        super(threadName);
    }

    @Override
    public void run() {

        synchronized (tiketAdmin) {
            if (tiketAdmin.getTiketNum() > 0) {
                try {
                    sleep(300 + tiketAdmin.getTiketNum());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //如果票数大于0,那就卖一张票
                if (tiketAdmin.getTiketNum() > 0) {

                    //模拟卖票
                    tiketAdmin.setTiketNum(tiketAdmin.getTiketNum() - 1);

                    //打印剩余票数
                    System.out.println(getName() + "卖了一张,剩余票数:" + tiketAdmin.getTiketNum());
                }
            }
        }

    }
}

现在数据对了!下面解释一下synchronized!
在这里插入图片描述

与同步代码块对应,Java的多线程安全支持还提供了同步方法,同步方法就是使用synchronized 关键字来修饰某个方法,则该方法称为同步方法。对于synchronized 修饰的实例方法(非static方法)而言,无须显式指定同步监视器,同步方法的同步监视器是this,也就是调用该方法的对象。

线程安全具有以下特征:

  • 该类的对象可以被多个线程安全地访问;
  • 每个线程调用该对象的任意方法之后都将得到正确结果;
  • 每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态。.

释放同步监视器锁定

任何线程进入同步代码块、同步方法之前,必须先获得对同步监视器的锁定,那么何时会释放对同步监视器的锁定呢?程序无法显式释放对同步监视器的锁定,线程会在如下几种情况下释放对同步监视器的锁定。

  • 当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器。
  • 当前线程在同步代码块、同步方法中遇到breakreturn 终止了该代码块、该方法的继续执行,当前线程将会释放同步监视器。
  • 当前线程在同步代码块、同步方法中出现了未处理的ErrorException, 导致了该代码块、该方法异常结束时,当前线程将会释放同步监视器。
  • 当前线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器。在如下所示的情况下,线程不会释放同步监视器。
  • 线程执行同步代码块或同步方法时,程序调用Thread.sleep()Thread.yield() 方法来暂停当前线程的执行,当前线程不会释放同步监视器。
  • 线程执行同步代码块时,其他线程调用了该线程的 suspend() 方法将该线程挂起,该线程不会释放同步监视器。当然,程序应该尽量避免使用 suspend()resume() 方法来控制线程。

Lock对象控制线程同步

import java.util.concurrent.locks.ReentrantLock;

public class TiketAdmin {
    
    private final ReentrantLock reentrantLock = new ReentrantLock();

    
    /**
     * 卖票操作
     */
    public void sellTiket() {
        
        reentrantLock.lock();
        try {
            //保证线程安全代码
        } finally {
            reentrantLock.unlock();
        }
    }
}

Lock提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock 允许实现更灵活的结构,可以具有差别很大的属性,并且支持多个相关的Condition对象。
Java 8新增了新型的StampedLock类,在大多数场景中它可以替代传统的ReentrantReadWriteLockReentrantReadWriteLock为读写操作提供了三种锁模式: WritingReadingOptimisticReading


死锁

死锁是这样的,当两个线程都在等待对方释放锁的时候,这就会发生死锁!由于Thread类的suspend()方法也很容易导致死锁,所以Java不再推荐使用该方法来
暂停线程的执行。所以多线程编程时应该采取措施避免死锁出现。


线程通信

Object实现线程通信

为了实现这种功能,可以借助于Object类提供的wait()notify()notifyAll() 三个方法,这三个方法并不属于Thread类,而是属于Object类。但这三个方法必须由同步监视器对象来调用,这可分成以下两种情况:

  • 对于使用synchronized修饰的同步方法,因为该类的默认实例(this) 就是同步监视器,所以可以在同步方法中直接调用这三个方法。
  • 对于使用synchronized修饰的同步代码块,同步监视器是synchronized后括号里的对象,所以必须使用该对象调用这三个方法。

关于wait()notify()notifyAll() 这三个方法,相关解释:

  • wait(): 导致当前线程等待,直到其他线程调用该同步监视器的 notify() 方法或notifyAll()方法来唤醒该线程。该 wait() 方法有三种形式一无时间参数的 wait 一直等待,直到其他线程通知)、带毫秒参数的 wait() 和带毫秒、毫微秒参数的 wait() 这两种方法都是等待指定时间后自动苏醒。调用 wait() 方法的当前线程会释放对该同步监视器的锁定。
  • notify():唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,则会选择唤醒其中一个线程。选择是任意性的。只有当前线程放弃对该同步监视器的锁定后使用 wait() 方法,才可以执行被唤醒的线程。
  • notifyAll():唤醒在此同步监视器上等待的所有线程。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。

Callable创建线程

Condition类提供了如下三个方法:

  • await(): 类似于隐式同步监视器上的wait()方法,导致当前线程等待,直到其他线程调用该Condition的signal()方法或signalAll()方法来唤醒该线程。该await()方法有更多变体,如long awaitNanos(longnanosTimeout)、void awaitUninterruptiblyOawaitUntil(Date deadline)等,可以完成更丰富的等待操作。
  • signal(): 唤醒在此Lock对象上等待的单个线程。如果所有线程都在该Lock对象上等待,则会选择唤醒其中一个线程。选择是任意性的。只有当前线程放弃对该Lock 对象的锁定后使用await(),才可以执行被唤醒的线程。
  • signalAll(): 唤醒在此Lock对象上等待的所有线程。只有当前线程放弃对该Lock对象的锁定后,才可以执行被唤醒的线程。

线程池

系统启动一个新线程的成本是比较高的,因为它涉及与操作系统交互。在这种情形下,使用线程池可以很好地提高性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池。与数据库连接池类似的是,线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象或Callable对象传给线程池,线程池就会启动一个线程来 执行它们的 run()call() 方法,当 run()call() 方法执行结束后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个 Runnable 对象的 run()call() 方法。

Java 5新增了一个 Executors 工厂类来产生线程池,该工厂类包含如下几个静态工厂方法来创建线程池:

  • newCachedThreadPool): 创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中;
  • newFixedThreadPool(int nThreads);创建一个可重用的、 具有固定线程数的线程池。
  • newSingleThreadExecutor() :创建一个只 有单线程的线程池,它相当于调用newFixedThread Pool() 方法时传入参数为1;
  • newScheduledThreadPool(int corePoolSize): 创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务。corePoolSize指池中所保存的线程数,即使线程是空闲的也被保存在线程池内;
  • newSingleThreadScheduledExecutor(): 创建只有一个线程的线程池,它可以在指定延迟后执行线程任务;
  • ExecutorService new WorkStealingPool(int parallelism):创建持有足够的线程的线程池来支持给定的并行级别,该方法还会使用多个队列来减少竞争;
  • ExecutorService new WorkStealingPool():该方法是前一个方法的简化版本。如果当前机器有4个CPU,则目标并行级别被设置为4,也就是相当于为前一个方法传入4作为参数。

使用线程池来执行线程任务的步骤如下:

  1. 调用 Executors 类的静态工厂方法创建一个 ExecutorService 对象,该对象代表一个线程池;
  2. 创建 Runnable 实现类或 Callable 实现类的实例,作为线程执行任务;
  3. 调用 ExecutorService 对象的 submit() 方法来提交 Runnable 实例或Callable实例;
  4. 当不想提交任何任务时,调用 ExecutorService 对象的 shutdown() 方法来关闭线程池。

代码演示:

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

/**
 * @author CSDN程忆难
 * @link https://myhub.blog.csdn.net
 */
public class ThreadPack {


    public static void main(String[] args) {


        //固定线程数
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        //Lambda创建线程
        Runnable runnable = () -> {
            for (int i = 0; i < 100; i++) {
                System.out.println("线程:" + Thread.currentThread().getName());
            }
        };

        //提交到线程池两个
        executorService.submit(runnable);
        executorService.submit(runnable);

        //关闭线程池
        executorService.shutdown();
    }
}

可以看到,在交替执行
在这里插入图片描述

上面程序中创建 Runnable 实现类与最开始创建线程池并没有太大差别,创建了Runnable 实现类之后程序没有直接创建线程、启动线程来执行该 Runnable 任务,而是通过线程池来执行该任务!

发布了115 篇原创文章 · 获赞 1443 · 访问量 88万+

猜你喜欢

转载自blog.csdn.net/qq_40881680/article/details/104934107