JUC并发编程(一):线程基础知识

85 阅读7分钟

线程基础知识

创建线程

  • 直接使用thread
Thread t1 = new Thread("t1"){
    @Override
    public void run() {
        log.debug("t1");
    }
};
  • 配合 runnable
Runnable task = new Runnable() {
            @Override
            public void run() {
                //要执行的任务
            }
        };
Thread thread = new Thread(task);
  • lamdba简化
    java8开始对于函数接口,可以使用lambda表达式进行简化
Thread thread = new Thread(() -> {
   log.debug("task1");
},"name");
  • FutureTask配合Thread
FutureTask<Integer> futureTask = new FutureTask<>(() -> {
    //执行的方法体 ,并返回值
    return 100;
});
new Thread(futureTask, "线程名").start();
try {
    Integer result = futureTask.get();
} catch (ExecutionException e) {
    e.printStackTrace();
}

查看进程、线程的方法

  • windows
    • tasklist 查看进程
    • taskkill 杀死进程
  • linux
    • ps -fe 查看所有进程
    • ps -fT -p 查看某个进程的所有线程
    • kill 杀死进程 -top 按大写 H 切换是否显示线程
    • top -H -p 查看某个进程的所有线程
  • java
    • jps 命令查看所有Java进程
    • jstack 查看某个Java进程的所有线程状态
    • jconsole 来查看某个Java进程中线程的运行情况(图形界面)

java命令对应的可执行程序都在 jdk的bin 文件夹下面,如果在cmd里直接执行有问题,可以检查下是否把bin路径添加到系统路径中

线程运行原理

  • 栈与栈帧
    JVM中由堆、栈、方法区组成,其中栈内存是给谁用的呢? 其实就是线程 ,每个线程启动后,虚拟机就会为其分配一块栈内存
    • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占的内存
    • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
  • 线程上下文切换
    因为以下一些原因导致cpu不再执行当前的线程,转而执行另一个线程的代码
    • 线程的cpu时间片用完
    • 垃圾回收
    • 有更高优先级的线程需要运行
    • 线程自己调用了 sleep yield wait join park synchronized lock等方法

线程常见方法

  • start

    启动一个新线程,在新的线程中运行 run 方法 。
    注意: start 方法只是让线程进入就绪,里面代码不一定立即运行(CPU时间片还没分给它)。每个线程对象的start只能调用一次,如果调用了多次会出现 IllegalThreadStateException 。有个threadStatus 属性,默认为 0 ,对应的线程状态就是NEW
    线程只能start一次,那么线程池里的线程为什么可以重复运行?
    因为线程池里的需要重复运行的线程根本就没有运行完成过,这些线程的run方法其实是一个死循环。如果有任务过来,就运行任务的run 方法,如果没有就是一个 while(ture) 的死循环

  • run

    新线程启动后会调用的方法; 如果在构造Thread 对象时传递了Runnable 参数,则线程启动后会调用Runnable对象的 run 方法,否则默认不执行任何操作。但可以创建Thread 的子类对象,来覆盖默认行为

  • join 等待线程结束

  • join(long n) 等待线程运行结束,最多等待 n 毫秒

  • getId() 获取线程长整型的id id 唯一

  • getName() 获取线程名

  • setName(String) 设置线程名

  • getPriority() 获取线程优先级

  • setPriotity(int ) 设置线程优先级 java中规定线程优先级是 1~10 ,默认优先级是 5 ,较大的优先级能提高该线程被CPU调度的几率

  • getState()

    获取线程状态。

    JAVA中线程状态是用 6个enum表示:

    NEW RUNNABLE BLOCKING WAITING TIMED_WAITING TERMINATED

  • isInterrupted() 判断是否被打断 不会清除打断标记

  • isAlive() 线程是否存活(还没有运行完毕)

  • interrpt()

    打断线程

    如果被打断的线程正在 sleep wait join 会导致被打断的线程抛出 InterruptedException ,并清除打断标记;

    如果打断的正在运行的线程,则会设置打断标记 ,具体要不要退出,需要线程自己决定;

    park 的线程被打断,也会设置 打断标记

  • intertupted() static 判断当前线程是否被打断 会清除打断标记

  • currentThread() static 获取当前正在执行的线程

  • sleep(long) static 让当前执行的线程休眠 n 毫秒,休眠时让出 CPU的时间片给其他线程

  • yield() static 提示线程调度器让出当前线程对CPU的使用 主要是为了测试和调试

线程常见方法详解

  • sleep

    • 调用 sleep 会让当前线程从Runnable 进入 timed_waiting 状态(阻塞)
    • 其他线程可以使用 interrupt 方法 打断正在睡眠的线程,这是 sleep 方法会抛出 InterruptedException
    • 睡眠结束后的线程未必会立即得到执行
    • 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好地可读性
  • yield

    • 调用yield 会让当前线程从 running 进入 Runnable 就绪状态(还是有机会重新获得时间片),然后调度执行其他线程
    • 具体的实现依赖于操作系统的任务调度器
  • 线程优先级

    • 线程优先级会提示 调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
    • 如果CPU比较忙,那么优先级高的线程会获得更多的时间片,但CPU闲时,优先级几乎没用
  • 防止 CPU 占用 100% - Sleep 实现

    在没有利用cpu来计算时,不要让 while(true) 空转浪费CPU (在单核的时候非常明显,基本上会全部占用CPU),这时可以使用 yield或 sleep 来让出cpu的使用权给其他程序

while(true) {
    try {
        Thread.sleep(50);
    } catch(InterruptedException e) {
        e.printStackTrace();
    }
}

可以使用wait 或者 条件变量 达到类似的效果

不同的是,后两种都需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景

sleeep 适用于无需锁同步的场景

  • wait 实现
    synchrozied(obj){
    //锁对象.wait()
}
  • join 方法详解

    等待线程结束,如果送入参数 ,则表示最多等待多久

Thread t1 = new Thread(()->{
    log.info("进入 sleep...");
    try {
        TimeUnit.SECONDS.sleep(2);
        count = 10;
    } catch (InterruptedException e) {
        log.info("wake up...");
        e.printStackTrace();
    }
});

t1.start();
t1.join(); //等待 t1 执行完成, 如果没有这个 count 的值还是默认值 0 
log.info("count的值为{}", count);
try {
    TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
    e.printStackTrace();
}

应用之同步

以调用方角度来讲,如果:

-  需要等待结果返回, 才能继续运行就是
-  不需要等待结果,就能继续运行就是异步
  • 打断标记

    使用 interrupte 打断线程时,如果线程处于 sleep wait join 时,即使被打断,也会将打断标记清除;

    如果是正常运行的线程被打断,则会添加打断标记;

Thread t1 = new Thread(()->{
    log.info("进入线程...");
    while(true) {
        if (Thread.currentThread().isInterrupted()) {
            log.info("收到打断请求,按照约定将结束该线程...");
            break;
        }
    }
});
​
t1.start();
try {
    TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
    e.printStackTrace();
}
log.info("准备打断,但如何处理打断标记还是需要线程本身来决定...");
t1.interrupt();

两阶段终止模式

在一个线程T1 中如何 “优雅” 终止 线程 T2 ? 这里的 【优雅】 指的是给T2 一个料理后事的机会

  • 错误思路

    • 使用对象的 stop 方法停止线程 (该方法已经废弃,不建议大家使用)

      stop 方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其他线程永远无法获取锁

    • 使用 System.exit() 方法停止线程

      目的仅是停止一个线程, 但这种做法会让整个程序都停止

private Thread monitor;
​
//启动监控线程
public void start() {
    monitor = new Thread(()-> {
        while(true) {
            //检查打断标记
            if (Thread.currentThread().isInterrupted()) {
                log.info("监控线程监测到打断标记,即将料理后事,之后将关闭线程...");
                break;
            }
            log.info("正常监控...");
            try {
                //休眠2秒
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                //如果在休眠期间被打断 ,这时候的打断标记已经被清除了 所以手动再设置回去
                log.info("休眠期间收到打断标记..,检查此时的打断标记:{}",       
                Thread.currentThread().isInterrupted());
                //Thread.interrupted()
                Thread.currentThread().interrupt();
                e.printStackTrace();
            }
        }
    }, "monitor");
    monitor.start();
}
​
public void stop() {
    monitor.interrupt();
}
  • park 线程

LockSupport 中提供两个方法:

 LockSupport.park () // 暂停当前线程
 LockSupport.unpark(暂停线程对象)  // 恢复某个线程
Thread t1 = new Thread(() -> {
    log.info("park");
    LockSupport.park(); //线程被暂停后,下面的代码不会被执行了
    //log.info("unpark...");
    log.info("打断状态: {}", Thread.currentThread().isInterrupted()); //此时打断标记为 true
    //LockSupport.park();//在打断标记为 true 的情况下,park 方法将不起作用
    log.info("unpark...");
}, "t1");
t1.start();
//主线程将主动打断 park线程 ,不然就一直暂停住了
TimeUnit.SECONDS.sleep(1);
//t1.interrupt();
log.info("即将恢复被暂停的线程...,此时该线程的状态为{}", t1.getState());
LockSupport.unpark(t1);

线程被暂停之后,如果使用 t1.interrupt ,可以恢复该线程 ,并且打断标记为 true;
如果使用 LockSupport.unpark 恢复线程,那么打断标记依旧为 false