【JUC】一、Java并发编程基础知识

JUC基础前置知识

进程与线程

进程

  • 程序由指令和数据组成,程序要执行就必须将指令加载到CPU,将数据加载至内存。同时在程序的运行过程中还需要用到磁盘、网络等设备,进程就是用来加载指令、管理内存、管理IO的。

  • 程序被执行时,程序会将代码加载至内存,这时就开启了一个进程

  • 进程可以视为是程序的一个实例。大部分程序可以同时运行多个实例(记事本、画图、浏览器等),但也有一部分程序只能运行一个实例(安全卫士、网易云音乐等)

线程

  • 一个线程就是一个指令流、将指令流中的一条条指令交由CPU执行
  • 线程是最小的调度单位,在Windows中、进程是不活动的,只单单作为线程的容器

并行与并发

在并发场景下线程还是串行执行的,操作系统中的任务调度器将CPU的时间片分给不同的线程使用(并发(cocurrent))

同步和异步

  • 方法的调用者需要等待结果返回(同步)
  • 方法的调用者不需要等待结果返回(异步)

小例子的举例:

在单核的场景下、多线程不会对性能提升有帮助、反而会因为线程的上下文切换影响程序的执行效率

在多核的场景下、多线程的使用就会对程序整体性能的提升有很大的帮助了()

注意:在IO以及网络请求的环节中,不会占用线程,但有可能发生阻塞(阻塞IO)从而影响效率

线程的创建方式

继承Thread类

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

        log.debug("running");
    }
}

实现Runnable接口

实现Runnable接口的方式实现了线程的定义与功能实现的解耦

@Slf4j(topic = "c.Test2")
public class Test2 {
    public static void main(String[] args) {
        Runnable r = new Runnable() {
            @Override
            public void run() {
                log.debug("running");
            }
        };
        Thread t = new Thread(r, "t2");
        t.run();
    }
}

  • 使用Runnable更容易于线程池中的高级API相配合
  • 使用Runnable可以让任务类脱离Thread体系,使其更加灵活

使用Lambda表达式简化只有一个接口的类方法的实现

被@FunctionalInterface注解标识的类是一个接口且其只有一个抽象方法,这种方法可以使用Lambda表达式进行简化。

@Slf4j(topic = "c.Test2")
public class Test2 {
    public static void main(String[] args) {

        // 使用Lambda表达式简化程序代码
        Runnable r = () -> {
                log.debug("running");
        };

        Thread t = new Thread(r, "t2");
        t.run();
    }
}

更简洁的写法:

@Slf4j(topic = "c.Test2")
public class Test2 {
    public static void main(String[] args) {

        // 直接在Thread下进行调用
        Thread t = new Thread(() -> {log.debug("running");}, "t2");
        t.run();
    }
}

使用FutureTask配合Thread接收Callable

Callable接口类似于Runnable、但Callable接口可以抛出异常,也可以通过.get()方法获取返回值

@Slf4j(topic = "c.Test2")
public class Test3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> task = new FutureTask(new Callable<Integer>() {
            // 在调用.start方法之后会执行下面的语句
            @Override
            public Integer call() throws Exception {
                log.debug("running.......");
                // 在此阻塞
                Thread.sleep(1000);
                return 1000;
            }
        });
        Thread t1 = new Thread(task, "t1");
        t1.start 
        // .get方法可以在callable接口中得到call方法的返回值,但其是阻塞的,主线程只有在得到返回值之后才会继续执行
        log.debug("{}", task.get());
    }
}

进程、线程的查看

Java可以使用使用jps来查看所有的Java进程

使用taskkill /F /PID 39228来杀死一个进程

在Linux下,可以使用ps -fe | grep java来查看Java进程

或直接使用jps进行查看

top -H -p 4262 这种形式的进程查看命令可以动态的查看Java的命令

jstack 4262 这种形式的命令会抓取对应Java程序在那一时刻的快照并显示所有线程的详细信息

使用jconsole在windows的控制台直接运行这个指令(不进黑框)进行连接就可以看见线程的相关信息:

记得要在Linux下优先配置以下信息(注意换行):

java -Djava.rmi.server.hostname=`ip地址` -Dcom.sun.management.jmxremote -
Dcom.sun.management.jmxremote.port=`连接端口` -Dcom.sun.management.jmxremote.ssl=是否安全连接 -
Dcom.sun.management.jmxremote.authenticate=是否认证 java类

java -Djava.rmi.server.hostname=192.168.202.130 -
Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=12345 -
Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false LinuxTest

线程的运行原理

每个线程启动时,虚拟机就会为其分配一块栈的内存,每个栈由多个栈帧组成,对应每次方法调用时占用的内存,每个线程也有一个栈,每一个栈中有一个个的栈帧,每个栈帧也对应着现在活动的方法。

栈帧中:局部变量表(存储局部变量、形参等)、返回地址、锁记录、操作数栈

注意如果存在多个线程、则线程之间会有独立的栈,其中又会有自己的栈帧,互不影响

线程上下文切换

  • CPU时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程中调用了sleep、yield、wait、join、park、synchronized、lock等方法时

Thread中的常见方法

start()方法:启动一个线程,并运行run方法(使线程就绪、具体什么时候开始运行要看操作系统的调度)

run()方法:在线程被启动后调用的方法

join()方法:等待线程运行结束

join(long n)方法:等待线程结束、且最多等待n毫秒

getId():获取线程的唯一Id

getName():获取线程名

setName():设置线程名

getPriority():获取线程的优先级

setPriority():设置线程的优先级:线程的优先级是1-10之间的整数,数字越大,优先级越高

setState():获取线程状态:Java中的线程是由6个enum表示的,分别为:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED

isInterrupted():判断是否被打断:若线程被打断的话,会有一个打断标记,这个方法不会清除打断标记

isAlive():判断线程是否存活(未执行完毕)

interrupt():打断线程,若被打断的线程正在sleep、wait、join则会导致被打断的线程抛出InterruptedException并清除打断标记,若打断正在运行的线程,则会设置打断标记,若park线程被打断,也会设置打断标记

cuurentThread():获取当前正在执行的线程

sleep(long n):让当前正在执行的线程休眠n(ms),将时间片让给其他线程使用

yield():提示线程调度器让出当前对于CPU的使用

Interrupt

若Interrupt方法打断了正常运行的线程,则可以通过isInterrupt()方法获取打断标识为true、若打断的是wait、sleep、join时的方法,则不会将打断标识置为true。

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

由于有些线程是不断的进行循环检测状态的类型,这些线程处于 检测、休眠、检测、休眠…的反复循环中,若在检测时被打断、则一切正常,但若在休眠时被打断,不会将interrupted标记设置为true、故我们会将休眠时的打断设置一个异常,在异常中将打断标记设置为true。

两阶段终止模式的示例:

import lombok.extern.slf4j.Slf4j;

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

@Slf4j(topic = "c.TwoPhaseTermination")
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);
                    log.debug("正在监控执行记录");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    current.interrupt();
                }
            }
        });

        monitor.start();
    }

    public void stop() {
        monitor.interrupt();
    }

}

.另外,除了isInterrupted方法可以判断线程是否被打断以外,interrupted方法也可以判断是否被打断,其区别在于

  • isInterrupted()方法只会获取到打断标记·
  • interrupted()方法在获取到打断标记之后会将打断标记设置为false

park

park不是Thread中的方法,它是LockSupport类中的一个方法,若打断标记为false,则这个方法会将当前线程阻塞,若打断标记为true,则不会生效。

守护线程

默认情况下,Java的进程只有在所有的线程都结束之后才会结束。但有一种特殊的线程叫做守护线程,只要其他非守护线程结束了,即使守护线程没有执行完,其也会强制结束。

t1.setDaemon(true) 这种操作就可以将t1线程设置为守护线程

  • 垃圾回收机制就是一种守护线程
  • Tomcat中的Acceptor(调用)和Poller(。。。)都是守护线程,所以在Tomcat接收到shutdown指令之后,不会等待他们处理完当前请求就直接关闭。

你可能感兴趣的:(JUC,java,开发语言)