草庐IT

一个Java线程的线生(线生 vs 人生)

博学谷狂野架构师 2023-08-10 原文

java线程的使用

1. Java多线程概述

下面我们看下Java的多线程

  • 作者: 博学谷狂野架构师
  • GitHub:GitHub地址 (有我精心准备的130本电子书PDF)

只分享干货、不吹水,让我们一起加油!?

1.1 java天生就是多线程的

一个Java程序从main()方法开始执行,然后按照既定的代码逻辑执行,看似没有其他线程参与,但实际上Java程序天生就是多线程程序,因为执行main()方法的是一个名称为main的线程。

1.1.1 代码案例

执行下面的代码

package chapter01;

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;

public class ThreadDemo {
    /**
     * 打印出java中所有的线程
     * @param args
     */
    public static void main(String[] args) {
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
        for (ThreadInfo info : threadInfos) {
            System.out.println("[" + info.getThreadId() + "]" + info.getThreadName());
        }
        System.out.println(Thread.activeCount());
    }
}

执行后我们会发现打印了如下的线程信息,说明Java本身就是多线程的

  • [6] Monitor Ctrl-Break //监控Ctrl-Break中断信号的

  • [5] Attach Listener //内存dump,线程dump,类信息统计,获取系统属性等

  • [4] Signal Dispatcher // 分发处理发送给JVM信号的线程

  • [3] Finalizer // 调用对象finalize方法的线程

  • [2] Reference Handler//清除Reference的线程

  • [1] main //main线程,用户程序入口

1.2 线程的生命周期

Thread类提供了六种状态

1.2.1 新建状态(NEW)

当线程对象对创建后,即进入了新建状态,如:Thread thread1 = new MyThread();

1.2.2 运行状态(RUNNABLE)

Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。

​ 线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。

1.2.3 阻塞状态(BLOCKED)

​ 处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态

1.2.4 等待状态(WAITING)

​ 进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)

1.2.5 超时等待(TIMED_WAITING)

​ 该状态不同于WAITING,它可以在指定的时间后自行返回。

1.2.6 终止状态(TERMINATED)

​ 线程执行完了或者因异常退出了run()方法,该线程结束生命周

2. 线程的创建方式

创建线程的方式有两种

2.1 继承Thread类

我们可以通过继承Thread类来使用Java的多线程

package chapter01.create;

/**
 * 创建一个线程并运行
 */
public class threadTest extends Thread {

    @Override
    public void run() {
        System.out.println("线程运行");
    }

    public static void main(String[] args) {
        threadTest threadCreate = new threadTest();
        threadCreate.start();
    }
}

2.2 实现 Runnable 接口

实现Runnable 接口并交给Thread进行运行

package chapter01.create;

public class RunnableTest implements Runnable {
    @Override
    public void run() {
        System.out.println("线程运行");
    }

    public static void main(String[] args) {
        RunnableTest runnableTest = new RunnableTest();
        Thread thread = new Thread(runnableTest);
        thread.start();
    }
}

2.3 Thread和Runnable的区别

Thread才是Java里对线程的唯一抽象,Runnable只是对任务(业务逻辑)的抽象,Thread可以接受任意一个Runnable的实例并执行。

2.3.1 注意事项

有些面试官会说实现线程的方式有三种 Thread、Runnable 以及Callable,但是按照java源码中Thread类中的注释说的实现类的防止只有两种,我们可以看下啊Thread类的源码

3. 线程的终止方式

下面我们看下线程的终止方式有哪些

3.1 线程自然终止

要么是run执行完成了,要么是抛出了一个未处理的异常导致线程提前结束。

3.2 stop终止

暂停、恢复和停止操作对应在线程Thread的API就是suspend()resume()stop(),但是这些API是过期的,也就是不建议使用的 。

3.2.1 为什么不建议使用

不建议使用的原因主要有:以suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。

​ 同样,stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下,正因为suspend()、resume()和stop()方法带来的副作用,这些方法才被标注为不建议使用的过期方法。

3.3 中断终止

推荐使用中断的方式来终止线程

​ 安全的中止则是其他线程通过调用某个线程A的interrupt()方法对其进行中断操作,,中断好比其他线程对该线程打了个招呼,“A,你要中断了”,不代表线程A会立即停止自己的工作,同样的A线程完全可以不理会这种中断请求,因为java里的线程是协作式的,不是抢占式的,线程通过检查自身的中断标志位是否被置为true来进行响应。

​ 线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()来进行判断当前线程是否被中断,不过Thread.interrupted()会同时将中断标识位改写为false。

​ 如果一个线程处于了阻塞状态(如线程调用了thread.sleep、thread.join、thread.wait等),则在线程在检查中断标示时如果发现中断标示为true,则会在这些阻塞方法调用处抛出InterruptedException异常,并且在抛出异常后会立即将线程的中断标示位清除,即重新设置为false。

3.3.1 代码案例
package chapter01.stop;

/**
 * 使用Runable的中断
 */
public class ThreadInterrupted implements Runnable {

    private int i = 0;

    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            i++;
            System.out.println("线程正在运行");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (i > 10) {
                Thread.currentThread().interrupt();
            }
            System.out.println(Thread.currentThread().isInterrupted());
        }
    }

    public static void main(String[] args) {
        new Thread(new ThreadInterrupted()).start();
    }
}

3.3.2 注意事项

不建议自定义一个取消标志位来中止线程的运行

​ 因为run方法里有阻塞调用时会无法很快检测到取消标志,线程必须从阻塞调用返回后,才会检查这个取消标志。这种情况下,使用中断会更好,状态位如果跨线程改变状态必须使用volatile来保证可见性。

  1. 一般的阻塞方法,如sleep等本身就支持中断的检查。
  2. 检查中断位的状态和检查取消标志位没什么区别,用中断位的状态还可以避免声明取消标志位,减少资源的消耗。

注意:处于死锁状态的线程无法被中断

3.4 状态位终止

状态位就是用一个变量来标识线程的运行状态,如果需要停止了就就改变状态位的状态,但是状态位一定要使用 volatile 关键字,否在可能造成多线程状态下的不可见

3.4.1 代码案例
3.4.1.1 使用volatile

使用volatile可以正常中断线程

package chapter01.stop;


public class ThreadFlag implements Runnable {

    protected long i = 0;
    private volatile boolean flag = false;

    @Override
    public void run() {
        while (!flag) {
            i++;
            if (i > 100000) {
                flag = true;
            }
            System.out.println(i);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadFlag threadFlag = new ThreadFlag();
        new Thread(threadFlag).start();
    }
}
3.4.1.2 不使用volatile

不使用volatile,会导致线程不能正常中断

package chapter01.stop;

public class ThreadInvisible implements Runnable {
    protected long i = 0;
    /**
     *  不加volatile 会造成多线程的变量不可见,判断不会停止
     */
    public  boolean flag = false;

    @Override
    public void run() {
        while (!flag) {
            i++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadInvisible threadFlag = new ThreadInvisible();
        new Thread(threadFlag).start();
        Thread.sleep(1000);
        threadFlag.flag = true;
    }
}

3. run和start的区别

Thread类是Java里对线程概念的抽象,可以这样理解:我们通过new Thread()其实只是new出一个Thread的实例,还没有操作系统中真正的线程挂起钩来。只有执行了start()方法后,才实现了真正意义上的启动线程。

start()方法让一个线程进入就绪队列等待分配cpu,分到cpu后才调用实现的run()方法,start()方法不能重复调用,如果重复调用会抛出异常。

​ 而run方法是业务逻辑实现的地方,本质上和任意一个类的任意一个成员方法并没有任何区别,可以重复执行,也可以被单独调用。

4. 其他的线程相关方法

下面我们看下线程的其他方法有哪些

4.1 sleep方法

使当前线程(即调用该方法的线程)暂停执行一段时间,让其他线程有机会继续执行,但它并不释放对象锁,也不释放占用的资源

​ 也就是说如果有synchronized同步快,其他线程仍然不能访问共享数据,注意该方法要捕捉异常。

​ 例如有两个线程同时执行(没有synchronized)一个线程优先级为MAX_PRIORITY,另一个为MIN_PRIORITY,如果没有Sleep()方法,只有高优先级的线程执行完毕后,低优先级的线程才能够执行;但是高优先级的线程sleep(500)后,低优先级就有机会执行了。

​ 总之,sleep()可以使低优先级的线程得到执行的机会,当然也可以让同优先级、高优先级的线程有执行的机会。

4.1.1 代码案例
package chapter01.method;

import util.ThreadUtils;

import java.util.concurrent.TimeUnit;

public class ThreadSleep {
    public static void main(String[] args) {
        sleep1();
        sleep2();

    }

    public static void sleep1() {
        Thread thread = new Thread(() -> {
            System.out.println("xxxxxxxxxxxxxxxxxx");
            ThreadUtils.sleep(1, TimeUnit.SECONDS);
        });
        //thread.setPriority(100);
        thread.start();
    }

    public static void sleep2() {
        Thread thread = new Thread(() -> {
            System.out.println("xxxxxxxxxxxxxxxxxx");
            ThreadUtils.sleep(1, TimeUnit.SECONDS);
        });
        //thread.set
        thread.start();
    }

}

4.2 join方法

把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行 。

​ 比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B

​ 注意: t.join()方法只会使主线程进入等待池并等待t线程执行完毕后才会被唤醒。并不影响同一时刻处在运行状态的其他线程

4.2.1 代码案例
package chapter01.method;

import util.ThreadUtils;

import java.util.concurrent.TimeUnit;

public class ThreadJoin {
    public static void main(String[] args) {
       Thread thread1 =  new Thread(()->{
           for(int i=0;i<10;i++) {
               System.out.println("111111111111111");
               ThreadUtils.sleep(1, TimeUnit.SECONDS);
           }
        });

        Thread thread2 =  new Thread(()->{
            try {
                thread1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for(int i=0;i<10;i++){
                System.out.println("2222222222222");
                ThreadUtils.sleep(1, TimeUnit.SECONDS);
            }
        });
        thread1.start();
        thread2.setDaemon(true);
        thread2.start();
    }
}

4.3 yield方法

yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。

​ 因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。

​ yield()是将线程从运行状态变更为就绪状态,不会变为等待/睡眠/阻塞状态,注意:yeid方法是不释放资源的

4.3.1 代码案例
package chapter01.method;

import util.ThreadUtils;

import java.util.concurrent.TimeUnit;

public class ThreadYield {

    public static void main(String[] args) {
        Thread thread1 = new Thread(()->{
           // Thread.yield();
            System.out.println("1111111111");
            ThreadUtils.sleep(1, TimeUnit.SECONDS);
        });

        Thread thread2 = new Thread(()->{
            System.out.println("22222222222");
            ThreadUtils.sleep(1, TimeUnit.SECONDS);
        });
        thread1.start();
        thread2.start();

    }
}

5 线程的优先级

在Java线程中,通过一个整型成员变量priority来控制优先级,优先级的范围从1~10

​ 在线程构建的时候可以通过setPriority(int)方法来修改优先级,默认优先级是5,优先级高的线程分配时间片的数量要多于优先级低的线程。

​ 设置线程优先级时,针对频繁阻塞(休眠或者I/O操作)的线程需要设置较高优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占,在不同的JVM以及操作系统上,线程规划会存在差异,有些操作系统甚至会忽略对线程优先级的设定

5.1 代码案例

package chapter01.priority;

import util.ThreadUtils;

import java.util.concurrent.TimeUnit;

/**
 * 执行的时候优先级越高 越容易执行到该线程
 */
public class ThreadPriority {

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            while (true) {
                System.out.println("1111111111111111");
                ThreadUtils.sleep(1, TimeUnit.SECONDS);
            }

        });
        Thread thread2 = new Thread(() -> {
            while (true) {
                System.out.println("22222222222222222");
                ThreadUtils.sleep(1, TimeUnit.SECONDS);
            }
        });
        thread1.setPriority(1);
        thread2.setPriority(7);
        thread1.start();
        thread2.start();
    }

}

6. 守护线程

Daemon(守护)线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。

​ 这意味着,当一个Java虚拟机中不存在Daemon线程的时候,Java虚拟机将会退出,可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程,我们一般用不上,比如垃圾回收线程就是Daemon线程

​ Daemon线程被用作完成支持性工作,但是在Java虚拟机退出时Daemon线程中的finally块并不一定会执行,在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑,也可以理解为等程序的所有的用户线程结束后,守护线程也将结束。

​ 注意:守护线程必须在start之前设置,否则会报错

6.1 代码案例

package chapter01.daemon;

import util.ThreadUtils;

import java.util.concurrent.TimeUnit;

public class ThreadDaemon {
    public static void main(String[] args) {

        Thread thread1 = new Thread(() -> {
            while (true) {
                System.out.println("1111111111111");
                ThreadUtils.sleep(1, TimeUnit.SECONDS);
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("22222222222");
                ThreadUtils.sleep(1, TimeUnit.SECONDS);
            }
        });

       /* Thread thread3 = new Thread(() -> {
            while (true) {
                System.out.println("3333333333333");
                ThreadUtils.sleep(1, TimeUnit.SECONDS);
            }
        });*/
        thread1.setDaemon(true);
        thread1.start();
        thread2.start();
        //thread3.start();

    }

}

本文由传智教育博学谷狂野架构师教研团队发布。

如果本文对您有帮助,欢迎关注点赞;如果您有任何建议也可留言评论私信,您的支持是我坚持创作的动力。

转载请注明出处!

有关一个Java线程的线生(线生 vs 人生)的更多相关文章

  1. ruby-on-rails - Railstutorial : db:populate vs. 工厂女孩 - 2

    在railstutorial中,作者为什么选择使用这个(代码list10.25):http://ruby.railstutorial.org/chapters/updating-showing-and-deleting-usersnamespace:dbdodesc"Filldatabasewithsampledata"task:populate=>:environmentdoRake::Task['db:reset'].invokeUser.create!(:name=>"ExampleUser",:email=>"example@railstutorial.org",:passwo

  2. ruby - 使用 Vim Rails,您可以创建一个新的迁移文件并一次性打开它吗? - 2

    使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta

  3. ruby-on-rails - Rails - 一个 View 中的多个模型 - 2

    我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何

  4. ruby-on-rails - 渲染另一个 Controller 的 View - 2

    我想要做的是有2个不同的Controller,client和test_client。客户端Controller已经构建,我想创建一个test_clientController,我可以使用它来玩弄客户端的UI并根据需要进行调整。我主要是想绕过我在客户端中内置的验证及其对加载数据的管理Controller的依赖。所以我希望test_clientController加载示例数据集,然后呈现客户端Controller的索引View,以便我可以调整客户端UI。就是这样。我在test_clients索引方法中试过这个:classTestClientdefindexrender:template=>

  5. ruby-on-rails - 如果 Object::try 被发送到一个 nil 对象,为什么它会起作用? - 2

    如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象

  6. ruby - 为什么 SecureRandom.uuid 创建一个唯一的字符串? - 2

    关闭。这个问题需要detailsorclarity.它目前不接受答案。想改进这个问题吗?通过editingthispost添加细节并澄清问题.关闭8年前。Improvethisquestion为什么SecureRandom.uuid创建一个唯一的字符串?SecureRandom.uuid#=>"35cb4e30-54e1-49f9-b5ce-4134799eb2c0"SecureRandom.uuid方法创建的字符串从不重复?

  7. java - 等价于 Java 中的 Ruby Hash - 2

    我真的很习惯使用Ruby编写以下代码:my_hash={}my_hash['test']=1Java中对应的数据结构是什么? 最佳答案 HashMapmap=newHashMap();map.put("test",1);我假设? 关于java-等价于Java中的RubyHash,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/22737685/

  8. ruby - RuntimeError(自动加载常量 Apps 多线程时检测到循环依赖 - 2

    我收到这个错误:RuntimeError(自动加载常量Apps时检测到循环依赖当我使用多线程时。下面是我的代码。为什么会这样?我尝试多线程的原因是因为我正在编写一个HTML抓取应用程序。对Nokogiri::HTML(open())的调用是一个同步阻塞调用,需要1秒才能返回,我有100,000多个页面要访问,所以我试图运行多个线程来解决这个问题。有更好的方法吗?classToolsController0)app.website=array.join(',')putsapp.websiteelseapp.website="NONE"endapp.saveapps=Apps.order("

  9. ruby-on-rails - Rails - 从另一个模型中创建一个模型的实例 - 2

    我有一个正在构建的应用程序,我需要一个模型来创建另一个模型的实例。我希望每辆车都有4个轮胎。汽车模型classCar轮胎模型classTire但是,在make_tires内部有一个错误,如果我为Tire尝试它,则没有用于创建或新建的activerecord方法。当我检查轮胎时,它没有这些方法。我该如何补救?错误是这样的:未定义的方法'create'forActiveRecord::AttributeMethods::Serialization::Tire::Module我测试了两个环境:测试和开发,它们都因相同的错误而失败。 最佳答案

  10. ruby - 用 Ruby 编写一个简单的网络服务器 - 2

    我想在Ruby中创建一个用于开发目的的极其简单的Web服务器(不,不想使用现成的解决方案)。代码如下:#!/usr/bin/rubyrequire'socket'server=TCPServer.new('127.0.0.1',8080)whileconnection=server.acceptheaders=[]length=0whileline=connection.getsheaders想法是从命令行运行这个脚本,提供另一个脚本,它将在其标准输入上获取请求,并在其标准输出上返回完整的响应。到目前为止一切顺利,但事实证明这真的很脆弱,因为它在第二个请求上中断并出现错误:/usr/b

随机推荐