JavaSE-12-Java多线程

Table of Contents

Java多线程

1:进程与线程

1.1:概念

1.2:java线程的运行机制

2:线程的创建与启动

2.1:拓展Thread类

2.1.1:创建线程

2.1.2:sleep方法

2.2:实现Runnable接口

3:线程状态的转换

3.1:新建状态

3.2:就绪状态

3.3:运行状态

3.4:阻塞状态

3.5:死亡状态

4:线程调度

4.1:调整各个线程的优先级

4.2:线程睡眠:Thread.sleep()

4.3:线程让步:Thread.yield()

4.4:等待其他线程结束:其他线程对象.join()---让别人先走,走完自己再走

4.5:废弃的停止线程方法:stop()

4.6:中断线程 : thread.interrupt()

4.6.1  使用中断信号量中断非阻塞状态的线程

4.6.2  中断阻塞状态线程

5:查看线程

5.1 获取当前线程的引用

5.2:查看所有的活动

6:后台进程

7:定时器

7.1:ScheduledExecutorService---多线程池的执行任务,有线程池 可以支持多个任务并发执行

7.1.1:ScheduledFuture schedule(Runnable command,long delay,TimeUnit unit)

7.1.2:ScheduledFuture scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit)

7.1.3:ScheduledFuture scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit)

7.2:Timer---单线程(当执行任务的时间间隔小于执行任务的时间, timer就会等待上一个任务执行结束才执行下一个)

7.3:ScheduledExecutorService的使用

7.2.1:基本使用

7.2.2:schedule Runnable 

 7.2.3:schedule Callable

8:线程的同步

8.1:同步代码块

8.2:同步方法

8.3:同步与并发

8.4:线程安全的类


Java多线程

1:进程与线程

1.1:概念

几乎每种操作系统都支持进程的概念——进程就是在某种程度上相互隔离的、独立运行的程序,每一个进程都有自己独立的内存空间。比如 IE 浏览器程序,每打开一个 IE 浏览器窗口,就启动一个新的进程。在 java 中,我们执行 java.exe 程序,就启动一个独立的 Java 虚拟机进程,该进程的任务就是解析并执行 Java 程序代码。

线程是指进程中的一个执行流程,一个进程可以由多个线程组成,即一个进程中可以同时运行多个不同的线程,它们分别执行不同的任务。当进程内的多个线程同时运行时,这种运行方式成为并发运行。

线程又被称为轻量级进程,它和进程一样拥有独立的执行控制,由操作系统进行调度。线程和进程的区别是:

  • 每个进程都有独立的代码和存储空间(进程上下文),进程切换的开销大。
  • 线程没有独立的存储空间,而是和所属进程中其他的线程共享代码和存储空间,但每个线程有独立的运行栈和程序计数器,因此线程切换的开销较小。
  • 多进程——在操作系统中能同时运行多个任务(程序),也称多任务。
  • 多线程——在同一应用程序中有多个顺序流同时执行。

许多服务器程序,如数据库服务器和 Web 服务器,都支持并发运行,这些服务器能同时响应来自不同客户的请求。

1.2:java线程的运行机制

在 java 虚拟机进程中,执行程序代码的任务是由线程来完成的。每个线程都有一个独立的程序计数器和方法调用栈。

程序计数器:也称为 PC 寄存器,当线程执行一个方法时,程序计数器指向方法区中下一条要执行的字节码指令。

方法调用栈:简称方法栈,用来跟踪线程运行中一系列的方法调用过程,栈中的元素称为栈帧,每当线程调用一个方法的时候,就会向方法栈压入一个新帧。帧用来存储方法的参数、局部变量和运算过程中的临时数据。

栈帧由以下三个部分组成:

  • 局部变量区:存放局部变量和方法参数。
  • 操作数栈:是线程的工作区,用来存放运算过程中生成的临时数据。
  • 栈数据区:为线程执行指令提供相关的信息,包括如何定位到位于堆区和方法区的特定数据,以及如何正常退出方法或者异常中断方法。

每当用 Java 命令启动一个 Java 虚拟机进程时,Java 虚拟机都会创建一个主线程,该线程从程序入口 main()方法开始执行。以下面程序为例,介绍线程的运行过程。

public class Test {
    private int num; //实例变量
    public int add(){
        int b=0; //局部变量
        num++;
        b=num;
        return b;
    }
    public static void main(String[] args) {
        Test t=new Test(); //局部变量
        int num=0; //局部变量
        num=t.add();
        System.out.println(num);
    }
}

主线程从 main()方法的程序代码开始运行,当它开始执行 method()方法的“a++”操作时,运行时数据区的状态如下图所示。

当主线程执行“a++”操作时,它能根据 method()方法的栈帧的栈数据区中的有关信息,正确地定位到堆区的 Test 对象的实例变量 num,并把它的值加 1。

当 add()方法执行完毕后,它的栈帧就会从方法栈中弹出,它的局部变量 b 结束生命周期。main()方法的栈帧就成为当前帧,主线程继续执行 main()方法。

方法区存放了线程所执行的字节码指令,堆区存放了线程所操作的数据(以对象的形式存放),Java 栈区则是线程的工作区,保存线程的运行状态。

在Java中可以通过 Thread 类的 currentThread 方法得到正在调用该方法的线程;

public class TestStream {
    public static void main(String[] args) {
        Thread thread = Thread.currentThread();
        System.out.println(""+thread.getName());//main
    }
}

2:线程的创建与启动

Java 在代码中对线程进行了支持,程序员可以创建自己的线程,它将和主线程并发运行。创建线程有两种方式:

  • 扩展 Thread 类
  • 实现 Runnable 接口

2.1:拓展Thread类

Thread 类代表线程类,它的最主要的两个方法是:

  • run() 包含线程运行时所执行的代码。
  • start() 用于启动线程

开发线程类只需要继承 Thread 类,覆盖Thread 类的 run()方法即可。在Thread 类中,run()方法的定义如下:public void run()

2.1.1:创建线程

public class MyThread extends Thread{
    @Override
    public void run() {
        //线程中的实现
        for (int i = 0; i <100 ; i++) {
            System.out.println(""+this.getName()+"---"+i);
        }
    }
}


public class TestMain {
    public static void main(String[] args) {
        MyThread myThread =  new MyThread();
        myThread.start();
        System.out.println(""+Thread.currentThread().getName());
    }

}

当执行“java MyThread“命令时,Java 虚拟机首先创建并启动主线程。主线程的任务是执行 main 方法,main 方法创建了一个 MyThread 对象,然后调用它的 start()方法启动MyThread 线程。MyThread 线程的任务是执行它的 run()方法。
主线程和自定义线程并发运行

在下面,创建两个MyThread线程并发运行

当主线程执行 main()方法时,会创建两个 MyThread 对象,然后启动两个 MyThread 线程。在 Java 虚拟机钟有两个线程并发执行 MyThread 对象的 run()方法。在两个线程各自的方法栈中都有代表 run()方法的栈帧,在这个帧中存放了局部变量 num,也就是每个线程都拥有自己的局部变量 num,它们都分别从 0 增加到 100。 

因为 Thread 类中有 getName()方法,MyThread 类继承了 Thread 类,所以在代码中可以使用 this.getName()得到当前线程的名字。mt 对象启动线程的名字是 Thread-0,mt2 对象启动线程的名字是 Thread-1。以此类推;

2.1.2:sleep方法

Thread类中有一个sleep方法,在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),就是线程睡眠一定的时间,也就是交出 CPU 时间片,根据参数来决定暂停时间长度,让给等待序列中的下一个线程 。 Sleep 方法抛出 InterruptedException 。

public class MyThread extends Thread{
    @Override
    public void run() {
        //线程中的实现
        for (int i = 0; i <100 ; i++) {
            //当前线程休眠100毫秒
            try {
                sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(""+this.getName()+"---"+i);
        }
    }
}


public class TestMain {
    public static void main(String[] args) {
        MyThread myThread1 =  new MyThread();
        MyThread myThread2 =  new MyThread();
        myThread1.start();
        myThread2.start();
        System.out.println(""+Thread.currentThread().getName());
    }

}

结果显示: 

当 Thread-0 线程执行打印后,休眠 100 毫秒,也就失去了 CPU 的时间片,Thread-1 线程就得到了CPU的时间片,执行了打印操作,也休眠100毫秒,100毫秒后,Thread-0线程先恢复到可运行状态,接着运行,这样两个线程交替运行。

不要随便覆盖 Thread  类的 start() 方法,创建一个线程对象后,线程并不自动开始运行,必须调用它的 start()方法才能启动线程。JDK 为 Thread 类的 start()方法提供了默认的实现,启动线程后调用 run()方法。如果不通过 start()方法启动线程,而是直接调用 run()方法,那只是普通的方法调用,

在 Thread 子类中不要随意覆盖 start()方法,假如一定要覆盖 start()方法,那么应该先调用 super.start()方法。

一个线程只能被启动一次
一个线程只能被启动一次,以下代码视图两次启动 MyThread 线程。

MyThread mt=new MyThread();
mt.start();
mt.start(); //抛出IllegalThreadStateException异常

2.2:实现Runnable接口

Java 类不允许一个类继承多个类,因此一旦一个类继承了 Thread 类,就不能再继承其他的类,为了解决这一问题,Java 提供了 java.lang.Runnable 接口,它有一个 run()方法,定义如下:public void run()

public class MyThread implements Runnable {
    int count = 0;
    @Override
    public void run() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + ":" + count++);
            if (count > 10) {
                break;//当count大于 10 的时候,循环结束
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


public class TestMain {
    public static void main(String[] args) {
        MyThread myThread =  new MyThread();
        Thread thread1 = new Thread(myThread);
        Thread thread2 = new Thread(myThread);
        thread1.start();
        thread2.start();
    }

}

在 Thread 类中定义了如下形式的构造方法:public Thread(Runnable runnable)

当线程启动时,将执行参数runnable所引用对象的run()方法。其实Thread类也实现了Runnable 接口。

在示例中,主线程创建了 t1 和 t2 两个线程对象。启动 t1 和 t2 线程将执行 MyThread 对象的 run()方法。t1 和 t2 共享同一个 MyThread 对象,在执行 run()方法时将操作同一个实例变量 count。

也可以用匿名内部类

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


        new Thread(new Runnable() {
            int count = 0;
            @Override
            public void run() {
                while (true) {
                    System.out.println(Thread.currentThread().getName() + ":" + count++);
                    if (count > 10) {
                        break;//当count大于 10 的时候,循环结束
                    }
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();


    }

}

2.3:简单测试多线程,使用Executors.newFixedThreadPool

package com.wenge.datagroup.storage;

import cn.hutool.http.HttpRequest;
import com.alibaba.fastjson.JSONObject;

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

/**
 * @author wangkanglu
 * @version 1.0
 * @description
 * @date 2024-07-03 10:42
 */
public class RunnableTest {
    public static void main(String[] args) {
        int numberOfThreads = 10; // 假设我们想要并发10个请求
        ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads);
        //三十次请求,每次都发送 10 个请求
        for (int j = 0; j < 30; j++) {
            System.out.println("---------------------------------------");
            for (int i = 0; i < numberOfThreads; i++) {
                int size = i + 1;
                int number = j + 1;
                executorService.submit(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            long start = System.currentTimeMillis();
                            //这里进行业务处理,或者进行第三方接口
                            JSONObject body = new JSONObject();
                            body.put("content", "");
                            String response = HttpRequest.post("http://localhost/tool/test").body(body.toJSONString()).execute().body();
                            long end = System.currentTimeMillis();
                            System.out.println("第"+ number+"请求----------第" + (size) + "次结果:" + response+"----用时:"+(end-start));
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                });
            }
            try {
                //这里睡不睡都行,睡是因为接口qps超时
                System.out.println("睡两秒,start:"+System.currentTimeMillis());
                Thread.sleep(2000);
                System.out.println("睡两秒,end:"+System.currentTimeMillis());
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        executorService.shutdown();
    }
}

3:线程状态的转换

3.1:新建状态

用 new 语句创建的线程对象处于新建状态,此时和其他 Java 对象一样,仅仅在堆区中被分配了内存。

3.2:就绪状态

当一个线程对象创建后,其他线程调用它的 start()方法,该线程就进入了就绪状态,Java 虚拟机会为它创建方法调用栈和程序计数器。处于这个状态的线程位于可运行池中,等待获得 CPU 的使用权。

3.3:运行状态

处于这个状态的线程占用 CPU,执行程序代码。在并发运行环境中,如果计算机只有一个CPU,那么任何时刻只会有一个线程处于这个状态。如果计算机有多个 CPU,那么同一时刻可以让几个线程占用不同的 CPU,使它们都处于运行状态。只有处于就绪状态的线程才有机会转到运行状态。

3.4:阻塞状态

塞状态是指线程因为某些原因放弃 CPU,暂时停止运行。当线程处于阻塞状态时,Java虚拟机不会给线程分配 CPU,直到线程重新进入就绪状态,才有机会转到运行状态。


阻塞状态可以分为以下 3 种:

  • 位于对象等待池中的阻塞状态:当线程处于运行状态时,如果执行了wait()方法,Java虚拟机就会把线程放到等待池中。
  • 位于对象锁池中的阻塞状态:当线程处于运行状态时,试图获得某个对象的同步锁时,如果该对象的同步锁已经被其他线程占用,Java 虚拟机就会把这个线程放到锁池中。
  • 其他的阻塞状态:当前线程执行了 sleep()方法,或者调用了其他线程的 join()方法,或者发出了 I/O 请求时,就会进入这个状态。

3.5:死亡状态

当线程退出 run(方法)时,就进入死亡状态,表示该线程结束生命周期。线程有可能是正常执行完 run()方法而退出的,也有可能是遇到异常而退出。不管线程正常结束还是异常结束,都不会对其他线程造成影响。

4:线程调度

计算机通常只有一个 CPU,在任何时刻只能执行一条机器指令,每个线程只有获得 CPU 的使用权才能执行指令。所谓多线程的并发,其实是指宏观上看,各个线程轮流获得 CPU 的使用权,分别执行各自的任务。在可运行池中,会有多个处于就绪状态的线程等待 CPU,Java 虚拟机的一项任务就是负责线程的调度。线程的调度是指按照特定的机制为多个线程分配 CPU 的使用权。
有两种调度模型: 分时调度模型和抢占式调度模型。

分时调度模型是指让所有线程轮流获得CPU的使用权,并且平均分配每个线程占用CPU的时间片。

Java 虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用 CPU,如果可运行池中线程的优先级相同,那么就随机选择一个线程,使其占用 CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU。一个线程会因为以下原因而放弃 CPU:

  • Java 虚拟机让当前线程暂时放弃 CPU,转到就绪状态,使其他的线程获得运行机会。
  • 当前线程因为某些原因而进入阻塞状态。
  • 线程运行结束。

值得注意的是,线程的调度不是跨平台的,它不仅取决于 Java 虚拟机,还依赖操作系统。在某些操作系统中,只要运行中的线程没有遇到阻塞,也会在运行一段时间后放弃 CPU,给其他线程运行的机会。

在 java 中,同时启动多个线程后,不能保证各个线程轮流获得均等的 CPU 时间片,从之前的例子中,大家可以体会到这一点。一个线程运行机毫秒后,就放弃的 CPU 时间片,另一个线程就得到了 CPU 时间片,各个线程交替运行。

4.1:调整各个线程的优先级

所有处于就绪状态的线程根据优先级存放在可运行池中,优先级低的线程获得较少的运行机会 , 优 先 级 高 的 线 程 获 得 较 多 的 运 行 机 会 。 Thread 类 的 setPriority(int) 和getPriority()方法分别用来设置优先级和读取优先级。优先级用整数表示,取值范围是1~10,Thread 类有以下 3 个静态常量。

  • MAX_PRIORITY:取值为 10,表示最高优先级。
  • MIN_PRIORITY:取值为 1,表示最低优先级。
  • NORM_PRIORITY:取值为 5,表示默认的优先级。

如果不设置线程的优先级,线程默认的优先级为 5。

值得注意的是,尽管Java提供了10个优先级,但它与多数操作系统都不能很好地映射。比如 Windows2000 有 7个优先级,并且不是固定的,而 Sun公司的 Solaris 操作系统有 2的 31次方个优先级。如果希望程序能移植到各个操作系统中,应该确保在设置线程的优先级时,只使用 MAX_PRIORITY、MIN_PRIORITY、NORM_PRIORITY 这 3 个优先级。这样才能保证在不同的操作系统中,对同样优先级的线程采用同样的调度方式。

  • MyThread mt=new MyThread();mt.setPriority(Thread.MAX_PRIORITY);//设置最高的优先级
  • MyThread mt=new MyThread();mt.getPriority();//读取线程的优先级

4.2:线程睡眠:Thread.sleep()

当一个线程在运行过程中执行了sleep()方法时,它就会放弃CPU,转到阻塞状态。下面示例中每执行一次循环,就睡眠 1000 毫秒。

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

        new Thread(new Runnable() {
            int count = 0;
            @Override
            public void run() {
                for (int i = 0; i <10 ; i++) {
                    try {
                        System.out.println(""+i);
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

    }

}

Thread 类的 sleep(long millis)方法是静态的,millis参数设定睡眠的时间,以毫秒为单位。当执行 sleep()方法时,就会放弃 CPU 开始睡眠,2 秒钟后线程结束睡眠,就会获得CPU,继续进行下一次循环。所以会感觉程序运行很慢。

值得注意的是,当某线程结束睡眠后,首先转到就绪状态,假如其他的线程正在占用 CPU,那么该线程就在可运行池中等待获得 CPU。

4.3:线程让步:Thread.yield()

当线程在运行中执行了Thread类的yield()静态方法,如果此时具有相同优先级的其他线程处于就绪状态,那么 yield()方法将把当前运行的线程放到可运行池中并使另一个线程运行。如果没有相同优先级的可运行线程,则 yield()方法什么也不做。

总的来说只让给同优先级的

class MyThread extends Thread {
    int count = 0;
    public void run() {
        while (true) {
            System.out.println(getName() + ":" + count++);
            yield();
        }
    }
}
class Client {
    public static void main(String[] arr) throws InterruptedException {
        MyThread mt=new MyThread();
        MyThread mt2=new MyThread();
        mt.start();
        mt2.start();
    }
}

sleep()方法和 yield()方法都是 Thread 类的静态方法,都会使当前处于运行状态的线程放弃 CPU,把运行机会让给其他线程。两者的区别在于:

sleep()方法会给其他线程运行机会,而不考虑其他线程的优先级,因此会给较低优先级线程一个机会;yield()方法只会给相同优先级或者更高优先级线程一个运行的机会。

当线程执行了 sleep(long millis)方法后,会转到阻塞状态,参数 millis 指定睡眠的时间;当线程执行了 yield()方法后,将转到就绪状态。

sleep()方法方法抛出InterrupedException异常,而yield()方法没有声明抛出任何异常。

sleep()方法比 yield()方法具有更好的可移植性。不能依靠 yield()方法来提高程序的并发性能。对于大多数程序员来说,yield()方法的唯一用途是在测试期间人为地提高程序的并发性能,以帮助发现一些隐藏的错误。

4.4:等待其他线程结束:其他线程对象.join()---让别人先走,走完自己再走

当前运行的线程可以调用另一个线程的 join()方法,当前运行的线程将转到阻塞状态,直至另一个线程运行结束,它才恢复运行。

join()方法有两种重载形式:

  • public void join()
  • public void join(long timeout)

timeout 参数设定当前线程被阻塞的时间,以毫秒为单位。如果把示例 main()方法中的mt.join() 改为 mt.join(10) ,那么当主线程被阻塞的时间超过了 10 毫秒,或者 mt 线程运行结束时,主线程就会恢复运行。

4.5:废弃的停止线程方法:stop()

stop 这个方法是臭名昭著了,早就被弃用了,但是现在任然有很多钟情与他的人,永远都放不下他,因为从他的字面意思上我们可以知道他貌似可以停止一个线程,这个需求是每个搞线程开发的人都想要的操作,但是他并非是真正意义上的停止线程,而且停止线程还会引来一些其他的麻烦事,下面就来详细的介绍一下这个方法的:


从 SUN 的官方文档可以得知,调用 Thread.stop()方法是不安全的,这是因为当调用Thread.stop()方法时,会发生下面两件事:

  • 1. 即刻抛出 ThreadDeath 异常,在线程的 run()方法内,任何一点都有可能抛出 ThreadDeath Error,包括在 catch 或 finally 语句中。
  • 2. 会释放该线程所持有的所有的锁,而这种释放是不可控制的,非预期的。当线程抛出 ThreadDeath 异常时, 可以在该线程 run()方法的任意一个执行点抛出, 会导致该线程的 run()方法突然返回来达到停止该线程的目的。

4.6:中断线程 : thread.interrupt()

线程的 thread.interrupt()方法是中断线程,将会设置该线程的中断状态位,即设置为true,中断的结果线程是死亡、还是等待新的任务或是继续运行至下一步,就取决于这个程序本身。线程会不时地检测这个中断标示位,以判断线程是否应该被中断(中断标示值是否为 true)。它并不像 stop 方法那样会中断一个正在运行的线程。

判断某个线程是否已被发送过中断请求,请使用Thread.currentThread().isInterrupted()方法(因为它将线程中断标示位设置为true后,不会立刻清除中断标示位,即不会将中断标设置为false),而不要使用thread.interrupted()(该方法调用后会将中断标示位清除,即重新设置为false)方法来判断,下面是线程在循环中时的中断方式:
 

4.6.1  使用中断信号量中断非阻塞状态的线程

中断线程最好的,最受推荐的方式是,使用共享变量(shared variable)发出信号,告诉线程必须停止正在运行的任务。线程必须周期性的核查这一变量,然后有秩序地中止任务。下面代码描述了这一方式:

public class MyThread extends Thread{
    int count = 0;
    @Override
    public void run() {
        for (int i = 0; i <10 ; i++) {
            //这里检测到中断位的话,该线程就终止,并复位改中断位
            if(Thread.currentThread().isInterrupted()){
                break;
            }
            System.out.println(Thread.currentThread().getName()+"---"+i);
        }
    }
}



public class TestMain {
    public static void main(String[] args) throws InterruptedException {
        MyThread mt=new MyThread();
        mt.start();
        System.out.println("main ...");
        for (int i = 0; i <100 ; i++) {
            System.out.println("main"+i);
            if(i==80){
                //这里将mt线程设置中断位,当线程检测到中断位后,mt线程终止
                mt.interrupt();
            }
        }

    }

}

4.6.2  中断阻塞状态线程

Thread.interrupt()方法不会中断一个非正在运行的线程。这一方法实际上完成的是,设置线程的中断标示位,在线程受到阻塞的地方(如调用 sleep、wait、join 等地方)抛出一个异常 InterruptedException,并且中断状态也将被清除,这样线程就得以退出阻塞的状态。

主线程执行Thread对象的interrupt方法,线程可以捕获到中断,立即抛出异常,停止循环,在catch中结束线程。
如果是wait方法休眠,同样会抛出InterruptedException异常。

public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i <10 ; i++) {
            
            System.out.println(Thread.currentThread().getName()+"---"+i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                //如果在休眠状态下该线程被调用.interrupt()方法,将被抛出异常,我们采用在这里终止
                e.printStackTrace();
                break;
            }
        }
    }
}


public class TestMain {
    public static void main(String[] args) throws InterruptedException {
        MyThread mt=new MyThread();
        mt.start();
        System.out.println("main ...");
        for (int i = 0; i <100 ; i++) {
            System.out.println("main"+i);
            if(i==80){
                //这里将mt线程设置中断位,当线程检测到中断位后,mt线程终止
                mt.interrupt();
            }
        }

    }

}


5:查看线程

5.1 获取当前线程的引用

Thread 类的 currentThread()静态方法返回当前线程对象的引用。在主线程 main 中执行 currentThread()方法时,返回主线程对象的引用。

public class TestMain {
    public static void main(String[] args) throws InterruptedException {
        Thread main = Thread.currentThread();
        //输出当前线程的名称
        System.out.println(""+main.getName());//main
    }

}

5.2:查看所有的活动

Thread.getAllStackTraces()方法用于获取虚拟机中所有线程的 StackTraceElement 对象。在实际项目中不妨调用这个方法做个管理员页面,可以随时使用浏览器来查看线程堆栈,代码如下:

public class TestMain {
    public static void main(String[] args) throws InterruptedException {
        //创建线程对象
        MyThread t1=new MyThread();
        MyThread t2=new MyThread();
        //启动线程
        t1.start();
        t2.start();

        Map<Thread, StackTraceElement[]> allStackTraces = Thread.getAllStackTraces();
        Set<Thread> threads = allStackTraces.keySet();
        //打印所有线程的名字
        threads.stream().forEach(x->System.out.println("thread:"+x.getName()));
    }

}

6:后台进程

后台线程是指为其他线程提供服务的线程,也成为守护线程。如果说演员是前台线程,那么其他工作人员就是后台线程。

Java 虚拟机的垃圾回收线程就是典型的后台线程,它负责回收其他线程不再使用的内存

后台线程的特点是:后台线程和前台线程相伴相随,只有前台线程都结束生命周期,后台线程才会结束生命周期。只要有一个前台线程还没有结束运行,后台线程就不会结束生命周期。

主线程在默认情况下是前台线程,由前台线程创建的线程在默认情况下也是前台线程。调用Thread 类的 setDaemon(true)方法,就能把一个线程设置为后台线程。Thread 类的isDaemon()方法用来判读一个线程是否是后台线程。

Java 虚拟机保证:当所有前台线程运行结束后,再终止后台线程,体现的是先后顺序。那么前台线程运行结束后,后台线程是一次也不运行吗?这取决于程序的实现。

只有线程启动前(即调用 start()方法以前),才能把它设置为后台线程。如果线程启动后,再调用这个线程的 setDaemon()方法,就会导致 IllegalThreadStateException 异常。

7:定时器

7.1:ScheduledExecutorService---多线程池的执行任务,有线程池 可以支持多个任务并发执行

在 JDK 的 java.util 包中提供了一个实用类 ScheduledExecutorService,它能够定时执行特定的任务。TimerTask 类表示定时器执行的一项任务。

public class TestMain {
    public static void main(String[] args) throws InterruptedException {
        int count = 0;
        //定时任务的工具类
        ScheduledExecutorService ses= Executors.newScheduledThreadPool(1);
        TimerTask timerTask = new TimerTask() {
            @Override
            public void run() {
                System.out.println("this time:"+new Date());
            }

        };

        //利用工具类按照不同的条件触发定时任务
        //设置执行一次的定时任务
        ses.schedule(timerTask,5000, TimeUnit.MILLISECONDS);
        //设置反复执行,固定频率的定时任务
//        ses.scheduleAtFixedRate(timerTask,0,5000,TimeUnit.MILLISECONDS);
        //设置反复执行,固定间隔的定时任务
//        ses.scheduleWithFixedDelay(timerTask,0,5000,TimeUnit.MILLISECONDS);
        //关闭ses
        Thread.sleep(6000);
        ses.shutdown();
    }

}

7.1.1:ScheduledFuture<?> schedule(Runnable command,long delay,TimeUnit unit)

创建并执行在给定延迟后启用的一次性操作。
参数:

  • command - 要执行的任务
  • delay - 从现在开始延迟执行的时间
  • unit - 延迟参数的时间单位

7.1.2:ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit)

创建并执行一个在给定初始延迟后首次启用的定期操作,后续操作具有给定的周期;也就是将在 initialDelay 后开始执行,然后在 initialDelay+period 后执行,接着在initialDelay + 2 * period 后执行,依此类推。如果任务的任何一个执行遇到异常,则后续执行都会被取消。否则,只能通过执行程序的取消或终止方法来终止该任务。如果此任务的任何一个执行要花费比其周期更长的时间,则将推迟后续执行,但不会同时执行。

参数:

  • command - 要执行的任务
  • initialDelay - 首次执行的延迟时间
  • period - 连续执行之间的周期
  • unit - initialDelay 和 period 参数的时间单位

7.1.3:ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit)

创建并执行一个在给定初始延迟后首次启用的定期操作,随后,在每一次执行终止和下一次执行开始之间都存在给定的延迟。如果任务的任一执行遇到异常,就会取消后续执行。否则,只能通过执行程序的取消或终止方法来终止该任务。
参数:

  • command - 要执行的任务
  • initialDelay - 首次执行的延迟时间
  • delay - 一次执行终止和下一次执行开始之间的延迟
  • unit - initialDelay 和 delay 参数的时间单位

7.2:Timer---单线程(当执行任务的时间间隔小于执行任务的时间, timer就会等待上一个任务执行结束才执行下一个)

在Java中为我们提供了Timer来实现定时任务,当然现在还有很多定时任务框架,比如说Spring、QuartZ、Linux Cron等等,而且性能也更加优越。但是我们想要深入的学习就必须先从最简单的开始。

在Timer定时任务中,最主要涉及到了两个类:Timer和TimerTask。他们俩的关系也特别容易理解,TimerTask把我们得业务逻辑写好之后,然后使用Timer定时执行就OK了。我们来看一个最基本的案例:

7.3:ScheduledExecutorService的使用

参考:Java定时调度机制 - ScheduledExecutorService - 简书

7.2.1:基本使用

/**
 * 带延迟时间的调度,只执行一次
 * 调度之后可通过Future.get()阻塞直至任务执行完毕
 */
1. public ScheduledFuture<?> schedule(Runnable command,
                                      long delay, TimeUnit unit);

/**
 * 带延迟时间的调度,只执行一次
 * 调度之后可通过Future.get()阻塞直至任务执行完毕,并且可以获取执行结果
 */
2. public <V> ScheduledFuture<V> schedule(Callable<V> callable,
                                          long delay, TimeUnit unit);

/**
 * 带延迟时间的调度,循环执行,固定频率
 */
3. public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                 long initialDelay,
                                                 long period,
                                                 TimeUnit unit);

/**
 * 带延迟时间的调度,循环执行,固定延迟
 */
4. public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                    long initialDelay,
                                                    long delay,
                                                    TimeUnit unit);

7.2.2:schedule Runnable 

该方法用于带延迟时间的调度,只执行一次。调度之后可通过Future.get()阻塞直至任务执行完毕。我们来看一个例子。

class MyFrame extends TimerTask {
    int count = 0;
    private String name;
    public MyFrame() {
    }

    public MyFrame(String name) {
        this.name = name;
    }

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


    }
}



public class TestMain {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        //定时任务的工具类
        ScheduledExecutorService ses= Executors.newScheduledThreadPool(1);
        System.out.println("main...");
        ScheduledFuture<?> future = ses.schedule(new MyFrame("wkl-"), 5000, TimeUnit.MILLISECONDS);
        //调用这个方法停止线程
        future.get();
    }

}

 7.2.3:schedule Callable

@Test public void test_schedule4Callable() throws Exception {
    ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
    ScheduledFuture<String> future = service.schedule(() -> {
        try {
            Thread.sleep(3000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("task finish time: " + format(System.currentTimeMillis()));
        return "success";
    }, 1000, TimeUnit.MILLISECONDS);
    System.out.println("schedule finish time: " + format(System.currentTimeMillis()));

    System.out.println("Callable future's result is: " + future.get() +
            ", and time is: " + format(System.currentTimeMillis()));
}

运行看到的结果和Runnable基本相同,唯一的区别在于future.get()能拿到Callable返回的真实结果。

8:线程的同步

一个线程在执行原子操作期间,应该采取措施使得其他线程不能操作共享资源,否则就会出现,共享资源被重复操作的问题。

8.1:同步代码块

为了保证每个线程能正常执行原子操作,Java 引入了同步机制,具体做法是在代表原子操作的程序代码前加上 synchronized 标记,这样的代码被称为同步代码块。

以上代码创建了 Object 对象 o,在同步块中,o 充当了同步锁的作用。

每个 Java 对象都有且只有一个同步锁,在任何时刻,最多只允许一个线程拥有这把锁。当第一个线程拥有了这个同步锁,执行同步块里的代码时,其他的线程因为没有拥有这把锁,就不能执行同步块里的代码。即使该线程睡眠了,其他线程也是不能执行同步块里的代码。直到该线程执行完同步块释放了 o 的同步锁,其他线程才有机会执行同步块里的代码。

public class MyThread extends Thread {
    //有 100 张票
    int count = 100;
    //同步锁
    Object o = new Object();

    public void run() {
        while (true) {
            synchronized (o) {
                if (count > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (Exception e) {
                    }
                    System.out.println(Thread.currentThread().getName()
                            + "=" + count);
                    count--;
                } else {
                    break;
                }
            }
        }
    }
}

当前对象也可以作为同步锁使用,所以也可以这样写同步块:

synchronized(this){}

当一个线程开始执行同步代码块时,并不意味着以不中断的方式运行。进入同步代码的线程也可以执行 Thread.sleep()或者执行 Thread.yield()方法,此时它并没有释放锁,只是把运行机会让给了其他的线程。

8.2:同步方法

使用 synchronized 关键字修饰的方法为同步方法,同步方法和同步块一样有线程同步的功能。

public class MyThread extends Thread {
    //有 100 张票
    int count = 100;
    //同步锁
    Object o = new Object();

    public void run() {
        while (true) {
            if (count < 1) {
                break;
            }
            sale();
        }
    }

    public synchronized void sale() {
        if (count > 0) {
            try {
                Thread.sleep(10);
            } catch (Exception e) {
            }
            System.out.println(Thread.currentThread().getName() + "=" + count);
            count--;
        }
    }
}

同步方法中使用当前对象 this 作为同步锁,所以不需要额外声明同步锁。
synchronized 声明不会被继承。如果一个用 synchronized 修饰的方法被子类覆盖,那么子类中这个方法不再保持同步,除非也用 synchronized 修饰。

8.3:同步与并发

同步是解决共享资源竞争的有效手段。当一个线程已经在操纵共享资源时,其他线程只能等待,只有当已经在操纵共享资源的线程执行同步代码后,其他线程才有机会操纵共享资源。

为了提高并发性能,应该使同步代码块中包含尽可能少的操作,使得一个线程能尽快释放锁,减少其他线程等待锁的时间。可以改为一个人打完一桶水后,就让其他人打水,大家轮流打水,直到每个人都打完 10 桶水。

8.4:线程安全的类

一个线程安全的类满足以下条件:
这个类的对象可以同时被多个线程安全的访问。
每个线程都能正常执行原子操作,得到正确的结果。
在每个线程的原子操作都完成后,对象处于逻辑上合理的状态。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

苍煜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值