Java并发编程(一)

1. 进程与线程

1.1 进程

  • 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的
  • 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
  • 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器
    等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)

1.2 线程。

  • 一个进程之内可以分为一到多个线程。
  • 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
  • Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作
    为线程的容器

1.3 二者对比

  • 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集

  • 进程拥有共享的资源,如内存空间等,供其内部的线程共享

  • 进程间通信较为复杂

    • 同一台计算机的进程通信称为 IPC(Inter-process communication)
    • 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
  • 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量

  • 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低

2. 并行与并发

单核 cpu 下,线程实际还是 串行执行 的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感觉是同时运行的 。总结为一句话就是: 微观串行,宏观并行
一般会将这种线程轮流使用 CPU 的做法称为并发(concurrent)
多核 cpu下,每个 核(core) 都可以调度运行线程,这时候线程可以是并行的。

引用 Rob Pike 的一段描述:

  • 并发(concurrent)是同一时间应对(dealing with)多件事情的能力
  • 并行(parallel)是同一时间动手做(doing)多件事情的能力
    例子
  • 家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做这多件事,这时就是并发
  • 家庭主妇雇了个保姆,她们一起这些事,这时既有并发,也有并行(这时会产生竞争,例如锅只有一口,一
    个人用锅时,另一个人就得等待)

3. 同步和异步

4. 创建和运行线程

4.1 直接使用 Thread


        Thread thread = new Thread() {
            @Override
            public void run() {
                //要执行的任务
                log.debug("Here is some DEBUG");
                log.info("Here is some INFO");
                log.warn("Here is some WARN");
                log.error("Here is some ERROR");
                log.fatal("Here is some FATAL");
            }
        };
        //线程启动
        thread.start();

12:28:39 [Thread-2] DEBUG com.it.Main -   Here is some DEBUG
12:28:39 [Thread-2] INFO  com.it.Main -   Here is some INFO
12:28:39 [Thread-2] WARN  com.it.Main -   Here is some WARN
12:28:39 [Thread-2] ERROR com.it.Main -   Here is some ERROR
12:28:39 [Thread-2] FATAL com.it.Main -   Here is some FATAL
//为线程指定的名字
        Thread thread = new Thread("T1") {
            @Override
            public void run() {
                //要执行的任务
                log.debug("Here is some DEBUG");
                log.info("Here is some INFO");
                log.warn("Here is some WARN");
                log.error("Here is some ERROR");
                log.fatal("Here is some FATAL");
            }
        };
        //线程启动
        thread.start();

4.2 使用 Runnable 配合 Thread

把【线程】和【任务】(要执行的代码)分开
Thread 代表线程
Runnable 可运行的任务(线程要执行的代码)

  Runnable runnable = new Runnable() {
            public void run() {
                //要执行的任务
                log.debug("Here is some DEBUG");
                log.info("Here is some INFO");
                log.warn("Here is some WARN");
                log.error("Here is some ERROR");
                log.fatal("Here is some FATAL");
            }
        };

        Thread thread = new Thread(runnable);
        thread.start();
 Runnable runnable = new Runnable() {
            public void run() {
                //要执行的任务
                log.debug("Here is some DEBUG");
                log.info("Here is some INFO");
                log.warn("Here is some WARN");
                log.error("Here is some ERROR");
                log.fatal("Here is some FATAL");
            }
        };

        Thread thread = new Thread(runnable,"T1");
        thread.start();

4.3 lambda

Runnable runnable = () -> {
            log.debug("Here is some DEBUG");
            log.info("Here is some INFO");
            log.warn("Here is some WARN");
            log.error("Here is some ERROR");
            log.fatal("Here is some FATAL");
        };
     new Thread(runnable, "T1").start();

Thread 与 Runnable 的关系


 new Thread(runnable, "T1").start();

class Thread implements Runnable {
}


如果Runnable方法存在,则执行Runnable中的方法,否则执行默认run方法

    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

4.4 FutureTask 配合 Thread

package com.it;

import org.apache.log4j.Logger;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * @author 
 * @date 2022/10/17 12:14
 * @remark
 */
public class Main {
    private static final Logger log = Logger.getLogger(Main.class);

    public static void main(String[] args) {
        FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {

            @Override
            public Integer call() throws Exception {
                log.debug("runing");
                Thread.sleep(2000);
                return 100;
            }
        });

        new Thread(task,"T1").start();

        //阻塞结果返回
        try{
            Integer integer = task.get();
            log.debug(integer);
        }catch (InterruptedException | ExecutionException e){
            System.out.println(e);
        }

    }

}

13:07:46:572 [T1] DEBUG com.it.Main -runing
13:07:48:578 [main] DEBUG com.it.Main -100

5. 常见方法

5.1 run和start对比

  • start()

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

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

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

5.2 sleep与yield对比

  • yield()

    • static
    • 功能说明:提示线程调度器让出当前线程对CPU的使用
    • 注意主要是为了测试和调试
  • sleep(long n)

    • static
    • 让当前执行的线程休眠n毫秒,休眠时让出 cpu 的时间片给其它线程
  • sleep

  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
  2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
  3. 睡眠结束后的线程未必会立刻得到执行
  4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
  • yield
  1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
  2. 具体的实现依赖于操作系统的任务调度器
    @Test
    public void test6()  {
        Thread thread = new Thread("T1"){
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                    //可能会被打断
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        //15:04:21:724 [main] DEBUG someTest -t1 state:RUNNABLE
        //子线程还没执行睡眠操作
        thread.start();
        log.debug("t1 state:"+thread.getState());

        //让主线程休眠
        try {
            Thread.sleep(500);
            //可能会被打断
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        log.debug("t1 state:"+thread.getState());
    }

    15:05:50:059 [main] DEBUG someTest -t1 state:RUNNABLE
    15:05:50:564 [main] DEBUG someTest -t1 state:TIMED_WAITING
 @Test
    public void testInterrupted() throws InterruptedException {

        Thread t1 = new Thread("T1"){
            @Override
            public void run() {
                try {
                    log.debug("t1 enter sleep");
                    Thread.sleep(2000);
                    //可能会被打断
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    log.debug("t1 wake up");
                }
            }
        };
        t1.start();
        Thread.sleep(1000);
        log.debug("t1 interrupt");
        //打断睡眠
        t1.interrupt();
    }
15:13:53:275 [T1] DEBUG someTest -t1 enter sleep
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at someTest$3.run(someTest.java:86)
15:13:54:275 [main] DEBUG someTest -t1 interrupt
15:13:54:276 [T1] DEBUG someTest -t1 wake up

sleep可读性

TimeUnit.SECONDS.sleep(1);

线程优先级

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

 t1.setPriority(Thread.MIN_PRIORITY);
 t2.setPriority(Thread.MAX_PRIORITY);

5.3 join

为什么需要 join

static int r = 0;
public static void main(String[] args) throws InterruptedException {
 test1();
}
private static void test1() throws InterruptedException {
 log.debug("开始");
 Thread t1 = new Thread(() -> {
 log.debug("开始");
 sleep(1);
 log.debug("结束");
 r = 10;
 });
 t1.start();
 log.debug("结果为:{}", r);
 log.debug("结束");
}
  • 分析
    • 因为主线程和线程 t1 是并行执行的,t1 线程需要 1 秒之后才能算出 r=10
    • 而主线程一开始就要打印 r 的结果,所以只能打印出 r=0
  • 解决方法
    • 用 sleep 行不行?为什么?肯定行,不知道要休眠多久
    • 用 join,加在 t1.start() 之后即可
等待一定时间
t1.join(1500);

5.4 interrupt()和isInterrupted()

sleep,wait,join 都是阻塞线程

  • isInterrupted() 不会清除 打断标记
  • interrupt() 如果被打断线程正在 sleep,wait,join 会导致被打断
    的线程抛出 InterruptedException,并清除 打断标记 ;如果打断的正在运行的线程,则会设置 打断标记 ;park 的线程被打断,也会设置 打断标记
5.4.1 打断 sleep 的线程
@Test
    public void test7() {
        Thread t1 = new Thread(() -> {
            try {
                log.debug("sleep");
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1");


        t1.start();
        log.debug(" 打断");
        t1.interrupt();
        log.debug(" 打断标记: {}" + t1.isInterrupted());
    }

java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at someTest.lambda$test7$0(someTest.java:108)
	at java.lang.Thread.run(Thread.java:748)
15:51:08:152 [t1] DEBUG someTest -sleep
15:51:08:152 [main] DEBUG someTest - 打断
15:51:08:152 [main] DEBUG someTest - 打断标记: {}false

Process finished with exit code 0

5.4.2 打断正常运行的线程

    @Test
    public void test8() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (true){
                boolean interrupted = Thread.currentThread().isInterrupted();
                if(interrupted){
                    log.info("死循环退出了");
                    break;
                }
            }
        }, "t1");

        t1.start();
        log.debug(" 打断");
        Thread.sleep(1000);
        t1.interrupt();
        log.debug(" 打断标记: {}" + t1.isInterrupted());
    }
15:54:23:175 [main] DEBUG someTest - 打断
15:54:23:176 [main] DEBUG someTest - 打断标记: {}true
终止模式之两阶段终止模式:Two Phase Termination

目标:在一个线程 T1 中如何优雅终止线程 T2
错误思想:

  • 使用线程对象的 stop() 方法停止线程:stop 方法会真正杀死线程,如果这时线程锁住了共享资源,当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁
  • 使用 System.exit(int) 方法停止线程:目的仅是停止一个线程,但这种做法会让整个程序都停止
public class Test {
    public static void main(String[] args) throws InterruptedException {
        TwoPhaseTermination tpt = new TwoPhaseTermination();
        tpt.start();
        Thread.sleep(3500);
        tpt.stop();
    }
}
class TwoPhaseTermination {
    private Thread monitor;
    // 启动监控线程
    public void start() {
        monitor = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    Thread thread = Thread.currentThread();
                    if (thread.isInterrupted()) {
                        System.out.println("后置处理");
                        break;
                    }
                    try {
                        Thread.sleep(1000);					// 睡眠
                        System.out.println("执行监控记录");	// 在此被打断不会异常
                    } catch (InterruptedException e) {		// 在睡眠期间被打断,进入异常处理的逻辑
                        e.printStackTrace();
                        // 重新设置打断标记,打断 sleep 会清除打断状态
                        thread.interrupt();
                    }
                }
            }
        });
        monitor.start();
    }
    // 停止监控线程
    public void stop() {
        monitor.interrupt();
    }
}
不推荐方法

不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁:

  • public final void stop():停止线程运行
    废弃原因:方法粗暴,除非可能执行 finally 代码块以及释放 synchronized 外,线程将直接被终止,如果线程持有 JUC 的互斥锁可能导致锁来不及释放,造成其他线程永远等待的局面

  • public final void suspend():挂起(暂停)线程运行

    废弃原因:如果目标线程在暂停时对系统资源持有锁,则在目标线程恢复之前没有线程可以访问该资源,如果恢复目标线程的线程在调用 resume 之前会尝试访问此共享资源,则会导致死锁

  • public final void resume():恢复线程运行

守护线程

默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。

log.debug("开始运行...");
Thread t1 = new Thread(() -> {
 log.debug("开始运行...");
 sleep(2);
 log.debug("运行结束...");
}, "daemon");
// 设置该线程为守护线程
t1.setDaemon(true);
t1.start();
sleep(1);
log.debug("运行结束...");

注意

  • 垃圾回收器线程就是一种守护线程
  • Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求
五种状态

在这里插入图片描述

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

从 Java API 层面来描述的,根据 Thread.State 枚举,分为六种状态

  1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
  2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
    线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
  3. 阻塞(BLOCKED):表示线程阻塞于锁。
  4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
  5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
  6. 终止(TERMINATED):表示该线程已经执行完毕。
    在这里插入图片描述
  • NEW 线程刚被创建,但是还没有调用 start() 方法
  • RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面RUNNABLE 状态涵盖了 操作系统 层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
  • BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节详述
  • TERMINATED 当线程代码运行结束
泡茶统筹规划
package com.it;

import org.apache.log4j.Logger;

/**
 * @author 
 * @date 2022/10/17 16:44
 * @remark
 */
public class TestState {

    private static final Logger log = Logger.getLogger(TestState.class);

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

        Thread t2 = new Thread("t2") {
            @Override
            public void run() {
                while (true) {
                }
            }
        };
        t2.start();


        Thread t3 = new Thread("t3") {
            @Override
            public void run() {
                log.debug("running...");
            }
        };
        t3.start();


        Thread t4 = new Thread("t4") {
            @Override
            public void run() {
                synchronized(TestState.class){
                    try {
                        Thread.sleep(1000000000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t4.start();


        //waiting
        Thread t5 = new Thread("t5") {
            @Override
            public void run() {
                try {
                    t2.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        t5.start();

        //拿不到锁 block
        Thread t6 = new Thread("t6") {
            @Override
            public void run() {
                synchronized(TestState.class){
                }
            }
        };
        t6.start();


        Thread.sleep(500);

        log.debug("t1 state "+t1.getState());
        log.debug("t2 state "+t2.getState());
        log.debug("t3 state "+t3.getState());
        log.debug("t4 state "+t4.getState());
        log.debug("t5 state "+t5.getState());
        log.debug("t6 state "+t6.getState());
    }
}

17:05:40:796 [main] DEBUG com.it.TestState -t1 state NEW
17:05:40:796 [main] DEBUG com.it.TestState -t2 state RUNNABLE
17:05:40:796 [main] DEBUG com.it.TestState -t3 state TERMINATED
17:05:40:796 [main] DEBUG com.it.TestState -t4 state TIMED_WAITING
17:05:40:796 [main] DEBUG com.it.TestState -t5 state WAITING
17:05:40:796 [main] DEBUG com.it.TestState -t6 state BLOCKED
package com.it;

import org.apache.log4j.Logger;

import java.util.concurrent.TimeUnit;

/**
 * @author 
 * @date 2022/10/17 17:16
 * @remark
 */
public class Tea {

    private static final Logger log = Logger.getLogger(Tea.class);

    public static void main(String[] args) {

        Thread t1 = new Thread("老王") {
            @Override
            public void run() {
                log.debug("洗水壶");
                Sleeper.sleepMin(1);
                log.debug("烧开水");
                Sleeper.sleepMin(5);
            }
        };

        t1.start();

        Thread t2 = new Thread("小李") {
            @Override
            public void run() {
                log.debug("洗茶壶");
                Sleeper.sleepMin(1);
                log.debug("洗茶杯");
                Sleeper.sleepMin(2);
                log.debug("拿茶叶");
                Sleeper.sleepMin(4);
                try {
                    t1.join();
                    log.debug("泡茶");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        t2.start();
        
    }


}

17:24:02:225 [小李] DEBUG com.it.Tea -洗茶壶
17:24:02:225 [老王] DEBUG com.it.Tea -洗水壶
17:24:03:228 [老王] DEBUG com.it.Tea -烧开水
17:24:03:228 [小李] DEBUG com.it.Tea -洗茶杯
17:24:05:241 [小李] DEBUG com.it.Tea -拿茶叶
17:24:09:245 [小李] DEBUG com.it.Tea -泡茶

思考:

  • 上面是小李等待老王的水烧开了,如果反过来,老王等小李的茶叶拿来,什么代码能适应两种情况
  • 上面线程其实是各执行各的,数据之间没有共享
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值