JUC 正在学习:线程相关的方法

JUC

0. 简单概念

0.1 进程和线程

线程:

  • 计算机调度的最小单元
  • 更加轻量,加一个线程只需要分配很少的存储空间,可以多个线程共享同一个进程的空间
  • 线程同步:互斥锁

进程:

  • 计算机分配资源的最小单元
  • 一个进程可以有多个线程,至少有一个线程
  • fork 创建子进程,当发生写操作时会复制父进程一块内存
  • 进程间通信:管道,消息队列,Socket,信号量

0.2 并行和并发

并行:多个线程同时执行,一手画圆,一手画方

并发:每个线程分配一定的时间执行,一会画圆,一会画方

0.3 同步和异步

同步:需要等待返回结果,才能继续运行

异步:不需要等待返回结果,就能继续运行

异步的小例子,主线程不需要等待另外一个线程执行结束就可以直接向下执行:

public static void main(String[] args) throws ClassNotFoundException {
    new Thread(() -> {
        try {
            long start = System.currentTimeMillis();
            Thread.sleep(2000);
            System.out.println("我执行完了, 执行时间: " + (System.currentTimeMillis() - start) + " ms");
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }).start();
    System.out.println("hello");
}

在这里插入图片描述

1. 创建线程的方法

参考我之前写的:Java 创建线程的三种方式

这里补充一些细节:

  • 使用 FutureTask 实例获取返回值的时候 task.get() 会使当前线程阻塞,一直等到结果返回才继续往下执行。
public static void main(String[] args) throws ExecutionException, InterruptedException {
   FutureTask<Integer> task = new FutureTask<Integer>(new Callable<Integer>() {
        @Override
        public Integer call() throws Exception {
            log.debug("线程执行");
            Thread.sleep(2000);
            return 200;
        }
    });

    new Thread(task, "线程1").start();
    log.debug("{}", task.get());
    log.debug("执行结束!");
}

简化版:

public static void main(String[] args) throws ExecutionException, InterruptedException {
    FutureTask<Integer> task = new FutureTask<Integer>(() -> {
        log.debug("线程执行");
        Thread.sleep(2000);
        return 200;
    });

    new Thread(task, "线程1").start();
    log.debug("{}", task.get());
    log.debug("执行结束!");
}

在这里插入图片描述

2. 查看进程和线程的方法

Linux:

  • ps -ef 查看所有进程信息
    JUC 正在学习:线程相关的方法_第1张图片
  • kill ,杀死进程
  • top 实时查看进程
    JUC 正在学习:线程相关的方法_第2张图片
  • top -Hp 查看进程中的线程
    JUC 正在学习:线程相关的方法_第3张图片

Java:

  • jps 查看 java 进程
    在这里插入图片描述
  • jstack 查看某个时刻某个 java 进程中的所有线程详情
    JUC 正在学习:线程相关的方法_第4张图片
  • jconsole 使用图形化界面查看进程情况
    JUC 正在学习:线程相关的方法_第5张图片
    JUC 正在学习:线程相关的方法_第6张图片

3. 线程运行

3.1 栈与栈帧

每个线程启动 JVM 会分配给该线程一块虚拟机栈内存,每个线程只能有一个活动栈帧,对应当前执行的方法。

3.2 线程上下文切换

切换时机:

  • 线程 CPU 时间片结束
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了 sleep(),yield(),wait(),join(),park(),synchronized,lock 等方法。

上下文切换需要记录当前线程的状态,JVM 实现此方法的就是程序计数器

线程不是越多越好,频繁的上下文切换也会导致性能下降。

3.3 Java Thread 常见方法

  • start():启动一个新线程,让一个线程进入就绪,不一定会立刻运行(CPU 时间片还没分配)
  • run():线程调用 start() 后会自动调用该方法,不能手动调用,手动调用就相当于一个普通方法来使用
  • join():等待线程运行结束
  • join(long n):等待线程运行结束,最多等待 n 毫秒
  • setPriority(int):设置优先级,默认为 5,最高为 10
  • getState(): 获取线程状态,Java 中共有 6 种状态
public enum State {
    NEW, // 新建
    RUNNABLE, // 可运行线程的线程状态。处于可运行状态的线程正在JVM中执行
    BLOCKED, // 阻塞
    WAITING, // 等待
    TIMED_WAITING, // 限时等待
    TERMINATED; // 终止
}

JUC 正在学习:线程相关的方法_第7张图片

  • isInterrupted():是否被打断,不清除打断标记
  • isAlive(): 线程是否存活
  • interrupt():打断线程,若被打断的线程正在 sleep,wait,join,则会导致被打断的线程抛出 InterruptException,并清除打断标记
  • interrupted():判断当前线程是否被打断,清除打断标记
  • sleep(long n) :当前线程休眠 n 毫秒,让出 cpu 的时间片给其他线程
  • yield():提示线程调度器让出当前线程对 CPU 的使用,主要为了测试和调试

3.3.1 start() 和 run()

public static void main(String[] args) {
    FutureTask<Integer> task = new FutureTask<>(() -> {
        log.debug("t1 线程执行完毕!");
        return 200;
    });

    new Thread(task, "t1").run();

    log.debug("main 执行完毕!");
}

在这里插入图片描述

  • 主动调用 run() 就相当于调用一个普通方法一样,不会以多线程的方式去执行。

  • 正确的方式应该调用 start() 方法,然后让系统自动调用 run() 方法。

start() 不能被调用两次,会抛出 IllegalThreadStateException

public static void main(String[] args) {
    FutureTask<Integer> task = new FutureTask<>(() -> {
        log.debug("t1 线程执行完毕!");
        return 200;
    });

    Thread t1 = new Thread(task, "t1");
    t1.start();
    t1.start();

    log.debug("main 执行完毕!");
}

在这里插入图片描述

  • 当线程创建后没有调用 start() 之前是 NEW 状态
  • 调用 start() 之后是 RUNNABLE 状态。

3.3.2 sleep() 和 yield()

sleep

  1. 调用 sleep 会让线程从 正在运行 进入 TIMED_WAITING
  2. 其他线程使用 interrupt() 方法打断正在睡眠的线程会抛出 InterruptedException
  3. 睡眠结束后线程未必会立即得到执行
public static void main(String[] args) throws InterruptedException {
    FutureTask<Integer> task = new FutureTask<>(() -> {
        try {
            log.debug("t1 进入睡眠...");
            Thread.sleep(2000);
        } catch (Exception e) {
            log.debug("t1 醒来");
            e.printStackTrace();
            return 201;
        }
        return 200;
    });

    Thread t1 = new Thread(task, "t1");
    t1.start();
    Thread.sleep(1000);
    t1.interrupt();

    log.debug("main 执行完毕!");
}

JUC 正在学习:线程相关的方法_第8张图片
4. ★建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性

TimeUnit.SECONDS.sleep(2); // 表示当前线程睡 2 秒

yield

  1. 调用 yield 会让线程从 正在运行 进入 RUNNABLE,然后调度执行其他同等优先级的线程,若没有同等优先级的线程,则不能保证让当前线程暂停
  2. 具体实现依赖 OS 的任务调度器

3.3.3 任务优先级

  • 线程优先级只是提示调度器优先调度该线程,任务调度器可以选择忽略
  • Java 中线程默认为 5,最小为 1,最大为 10
  • 若 CPU 繁忙,优先级高的线程会获得更多的时间片
  • 若 CPU 空闲,优先级几乎没什么用

3.3.4 sleep() 应用

防止 while(true) 导致 CPU 空转,浪费资源。可以使用 yield 或者 sleep 来让出 CPU 的使用权给其他程序,即使死循环 sleep(1) 也能大大减少 CPU 的利用。

  • 也可以使用 wait 或者条件变量达到类似的效果(需要加锁,一般适用于同步的场景)
  • sleep 适用于无需锁同步的场景

3.3.5 join()

在当前线程中调用另外一个线程的 join() 方法,表示等待另外一个线程结束后,当前线程才继续往下执行。

@Slf4j
public class InitTest {
    static int a = 0;
    public static void main(String[] args) throws InterruptedException {
        FutureTask<Integer> task = new FutureTask<>(() -> {
            log.debug("t1 线程开始执行...");
            TimeUnit.SECONDS.sleep(1);
            a = 10;
            return 0;
        });

        Thread t1 = new Thread(task, "t1");
        t1.start();

        // 调用 join 方法同步等待 t1 线程执行结束
        t1.join();

        log.debug("a 的值为: {}", a);
    }
}

在这里插入图片描述
join(long n) 可以进行有时间限制的等待。

3.3.6 interrupt()

若以异常的方式打断 wait(), sleep(), join(),会把打断标记置为 false

@Slf4j
public class InitTest {
    public static void main(String[] args) throws InterruptedException {
        FutureTask<Integer> task = new FutureTask<>(() -> {
            try {
                log.debug("t1 进入睡眠...");
                TimeUnit.SECONDS.sleep(2);
            } catch (Exception e) {
                log.debug("t1 醒来");
                e.printStackTrace();
            }
            return 200;
        });

        Thread t1 = new Thread(task, "t1");
        t1.start();
        TimeUnit.SECONDS.sleep(1);
        t1.interrupt();

        log.debug("是否打断: {}", t1.isInterrupted());
    }
}

JUC 正在学习:线程相关的方法_第9张图片

正常打断(可以用于正常停止线程):

@Slf4j
public class InitTest {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (true) {
                boolean interrupted = Thread.currentThread().isInterrupted();
                if (interrupted) {
                    log.debug("线程 t1 被打断了, 退出循环");
                    break;
                }
            }
        }, "t1");
        t1.start();

        TimeUnit.SECONDS.sleep(1);
        t1.interrupt();
        log.debug("t1 线程是否被打断: {}", t1.isInterrupted());
    }
}

在这里插入图片描述
两阶段停止
在一个线程 T1 中如何 “优雅” 停止一个 T2?

  • 错误思路1:用 stop() 方法(已弃用),如果 T2 锁住共享资源,那么 T2 被杀死后将无法释放锁
  • 错误思路2:System.exit(int) 方法停止线程,该方法会让整个程序都停止

设计一个后台系统监控程序:
JUC 正在学习:线程相关的方法_第10张图片

/**
 * 后台监控程序, 两阶段提交
 */
@Slf4j(topic = "c.InitTest")
public class InitTest {
    public static void main(String[] args) throws InterruptedException {
        MonitorThread monitorThread = new MonitorThread();
        monitorThread.start();

        TimeUnit.SECONDS.sleep(5);
        monitorThread.stop();
    }
}

@Slf4j(topic = "c.MonitorThread")
class MonitorThread {
    private Thread monitor;

    // 启动监控
    public void start() {
        monitor = new Thread(() -> {
            while (true) {
                Thread thread = Thread.currentThread();
                if (thread.isInterrupted()) {
                    log.debug("准备退出......");
                    break;
                }
                try {
                    TimeUnit.SECONDS.sleep(2); // case 1 :该处被打断, 打断标记还是 false
                    log.debug("执行监控记录");          // case 2 : 该处被打断, 打断标记置为 true
                } catch (InterruptedException e) {
                    log.debug("睡眠时间被打断, 清除打断标记");
                    e.printStackTrace();
                    thread.interrupt();  // case 2 : 重新设置打断标记
                }
            }
        });
        monitor.start();
    }

    // 停止监控
    public void stop() {
        monitor.interrupt();
    }
}

JUC 正在学习:线程相关的方法_第11张图片
打断 park() 中的线程park() 方法若被打断,再次 park() 将执行失效,如果想再次 park() 可以使用 Thread.interrupted() 清除标记:

@Slf4j(topic = "c.InitTest")
public class InitTest {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            log.debug("park......");
            LockSupport.park();
            log.debug("unpark.....");
            log.debug("当前线程是否被打断: {}", Thread.currentThread().isInterrupted());
            
			LockSupport.park(); // 若被打断就无法再次 park
            log.debug("unpark.....");
        });
        t1.start();
        TimeUnit.SECONDS.sleep(1);
        t1.interrupt();
    }
}

在这里插入图片描述

3.3.7 过时方法

以下方法不推荐使用,容易破坏同步代码块,造成线程死锁:

  • stop():停止线程运行
  • suspend() :挂起(暂停)线程运行
  • resume():恢复线程运行

3.3.8 守护线程

3.4 练习

你可能感兴趣的:(JUC,学习,java)