并发编程(学习笔记-Java线程)-part2

Posted LL.LEBRON

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并发编程(学习笔记-Java线程)-part2相关的知识,希望对你有一定的参考价值。

并发编程-Java线程-2

本文章视频指路👉黑马程序员-并发编程

1.创建和运行线程

1-1 方法一:直接使用Thread

// 创建线程对象
Thread t = new Thread() {
    public void run() {
     // 要执行的任务
    }
};
// 启动线程
t.start();

例如:

// 构造方法的参数是给线程指定名字,推荐
Thread t1 = new Thread("t1") {
    @Override
    // run 方法内实现了要执行的任务
    public void run() {
        log.debug("hello");
    }
};
t1.start();

1-2 方法二:使用Runnable配合Thread

把【线程】和【任务】(要执行的代码)分开

  • Thread代表线程

  • Runnable可运行的任务(线程要执行的代码)

    public class Test2 {
        public static void main(String[] args) {
            //创建线程,将Runnable对象传入进来
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("Run");
                }
            }, "xpp");
            t.start();
        }
    }
    

    java8以后可以使用lambda精简代码

    public class Test2 {
        public static void main(String[] args) {
            Thread t = new Thread(() -> System.out.println("Run"), "xpp");
            t.start();
        }
    }
    

原理之Thread与Runnable的关系:

分析Thread的源码,理清它与Runnable的关系

  • 方法1是把线程和任务合并在一起,方法2是把线程和任务分开了(推荐方法2)。
  • 用Runnable更容易与线程池等高级API配合。
  • 用Runnable让任务类脱离了Thread继承体系,更灵活。

1-3 方法三:FutureTask配合Thread

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

public class Test3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建任务对象,并传入一个Callable对象
        FutureTask<Integer> task=new FutureTask<>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                System.out.println("running...");
                Thread.sleep(1000);
                return 100;
            }
        });
        Thread t1 = new Thread(task, "t1");
        t1.start();
        //获取线程中方法执行后返回的结果
        System.out.println(task.get());//get方法会一直等待task完成,才会得到结果
        
    }
}

2.观察多个线程同时运行

@Slf4j
public class TestMultiThread {
    public static void main(String[] args) {
        new Thread(() -> {
            while (true) {
                log.debug("running");
            }
        }, "t1").start();
        new Thread(() -> {
            while (true) {
                log.debug("running");
            }
        }, "t2").start();
    }
}

得出的结果:

  • 交替执行
  • 谁先谁后,不由我们控制

3.查看进程线程的方法

windows

  • 任务管理器可以直接查看进程和线程数,也可以用来杀死进程。

  • tasklist查看进程(通过cmd)。

  • taskkill杀死进程。

linux

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

Java

  • jps命令查看所有Java进程。
  • jstack <PID>查看某个Java进程(PID)的所有线程状态。
  • jconsole来查看某个Java线程中线程的运行情况(图形界面)。

4.原理之线程运行

栈与栈帧

Java Virtual Machine Stacks(Java 虚拟机栈)

我们都知道JVM中由堆,栈,方法区所组成,其中栈内存是给谁用呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。

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

线程上下文切换(Thread Context Switch)

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

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

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

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

5.常用方法

方法名static功能说明注意
start()启动一个新线程,在新的线程运行run方法中的代码start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的 start方法只能调用一次,如果调用了多次会出现 IllegalThreadStateException
run()新线程启动后会调用的方法如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象, 来覆盖默认行为
join()等线程运行结束
join(long n)等待线程运行结束,最多等待n毫秒
getld()获取线程长整型的idid唯一
getName()获取线程名
setName(String)修改线程名
getPriority()获取线程优先级
setPriority(int)修改线程优先级java中规定线程优先级是1~10 的整数,较大的优先级能提高该线程被 CPU 调度的机率
getState()获取线程状态Java 中线程状态是用 6 个 enum 表示,分别为: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
isInterrupted()判断是否被打断不会清除 打断标记
isAlive()线程是否存活(还没有运行完毕)
interrupt()打断线程如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除打断标记 ;如果打断的正在运行的线程,则会设置打断标记 ;park 的线程被打断,也会设置打断标记
interrupted()static判断当前线程是否被打断会清除打断标记
currentThread()static获取当前正在执行的线程
sleep(long n)static让当前执行的线程休眠n毫秒,休眠时让出cpu的时间片给其它线程
yield()static提示线程调度器让出当前线程对cpu的使用主要是为了测试和调试

5-1 start与run

start

  1. 启动一个新线程,在新的线程运行run方法中的代码。
  2. start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的 start方法只能调用一次,如果调用了多次会出现 IllegalThreadStateException

run

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

调用run

程序还是在main线程执行,还是同步执行的

@Slf4j
public class Test4 {
    public static void main(String[] args) {
        Thread t1 = new Thread("t1") {
            @Override
            public void run() {
                log.debug("running...");
            }
        };
        t1.run();
        log.debug("running");
    }
}

输出

12:17:47 DEBUG [main] (Test4.java:11) - running...
12:17:47 DEBUG [main] (Test4.java:15) - runnin

调用start

此时是两个线程,异步执行

@Slf4j
public class Test4 {
    public static void main(String[] args) {
        Thread t1 = new Thread("t1") {
            @Override
            public void run() {
                log.debug("running...");
            }
        };
        t1.start();
        log.debug("running");
    }
}

输出

12:14:15 DEBUG [main] (Test4.java:15) - running
12:14:15 DEBUG [t1] (Test4.java:11) - running...

小结

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

5-2 sleep与yield

sleep

  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态,可通过state()方法查看

  2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException

  3. 睡眠结束后的线程未必会立刻得到执行

  4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性 。

    //休眠一秒
    TimeUnit.SECONDS.sleep(1);
    //休眠一分钟
    TimeUnit.MINUTES.sleep(1);
    

yield

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

线程优先级

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

举个栗子:

@Slf4j
public class Test9 {
    public static void main(String[] args) throws InterruptedException {
        Runnable task1 = () -> {
            int count = 0;
            for (; ; ) {
                System.out.println("--->1  " + count++);
            }
        };
        Runnable task2 = () -> {
            int count = 0;
            for (; ; ) {
                Thread.yield();
                System.out.println("  --->2  " + count++);
            }
        };
        Thread t1 = new Thread(task1, "t1");
        Thread t2 = new Thread(task2, "t2");
		//t1.setPriority(Thread.MAX_PRIORITY);
	    //t2.setPriority(Thread.MIN_PRIORITY);
        t1.start();
        t2.start();

    }
}

可以看出yield()起到了作用,t2进入Runnable状态,开启设置优先级后结果也类似,需要注意的是,最终的结果无论是哪种方式,都是由调度器决定最后时间片的分配

5-3 join方法详解

join

用于等待某个线程结束。哪个线程内调用join()方法,就等待哪个线程结束,然后再去执行其他线程。

为什么需要join

@Slf4j
public class Test10 {
    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(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("结果");
            r = 10;
        });
        t1.start();
        //t1.join();
        log.debug("结果为:{}", r);
        log.debug("结束");
    }
}
13:36:47 DEBUG [main] (Test10.java:18) - 开始
13:36:47 DEBUG [Thread-0] (Test10.java:20) - 开始
13:36:47 DEBUG [main] (Test10.java:30) - 结果为:0
13:36:47 DEBUG [main] (Test10.java:31) - 结束
13:36:48 DEBUG [Thread-0] (Test10.java:26) - 结束

分析

  • 因为主线程和线程t1是并行执行的,t1线程需要1秒之后才能算出r=10
  • 而主线程一开始就要打印r的结果,所以只能打印出r=0

解决方法

  • join,加在t1.start()之后即可。

应用之同步:

以调用的角度来讲,如果

  • 需要等待结果返回,才能继续运行就是同步
  • 不需要等待结果返回,就能继续运行就是异步

5-4 interrupt方法详解

用于打断阻塞(sleep wait join…)的线程。 处于阻塞状态的线程,CPU不会给其分配时间片。

  • 如果一个线程在在运行中被打断,打断标记会被置为true
  • 如果是打断因sleep wait join方法而被阻塞的线程,会将打断标记置为false

打断sleep,wait,join的线程

这里以sleep为例:

@Slf4j
public class Test11 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            log.debug("sleep...");
            try {
                Thread.sleep(5000);//wait,join
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1");
        t1.start();
        Thread.sleep(1000);
        log.debug("interrupt");
        t1.interrupt();
        log.debug("打断标记:{}", t1.isInterrupted());//false
    }
}

打断正常运行的线程

打断正常运行的线程,不会清空打断状态

@Slf4j
public class Test12 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (true) {
                boolean interrupted = Thread.currentThread().isInterrupted();
                if (interrupted) {
                    log.debug("退出循环!");
                    break;
                }
            }
        }, "t1");
        t1.start();
        Thread.sleep(1000);
        log.debug("interrupt");
        t1.interrupt();
        log.debug("打断标记:{}", t1.isInterrupted());//true
    }
}
18:51:03 DEBUG [main] (Test12.java:20) - interrupt
18:51:03 DEBUG [t1] (Test12.java:13) - 推出循环!
18:51:03 DEBUG [main] (Test12.java:22) - 打断标记:true

模式之两阶段终止:

代码实现:

@Slf4j
public class Test23 {
    public static void main(String[] args) throws InterruptedException {
        TwoPhaseTermination tpt = new TwoPhaseTermination();
        tpt.start();
        Thread.sleep(3500);
        tpt.stop();
    }
}

@Slf4j
class TwoPhaseTermination {
    private Thread monitor;//设置监控线程

    //启动监控线程
    public void start() {
        monitor = new Thread(() -> {
            while (true) {
                Thread current = Thread.currentThread();
                if (current.isInterrupted()) {
                    log.debug("料理后事");
                    //终止线程执行
                    break;
                }
                try {
                    Thread.sleep(1000); //情况1,sleep打断,打断标记为false
                    log.debug("执行监控记录");  //情况2,正常打断,打断标记为true
                } catch (InterruptedException e) { 以上是关于并发编程(学习笔记-Java线程)-part2的主要内容,如果未能解决你的问题,请参考以下文章

《Java并发编程实战》学习笔记

Java并发编程学习笔记

JAVA并发编程学习笔记------线程的三种创建方式

Java学习笔记—多线程(同步容器和并发容器)

Java多线程编程(学习笔记)

多线程编程学习笔记——使用并发集合