并发篇-线程方法和线程状态

并发篇-线程方法和线程状态,第1张

文章为在B站学习黑马并发编程视频整理笔记和个人理解,如果有错误,望指正

文章目录
      • 创建线程
      • 线程上下文切换
      • 线程状态
      • 线程常用方法
      • 二阶段停止线程
      • 临界区和竞态条件

创建线程
  1. 继承Thread类,不演示

  2. 实现runnable接口,不演示

  3. FutureTask 配合 Thread

FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况

通过调用get方法可以获取call方法返回值,同时可以补货到call方法中的异常信息

@Slf4j
public class TestCallable {

    public static void main(String[] args) {
        try {
            FutureTask<String> futureTask = new FutureTask<>(new Callable<String>() {
                @Override
                public String call() throws Exception {
                    Thread.sleep(100);
                    return "处理完毕";
                }
            });
            Thread thread = new Thread(futureTask);
            thread.start();
            log.info("futureTask返回结果为:{}",futureTask.get());
        } catch (Exception e) {
            log.info("出现异常 :{}",e);
        }
    }
}

返回结果:14:32:04.716 [main] INFO test.example.juc.TestCallable - futureTask返回结果为:处理完毕

@Slf4j
public class TestCallable {

    public static void main(String[] args) {
        try {
            FutureTask<String> futureTask = new FutureTask<>(new Callable<String>() {
                @Override
                public String call() throws Exception {
                    Thread.sleep(100);
                    int i = 1 / 0;
                    return "处理完毕";
                }
            });
            Thread thread = new Thread(futureTask,"th1");
            thread.start();
            log.info("futureTask返回结果为:{}", futureTask.get());
        } catch (Exception e) {
            log.info("出现异常 :{}", e);
        }
    }
}

可以发现在主线程中捕获到了线程th1中的算术异常

线程上下文切换

那种情况会发生上下文切换

  • 线程的 cpu 时间片用完(每个线程轮流执行,看前面并行的概念)
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了 sleepyieldwaitjoinparksynchronizedlock 等方法

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

线程状态
  1. 初始(NEW):新创建一个线程对象,但还没有调用start()方法。

  2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被CPU调度,获取CPU的使用权,此时处于就4.绪状态(ready):就绪状态的线程在获得CPU时间片后变为运行中状态(running)。

  3. 阻塞(BLOCKED):表示线程阻塞于锁。

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

  5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。

  6. 终止(TERMINATED):表示该线程已经执行完毕。

线程常用方法
  1. sleep():让线程进入到等待状态,会抛出InterruptedException打断异常,进入打断异常异常后会清除掉打断标记;
  2. yield(): 让出当前线程的CPU调度进入到就绪状态,拥有锁不释放锁,并回到可运行状态(Ready),然后等待CPU调度,此时CPU依旧会调用到该线程。
  3. join():使当前线程进入等待状态,等待调用线程执行完后才能继续运行,设置join(time)使得线程在预计时间后和当前线程并行执行,会抛出InterruptedException打断异常,进入打断异常异常后会清除掉打断标记;
  4. wait(): 将当前线程锁释放并进入等待状态(Waiting),wait(time)进入等待状态(TimeWaiting),在等待时间内未被唤醒则自动醒来,防止永久等待,必须在同步方法中调用
  5. notify() / notifyAll():唤醒某个(无法确定是哪个线程) / 所有等待中的线程当前线程不释放锁。即唤醒monitor中等待线程列表中的线程;
  6. interrupt():打断某个线程(设置中断标志为true),并且会设置打断标记,但是不能打断正在竞争锁的线程,如果对调用sleep()、wait()、join()的线程设置标志位会抛异常InterruptedException,必须进行捕获处理。如果想打断线程争抢锁(设置标志位),那么使用ReentrantLock,用锁对象调lockInterruptibly(),如果有设置标志位则抛异常(InterruptedException)
  7. isInterrupted():判断某线程是否有打断标记;
  8. interrupted():判断当前线程是否有打断标记,如果打断过,返回打断标记并且清除打断标记
  9. stop():直接暂停线程,释放所有锁,容易产生数据不一致问题,不建议使用(已过时方法)
二阶段停止线程

一般如果直接使用stop方法停止线程,会导致线程中一些例如io资源等无法正常释放,通过二阶段来停止线程,能够在线程停止前,进行一些后事处理,从而优雅的停止线程;

@Slf4j
public class TestTwoStage {
    public static void main(String[] args) throws InterruptedException {
        TwoPhraseMonitor twoPhraseMonitor = new TwoPhraseMonitor();
        twoPhraseMonitor.start();
        TimeUnit.SECONDS.sleep(1);
        twoPhraseMonitor.stop();
    }
}
@Slf4j
class TwoPhraseMonitor {
    private Thread monitor;
    /**
     * @Description 开启监控
     **/
    public void start() {
        monitor = new Thread(() -> {
            while (true) {
                Thread thread = Thread.currentThread();
                //被打断了 优雅关闭
                if (thread.isInterrupted()) {
                    log.info("处理关闭前的逻辑,优雅关闭线程");
                    break;
                }
                //休眠100毫秒
                try {
                    Thread.sleep(100);
                    log.info("处理监控逻辑");
                } catch (InterruptedException e) {
                    log.info("线程在休眠中被打断");
                    //线程被主动打断了 但是通过这个异常处理的打断 处理后会清除打断标记
                    thread.interrupt();
                }
            }
        },"监控线程");
        monitor.start();
    }

    public void stop() {
        if (monitor != null) {
            monitor.interrupt();
        }
    }

}
临界区和竞态条件

临界区 Critical Section

​ 一个程序运行多个线程本身是没有问题的,问题出在多个线程访问共享资源,多个线程读共享资源其实也没有问题,在多个线程对共享资源读写 *** 作时发生指令交错,就会出现问题;一段代码块内如果存在对共享资源的多线程读写 *** 作,称这段代码块为临界区

i++ 的字节码指令详解

getstatic 读取静态变量 i
iconst_1 准备常数1
iadd 执行加法
putstatic 写入静态变量i

public static int i = 0;
public static void add() {  
    //涉及对共享资源i的读写 *** 作 详情可参考以上字节码,从字节码看出涉及读写
    i++;
}

public static void dec() {
    i--;
}

竞态条件 Race Condition

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

代码分析

public class TestNoSyn {

    public static int i = 0;
    
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            for (int j = 0; j < 500000; j++) {
                //临界区
                add();
            }
        }, "线程1");

        Thread thread2 = new Thread(() -> {
            for (int j = 0; j < 500000; j++) {
                //临界区
                dec();
            }
        }, "线程2");

        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();

        System.out.println(i);
    }

    public static void add() {
      	//临界区代码
        i++;
    }

    public static void dec() {
        //临界区代码
        i--;
    }
}

从返回结果可以看出,创建两个线程来分别进行50万次进行++和– *** 作,结果几乎不可能出现0的情况;因为i++和i–在字节码指令级别,并不是原子 *** 作;如果其指令集的读写期间发生上下文切换,就发生了竞态条件;

欢迎分享,转载请注明来源:内存溢出

原文地址:https://www.54852.com/langs/924602.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2022-05-16
下一篇2022-05-16

发表评论

登录后才能评论

评论列表(0条)

    保存