JUC并发编程之Java线程(二)

二、Java线程

2.1 创建和运行线程

方法一:Thread创建线程方式:

  • 继承Thread类
  • 匿名内部类方式
public class CreateThread01 {

    public static void main(String[] args) {
        // 匿名内部类方式
        Thread t1 = new Thread("t1"){
            // run 方法内实现了要执行的任务
            public void run() {
                System.out.println("匿名内部类方式....."+ Thread.currentThread().getName());
            }
        };
        t1.start();
		
       	// 继承Thread类
        MyThread t2 = new MyThread();
        t2.start();
    }
}

class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("创建类继承Thread接口....." + Thread.currentThread().getName());
    }
}

方法二:实现Runnable配合Thread

  • 实现Runnale接口
  • 匿名内部类方式
public class CreateThread02 {

    public static void main(String[] args) {
        // 实现Runnale接口
        MyThread1 myThread1 = new MyThread1();
        Thread t1 = new Thread(myThread1);
        t1.start();

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("匿名内部类的方式....."+Thread.currentThread().getName());
            }
        };
        Thread t2 = new Thread(runnable);
        t2.start();
    }
}

// 实现Runnale接口
class MyThread1 implements Runnable{

    @Override
    public void run() {
        System.out.println("使用Runnable接口创建线程......"+ Thread.currentThread().getName());
    }
}

方式一和方式二的比较:

  • 开发中优先选择实现Runnable接口的方式
  • 原因:
    (1)实现的方式没有类的单继承性的局限性
    (2)实现的方式更适合来处理多个线程有共享数据的情况
  • 相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中

方法三:实现Callable接口

实现Callable接口:

  1. 定义一个线程任务类实现Callable接口,申明线程执行结果类型
  2. 重写Callable接口的call方法,这个方法可以直接返回执行结果
  3. 创建一个Callable的线程任务对象
  4. 把Callable的线程任务对象包装成一个未来任务对象
  5. 把未来任务对象包装成线程对象
  6. 调用线程的start()方法启动线程
  • public FutureTask(Callable<V> callable):未来任务对象,在线程执行完后得到线程的执行结果。

  • FutureTask就是Runnable对象,因为Thread类只能执行Runnable实例的任务对象,所以把Callable包装成未来任务对象

  • public V get():同步等待 task 执行完毕的结果,如果在线程中获取另一个线程执行结果,会阻塞等待,用于线程同步

    • get()线程会阻塞等待任务完成
    • run()执行完后会把结果设置到FutureTask的一个成员变量,get()线程可以获取到该变量的值
public class CreateThread03 {

    public static void main(String[] args)  {
        // 第一种将实现Callable接口类传入FutureTask
        FutureTask<String> task = new FutureTask<String>(new MyCallable());

        new Thread(task,"t3").start();
 		// 获取call方法返回的结果(正常/异常结果)
        String result = null;
        try {
            result = task.get();
            System.out.println(result);
        } catch (Exception e) {
            e.printStackTrace();
        }


        // 第二种内部类实现
        FutureTask<String> task1 = new FutureTask<String>(() -> {
            return Thread.currentThread().getName() + "->" +"FutureTask";
        });
        new Thread(task1,"t4").start();
        try {
            result = task1.get();
            System.out.println(result);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class MyCallable implements Callable<String> {
    @Override  //重写线程任务类方法
    public String call() throws Exception {
        return Thread.currentThread().getName() + "->" +"实现Callable接口";
    }
}

优点:

  • 可以定义返回值

  • 可以抛出异常

方法四:利用线程池创建

使用 ThreadPoolExecutor 创建线程池,并从线程池中获取线程用于执行任务。在 JUC 中,Executor 框架已经实现了几种线程池,以 Executor 的 newFixedThreadPool 来作为 Demo 的展示。

public class CreateThread4 {
    public static void main(String[] args) throws ExecutionException,
            InterruptedException {
        System.out.println("----程序开始运行----");
        Date date1 = new Date();

        int taskSize = 5;
        // 创建一个线程池
        ExecutorService pool = Executors.newFixedThreadPool(taskSize);
        // 创建多个有返回值的任务
        List<Future> list = new ArrayList<Future>();
        for (int i = 0; i < taskSize; i++) {
            Callable c = new MyCallable1(i + " ");
            // 执行任务并获取Future对象
            Future f = pool.submit(c);
            // System.out.println(">>>" + f.get().toString());
            list.add(f);
        }
        // 关闭线程池
        pool.shutdown();

        // 获取所有并发任务的运行结果
        for (Future f : list) {
            // 从Future对象上获取任务的返回值,并输出到控制台
            System.out.println(">>>" + f.get().toString());
        }

        Date date2 = new Date();
        System.out.println("----程序结束运行----,程序运行时间【"
                + (date2.getTime() - date1.getTime()) + "毫秒】");
    }
}

class MyCallable1 implements Callable<Object> {
    private String taskNum;

    MyCallable1(String taskNum) {
        this.taskNum = taskNum;
    }

    public Object call() throws Exception {
        System.out.println(">>>" + taskNum + "任务启动");
        Date dateTmp1 = new Date();
        Thread.sleep(1000);
        Date dateTmp2 = new Date();
        long time = dateTmp2.getTime() - dateTmp1.getTime();
        System.out.println(">>>" + taskNum + "任务终止");
        return taskNum + "任务返回运行结果,当前任务时间【" + time + "毫秒】";
    }
}
  • ExecutorService、Callable、Future 实际上都是属于 Executor 框架。

  • 线程池支持有返回结果和无返回结果的任务,有返回值的任务必须实现Callable接口,无返回值的任务必须实现Runnable接口。

  • 对于有结果的任务,执行 Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到Callable任务返回的 Object 了,

  • 但需要注意的是:get方法是阻塞的,如果线程未返回结果,那么 get() 方法会一直等待,直到有结果返回或者超时。

2.2 查看进程线程的方法

windows

- 任务管理器可以查看线程和进程数,也可以用来杀死进程
- tasklist      查看进程
- taskkill      杀死进程

linux

ps -fe    查看所有进程
ps -fT -p PID   查看某个进程(PID)的所有线程
kill    杀死进程
top     按大写H切换是否显示线程
top -H -p PID   查看某个进程(PID)的所有线程

Java

jps    查看所有java进程
jstack pid   查看某个java进程(Pid)的所有进程状态
jconsole   查看某个java进程中线程运行情况(图形界面)

jconsole 远程监控配置

  • 需要以如下方式运行你的 java 类

    java -Djava.rmi.server.hostname=`ip地址` -Dcom.sun.management.jmxremote -
    Dcom.sun.management.jmxremote.port=`连接端口` -Dcom.sun.management.jmxremote.ssl=是否安全连接 -
    Dcom.sun.management.jmxremote.authenticate=是否认证 java类
    
    
  • 修改/etc/hosts文件将127.0.0.1映射至主机名

  • 如果要认证

    • 复制 jmxremote.password 文件
    • 修改 jmxremote.password 和 jmxremote.access 文件的权限为 600 即文件所有者可读写
    • 连接时填入 controlRole(用户名),R&D(密码)

2.3原理之线程运行

栈与栈帧

Java Virtual Machine Stacks (Java 虚拟机栈)

​ JVM由堆、栈、方法去所组成,每个线程启动后,虚拟机就会为其分配一块栈内存

  • 每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

线程上下文切换

导致 cpu 不再执行当前的线程,转而执行另一个线程的代码原因:

  • 线程的cpu时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了sleep、yield、join、wait、park、synchronized、lock等方法

​ 当上下文切换(Context Switch发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java中对应的概念就是程序计数器器(Program Counter Register),它的作用是记住下一条jvm指令的执行地址,是线程私有的。

  • 状态包括程序计数器、虚拟机中每个栈帧的信息,如局部变量、操作数栈、返回地址等
  • 线程上下文切换(Context Switch)频繁发生会影响性能

2.4 常用api以及详解

方法说明
start()启动一个新线程,Java虚拟机调用此线程的 run 方法
run()线程启动后调用该方法
join()等待线程运行结束
join(long n)等待线程运行结束,最多等n毫秒
getId()获得线程长整型id id是唯一的
getName()获得线程名
getName(String name)修改线程名
getPriority()获得线程优先级
setPriority(int n)修改线程优先级,java中规定线程优先级是1-10的整数,较大的优先级能提高该线程被CPU调度的几率,注意:这不是一定按照优先级的大小来进行调度的
getState()获取线程的状态,Java 中线程状态是用 6 个 enum 表示,分别为: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
isInterrupted()判断是否被打断,不会清楚打断标记
isAlive()线程是否存活,即是否运行完毕
interrupt()打断线程,如果打断线程正在sleep、wait、join中会导致被打断的线程抛出出 InterruptedException,并清除打断标记;如果被打断的是正在运行的线程,则会设置打断标记,则会设置打断标记;park线程被打断,也会设置打断标记
isInterrupt()判断当前线程是否被打断,会清楚打断标记
currentThread()获取当前正在执行的线程
sleep(long n)让当前线程休眠n毫秒,休眠时让出cpu时间片给其他线程,睡眠结束后的线程未必会立刻得到执行
yield()提示线程调度器让出当前线程对CPU的使用

start与run

  • 直接调用 run 是在主线程中执行了 run,没有启动新的线程
  • 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码

sleep与yield

sleep

  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
  2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
  3. 睡眠结束后的线程未必会立刻得到执行
  4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性

yield

  1. 调用yield会让当前线程从Running进入Runnable就绪状态,然后调度执行其他线程
  2. 具体实现依赖于操作系统的任务调度器

线程优先级

  • 线程优先级会提示(hint)调度器优先调度该线程,但仅仅是一个提示,调度器可以忽略。
  • 如果cpu比较忙,那么优先级高的线程会获得更多的时间片,但cpu闲时,优先级几乎没有作用

join方法

@Slf4j
public class ThreadJoinTest {

    static int r = 0;
    public static void main(String[] args) {
        test1();
    }

    private static void test1(){
        log.debug("开始");
        Thread t1 = new Thread(() -> {
            log.debug("开始");
            try {
                sleep(1);
                log.debug("结束");
                r = 10;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t1.start();
        log.debug("结束:{}",r);
        log.debug("结束");
    }
}


//注意: sleep用的是工具类
public class Sleeper {
    public static void sleep(int i) {
        try {
            TimeUnit.SECONDS.sleep(i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void sleep(double i) {
        try {
            TimeUnit.MILLISECONDS.sleep((int) (i * 1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这里插入图片描述

打印结果为什么为0,而不是打印r=10 ?

  • 这个问题是因为主线程和t1线程是并行的,t1线程需要1秒才能算出r=10

  • 而主线程直接就打印出了r的值,所以只能打印出r=0

解决方法:

  • 我们能不能让主线程等待t1线程运行完,在打印出值。这就用到join()
@Slf4j
public class ThreadJoinTest {

    static int r = 0;
    public static void main(String[] args) throws InterruptedException {
        test1();
    }

    private static void test1() throws InterruptedException {
        log.debug("开始");
        Thread t1 = new Thread(() -> {
            log.debug("开始");
            try {
                sleep(1);
                log.debug("结束");
                r = 10;
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        t1.start();
        // 在主线程打印r值的前面加一个join,作用是等待t1线程运行完,在继续往下运行
        t1.join();
        log.debug("结束:{}",r);
        log.debug("结束");
    }
}

在这里插入图片描述

还有一个有时效的join方法

没等够时间

@Slf4j
public class ThreadJoinTest {

    static int r = 0;
    public static void main(String[] args) throws InterruptedException {
        test1();
    }

    private static void test1() throws InterruptedException {
        log.debug("开始");
        Thread t1 = new Thread(() -> {
            log.debug("开始");
            try {
                sleep(2);  // 睡2s
                log.debug("结束");
                r = 10;
            } catch (Exception e) {
                e.printStackTrace();
            }
        });

        t1.start();
        // 等1s
        t1.join(1000);
        log.debug("结束:{}",r);
        log.debug("结束");
    }
}

在这里插入图片描述

等够时间

@Slf4j
public class ThreadJoinTest {

    static int r = 0;
    public static void main(String[] args) throws InterruptedException {
        test1();
    }

    private static void test1() throws InterruptedException {
        log.debug("开始");
        Thread t1 = new Thread(() -> {
            log.debug("开始");
            try {
                sleep(2);  // 睡2s
                log.debug("结束");
                r = 10;
            } catch (Exception e) {
                e.printStackTrace();
            }
        });

        t1.start();
        // 等3s
        t1.join(3000);
        log.debug("结束:{}",r);
        log.debug("结束");
    }
}

在这里插入图片描述

interrupt方法

打断sleep线程

打断 sleep,wait,join 的线程

会让这几个线程都进入阻塞状态

打断sleep的线程,会清空打断标记

@Slf4j
public class InterruptTest01 {

    public static void main(String[] args) {
        Runnable r1 = (() -> {
            // sleep 导入工具类  现在是以秒为单位
            sleep(1);
        });
        Thread t1 = new Thread(r1,"t1");
        t1.start();
        sleep(0.5);
        t1.interrupt();
        log.debug("打断状态:{}",t1.isInterrupted());
    }
}

  • 打断sleep的线程会抛出InterruptedException异常
  • 并且清空打断标记
打断正常线程

打断正常运行的线程,不会清空打断标记

@Slf4j
public class InterruptTest02 {

    public static void main(String[] args) {
        Runnable r1 = (() -> {
           while(true){
               Thread currentThread = Thread.currentThread();
               boolean interrupted = currentThread.isInterrupted();
               if (interrupted){
                   log.debug("打断状态:{}",interrupted);
                   break;
               }
           }
        });
        Thread t1 = new Thread(r1,"t1");
        t1.start();
        sleep(0.5);
        t1.interrupt();
    }
}

在这里插入图片描述

注意:

  • 并不是说我打断这个正常运行的线程,他就停止了,如果光打断正常运行的线程,不判断打断状态的话,并不会停止,只是打断标记为true而已。
打断park线程

park: 暂停当前线程,处于 WAIT 状态

unpark: 既可以在 park 之前调用或之后调用,都是用来恢复某个线程的运行,简单的说,调用 unpark 后再调用 park 线程依然不会暂停,类似提前“解毒”。

打断 park 线程, 不会清空打断状态

@Slf4j
public class InterruptTest03 {
    public static void main(String[] args) {
        Runnable r1 = (() -> {
            log.debug("park....");
            LockSupport.park();
            log.debug("打断状态:{}",Thread.currentThread().isInterrupted());
        });
        Thread t1 = new Thread(r1,"t1");
        t1.start();

        sleep(0.5);
        t1.interrupt();
    }
}

在这里插入图片描述

注意:并不会停止运行,只是将打断标记设置为true

但是如果打断标记已经是true,则park将会失去作用

@Slf4j
public class InterruptTest03 {

    public static void main(String[] args) {
        Runnable r1 = (() -> {
            for (int i =0; i < 5; i++){
                log.debug("park....");
                LockSupport.park();
                log.debug("打断状态:{}",Thread.currentThread().isInterrupted());
            }
        });
        Thread t1 = new Thread(r1,"t1");
        t1.start();

        sleep(0.5);
        t1.interrupt();
    }
}

在这里插入图片描述

模式之两阶段终止

在一个线程t1中如何终止线程t2,并且能让t2在终止前,能有一个操作的机会。
在这里插入图片描述

利用isInterrupt()方法

interrupt 可以打断正在执行的线程,无论这个线程是在 sleep,wait,还是正常运行。

@Slf4j
public class InterrruptApplyTest {

    public static void main(String[] args) throws InterruptedException {
        TPTInterrupt t1 = new TPTInterrupt();
        t1.start();

        Thread.sleep(3500);
        log.debug("调用封装的stop");
        t1.stop();
    }
}

@Slf4j
class TPTInterrupt{

    private Thread thread;

    public void start(){
        thread = new Thread(()-> {
            while(true){
                Thread current = Thread.currentThread();
                if (current.isInterrupted()){
                    log.debug("最后的操作!");
                    break;
                }
                try {
                    // 这里的sleep并不是封装的 因为我们想让他抛出异常才能进行我们的下一步操作
                    sleep(1000);
                    log.debug("业务操作!");
                } catch (InterruptedException e) {
                    // 防止在睡的时候被主线程打断,我们在这里再次打断就可以继续运行
                    current.interrupt();
                }
            }
        },"工作线程");
        thread.start();
    }

    public void stop(){
        thread.interrupt();
    }
}
利用停止标记

volatile:保证了可见性、有序性

  • 保证此变量对所有线程的可见性,当一个线程修改了这个变量的值,volatile保证了新值能立即同步到主内存中,每次使用前从主内存中取值
  • 禁止指令重排序优化,这个操作相当于一个内存屏障,指令重排序时不能把后面的指令重排序到内存屏障之前的位置,在懒汉式的优化中也经常这样使用。
@Slf4j
public class InterrruptApplyTest {

    public static void main(String[] args) throws InterruptedException {
        TPTVolatile t1 = new TPTVolatile();
        t1.start();

        Thread.sleep(3500);
        log.debug("调用封装的stop");
        t1.stop();
    }
}

@Slf4j
class TPTVolatile{

    private Thread thread;
    // 停止标记用 volatile 是为了保证该变量在多个线程之间的可见
    // 主线程把它改成true,对t1线程可见
    private volatile boolean stop = false;

    public void start(){
        thread = new Thread(()-> {
            while(true){
                Thread current = Thread.currentThread();
                if (stop){
                    log.debug("最后的操作!");
                    break;
                }
                try {
                    // 这里的sleep并不是封装的 因为我们想让他抛出异常才能进行我们的下一步操作
                    sleep(1000);
                    log.debug("业务操作!");
                } catch (InterruptedException e) {
                    // 防止在睡的时候被主线程打断,我们在这里再次打断就可以继续运行
                    current.interrupt();
                }
            }
        },"工作线程");
        thread.start();
    }

    public void stop(){
        stop = true;
    }
}

在这里插入图片描述

加粗样式

2.5 主线程和守护线程

默认情况下,Java进程需要等待所有线程都运行结束,才会结束,有一种特殊线程叫做守护线程,只要其他非守护线程运行结束了,即使守护线程里的代码没执行完,也会强制结束。

@Slf4j
public class GuardThreadTest {

    public static void main(String[] args) {
        log.debug("开始运行...");
        Thread t1 = new Thread(() -> {
            log.debug("开始运行....");
            // 2s后
            sleep(2);
            log.debug("运行结束....");
        },"daemon");

        // 设置该线程是守护线程
        t1.setDaemon(true);
        t1.start();

        sleep(1);
        log.debug("运行结束...");
    }
}

在这里插入图片描述

主线程结束后,守护线程并没有执行 运行结束

  • 垃圾回收器线程就是一种守护线程
  • Tomcat中的Acceptor和Poller线程都是守护线程,所以Tomcat接收到shutdown命令后,不会等待它门处理完当前请求

2.6五种状态

操作系统层面

  • 初始状态 仅仅是在语言层面创建了线程对象,还未与操作系统的线程关联
  • 可运行状态 (就绪状态) 指线程已经被创建(与操作系统的线程关联),可以由CPU调度执行
  • 运行状态 指已获取了CPU时间片运行中的状态
  • 阻塞状态
    • 如果调用了阻塞API,如BIO读写文件,这时该线程实际不会用到CPU,会导致线程上下文切换,进入阻塞状态
    • 等BIO操作完,会由操作系统唤醒阻塞的线程,转换至 可运行状态
    • 可运行状态的区别是,对 阻塞状态的线程来说只要一直不唤醒,调度器就一直不会考虑调度它们
  • 终止状态表示线程已经执行完毕,生命周期已经结束,不会再转换为奇它状态

2.7六种状态

Java层面 根据 Thread.State 枚举,分为六种状态

在这里插入图片描述

  • NEW 线程刚被创建,但是还没调用start()方法
  • RUNNABLE 当调用了start()方法之后,在Java API层面的 RUNNABLE状态涵盖了操作系统层面的 可运行状态运行状态阻塞状态(由于BIO导致的线程阻塞,在Java里无法区分,仍然认为是可运行)
  • BLOCKEDWAITINGTIME_WAITING都是Java API层面对 阻塞状态的细分
  • TERMINATED当线程代码运行结束
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

晓风残月Lx

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

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

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

打赏作者

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

抵扣说明:

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

余额充值