Bootstrap

并开发笔记(未完)

并发编程

一、线程和进程

进程:是一个应用程序

线程:是一个进程中的执行场景/执行单元

注:一个进程中可以有多个线程

二者对比

  • 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
  • 进程拥有共享的资源,如内存空间等,供其内部的线程共享
  • 进程间通信较为复杂
    • 同一台计算机的进程通信成为IPC
    • 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如HTTP
  • 线程间的通信比较简单,因为他们共享进程的内存,一个例子是多个线程可以访问一个共享变量
  • 线程更轻量,线程上下文切换成本一般比进程上下文切换低

Java 给多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。

这里定义和线程相关的另一个术语 - 进程:一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。

多线程能满足程序员编写高效率的程序来达到充分利用 CPU 的目的。

1.1并行和并发

1.1.1并发:

早期计算机的 CPU 都是单核的,一个 CPU 在同一时间只能执行一个进程/线程,当系统中有多个进程/线程等待执行时,CPU 只能执行完一个再执行下一个。

为了表述简单,下文我们将不再区分进程和线程,而将它们统一称为“任务”。在不同的场景下,一个任务可能是进程,也可能是线程。

计算机在运行过程中,有很多指令会涉及 I/O 操作,而 I/O 操作又是相当耗时的,速度远远低于 CPU,这导致 CPU 经常处于空闲状态,只能等待 I/O 操作完成后才能继续执行后面的指令。

为了提高 CPU 利用率,减少等待时间,人们提出了一种 CPU 并发工作的理论。

所谓并发,就是通过一种算法将 CPU 资源合理地分配给多个任务,当一个任务执行 I/O 操作时,CPU 可以转而执行其它的任务,等到 I/O 操作完成以后,或者新的任务遇到 I/O 操作时,CPU 再回到原来的任务继续执行。

下图展示了两个任务并发执行的过程:

img

虽然 CPU 在同一时刻只能执行一个任务,但是通过将 CPU 的使用权在恰当的时机分配给不同的任务,使得多个任务在视觉上看起来是一起执行的。CPU 的执行速度极快,多任务切换的时间也极短,用户根本感受不到,所以并发执行看起来才跟真的一样。

操作系统负责将有限的 CPU 资源分配给不同的任务,但是不同操作系统的分配方式不太一样,常见的有:

  • 当检测到正在执行的任务进行 I/O 操作时,就将 CPU 资源分配给其它任务。
  • 将 CPU 时间平均分配给各个任务,每个任务都可以获得 CPU 的使用权。在给定的时间内,即使任务没有执行完成,也要将 CPU 资源分配给其它任务,该任务需要等待下次分配 CPU 使用权后再继续执行。
    将 CPU 资源合理地分配给多个任务共同使用,有效避免了 CPU 被某个任务长期霸占的问题,极大地提升了 CPU 资源利用率。
1.1.2并行:

并发是针对单核 CPU 提出的,而并行则是针对多核 CPU 提出的。和单核 CPU 不同,多核 CPU 真正实现了“同时执行多个任务”。

多核 CPU 内部集成了多个计算核心(Core),每个核心相当于一个简单的 CPU,如果不计较细节,你可以认为给计算机安装了多个独立的 CPU。

多核 CPU 的每个核心都可以独立地执行一个任务,而且多个核心之间不会相互干扰。在不同核心上执行的多个任务,是真正地同时运行,这种状态就叫做并行。

例如,同样是执行两个任务,双核 CPU 的工作状态如下图所示:

img

双核 CPU 执行两个任务时,每个核心各自执行一个任务,和单核 CPU 在两个任务之间不断切换相比,它的执行效率更高。

1.1.3并发+并行

在上图中,执行任务的数量恰好等于 CPU 核心的数量,是一种理想状态。但是在实际场景中,处于运行状态的任务是非常多的,尤其是电脑和手机,开机就几十个任务,而 CPU 往往只有 4 核、8 核或者 16 核,远低于任务的数量,这个时候就会同时存在并发和并行两种情况:所有核心都要并行工作,并且每个核心还要并发工作。

例如一个双核 CPU 要执行四个任务,它的工作状态如下图所示:

img

每个核心并发执行两个任务,两个核心并行的话就能执行四个任务。当然也可以一个核心执行一个任务,另一个核心并发执行三个任务,这跟操作系统的分配方式,以及每个任务的工作状态有关系。

1.1.4总结

并发针对单核 CPU 而言,它指的是 CPU 交替执行不同任务的能力;并行针对多核 CPU 而言,它指的是多个核心同时执行多个任务的能力。

单核 CPU 只能并发,无法并行;换句话说,并行只可能发生在多核 CPU 中。

在多核 CPU 中,并发和并行一般都会同时存在,它们都是提高 CPU 处理任务能力的重要手段。

1.2线程生命周期

  • 新建状态:

    使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。

  • 就绪状态:

    当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

  • 运行状态:

    如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

  • 阻塞状态:

    如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

    • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
    • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
    • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
  • 死亡状态:

    一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

1.2.1线程的优先级

每一个 Java 线程都有一个优先级,这样有助于操作系统确定线 程的调度顺序。

Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。

默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。

具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。

1.3应用

1.3.1异步调用

从方法调用的角度来讲,如果

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

注意:同步在多线程中还有另外一层意思,是指多个线程步调一致

1.3.1.1设计

多线程可以让方法执行变为异步的,比如磁盘读取文件时,假设读取操作花费了5秒钟,如果没有线程调度机制,这五秒调用者什么都做不了,代码都要暂停

1.2.1.2结论
  • 在项目中,视频文件需要转化格式等操作比较费时,这是开一个新的线程处理视频转换,避免阻塞主线程
  • tomcat的异步servlet也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞tomcat的工作线程
  • UI程序中,开线程进行其他操作,避免阻塞UI线程
1.3.2提高效率

二、线程

2.1创建和运行线程

方法一,直接使用Thread

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

例如:

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

        thread.setName("t1");
        thread.start();

        log.debug("running");
    }
}

输出:

15:54:31.125 [main] DEBUG com.example.Test.Test_01 - running
15:54:31.125 [t1] INFO com.example.Test.Test_01 - running

方法二、使用Runnable配合Thread

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

  • Thread代表线程
  • Runnable可运行的任务(线程要执行的代码)
Runnable runnable = new Runnable(){
    public void run(){
        //要执行的任务
    }
};
//创建线程对象
Thread t = new Thread(runnable);
//启动线程
t.start();

例如:

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

输出:

16:02:56.798 [t2] DEBUG com.example.Test.Test01 - running

Java8之后的简化写法()

@Slf4j
public class Test_02 {
    public static void main(String[] args) {
        //第一次简化
        Runnable runnable = () -> log.debug("running");
        
        Thread t= new Thread(runnable,"t2");
        t.start();
    }
    public void test(){
        //第二次简化
        Thread t= new Thread(() -> log.debug("running"),"t2");
    }
}

输出:

16:02:56.798 [t2] DEBUG com.example.Test.Test01 - running

小结

  • 案例1是把线程和任务合并在了一起,案例2是把线程和任务分开了
  • 用Runnable更容易与线程池的高级API配合
  • 用Runnable让任务脱离了Thread集成体系,更加灵活

方法三、FutureTask配合Thread

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

FutureTask<Integer> task = new FutureTask<>(() ->{
    log.debug("hello");
    return 100;
});

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

Integer result = task.get();
log.debug("结果是{}",result);

例子:

@Slf4j
public class FutureTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                log.debug("running");
                Thread.sleep(2000);
                return 100;
            }
        });

        Thread t = new Thread(task,"t");
        t.start();
        log.debug("结果是{}",task.get());
    }
}

输出:

16:43:06.196 [t] DEBUG com.example.Test.FutureTest - running
16:43:08.200 [main] DEBUG com.example.Test.FutureTest - 结果是100

2.2多个线程执行

主要理解

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

2.3查看进程的方法

Windows
  • 任务管理器
  • tasklist查看进程
  • taskkill杀死进程

2.4线程运行原理

栈与栈帧

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

  • 每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存
  • 每个栈帧只能有一个活动栈帧,对应着当前正在执行的那个方法
线程上下文切换(Thread Context Switch)

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

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

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

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

2.5 常用方法

2.6 start和run

调用run

@Slf4j
public class StartAndRun {
    public static void main(String[] args) {
        Thread thread = new Thread(){
            @Override
            public void run() {
                log.debug("running....");
                User user = new User(123,"张三");
               log.info("{}",user.toString());
            }
        };
        thread.run();
        log.debug("do other things.....");
    }
}

运行结果

08:58:37.718 [main] DEBUG com.example.Test.StartAndRun - running....
08:58:37.722 [main] INFO com.example.Test.StartAndRun - User(userId=123, name=张三)
08:58:37.723 [main] DEBUG com.example.Test.StartAndRun - do other things.....

可以看到都是在主线程执行的

调用start

@Slf4j
public class StartAndRun {
    public static void main(String[] args) {
        Thread thread = new Thread("t1"){
            @Override
            public void run() {
                log.debug("running....");
                User user = new User(123,"张三");
               log.info("{}",user.toString());
            }
        };
        thread.start();
        log.debug("do other things.....");
    }
}

运行结果

09:04:42.188 [main] DEBUG com.example.Test.StartAndRun - do other things.....
09:04:42.188 [t1] DEBUG com.example.Test.StartAndRun - running....
09:04:42.191 [t1] INFO com.example.Test.StartAndRun - User(userId=123, name=张三)

可以看到主线程有运行的,另外一个线程(t1)也有运行的

多次调用start

@Slf4j
public class StartAndRun {
    public static void main(String[] args) {
        Thread thread = new Thread("t1"){
            @Override
            public void run() {
                log.debug("running....");
                User user = new User(123,"张三");
               log.info("{}",user.toString());
            }
        };
        System.out.println(thread.getState());
        thread.start();
        thread.start();
        System.out.println(thread.getState());
        log.debug("do other things.....");
    }
}

运行结果

NEW
09:08:38.676 [t1] DEBUG com.example.Test.StartAndRun - running....
09:08:38.682 [t1] INFO com.example.Test.StartAndRun - User(userId=123, name=张三)
Exception in thread "main" java.lang.IllegalThreadStateException
	at java.lang.Thread.start(Thread.java:708)
	at com.example.Test.StartAndRun.main(StartAndRun.java:23)

多次调用start()报异常

2.7sleep和yield

sleep

  • 调用slee[线程休眠,会从***Running***进入***Timed Waiting***状态
  • 其他线程可以使用interrupt方法打断正在睡眠的线程,这时的sleep方法会爆出InterruptedException
  • 睡眠结束后后的线程未必会立刻得到执行
  • 建议用TImeUnit的sleep代替Thread的sleep来获得更好的可读性
@Slf4j
public class SleepAndYield {
    public static void main(String[] args) {
        Thread thread = new Thread("t1") {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        thread.start();
        log.debug("t1 state:{}",thread.getState());
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("t1 state:{}",thread.getState());
    }
}

运行结果

09:17:15.567 [main] DEBUG com.example.Test.SleepAndYield - t1 state:RUNNABLE
09:17:16.086 [main] DEBUG com.example.Test.SleepAndYield - t1 state:TIMED_WAITING

t1线程由RUNNABLE–>TIMED_WAITING,运行到睡眠状态

yiled

  • 调用yield会让当前线程从Running进入Runnable状态,然后调度执行其他同优先级的线程。如果这时没有同优先级的线程,那么不能保证当前线程暂停的效果
  • 具体的实现依赖于操作系统的任务调度器

线程优先级

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

案例-防止CPU占用100%内存

  • sleep实现

在没有利用CPU来计算时,不要让while(true) 空转浪费CPU,这时可以使用yield或sleep来让出CPU的使用权给其他程序

		while (true){
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
  • 可以用wait或条件变量达到类似的效果
  • 不同的是,后两种都需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景
  • sleep适用于无需锁同步的场景

2.8join方法详解

  • join -----等待线程结束(谁调用就是哪个线程,相当于按行执行代码了,因为只有当该线程结束之后,才会调用后面行的代码)

以调用角度来讲,如果:

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

例:

@Slf4j
public class JoinTest {
    static int r1 = 0;
    static int r2 = 0;

    public static void main(String[] args) throws InterruptedException{
        test2();
    }

    private static void test2() throws InterruptedException{
        Thread t1 = new Thread(()->{
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r1 = 10;
        });

        Thread t2 = new Thread(()->{
            try {
                sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r2 = 20;
        });

        t1.start();
        t2.start();

        long start = System.currentTimeMillis();
        log.debug("join begin");
        t1.join();
        log.debug("t1 join end");
        t2.join();
        log.debug("t2 join end");
        long end = System.currentTimeMillis();
        log.debug("r1:{}  r2:{}  cost:{}",r1,r2,end-start);
    }
}

运行结果

08:20:30.149 [main] DEBUG com.example.Test.JoinTest - join begin
08:20:31.150 [main] DEBUG com.example.Test.JoinTest - t1 join end
08:20:32.155 [main] DEBUG com.example.Test.JoinTest - t2 join end
08:20:32.155 [main] DEBUG com.example.Test.JoinTest - r1:10  r2:20  cost:2010

可以看到,当t1线程调用join方法之后,才会开始执行后面的代码,为什么总耗时为2s呢,因为join调用之前,t1和t2同时开始,所以就算要等到t1结束t2早就已经等够了2s,所以总耗时为2s

[如图]

2.8.2有时效的Join
方法名static功能说明注意
join等待线程运行结束
join(long n)等待线程运行结束,最多等待n毫秒

例:

	public static void test3() throws InterruptedException {
        Thread t1 = new Thread(()->{
            try {
                sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r1 = 10;
        });
        long start = System.currentTimeMillis();
        t1.start();
        log.debug("join begin");
        t1.join(1500);
        long end = System.currentTimeMillis();
        log.debug("r1: {}  costs: {}",r1,end-start);
    }

运行结果

08:36:43.223 [main] DEBUG com.example.Test.JoinTest - join begin
08:36:43.245 [main] DEBUG com.example.Test.JoinTest - r1: 10  costs: 25

2.9interrupt方法详解

阻塞

2.9.1打断sleep、wait、join状态的线程

以sleep为例:

@Slf4j
public class InterruptTest {
    public static void main(String[] args) {
        test1();
    }
    public static void test1(){
        Thread t1 = new Thread(()->{
            try {
                //1s
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"t1");
        t1.start();
        try {
            //0.5s
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t1.interrupt();
        log.debug("打断状态{}",t1.isInterrupted());
    }
}

运行结果

java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.example.Test.InterruptTest.lambda$test1$0(InterruptTest.java:19)
	at java.lang.Thread.run(Thread.java:748)
08:43:59.882 [main] DEBUG com.example.Test.InterruptTest - 打断状态false

正常运行的线程打断之后就是true,sleep之类的打断之后才是false

2.9.2两阶段终止模式

Two Phase Termination

在一个线程T1中如何优雅的终止线程T2?这里的“优雅”是指给T2一个料理后事的机会

1、错误思路
  • 使用线程对象的stop()方法停止线程
    • stop方法会真正杀死进程,如果这时线程锁住了共享资源,那么当它被杀死只会就再也没有机会释放锁,其他线程将会永远无法获取锁
  • 使用System.exit(int) 方法停止线程
    • 目的仅是停止一个线程,但这种方法会让整个程序都停止

示例:

无异常
有异常
while(true)
是否被打断
料理后事
循环结束
睡眠2s
执行监控记录
设计打断标记
@Slf4j
public class Test3 {
    public static void main(String[] args) throws InterruptedException {
        TwoPhaseTermination twoPhaseTermination = new TwoPhaseTermination();
        twoPhaseTermination.start();

        //3.5s后打断,打断睡眠状态
        Thread.sleep(3500);
        twoPhaseTermination.stop();
    }
}
@Slf4j
class TwoPhaseTermination{
    private Thread monitor;

    public void start(){
        monitor = new Thread(()->{
            Thread current = Thread.currentThread();
            while (true){
                //获取当前线程
                //打断标记如果为true 那么跳出循环
                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();
    }
}

运行结果

09:40:27.488 [Thread-0] DEBUG com.example.Test.TwoPhaseTermination - 执行监控记录
09:40:28.498 [Thread-0] DEBUG com.example.Test.TwoPhaseTermination - 执行监控记录
09:40:29.498 [Thread-0] DEBUG com.example.Test.TwoPhaseTermination - 执行监控记录
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.example.Test.TwoPhaseTermination.lambda$start$0(Test3.java:37)
	at java.lang.Thread.run(Thread.java:748)
09:40:29.987 [Thread-0] DEBUG com.example.Test.TwoPhaseTermination - 料理后事

在3s后,sleep开始被打断,运行到跳出循环,说明设置的监控标志没问题

isInterrupted() 和 interrupter()

方法名static功能说明注意
isInterrupted()判断是否被打断不会清除打断标记
interrupter()打断线程会清除打断标记

2.10不推荐使用的方法

方法名static功能说明
stop()停止线程运行
suspend()挂起(暂停)线程
resume()恢复线程运行

2.11主线程和守护线程

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

@Slf4j
public class DaemonTest {
    public static void main(String[] args) throws Exception{
        Thread thread = new Thread(()->{
            while (true){
                if(Thread.currentThread().isInterrupted()){
                    break;
                }
            }
            log.debug("结束");
        });
        thread.setDaemon(true);
        thread.start();

        Thread.sleep(1000);
        log.debug("结束");
    }
}

运行结果

22:24:28.681 [main] DEBUG com.example.Test.DaemonTest - 结束

由此可见,当main线程执行结束之后,thread线程也执行结束,并且直接结束了thread线程,不仅结束了while循环,而且下面的代码也没有执行,执行的结束是main方法内的结束代码

| 注意

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

2.12五种状态

2.13六种状态

Java API层面来描述

根据Thread.State枚举,分为六种状态

  • NEW 线程刚被创建,但是没有调用star()方法
  • RUNNABLE 当调用了start()方法之后,注意,Java API层面的RUNNABLE状态涵盖了操作系统层面的【可运行状态】、【运行装填】和【阻塞状态】(由于BIO导致的线程阻塞,在Java里无法区分,仍然认为是可运行)
  • BLOCKED、WAITING、TIMED_WAITING都是Java API层面对【阻塞部分】的细分,后面会在状态转换这一节详述
  • TERMINATED当线程代码运行结束

小结

应用:
  • 异步调用:主线程执行期间,其他线程异步执行耗时操作
  • 提高效率:并行计算,缩短运算时间
  • 同步等待:join
  • 统筹规划:合理使用线程,得到最优效果
原理:
  • 线程运行流程:栈、栈帧、上下文切换、程序计数器
  • Thread两种创建方式的源码

三、共享模型之管程

4.1临界区(Critical Section)

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

例如如下的临界区

static int counter = 0;

static void increment(){
    counter++;
}

static void decrement(){
    counter--;
}

4.2synchronized解决方案

互斥

为了避免临界区的竞态条件发生,有多种手段可以达到目的

  • 阻塞式的解决方案:synchronized、Lock
  • 非阻塞式的解决方案:原子变量

synchronized,俗称【对象锁】

本次课使用阻塞式的解决方案: synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换

注意:虽然java中互斥和同步都可以采用synchronized关键字来完成,但它们还是有区别的

  • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
  • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
4.2.1synchronized

语法

synchronized(对象){   //线程1、线程2(blocked)
    临界区
}

实例

@Slf4j
public class SynchronizedTest {
    static int count = 0;
    static Object object = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synchronized (object){
                    count++;
                }
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synchronized (object){
                    count--;
                }
            }
        });

        t1.start();
        t2.start();
        log.debug("{}",count);
    }

运行结果

08:18:53.134 [main] DEBUG com.example.Test.SynchronizedTest - 0

可以看到,运行结果为0,说明加锁之后可以确保在一个线程执行的时候,另一个线程会被阻塞,所以无论运行多少次,结果都是0,但是如果不加锁,那么运行结果会有很多种结果

思考

synchronized实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断

为了加深理解:

  • 如果把synchronized(obj)放在for循环外面,如何理解

    • 可以
  • 如果把t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎么样运作?

    • 不会,必须是同一个对象
  • 如果t1 synchronize(obj) 而t2没有加会怎么样?如何理解?

    • 不会,数据仍然不对应

4.3方法上的synchronized

class Test{
    public synchronized void test(){
        
    }
}

//等价于

class Test{
    public void test(){
        synchronized(this){
            
        }
    }
}
class Test{
    public synchronized static void test(){
        
    }
}

//等价于

class Test{
    public static void test(){
        synchronized(this){
            
        }
    }
}

上面的案例优化

@Slf4j
public class SynchronizedTest {
    public static void main(String[] args) throws InterruptedException {
        Room room = new Room();
        Thread t1 = new Thread(() -> {
            log.debug("{}","线程1run");
            for (int i = 0; i < 5000; i++) {
                room.add();
            }
        });

        Thread t2 = new Thread(() -> {
            log.debug("线程2run");
            for (int i = 0; i < 5000; i++) {
                room.decrease();
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("{}",room.getCount());
    }
}

class Room{
    private int count = 0;
    public synchronized void add(){
        count++;
    }

    public synchronized void decrease(){
        count--;
    }

    public synchronized int getCount(){
        return count;
    }

}

运行结果

08:46:26.954 [Thread-0] DEBUG com.example.Test.SynchronizedTest - 线程1run
08:46:26.954 [Thread-1] DEBUG com.example.Test.SynchronizedTest - 线程2run
08:46:26.958 [main] DEBUG com.example.Test.SynchronizedTest - 0
“线程八锁”

其实就是考察synchronized锁住的是哪个对象

案例1 互斥 12或21

@Slf4j
class Number{
    public synchronized void a(){
        log.debug("1");
    }
    public synchronized void b(){
        log.debug("2");
    }
}

public static void main (String[] args){
    Number n1 = new Number();
    new Thread(()->{ n1.a(); }).start();
    new Thread(()->{ n1.a(); }).start();
}

结果

07:46:46.556 [Thread-0] DEBUG com.example.EightSynchroizedDemo.Number - 1
07:46:46.558 [Thread-1] DEBUG com.example.EightSynchroizedDemo.Number - 2

锁住的是同一个对象(this) 所以会有互斥,可能是线程1先执行,也有可能是2先执行

案例2 互斥 12或21

@Slf4j
class Number{
    public synchronized void a(){
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("1");
    }
    public synchronized void b(){
        log.debug("2");
    }
}

 public static void main(String[] args) {
        Number n1 = new Number();
        new Thread(()->{ n1.a(); }).start();
        new Thread(()->{ n1.b(); }).start();
  }

结果

07:49:30.848 [Thread-0] DEBUG com.example.EightSynchroizedDemo.Number - 1
07:49:30.852 [Thread-1] DEBUG com.example.EightSynchroizedDemo.Number - 2

同案例一,看谁先调度

案例3

@Slf4j
class Number{
    public synchronized void a(){
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("1");
    }
    public synchronized void b(){
        log.debug("2");
    }

    public void c(){
        log.debug("3");
    }
}

 public static void main(String[] args) {
        Number n1 = new Number();
        new Thread(()->{ n1.a(); }).start();
        new Thread(()->{ n1.b(); }).start();
        new Thread(()->{ n1.c(); }).start();
  }

结果

07:51:46.658 [Thread-2] DEBUG com.example.EightSynchroizedDemo.Number - 3
07:51:47.660 [Thread-0] DEBUG com.example.EightSynchroizedDemo.Number - 1
07:51:47.660 [Thread-1] DEBUG com.example.EightSynchroizedDemo.Number - 2
//3 1s 12
//23 1s 1
//32 1s 1

线程3和其他两个线程并行执行,但是线程1和线程2互斥

案例4

@Slf4j
class Number{
    public synchronized void a(){
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("1");
    }
    public synchronized void b(){
        log.debug("2");
    }
}
 public static void main(String[] args) {
        Number n1 = new Number();
        Number n2 = new Number();
        new Thread(()->{ n1.a(); }).start();
        new Thread(()->{ n2.b(); }).start();
  }

结果

07:56:06.328 [Thread-1] DEBUG com.example.EightSynchroizedDemo.Number - 2
07:56:07.340 [Thread-0] DEBUG com.example.EightSynchroizedDemo.Number - 1

锁住的对象不同,无互斥,所以总是线程2先执行,因为线程1睡眠1s

案例5

@Slf4j
class Number{
    public static synchronized void a(){
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("1");
    }
    public synchronized void b(){
        log.debug("2");
    }
}
public static void main(String[] args) {
        Number n1 = new Number();
        new Thread(()->{ n1.a(); }).start();
        new Thread(()->{ n1.b(); }).start();
  }

结果

07:58:07.288 [Thread-1] DEBUG com.example.EightSynchroizedDemo.Number - 2
07:58:08.282 [Thread-0] DEBUG com.example.EightSynchroizedDemo.Number - 1

同案例5,但是a方法加了static,所以此时a方法实际上锁住的是类对象,但是此时都是n1对象调用的a方法和b方法。

案例6

@Slf4j
class Number{
    public static synchronized void a(){
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("1");
    }
    public static synchronized void b(){
        log.debug("2");
    }
}
public static void main(String[] args) {
        Number n1 = new Number();
//        Number n2 = new Number();
        new Thread(()->{ n1.a(); }).start();
        new Thread(()->{ n1.b(); }).start();

}

结果

08:00:53.791 [Thread-0] DEBUG com.example.EightSynchroizedDemo.Number - 1
08:00:53.797 [Thread-1] DEBUG com.example.EightSynchroizedDemo.Number - 2

此时a方法和b方法都是对类对象加锁,但是类对象只有一份,所以此时两线程会互斥,看谁先被调度

案例7

@Slf4j
class Number{
    public static synchronized void a(){
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("1");
    }
    public  synchronized void b(){
        log.debug("2");
    }
}
public static void main(String[] args) {
        Number n1 = new Number();
        Number n2 = new Number();
        new Thread(()->{ n1.a(); }).start();
        new Thread(()->{ n2.b(); }).start();
}

结果

08:03:00.603 [Thread-1] DEBUG com.example.EightSynchroizedDemo.Number - 2
08:03:01.605 [Thread-0] DEBUG com.example.EightSynchroizedDemo.Number - 1

不互斥,a方法是类对象,b方法是this对象,通过不同的对象实例调用,不是对同一个方法生效,所以不互斥

案例8

@Slf4j
class Number{
    public static synchronized void a(){
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("1");
    }
    public static synchronized void b(){
        log.debug("2");
    }
}
public static void main(String[] args) {
        Number n1 = new Number();
        Number n2 = new Number();
        new Thread(()->{ n1.a(); }).start();
        new Thread(()->{ n2.b(); }).start();
}

结果

08:06:45.100 [Thread-0] DEBUG com.example.EightSynchroizedDemo.Number - 1
08:06:45.104 [Thread-1] DEBUG com.example.EightSynchroizedDemo.Number - 2

a方法和b方法都是锁住类对象,但是类对象只有一个,此时在运行时,即使调用对象不同,但是也是互斥。

说明

用static修饰的加锁方法,为什么锁住的是类对象,可以这样理解,在调用静态方法时,可以通过对象名.方法名(Number.a())调用,静态方法的调用基本都是这样,所以没吃调用静态方法实际上都是对象名.方法名(Number.a()),长的是不是都一样,只是后面方法名不通过罢了,所以静态变量锁会互斥

4.4变量的线程安全分析

成员变量和静态变量是否线程安全
  • 如果没共享,那么安全

  • 如果被共享,根据他们状态是否能改变,有分两种情况

    • 如果只有读操作,则线程安全

    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量是否线程安全?
  • 局部变量是线程安全的
  • 但局部变量引用的对象则未必
    • 如果该对象没有逃离方法的作用访问,它是线程安全的
    • 如果该对象逃离方法的作用范围,需要考虑线程安全
;