Java多线程你真的理解透彻了吗?带你玩转一次多线程!

BAT面试官有点懵系列,Java多线程你真的理解透彻了吗?带你玩转一次多线程!Let's go!别再out了! 文章目录基本概念和使用线程和进程多线程的优势线程创建方式继承Thread类来创建和启动实现Runnable接口创建线程类线程run?start?线程生命周期线程死亡线程控制线程同步的“必要性”synchronized线程同步Lock对象控制线程同步Object实现线程通信条件变量实现线程通信管道流实现线程通信Callable创建线程线程池 基本概念和使用 Java语言提供了非常优秀的多线程支持... 阅读详情

不知道怎么引入正文

相信后端同学在开发的时候多多少少都会涉及到多线程开发,做为Java开发的我也同样会经常用到多线程开发。

我认为Java语言在处理多线程上是非常优秀的,我们可以使用简明的代码实现线程的创建、启动、管理等。

话不多说,下面我们就来详细的看一下吧!!!

高级分享:Java多线程你真的理解透彻了吗?带你玩转一次多线程!

 

 

想要了解更多Java架构技术的,可以关注我一下,我后续也会整理更多关于架构技术这一块的知识点分享出来,里面会分享一些:spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化,并发编程这些成为架构师必备的知识体系.私信获取

线程和进程

这是个老生常谈的问题了,做为后端程序员一定要分清楚线程和进程的区别,几乎所有的操作系统都有“进程”这一概念!

一任务,一程序,每一个运行中的程序就是一个进程!

当程序运行时,其内部包含了多个顺序执行流,每一个顺序执行流就是一个线程!

进程有以下三种特征:

  • 独立性:进程是系统中独立存在的实体,可拥有自己的独立资源,每个进程都有自己私有的地址空间。在没有经过进程本身允许的情况下,用户不能够直接访问其它进程的地址空间!
  • 动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中动的指令集合。在进程中加入了时间的概念。进程具有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的。
  • 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响。

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

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

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

高级分享:Java多线程你真的理解透彻了吗?带你玩转一次多线程!

 

多线程的好处

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

线程创建方式

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

继承Thread类来创建和启动

  • 定义Thread类的子类,并写该类的run()方法,该run()方法的方法体就代表了线程需要完成

的任务。因此把run()方法称为线程执行体。

  • 创建Thread子类的实例,即创建了线程对象。
  • 调用线程对象的start()方法来启动该线程。
public class Main extends Thread {

     *
     * @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());
    }
}
复制代码

运行共启动三个线程,一个主线程,两个子线程!

高级分享:Java多线程你真的理解透彻了吗?带你玩转一次多线程!

 

主线程的执行体不是由run()方法确定的,而是由main方法确定的,main方法的方法体代表主线程的线程执行体。

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

特别注意:

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

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

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

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

public class Main implements Runnable {
    /**
     *
     * @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。

高级分享:Java多线程你真的理解透彻了吗?带你玩转一次多线程!

 

使用 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对象。

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

  • 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,且该 call() 方法有返回值,再创建 Callable 实现类的实例。从Java 8开始,可以直接使用 Lambda 表达式创建 Callable 对象;
  • 使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该Callable 对象的 call() 方法的返回值;
  • 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程;
  • 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Main {
    /**
     *
     * @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();
        }
        
    }
}
复制代码

当线程顺序执行完毕后,然后返回!

高级分享:Java多线程你真的理解透彻了吗?带你玩转一次多线程!

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

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

通过继承 Thread 类或实现 Runnable 、 Callable 接口都可以实现多线程,不过实现 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() 方法执行完成,线程正常结束;
  • 线程抛出一个未捕获的Exception 或Error;
  • 直接调用该线程的 stop() 方法来结束该线程一该方法容 易导致死锁,通常不推荐使用。

生命周期流程图如下:

高级分享:Java多线程你真的理解透彻了吗?带你玩转一次多线程!

线程控制

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

join线程

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

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

public class JoinThread extends Thread {

    /**
     *
     * @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 {
    /**
      *
     * @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 {
    /**
     *
     * @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 {
    /**
     *
     * @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 {
    /**
          *
     * @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;
    }
}复制代码

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

/**
 */
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多线程你真的理解透彻了吗?带你玩转一次多线程!

 

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

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

synchronized线程同步

现在加上synchronized

/**
*
*
 */
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多线程你真的理解透彻了吗?带你玩转一次多线程!

 

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

线程安全具有以下特征:

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

释放同步监视器锁定

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

  • 当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器。
  • 当前线程在同步代码块、同步方法中遇到break、 return 终止了该代码块、该方法的继续执行,当前线程将会释放同步监视器。
  • 当前线程在同步代码块、同步方法中出现了未处理的Error 或Exception, 导致了该代码块、该方法异常结束时,当前线程将会释放同步监视器。
  • 当前线程执行同步代码块或同步方法时,程序执行了同步监视器对象的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类,在大多数场景中它可以替代传统的ReentrantReadWriteLock,ReentrantReadWriteLock为读写操作提供了三种锁模式: Writing、 ReadingOptimistic、 Reading。

死锁

死锁是这样的,当两个线程都在等待对方释放锁的时候,这就会发生死锁!由于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作为参数。

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

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

代码演示:

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

/**
 * 
 * 
 */
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();
    }
}
可以看到,在交替执行
复制代码

可以看到,在交替执行

高级分享:Java多线程你真的理解透彻了吗?带你玩转一次多线程!

 

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

好了,今天就给大家分享到这里,感兴趣的朋友记得点个赞,关注一下哦!

想要了解更多Java架构技术的,可以关注我一下,我后续也会整理更多关于架构技术这一块的知识点分享出来,里面会分享一些:spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化,并发编程这些成为架构师必备的知识体系.更多交流和获取方式可以先关注再私信博主

 

工业级24V电源保护电路深度解析:五重防护机制详解 自恢复保险丝(PPTC,Positive Temperature Coefficient)是一种过流保护元件✅可恢复:过流后自动恢复,无需更换⚡响应快:电流超标立即动作🔄多次使用:在额定范围内可反复使用第1层(F1):阻挡过流 → 物理熔断第2层(D3):吸收浪涌 → 能量泄放第3层(Q1):防止反接 → 逻辑阻断第4层(Q2/Q3):抵御过压 → 主动切断第5层(CR):平滑启动 → 时序控制设计哲学:"让每一级保护专注做好一件事"通过这篇文章,我们深入理解了:✅自恢复保险丝。 阅读详情

相关推荐

三菱FX3U PLC红绿灯ST编程实战:从基础逻辑到急停功能完整实现

本文以十字路口红绿灯控制为例,详细讲解了三菱FX3U PLC使用ST编程的完整实战过程。内容涵盖从基础时序逻辑、状态机设计,到急停功能、黄灯闪烁、昼夜模式切换等高级功能的实现,并提供了代码优化与调试技巧,帮助工程师掌握结构化文本在工业控制中的清晰、结构化应用。

weixin_29070861的博客 535

java 线程池的应用场景

java 线程池

orton777的博客 1170

当LightGBM推理遇上GPU:3种替代方案性能对比(含ONNX实战代码)

本文针对LightGBM官方不支持GPU推理的性能瓶颈,深度评测了RAPIDS cuML、ONNX Runtime GPU和Triton Inference Server三种GPU加速推理替代方案。通过详尽的性能对比和实战代码,为高并发、低延迟的在线预测场景提供了清晰的选型指南,帮助工程师打破推理性能枷锁,实现显著的吞吐量提升。

salt9的博客 813

java多线程有哪些实际的应用场景?

多线程实际的应用场景有哪些、线程安全的实现方式、锁的性能优化

qq_39350797的博客 4093

n8n 快速入门1:构建一个简单的工作流

n8n 快速入门指南

engchina的专栏 1万+

【Linux】实际项目应该如何完成?--安防监控系统项目的总结

到目前为止我也做个5、6个嵌入式项目,但以往都是从零开始自己做,也就是毫无计划,想到那里做那里,有想法就会去实现,就这样导致我碰到了很多坑,也消耗了我许多时间,当然,不是说这样不好,因为在其中我也得到了一个很好的锻炼与练习,但是这不适用于在工作中去这样做。...

修成真的博客 6731

JAVA多线程的几个使用场景

然而,需要注意的是,如果调用者线程本身也需要执行其他重要任务,那么这种策略可能会导致调用者线程被大量任务占用,从而影响系统整体性能。:使用此策略时,需要注意队列中任务的优先级和依赖关系,以免因丢弃重要任务而导致业务逻辑错误。:此策略适用于那些对任务执行顺序要求不高的场景,且任务之间相对独立,不会因为某个任务的缺失而影响整体业务逻辑。如果线程池已经关闭,则任务将被丢弃。:当任务被拒绝时,它会尝试丢弃队列中等待时间最长的任务,并尝试执行新任务。:当任务被拒绝时,它会被默默地丢弃,既不抛出异常也不执行任何任务。

Flying_Ape的博客 3240

采用MPU6050的手势遥控器

随着社会的发展与进步,电子产品日益更新,个人家庭迎来了越来越多的智能家电。然而,现在的智能电子设备产品的遥控器多数都是按键形的。操作繁琐不直观,学习成本大。并且对于有老人小孩的家庭,给他们的操作带来了巨大的障碍。手势遥控器是指利用简单的手势来指挥电子设备,获得便捷的操作体验。让家庭成员都可以享受科技发展带来的福利。MPU6050是一个运动传感器,内置陀螺仪,再由电子设备利用判断结果做出相应的响应即...

Sakura的博客小站 6603

java真的多线程吗

    (1):主线程1启动了一个线程2,    在线程1里将线程2赋值为NULL,结果线程2根本不会结束     (2) 线程2实现Runnable接口实现线程的独立,在线程1中创建线程2的时候不用Thread  x=new Thread(new  myThread())的形式可以直接调用线程2的run()方法;      (3)当程序运行到线程2的死循环的时候,窗口不响应任何事件,让人感觉...

sealrose的专栏 393

借助Claude实现Playwright的自动化(MCP Server)

MCP Server自己开发也是可以的,现在也有很多开箱即用的MCP Server,可以在https://github.com/modelcontextprotocol/servers中查找。

Criss@陈磊 4229

Python爬虫实战:微信小程序内容采集的全面指南

在移动互联网时代,微信小程序已成为企业和个人提供服务的重要渠道。小程序中包含了大量有价值的信息,但微信官方并未提供便捷的数据获取接口。本文将详细介绍如何使用Python爬虫技术采集微信小程序内容,包括最新技术方案、完整代码实现以及应对各种反爬机制的策略。小程序运行在微信封闭环境中,无法直接访问数据通信主要通过HTTPS加密传输内容动态加载,依赖JavaScript渲染微信有严格的反爬机制。

2201_76125261的博客 3120

在java项目上使用多线程的场景都有哪些?

例如,在一个 Web 应用中,可以使用多线程来并发地处理多个用户的请求。在一些应用中,可能需要在后台执行耗时的任务(如日志记录、数据备份),以避免影响主线程的性能。在项目中,多线程的使用非常广泛,主要用于提高程序的并发性和响应速度。5. 异常处理:在多线程环境中,异常处理尤为重要,需要确保每个线程的异常都能被正确捕获和处理。5. 异步处理:多线程可以实现异步处理,使得某些任务可以在后台运行,而不影响前台任务的执行。在实时系统中,多线程可以用来处理不同的传感器输入或控制输出,确保系统的响应时间满足要求。

Java开发者,专注后端技术分享,深耕框架实战与架构优化,持续输出优质技术干货。 1445

无线充电Qi协议正向通信FSK的解调设计

常见的FSK解调方法是使用频率锁相环(PLL)来跟踪输入信号中的频率变化。PLL根据输入信号的频率变化来生成数字信号,从而实现FSK信号的解调。本文将详细介绍无线充电Qi协议正向通信FSK的解调设计,并提供相应的源代码。在上述示例代码中,我们首先定义了FSK解调的参数,包括采样率、载波频率、每个比特的持续时间和比特判定阈值。最后,通过比较滤波后的信号与比特判定阈值,我们将解调后的数字数据转换为0和1的序列。请注意,上述代码仅为示例,实际的解调设计可能需要根据具体的应用场景和系统要求进行调整和优化。

IunScala的博客 2444

BAT面试官有点懵系列,Java多线程你真的理解透彻了吗?带你玩转一次多线程!Let‘s go!别再out了!

这几年的Crud经历,让我明白自己真的算是菜鸡中的战斗机,也正因为Crud,导致自己技术比较零散,也不够深入不够系统,所以重新进行学习是很有必要的。我差的是系统知识,差的结构框架和思路,所以通过视频来学习,效果更好,也更全面。关于视频学习,个人可以推荐去B站进行学习,B站上有很多学习视频,唯一的缺点就是免费的容易过时。“编程语言是程序员的表达的方式,而架构是程序员对世界的认知”。所以,程序员要想快速认知并学习架构,读源码是必不可少的。阅读源码,是解决问题 + 理解事物,更重要的:看到源码背后的想法;

2401_84692751的博客 975

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

java 禁止起两个相同的进程_高级分享:Java多线程你真的理解透彻了吗?带你玩转一次多线程!

不知道怎么引入正文相信后端同学在开发的时候多多少少都会涉及到多线程开发,做为Java开发的我也同样会经常用到多线程开发。我认为Java语言在处理多线程上是非常优秀的,我们可以使用简明的代码实现线程的创建、启动、管理等。话不多说,下面我们就来详细的看一下吧!!!线程和进程这是个老生常谈的问题了,做为后端程序员一定要分清楚线程和进程的区别,几乎所有的操作系统都有“进程”这一概念!一任务,一程序,每一个...

weixin_39550172的博客 305

RU DOS版、EFI版32bit/64bit BIOS常用工具

DOS版、EFI版的RU工具,可用来查看IO、MEM、PCI、SMBIOS等资源。亲测在EFI SHELL下可用。

call执行带返回值的存储过程_高级分享:Java多线程你真的理解透彻了吗?带你玩转一次多线程!...

不知道怎么引入正文相信后端同学在开发的时候多多少少都会涉及到多线程开发,做为Java开发的我也同样会经常用到多线程开发。我认为Java语言在处理多线程上是非常优秀的,我们可以使用简明的代码实现线程的创建、启动、管理等。话不多说,下面我们就来详细的看一下吧!!!线程和进程这是个老生常谈的问题了,做为后端程序员一定要分清楚线程和进程的区别,几乎所有的操作系统都有“进程”这一概念!一任务,一程序,每一个...

weixin_29192365的博客 693

call线程起名字_高级分享:Java多线程你真的理解透彻了吗?带你玩转一次多线程!...

不知道怎么引入正文相信后端同学在开发的时候多多少少都会涉及到多线程开发,作为Java开发的我也同样会经常用到多线程开发。我认为Java语言在处理多线程上是非常优秀的,我们可以使用简明的代码实现线程的创建、启动、管理等。话不多说,下面我们就来详细的看一下吧!!!线程和进程这是个老生常谈的问题了,作为后端程序员一定要分清楚线程和进程的区别,几乎所有的操作系统都有“进程”这一概念!一任务,一程序,每一个...

weixin_39550486的博客 132

高级分享:Java多线程你真的理解透彻了吗?带你玩转一次多线程

  不知道怎么引入正文   相信后端同学在开发的时候多多少少都会涉及到多线程开发,做为Java开发的我也同样会经常用到多线程开发。   我认为Java语言在处理多线程上是非常优秀的,我们可以使用简明的代码实现线程的创建、启动、管理等。   话不多说,下面我们就来详细的看一下吧!!!      线程和进程   这是个老生常谈的问题了,做为后端程序员一定要分清楚线程和进程的区别,几乎所有的操作系统都有“进程”这一概念!   一任务,一程序,每一个运行中的程序就是一个进程!   当程序运行..

iqifenxia的博客 169

Java多线程你真的理解透彻了吗?带你一次搞明白Java多线程高并发

线程是进程中的一个独立控制单元,线程在控制着进程的执行,一个进程中至少有一个线程。多线程可以更好地利用cpu的资源,线程之间还能进行数据共享。 在Java中,一个线程是指进程中的一个执行流程,一个进程可以运行多个线程,Java中每个线程都有一个调用栈,即使不在程序中创建任何新的线程,也有一个main()方法运行在一个线程内,称为主线程,一旦创建一个新的线程,就产生一个新的调用栈。 通过多线程专题课程的系统学习,让大家一次性搞明白Java中的多线程。 环境参数:idea, jdk1.8 课程..

m0_67322837的博客 395

【Java 多线程 全面、透彻】简单理解

多线程 七、悲观锁和乐观锁1.悲观锁1.1概念1.2 悲观锁机制存在的问题2、乐观锁2.1概念2.2 乐观锁的实现方式2.3 乐观锁存在的问题八、死锁1.造成死锁的条件2.常见的死锁问题3.哲学家就餐问题4.死锁的预防 七、悲观锁和乐观锁 1.悲观锁 1.1概念 总是假设最坏的情况,也就是每一次去拿数据都默认别人会修改,所以每次拿数据都会上锁,这会导致有其他人想要拿到数据就会阻塞知道获取到这把锁 synchornized关键字的实现就是:悲观锁 1.2 悲观锁机制存在的问题 1、多线程竞争下,加锁,解锁都会

qq_44682003的博客 757

Java软件开发面试题!高并发你真的理解透彻了吗

一、前言 有句话:正因为你优秀,所以难以卓越! 刚开始听这句话还在上学,既不卓越、也不优秀,甚至可能还有点笨!但突然从某次爬到班级的前几名后,开始喜欢上了这种感觉,原来前面的风景是如此灿烂????! 优秀和卓越差的不是一个等级,当你感觉自己优秀后,还能保持空瓶的心态开始,才能逐步的像卓越迈进,并漫漫长! 是不小时候更容易学会更多的知识,但越大越笨了!人可能很容易被自己的年纪大了,当成长者。却很少能保持一个低姿态谦卑的心态,不断的学习。所以最后,放不下自己,也拾不起能力。 喜欢一句话,蓝是天的颜色、红是火的象

m0_57286743的博客 138

这篇文章就是要让你入门java多线程【多线程入门】

就在前几天,有位朋友私信宜春,说期待出一篇多线程的文章,我当时内心是小鹿乱撞啊....于是这几天茶不思饭不想,好几天深夜皆是辗转反侧,两目深凝,以至于这几天走起路来格外飘飘然~我承认太夸张了,感觉又要被打~。最终还是君意不可违,答应了这位朋友,从这位朋友的头像可以看出来,这位朋友绝bi历经沧桑,对生活无尽的坦然浩对,看透俗世凡尘、世态炎凉、趋炎附势,拥有着极高的安心恬荡情怀...啥?啥子?这个是系统默认头像....那个那个宜春啥都没说哈,别把什么事都扯宜春身上,我啥都没说。。。。

宜春 1万+

理解透彻!java工程师应聘求职

前言 当你准备跳槽面试的时候,明明只是一份15K的工作,却问你会不会多线程,懂不懂高并发,火箭造得让你猝及不防,结果就是两个字——凉凉!而在如今的市场,什么多线程、高并发、分布式、负载均衡、集群、微服务等等等等,都可说是Java高级后端开发求职的必备技能。 2021的春招可以说也是完美收官,前段时间收到字节跳动的面试邀请,虽有点震惊,但却依旧保持平常心去对待,结果一不小心就吊打了面试官 1.Java基础面试知识点 Java中==和equals和hashCode的区别 int、char、long各占多少字

目标检测专栏持续更新中,改进YOLO系列通用,适用v5、v7、v8、所有博客均是团队原创博客,所有文章禁止转载,违者必究。 333

大牛深入讲解!高并发你真的理解透彻了吗

前言 最其实不管什么时候,找工作都跑不了面试。目前很多小编都做了面试手册了,那就是别人家的孩子都有糖了,作为一个自觉的小编,必须搞。 容我先絮叨一下,制作这个面试手册差不多花了3个多星期时间,过程还是比较磨人的,但是也很期待。要是因为我做的手册,帮助到了大家,那就真的开心啊!这就是这篇文章的目的,**帮你搜集了大量的面试题,已经整理成了一个Java面试手册PDF,《互联网面试2400页》,目前有65份PDF,共有2400多页,**后续还会不断的完善更新。 直接进入正题,由于pdf文档里的细节内容实在过多所

m0_57081562的博客 190

回调函数透彻理解Java

转载自:https://blog.youkuaiyun.com/allen_zhao_2012/article/details/8056665# 回调函数,第一次见是在Java编程思想中,以及后来的观察者模式也应用了回调函数的思想。但是一直都没有重视,终于在一次面试的时候吃亏了,越来越理解为什么很多人说Java编程思想这本书要一遍遍的看,吃透! 首先在网络上搜索很多文章介绍什么是回调函数,...

qq_38125626的博客 105

理解透彻!带你快速通过字节跳动面试

前言 说起来开始进行面试是年前倒数第二周,上午9点,我还在去公司的公交上,突然收到蚂蚁的面试电话,其实算不上真正的面试。面试官只是和我聊了下他们在做的事情(主要是做双十一这里大促的稳定性保障,偏中间件吧),说的很详细,然后和我沟通了下是否有兴趣,我表示有兴趣,后面就收到正式面试的通知,最后没选择去蚂蚁表示抱歉。 当时我自己也准备出去看看机会,顺便看看自己的实力。当时我其实挺纠结的,一方面现在部门也正需要我,还是可以有一番作为的,另一方面觉得近一年来进步缓慢,没有以前飞速进步的成就感了,而且业务和技术偏于稳定

m0_57983503的博客 130

程序员真的是吃青春饭吗,高并发你真的理解透彻了吗

对于面试,一定要有良好的心态,这位小伙伴面试美团的时候没有被前面阿里的面试影响到,发挥也很正常,也就能顺利拿下美团的offer。小编还整理了大厂java程序员面试涉及到的绝大部分面试题及答案,希望能帮助到大家,也祝愿大家都能够升职加薪!帮助到大家,[外链图片转存中…(img-nUO5Q0xc-1710227683241)][外链图片转存中…(img-goJOrqYH-1710227683241)]也祝愿大家都能够升职加薪!本文已被。

m0_60607895的博客 889
上一篇: 看了Java的Class的源码,我自闭了
下一篇: 阿里面试官:RocketMQ与Kafka中如何实现事务?
Java架构技术官
博客等级 码龄6年 9粉丝 58原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值