并发编程基础

进程:进程可以看作是程序的实例,大部分程序可以同时运行多个实例进程 比如记事本,画图,浏览器,也有些只能启动一个进程实例 比如 电脑管家,360等。

线程:一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行,线程是存在于进程内的,一个进程内可以有一到多个线程;

并行:同一时间同时做多件事情,称为并行;

并发:同一时刻,线程轮流使用CPU的做法,同一时间应对多件事情,称为并发;

一,创建线程的方法:

1.直接new Thread(){}; 通过匿名内部类实现;

@Slf4j
public class ThreadExample1 {
    public static void main(String[] args) {
        Thread t = new Thread(){
            @Override
            public void run() {
                log.info("thread running");
            }
        };
        // 启动线程
        t.start();
    }
}

2.把线程和任务分开:Runnable放线程执行的代码;

@Slf4j
public class ThreadExample2 {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                log.info("runnable running");
            }
        };
        // 开启线程
        new Thread(runnable).start();
        // lambda 简化 Runnable
        Runnable runnable2 = () -> log.info("runnable running");
         new Thread(() ->log.info("runnable running")).start();
    }
}

 3.FutureTask 配合Thread;FutureTask 提供了返回值;取返回值会阻塞等待

@Slf4j
public class ThreadExample3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 参数为callable接口
        FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                log.info("task is running");
                return 10;
            }
        });
        // 开启线程
        new Thread(task).start();
        // 可获取的返回结果
        log.info("return value:{}",task.get());
    }
}

二,查看进程线程的方法:

windows: 通过任务管理器 查看pid,

                   tasklist查看进程,

                   通过taskkill /F /PID pid号  杀死进程

Linux:ps-fe 查看所有进程;

           ps -fe | grep java(java为查找的关键字)查看有关键字的进程; 

           ps -fT -p <PID> 查看某个进程(PID)的所以线程; 

           kill 杀死进程 ;

           top 按大写H切换是否显示线程; 

           top -H -p <PID> 查看某个进程(PID)的所有 线程

Java:    jps查看所有java进程;

           jstack <PID> 查看某个java进程(PID)的所有线程状态;

           jconsole 查看某个java进程中某个线程的运行情况(图形界面)

三,栈帧:栈内存是给线程使用,每个线程启动后,虚拟机就会为其分配一块栈内存;每个栈由多个栈帧(Frames)组成,                     对应着每次方法调用时所占用的内存;每个线程只能有一个活动栈帧,对应当前正在执行的方法;按照先进后                     出的顺序,生命周期随着方法的调用完毕而结束;

四,上下文切换:多个线程在执行时,会有一些一下原因导致cpu不再执行当前的线程转而执行另一个线程的代码;

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

                  当上下文切换时(Context Switch),需要由操作系统保存当前线程的状态,并恢复另外一个线程的状态,java                      中对应概念就是程序计数器,它纪录着线程执行代码行号的地址指令,且它是线程私有的;频繁切换上下问会                    影响性能;

                  线程状态包括计数器,虚拟机中中每个栈帧信息,如局部变量,操作数栈,返回地址等。

五,一些常用方法:

sleep:
1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出  InterruptedException
3. 睡眠结束后的线程未必会立刻得到执行
4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
yield:
1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
2. 具体的实现依赖于操作系统的任务调度器

intrerept:

上面方法说了,打断(阻塞)sleep,wait,join会抛异常,并且把打断标记清空,也就是interrupted的值为false;

在正常打断的情况下则不会抛异常,且打断标记不会清空,但是线程不会停下来,需要手动停止;

@Slf4j
public class ThreadExample5 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (true) {
                // 判断线程是否被打断
                boolean interrupted = Thread.currentThread().isInterrupted();
                if (interrupted) {
                    log.info("t1线程被打断,退出循环");
                    break;
                }
            }
        }, "t1");
        t1.start();
        Thread.sleep(1000);
        log.info("main休眠结束,打断线程");
        t1.interrupt();
    }
}

六,设计模式-两阶段终止

使用stop()方法停止线程的弊端:stop方法会真正杀死线程,如果这时线程锁住了共享资源,那么当他被杀死后就没有机会释放锁,导致其他线程无法获取锁;

/**
 * 两阶段终止模式 演示
 */
@Slf4j
public class ThreadExample6 {
    public static void main(String[] args) throws InterruptedException {
        TwoPhaseTermination termination = new TwoPhaseTermination();
        termination.strat();
        // 主线程睡眠两秒
        Thread.sleep(3000);
        // 休眠结束后 手动停止线程
        termination.stop();
    }
}
@Slf4j
class TwoPhaseTermination{
    private Thread thread;

    // 启动监控线程
    public void strat(){
        thread=new Thread(()->{
            Thread innertThread = Thread.currentThread();
            while (true){
                // 是否被打断
                if (innertThread.isInterrupted()){
                    log.info("线程被打断,退出监控-记录日志");
                    break;
                }
                try {
                    Thread.sleep(1000);
                    log.info("正常记录日志操作");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    // 当阻塞时被打断,打断状态会被清空,重新打断
                    innertThread.interrupt();
                }
            }
        });
        thread.start();
    }
    // 手动 停止监控线程
    public void stop(){
        thread.interrupt();
    }
}

 七:LockSupport.park()方法:会让当前线程停下来,只有打断标记为false的时候才生效;

/**
 * park打断
 */
@Slf4j
public class ThreadExample7 {
    public static void main(String[] args) throws InterruptedException {
        test();
    }

    public static void test() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            log.info("park..");
            // park阻塞
            LockSupport.park();
            log.info("uppack..");
            log.info("打断状态:{}", Thread.currentThread().isInterrupted());
        }, "t1");
        t1.start();

        Thread.sleep(2000);
        // 打断线程,将打断标记设为true,并影响park
        t1.interrupt();
    }
}

在调用了LockSupport.park()方法后,线程就不会往下执行了(阻塞),上面代码中主线程2秒后使用interrupt将线程打断,并将打断标记设置为true后,线程才得以继续往下运行;一旦打断标记为true后,再调用LockSupport.park()方法也不会起作用;之前说过interrupt打断sleep,join,wait会抛出异常,但是这个park不会;

Thread.interrupted()方法使用后,会清除打断标记,也就是设为false;

@Slf4j
public class ThreadExample7 {
    public static void main(String[] args) throws InterruptedException {
        test();
    }
    public static void test() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            log.info("park..");
            // park阻塞
            LockSupport.park();
            log.info("uppack..");
            // 查看打断状态,并清除打断标记
            log.info("打断状态:{}", Thread.interrupted());
            log.info("二次park..");
            LockSupport.park();
             log.info("二次park后...");
        }, "t1");
        t1.start();
        Thread.sleep(2000);
        // 打断线程,将打断标记设为true,并影响park
        t1.interrupt();
    }
}

此时就是处于这样一种阻塞状态;

八;不推荐使用的过时方法;这些方法容易造成线程死锁;

stop():停止线程运行;可以用两阶段终止代替;

supend():暂停线程运行

resume():恢复线程运行

九:守护线程,Java进程需要等待所有线程运行结束后,才会结束;而守护线程,只有其他的非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束;

setDaemon(true)则是设置为守护线程的方法;

@Slf4j
public class ThreadExample8 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
          log.info("thread start...");
          while (true){
              if (Thread.currentThread().isInterrupted())
                  break;
          }
          log.info("thread end...");
        }, "t1");
        t1.start();
        Thread.sleep(1000);
        log.info("main end..");
    }
}

 可以看到t1线程一直在循环,main线程结束,t1还没有结束,导致java进程一直在等待线程结束;

现在我们给t1线程设置为守护线程,那么在main线程运行结束后,t1线程也会被强制结束;

main线程执行1秒后结束,守护线程也结束了

常见的应用场景:垃圾回收器线程也是一种守护线程;

                    Tomcat中的Acceptor和Poller线程都是守护线程,在tomcat使用shutdown时,会直接带着他们一起结束;  

十:线程状态

①,操作系统层面:分为五种状态

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

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

NEW,线程刚被创建,但还未调用start()放

RUNNABLE,当调用了start()方法后,Java Api 层面的RUNNABLE涵盖了操作系统层面的【可运行状态】,【运行状态】,【阻塞状态】(BIO导致的线程阻塞,java里无法区分,也是可运行状态)

BOLCK,WAITING,TINE_WAITING都是Java Api层面对阻塞状态的细分

TERMINATED,线程代码运行结束

下面演示一下六种状态:

/**
 * 6种线程状态
 */
@Slf4j
public class ThreadExample9 {
    public static void main(String[] args) throws InterruptedException {
        // 演示 NEW
        Thread t1 = new Thread(() -> {
        }, "t1");

        //演示 RUNNABLE
        Thread t2 = new Thread(() -> {
            while (true) {
            }
        }, "t2");
        t2.start();
        
        //演示 TIMED_WAITING
        Thread t5 = new Thread(() -> {
            synchronized (ThreadExample9.class) {
                try {
                    // 休眠1000秒
                    TimeUnit.SECONDS.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t5");
        t5.start();

        //演示 BLOCKED
        Thread t3 = new Thread(() -> {
            synchronized (ThreadExample9.class) {
            }
        }, "t3");
        t3.start();

        //演示 WAITING
        Thread t4 = new Thread(() -> {
            try {
                t2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t4");
        t4.start();

        //演示 TERMINATED
        Thread t6 = new Thread(() -> {
        }, "t6");
        t6.start();

        Thread.sleep(1000);
        log.info("t1-State:{}", t1.getState());
        log.info("t2-State:{}", t2.getState());
        log.info("t3-State:{}", t3.getState());
        log.info("t4-State:{}", t4.getState());
        log.info("t5-State:{}", t5.getState());
        log.info("t6-State:{}", t6.getState());
    }
}

 可以看到结果,现在逐一分析一下:

t1线程因为没有调用start()方法,所以是NEW状态;

t2线程属于while true 无线循环,不会结束线程,所以RUNNABL状态

t3线程使用了synchronized对类加锁,作用于这个类内的所有对象,在t3想获取锁前,t5已经获取锁了,而t5休眠1000秒,所以t3需要等待t5把锁释放,所以此时t3的状态是BLOCKED状态

t4线程使用了t2.join()方法,会阻塞,需要等待t2运行结束才能够往下执行,而t2是无线循环则不会结束,所以t4是WAITING状态,这个状态代表等待无时间限制

t5线程休眠了1000秒了,是有时间限制的,所=所以是TIMED_WAITING状态

t6线程则是在main线程休眠结束之前已经执行完毕了,结束了,所以是TERMINATED终止状态

 

posted @   超级大菜鸡  阅读(27)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示