JUC学习(上)(完结)

预备知识

  1. 不是一个初学者
  2. 线程安全问题,需要接触过Java Web开发,Jdbc开发,Web服务器,分布式框架时才会遇到
  3. 基于JDK8,对函数式编程,lambda有一定的了解
  4. 采用slf4j打印日志,是好的实践
  5. 采用了lombok简化java bean的书写
  6. 给每个线程一个好名字,也是一个好的实践

pom.xml的依赖如下

 <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.8</version>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>
    </dependencies>

logback.xml如下

<?xml version="1.0" encoding="UTF-8" ?>
<configuration xmlns="http://ch.qos.logback/xml/ns/logback"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://ch.qos.logback/xml/ns/logback logback.xsd">
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%date{HH:mm:ss} [%t] %logger - %m%n</pattern>
        </encoder>
    </appender>
    <logger name="c" lever="debug" additivity="false">
        <appender-ref ref="STDOUT"/>
    </logger>
    <root lever="ERROR">
        <appender-ref ref="STDOUT"/>
    </root>
</configuration>

2.1 进程与线程

进程

当一个程序被运行,从磁盘加载代码到内存,就开启了一个进程

线程

一个进程中有多个线程,一个线程就是一个指令流,将指令流中的一个个指令按照一定的顺序交给CPU执行

2.2 并行与并发

在单核CPU下,线程实际上还是串行执行的,操作系统中有一个组件叫做任务调度器,将CPU分给不同的线程使用,只是由于CPU之间的切换非常快,所以人类感觉是同时运行的。总结一句话,微观串行,宏观并行

一般会将这种线程轮流使用CPU的做法称为并发,concurrent

2.3应用

从方法调用的角度来看,如果
*需要等待结果返回摩擦,才能继续运行就是同步
*不需要等待结果返回摩擦,就能继续运行就是异步
*同步在多线程中还有另外一层意思,是让多个线程步调一致

  • 应用之异步调用示例
@Slf4j
public class Main {
    public static void main(String[] args){
        for(int i = 0 ; i < 5 ; i++){
            log.debug("do things");
        }
        log.debug("do other things");
    }
}

上面代码的输出结果为
在这里插入图片描述
可以看到do other things必须要等待循环结束才能够执行
我们用异步方法做一个对比

@Slf4j
public class Main {
    public static void main(String[] args){
        new Thread(()->{
            for(int i = 0 ; i < 5 ; i++){
                log.debug("do things");
            }
        }).start();
        log.debug("do other things");
    }
}

此时代码的输出结果为
在这里插入图片描述
可以看到,do other things并没有等待循环的执行
当然线程的语法后面会说,现在只是先演示一下

结论

  1. 比如在项目中,视频文件等需要转换格式等操作比较费时间,此时新开一个线程处理视频转换,避免阻塞主线程
  2. tomcat的servlet也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞tomcat的工作线程
  3. ui程序中,开线程进行其他操,避免阻塞ui线程

3.Java线程

本章内容介绍

  1. 创建和运行线程
  2. 查看线程
  3. 线程API
  4. 线程状态

3.1 创建和运行线程

方法1,直接使用Thread

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

例如

 //创建线程对象 最好给线程起个名字 这样在输出的结果中比较清晰
       Thread t = new Thread("t1"){
           @Override
           public void run() {
               log.debug("running");
           }
       };
       //启动线程
       t.start();
       log.debug("running");

输出结果为
在这里插入图片描述
可以看出分别是main和t1两个不同的线程进行的

方法2,使用Runnable配合Thread

即把【线程】和【任务】分开
Thread代表线程,Runnable代表可运行的任务
在这里插入图片描述
查看源码后可以发现,Runnable是一个接口,没有返回值也不能抛出异常

@Slf4j
public class Main {
  public static void main(String[] args) {
      Runnable runnable = new Runnable() {
          @Override
          public void run() {
              //要执行的任务
          }
      };
      //创建线程对象
      Thread t = new Thread(runnable);
      //执行线程
      t.start();
  }
}

例如

@Slf4j
public class Main {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
               log.debug("running");
            }
        };
        //创建线程对象
        Thread t = new Thread(runnable,"t1");
        //执行线程
        t.start();
    }
}

此代码输出结果与之前类似

回到刚才的Runnable源码,可以发现Runnable接口只有一个抽象方法,在JDK中,会对于这样的接口加入一个@FunctionalInterface的注解,带有此注解的就可以用lambda表达式进行简化
如下

@Slf4j
public class Main {
    public static void main(String[] args) {
        Runnable runnable = () -> {
            log.debug("running");
        };
        //创建线程对象
        Thread t = new Thread(runnable,"t1");
        //执行线程
        t.start();
    }
}

原理之Thread与Runnable的关系
查看Thread的源码可以发现有此方法
在这里插入图片描述
让我们看看他把此runnable对象传到了哪里去
在这里插入图片描述
传递到了另一个被重载的init函数,继续跟踪
在这里插入图片描述
可以发现他在这个方法里面被赋值给了target对象
在这里插入图片描述
可以发现,这个target是被他Thread的run方法里面用到了,Thread若发现runnable不为空,那么会优先使用runnable的任务

小结

  1. 用Runnable更容易与线程池等高级API配合
  2. 用Runnable让任务类脱离了Thread继承体系,更灵活(java中组合优于继承)

方法3,FutureTask配合Thread

FutureTask能够接收Callable类型的参数,用来处理有返回结果的类型
在这里插入图片描述
查看其源码可以发现他是一个实现类,实现了RunnableFuture接口
在这里插入图片描述
再查看RunnableFuture源码,可以发现他间接实现了Runnable接口,所以他也可以作为一个任务对象,此外他多了一个Future接口,是用来返回结果的

我们再去查看Callable源码
在这里插入图片描述
可以发现他与Runnable接口类似,但是有返回类型并且可以抛出异常
代码示例如下

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

输出结果为
在这里插入图片描述
我们可以发现,似乎这个返回值并没有任何体现,这是因为我们并没有使用他
如果我们想要主线程使用则可以使用get()方法,如下

@Slf4j
public class Main {
    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");
                return 100;
            }
        });
        Thread t = new Thread(task);
        t.start();
        log.debug("{}",task.get());
    }
}

若task方法执行较慢,则get方法会一直等待他执行完成返回结果才会继续往下执行,也就是阻塞主线程,如下示例

@Slf4j
public class Main {
    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.start();
        log.debug("{}",task.get());
    }
}

输出结果为
在这里插入图片描述
可以发现,主线程输出的100是等待了两秒之后的。

观察两个线程交替执行

代码如下

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

输出结果为
在这里插入图片描述
可以发现两个线程是交替执行的

查看进程线程的方法

Windows

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

  1. tasklist 查看进程
  2. taskkill 杀死进程
    输入tasklist我们可以看见有这么多的进程
    在这里插入图片描述
    我们将刚刚的java程序启动,由于tasklist的进程太多,我们需要筛选一下,
    在这里插入图片描述
    但是这里的java我们仍然不知道哪个才是我们运行中的进程,所以我们用jdk中的命令jps可以发现具体哪个才是我们运行的
    在这里插入图片描述
    然后使用taskkill通过PID杀死进程
    在这里插入图片描述
    此时进程也会结束
    在这里插入图片描述
linux
  1. ps -fe 查看所有进程
  2. ps -fT -p < PID > 查看某个进程的全部线程
  3. kill 杀死进程
  4. top 按大写H切换是否显示线程
  5. top -H -p < PID > 查看某个进程的所有线程
Java
  1. jps 查看所有java进程
  2. jstack < PID > 查看某个Java进程所有的线程状态
  3. jconsole 查看某个Java进程中线程的运行情况(图形界面)

原理之线程运行

栈与栈帧

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

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

这样说比较模糊,我们用代码来演示一下

@Slf4j
public class Main {
    public static void main(String[] args){
        method1(10);
    }
    private static void method1(int x){
        int y = x + 1;
        Object m = method2();
        System.out.println(m);
    }
    private static Object method2(){
        Object n = new Object();
        return n;
    }
}

我们在method1上打一个断点,用debug方式来运行,在运行过程中我们可以发现
在这里插入图片描述
随着方法的进行,会多出几个栈帧,并且存储他们的局部变量
在这里插入图片描述
在方法执行结束之后,就释放其内存,每个栈帧对应一次方法的调用

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

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

  1. 线程的cpu时间片用完
  2. 垃圾回收
  3. 有更高优先级的线程需要运行
  4. 线程自己调用了sleep,yield,wait,join,park,synchronized,lock等方法
    当Context Switch发生,需要由操作系统保存他当前的状态,并恢复另一个线程的状,Java中对应的概念就是程序计数器(Program Counter Register),他的作用是记住下一条jvm指令的执行地址,是线程私有的
  5. 状态包括程序计数器,虚拟机栈用每个栈帧的信息,如局部变量,操作数栈,返回地址等
  6. Context Switch频繁发生会影响性能

3.4 常见方法

3.4.1 start vs run

调用run

既然t1里面有run方法我们为何不直接调用这个run方法而是用start呢

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

运行结果如下
在这里插入图片描述

我们可以发现是主线程进行的输出,所以run方法并不能够起到异步的作用,并不能启动新的线程,也不能够提高性能,因此启动线程必须是用start(),才能够启动新的线程

调用start

调用start后我们可以用getState()方法来获取线程的状态,代码如下

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

输出结果为
在这里插入图片描述
当一个线程没有被start调用时的状态是NEW,被调用了则是RUNNABLE
此外,一个线程的start方法不能被调用两次,否则会报错

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

报错为Exception in thread “main” java.lang.IllegalThreadStateException,如下
在这里插入图片描述

3.4.2 sleep与yield

sleep
  1. 调用sleep会让当前线程从Running状态进入Timed Waiting状态(阻塞)(任务调度器不会考虑这种状态)
  2. 其他线程可以使用interrupt方法打断正在睡眠的进程,这时sleep方法会抛出InterruptedException
  3. 睡眠结束后的线程未必会立刻得到执行
  4. 建议用TimeUnit的sleep代替Thread的sleep来获得更好的可读性
yield
  1. 调用yield会让当前线程从Running状态进入Runnable就绪状态(任务调度器仍有可能分配时间片给他),然后调度执行其他同优先级的线程,如果这时没有同优先级的线程,那么不能保证让线程暂停的效果
  2. 具体的实现依赖于操作系统的任务调度器
线程优先级
  1. 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略他
  2. 如果cpu比较忙,那么优先级高的线程会获得更多的时间片,但cpu闲的时候,优先级几乎没有作用
sleep方法示例1
@Slf4j
public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(){
            @Override
            public void run() {
                try {
                    //单位为毫秒
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                log.debug("running");
            }
        };
        t1.start();
        //防止主线程先被调用
        Thread.sleep(500);
        System.out.println(t1.getState());
    }
}

输出结果为
在这里插入图片描述
可以发现线程状态为TIMED WAITING

sleep方法示例2
@Slf4j
public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(){
            @Override
            public void run() {
                log.debug("enter sleep");
                try {
                    //单位为毫秒
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    log.debug("wake up");
                    throw new RuntimeException(e);
                }

            }
        };
        t1.start();
        //防止主线程先被调用
        Thread.sleep(1000);
        t1.interrupt();
    }
}

输出结果为
在这里插入图片描述
发现这个线程在一秒后被叫醒

sleep方法示例3
@Slf4j
public class Main {
    public static void main(String[] args) throws InterruptedException {
        log.debug("begin");
        TimeUnit.SECONDS.sleep(1);
        log.debug("end");
    }
}

输出结果为
在这里插入图片描述
可以发现用TimeUnit的效果是相同的,并且可读性更高

yield方法以及线程优先级示例
@Slf4j
public class Main {
    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(;;){
                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();
    }
}

在这里插入图片描述
可以看到,在不加优先级的时,两个线程被调用的次数类似
但是在设置了优先级之后,他们被调用的次数差距就变大了
在这里插入图片描述
或者在线程2中添加一个yield方法,让他把线程让出去,也可以发现两个线程被调用的次数差距拉大

@Slf4j
public class Main {
    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.start();
        t2.start();
    }
}

在这里插入图片描述

总结

不管是优先级还是yield,都只是对任务调度器的一个提示而已,没有必定的效果

3.4.3 join

先提出一个问题,下面的代码执行,打印出的r的值是多少

@Slf4j
public class Main {
    static int r = 0;
    public static void main(String[] args) throws InterruptedException {
        log.debug("开始");
        Thread t1 = new Thread(()->{
            log.debug("开始");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            log.debug("结束");
            r = 10;
        });
        t1.start();
        log.debug("结果为:{}",r);
        log.debug("结束");
    }
}

结果为r = 0
在这里插入图片描述
分析:主线程和t1线程是并行执行的,但是t1线程需要一秒后才能算出结果 r = 10,而主线程一开始就会打印r的结果,因此打印出来的结果为r = 0

解决方法:
用sleep()行不行? 当然可以,但是不太好,因为你不知道线程会在多久之后得到正确的结果,可是join()方法就可以完美的解决这个问题,join()的作用就是等待线程运行结束。
只需要在原来的代码的基础上,加一行t1.join();

public class Main {
    static int r = 0;
    public static void main(String[] args) throws InterruptedException {
        log.debug("开始");
        Thread t1 = new Thread(()->{
            log.debug("开始");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            log.debug("结束");
            r = 10;
        });
        t1.start();
        t1.join();
        log.debug("结果为:{}",r);
        log.debug("结束");
    }
}

输出结果为
在这里插入图片描述
可以发现输出结果确实等待了其一秒钟

除此之外join还可以添加一个参数,代表最大的等待时间

@Slf4j
public class Main {
    static int r = 0;
    public static void main(String[] args) throws InterruptedException {
        log.debug("开始");
        Thread t1 = new Thread(()->{
            log.debug("开始");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            log.debug("结束");
            r = 10;
        });
        t1.start();
        t1.join(1500);
        log.debug("结果为:{}",r);
        log.debug("结束");
    }
}

输出结果为
在这里插入图片描述
由于线程中睡眠了两秒钟,但是最大等待时间为一点五秒,所以他输出的结果依然为 0

3.4.4 interrupt

打断阻塞线程(sleep,wait,join)
@Slf4j
public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            log.debug("sleep");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        },"t1");
        t1.start();
        Thread.sleep(500);
        log.debug("interrupt");
        t1.interrupt();
        log.debug("打断标记:{}",t1.isInterrupted());
    }
}

在这里插入图片描述
按理来说打断标记应该为true,但是对于阻塞线程,他们会用异常方式来代表自己被打断了,因此他们会将打断标记设置为false,这个打断标记可以用来判断线程被打断后是继续运行还是就此终止

打断正常运行的线程
@Slf4j
public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            while(true){
                
            }
        },"t1");
        t1.start();
        Thread.sleep(1000);
        log.debug("interrupt");
        t1.interrupt();
    }
}

可以发现再被打断后,线程并没有结束,此时我们便可以用上打断标记来终止他
在这里插入图片描述
使用打断标记来终止的代码如下

@Slf4j
public class Main {
    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();
    }
}

输出结果为
在这里插入图片描述

interrupt细节

Thread的interrupt有两个类似的方法,一个叫做isInterrupted(),另一个叫做interrupted()。他们的区别为,后者为静态方法,且会清除打断标记

两阶段终止模式(Two Phase Termination)

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

错误思路

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

两阶段终止模式运行流程分析

在这里插入图片描述
代码如下

@Slf4j
public class TwoPhaseTermination {
    public static void main(String[] args) throws InterruptedException {
        twoPhaseTermination twoPhaseTermination = new twoPhaseTermination();
        twoPhaseTermination.start();
        Thread.sleep(3500);
        twoPhaseTermination.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
                    log.debug("执行监控");//情况2
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    //重新设置打断标记,因为如果在睡眠状态被interrupt()会导致其打断标记仍然为false
                    current.interrupt();
                }

            }
        });
    }
    public void stop(){
        monitor.interrupt();
    }
}
interruput常见方法-----打断park
@Slf4j
public class InterruptPark {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            log.debug("park...");
            //park并不是线程里面的方法,而是锁支持里面的
            LockSupport.park();
            log.debug("unpark...");
            log.debug("打断状态:{}",Thread.currentThread().isInterrupted());
            LockSupport.park();
            log.debug("park...");
        },"t1");
        t1.start();
        Thread.sleep(1000);
        t1.interrupt();
    }
}

打印结果为
在这里插入图片描述
可以发现,打断状态标记为true的线程是不可以再次被park的,如果想要再次被park,则可以将Thread.currentThread().isInterrupted()改为上文提到的可以清除打断标记的Thread.interrupted()方法

3.4.5 过时方法

还有一些不推荐使用的方法,这些方法已经过时,容易破坏同步代码块,造成线程死锁,一定要避免使用

  1. stop() :停止线程运行
  2. suspend() :挂起(暂停)线程运行
  3. resume():恢复线程运行

3.4.6 主线程与守护线程

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

@Slf4j
public class DaemonThread {
    public static void main(String[] args) throws InterruptedException {
        log.debug("开始运行");
        Thread t1 = new Thread(()->{
            log.debug("开始运行");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            log.debug("运行结束");
        },"daemon");
        //设置为守护线程,默认为false
        t1.setDaemon(true);
        t1.start();
        Thread.sleep(1000);
        log.debug("运行结束");
    }
}

输出
在这里插入图片描述
可以发现他daemon线程里面没有打印运行结束,即其没有运行结束就结束了

注意
  1. 垃圾回收器就是一种守护线程
  2. Tomcat中的Acceptor和Pollar线程都是守护线程,所以Tomcat接收到shutdown命令后,不会等待它们处理完当前的请求

3.4.6 五种状态(这是从操作系统层面来描述的)

在这里插入图片描述

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

3.4.6 六种状态(这是从Java API层面来描述的)

根据Thread.State枚举,分为六种状态
在这里插入图片描述

  1. 【NEW】线程刚被创建,还没有调用start()方法
  2. 【RUNNABLE】当调用了start()方法后,注意,Java API层面的RUNNABLE状态涵盖了操作系统层面的【可运行状态】,【运行状态】和【阻塞状态】(由于BIO导致的线程阻塞,在Java里面无法区分,仍然认为是可运行)
  3. 【BLOCKED】,【WAITING】,【TIMED_WAITING】都是Java API层面对【阻塞状态】的细分,后面会进一步解释
  4. 【TERMINATED】当线程代码运行结束

六种状态演示

代码如下

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

        // 2
        Thread t2 = new Thread("t2"){
            @Override
            public void run() {
                while(true){ // runnable

                }
            }
        };
        t2.start();

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

        // 4
        Thread t4 = new Thread("t4"){
            @Override
            public void run() {
                synchronized (TestState.class){
                    try {
                        Thread.sleep(1000000); //timed_waiting
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };
        t4.start();

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

        // 6
        Thread t6 = new Thread("t6"){
            @Override
            public void run() {
                synchronized (TestState.class){ // blocked
                    try {
                        Thread.sleep(1000000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };
        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());
    }
}

输出结果为
在这里插入图片描述

本章总结

本章的重点在于掌握

  1. 线程创建
  2. 线程重要的api,如start,run,sleep,join,interrupt等
  3. 线程状态
  4. 应用方面
    a)异步调用:主线程执行期间,其他线程异步执行耗时操作
    b)提高效率:并行计算,缩短运算时间
    c)同步等待:join
    d)统筹规划:合理使用线程,得到最优效果
  5. 原理方面
    a)线程运行流程:栈,栈帧,上下文切换,程序计数器
    b)Thread两种创建方式的源码
  6. 模式方面
    a)两阶段终止

4 共享模型之管程

4.1 上下文切换

如下代码中的两个线程分别对counter做五千次++和–操作,按理来说输出的结果应该为0,但实际的输出结果

@Slf4j
public class Test {
    static int counter = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for(int i = 0 ; i < 5000 ; i ++){
                counter++;
            }
        },"t1");
        Thread t2 = new Thread(()->{
            for(int i = 0 ; i < 5000 ; i ++){
                counter--;
            }
        },"t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("{}",counter);
    }
}

并不是0
在这里插入图片描述

问题分析

以上的结果可能是正数,负数,也有可能是零。为什么呢?因为从Java中对静态变量的自增,自减,并不是原子操,要彻底理解,必须从字节码进行分析
例如对i++而言(i是静态变量),实际会产生如下的JVM字节码指令

getstatic i //获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i

对应的i–也十分类似

getstatic i //获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i

而Java的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换
在这里插入图片描述
如果是单线程顺序执行(不会交错) 是没有问题的
在这里插入图片描述
但多线程下这些代码可能交错运行,但根本原因还是上下文切换
出现负数的情况:

出现负数的情况:
*在这里插入图片描述

临界区 Critical Section

  1. 一个程序运行多个线程本身是没有问题的
  2. 问题出在多个线程访问共享资源
    a)多个线程读共享资源本身也是没有问题的
    b)在多个线程对共享资源读写操作时发生指令交错,就会出现问题
  3. 一个代码内如果存在对共享资源的多线程读写操作,那称这块代码块为临界区
    例如,下面代码中的临界区
public class CriticalSection {
    static int counter = 0;
    static void increment(){
    // 临界区
        counter++;
    }
    static void decrement(){
    // 临界区
        counter--;
    }
}

竞态条件 Race Condition

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

4.2 synchronized 解决方案

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

synchronized语法介绍

语法

synchronized(对象){ // 线程1 执行代码时会获得这个锁,当有其他线程再来到时,就不能获取这个锁处于阻塞状态(blocked)
	临界区
}

解决刚刚的问题

@Slf4j
public class Test {
    static int counter = 0;
    static Object lock = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for(int i = 0 ; i < 5000 ; i ++){
                synchronized (lock){
                    counter++;
                }
            }
        },"t1");
        Thread t2 = new Thread(()->{
            for(int i = 0 ; i < 5000 ; i ++){
                synchronized (lock){
                    counter--;
                }
            }
        },"t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("{}",counter);
    }
}

运行后会发现问题已被解决

synchronized 理解

在这里插入图片描述

在这里插入图片描述

synchronized思考

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

  1. 如果把synchronized(obj)放在for循环的外面会怎么样?
    ——线程会将循环体内的代码全部执行完才会释放锁
  2. 如果线程1 synchronized(obj1) 而线程2 synchronized(obj2)会怎么样运作?
    ——这样不可以保护线程的安全运行
  3. 如果线程1 synchronized(obj1) 而线程2没有加会怎么样?
    ——不可以,这样做线程2不会去尝试获取锁

面对对象改进

把需要保护的共享变量放入一个类

@Slf4j
public class Test1 {
    public static void main(String[] args) throws InterruptedException {
        Room room = new Room();
        Thread t1 = new Thread(()->{
            for(int i = 0 ; i < 5000 ; i++){
                room.increment();
            }
        },"t1");
        Thread t2 = new Thread(()->{
            for(int i = 0 ; i < 5000 ; i++){
                room.decrement();
            }
        },"t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("{}",room.getCounter());
    }
}
class Room{
    private int counter = 0;
    //this 即Room对象
    public void increment(){
        synchronized (this){
            counter++;
        }
    }
    public void decrement(){
        synchronized (this){
            counter--;
        }
    }
    public int getCounter(){
        synchronized (this){
            return counter;
        }
    }
}

4.3 方法上的synchronized

public synchronized void test(){

}
//等价于
public void test(){
	synchronized(this){
	
	}
}
public synchronized static void test(){

}
//等价于
public static void test(){
	synchronized(Test.class){
	
	}
}

不加synchronized的方法是没有办法保证原子性的

所谓的“线程八锁”

1

@Slf4j
public class Test8Locks {
    public static void main(String[] args) {
        Number number = new Number();
        new Thread(()->{
            log.debug("begin");
            number.a();
        }).start();
        new Thread(()->{
            log.debug("begin");
            number.b();
        }).start();

    }
}

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

他的输出结果为“12”或者“21”

2

@Slf4j
public class Test8Locks {
    public static void main(String[] args) {
        Number number = new Number();
        new Thread(()->{
            log.debug("begin");
            try {
                number.a();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();
        new Thread(()->{
            log.debug("begin");
            number.b();
        }).start();

    }
}

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

输出结果“一秒后12”或者“2一秒后1”

3

@Slf4j
public class Test8Locks {
    public static void main(String[] args) {
        Number number = new Number();
        new Thread(()->{
            log.debug("begin");
            try {
                number.a();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();
        new Thread(()->{
            log.debug("begin");
            number.b();
        }).start();
        new Thread(()->{
            log.debug("begin");
            number.c();
        }).start();
    }
}

@Slf4j
class Number{
    public synchronized void a() throws InterruptedException {
        Thread.sleep(1000);
        log.debug("1");
    }
    public synchronized void b(){
        log.debug("2");
    }
    public void c(){
        log.debug("3");
    }
}

输出结果为“3一秒后12”或“32一秒后1”或“23一秒后1”

4

@Slf4j
public class Test8Locks {
    public static void main(String[] args) {
        Number number = new Number();
        Number number1 = new Number();
        new Thread(()->{
            log.debug("begin");
            try {
                number.a();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();
        new Thread(()->{
            log.debug("begin");
            number1.b();
        }).start();
    }
}

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

输出结果为“2一秒后1”

5

@Slf4j
public class Test8Locks {
    public static void main(String[] args) {
        Number number = new Number();
        new Thread(()->{
            log.debug("begin");
            try {
                number.a();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();
        new Thread(()->{
            log.debug("begin");
            number.b();
        }).start();
    }
}

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

输出结果为“2一秒后1” ,因为对于静态方法,锁的是类对象

6

@Slf4j
public class Test8Locks {
    public static void main(String[] args) {
        Number number = new Number();
        new Thread(()->{
            log.debug("begin");
            try {
                number.a();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();
        new Thread(()->{
            log.debug("begin");
            number.b();
        }).start();
    }
}

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

输出结果“一秒后12”或者“2一秒后1”

7

@Slf4j
public class Test8Locks {
    public static void main(String[] args) {
        Number number = new Number();
        Number number1 = new Number();
        new Thread(()->{
            log.debug("begin");
            try {
                number.a();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();
        new Thread(()->{
            log.debug("begin");
            number1.b();
        }).start();
    }
}

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

输出结果为“2一秒后1”

8

@Slf4j
public class Test8Locks {
    public static void main(String[] args) {
        Number number = new Number();
        Number number1 = new Number();
        new Thread(()->{
            log.debug("begin");
            try {
                number.a();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();
        new Thread(()->{
            log.debug("begin");
            number1.b();
        }).start();
    }
}

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

输出结果“一秒后12”或者“2一秒后1”,因为他们锁住的都是类对象,而类对象只有一个

4.4 变量的线程安全分析

成员变量和静态变量是否线程安全

  1. 如果他们没有共享,则线程安全
  2. 如果他们被共享了,根据他们的状态是否能够改变,又分为两种情况
    a)如果只有读操作则线程安全
    b)如果有读写操作,则这段代码时临界区,需要考虑线程安全

局部变量是否线程安全

  1. 局部变量是线程安全的
  2. 但局部变量的引用对象则未必
    a)如果该对象没有逃离方法的作用访问,它是线程安全的
    b) 如果该对象逃离方法的作用范围,需要考虑线程安全

局部变量线程安全分析

public static void test1(){
	int i = 10;
	i++;
}

每个线程调用test1()方法时局部变量i,会在每个线程的栈帧内存中被创建多份,因此不存在共享
在这里插入图片描述
如图
在这里插入图片描述
局部变量的引用则稍有不同

先看一个成员变量的例子

public class TestThreadUnsafe {
    public static void main(String[] args) {
        ThreadUnsafe test = new ThreadUnsafe();
        for(int i = 0 ; i < 2 ; i++){
            new Thread(()->{
                test.method1(200);
            },"Thread" + (i + 1)).start();

        }
    }
}
class ThreadUnsafe{
    ArrayList<String> list = new ArrayList<>();
    public void method1(int loopNumber){
        for(int i = 0 ; i < loopNumber ; i++){
        // 临界区 会产生竞态条件
            method2();
            method3();
        }// 临界区
    }
    private void method2(){
        list.add("1");
    }
    private void method3(){
        list.remove(0);
    }
}

其中一种情况是线程1还没有add线程2就remove就会发生报错
在这里插入图片描述

分析:

  1. 无论哪个线程中的method2引用的都是同一个对象中的list变量
  2. method3同理
    在这里插入图片描述

改为局部变量之后此问题就会得到解决

class ThreadSafe{
    public void method1(int loopNumber){
        ArrayList<String> list = new ArrayList<>();
        for(int i = 0 ; i < loopNumber ; i++){
            method2(list);
            method3(list);
        }
    }
    private void method2(ArrayList<String> list){
        list.add("1");
    }
    private void method3(ArrayList<String> list){
        list.remove(0);
    }
}

分析:

  1. list是局部变量,每个线程调用时会创建不同的实例,没有共享
  2. 而method2中的参数是从method1中传进来的,与method1中引用同一个对象
  3. method3同method2
    在这里插入图片描述
    方法修饰符带来的思考,如果把method2和method3改为Public会不会代理线程安全问题
  4. 其他线程调用method2或者method3 ————没有问题,并不是同一个List对象
public class TestThreadUnsafe {
    public static void main(String[] args) {
        ThreadSageSubClass test = new ThreadSageSubClass();
        for(int i = 0 ; i < 2 ; i++){
            new Thread(()->{
                test.method1(200);
            },"Thread" + (i + 1)).start();

        }
    }
}
class ThreadSafe{
    public void method1(int loopNumber){
        ArrayList<String> list = new ArrayList<>();
        for(int i = 0 ; i < loopNumber ; i++){
            method2(list);
            //调用method3后,在子类中又会产生一个新的线程,并且list对于新线程和原来的线程是一样的
            method3(list);
        }
    }
    public void method2(ArrayList<String> list){
        list.add("1");
    }
    public void method3(ArrayList<String> list){
        list.remove(0);
    }
}
class ThreadSageSubClass extends ThreadSafe{
    @Override
    public void method3(ArrayList<String> list) {
        new Thread(()->{
            list.remove(0);
        }).start();

    }
}

从这个例子可以看出private和final提供【安全】的意义所在,体会开闭原则中的【闭】

常见线程安全类

  1. String
  2. Integer
  3. StringBuffer
  4. Random
  5. Vector
  6. Hashtable
  7. java.util.concurrent包下的包装类

这里说他们是线程安全的是指,多个线程调用他们同一个实例的某个方法时,是线程安全的,也可以理解为

  1. 他们的每个方法都是原子的
  2. 注意他们多个方法的组合不是原子的,后面会进一步分析

4.5 线程安全类方法的组合

分析下面的代码是否线程安全?

Hashtable table = new Hashtable();
//线程1 线程2
if(table,get("key") == null){
	table.put("key",value);
}

在这里插入图片描述
有可能会导致另一个线程的put覆盖了本来的意图

不可变类线程安全性

String,Interger等都是不可变类,因为其内部的状态不可以改变,因此他们的方法都是线程安全的
有人可能会有疑问,String有replace,substring等方法【可以】改变值啊,那么这些方法又是怎么保证线程安全的呢?
我们用String的substring方法作为一个例子,查看其源码
在这里插入图片描述
可以发现substring方法最后返回的是一个新对象,并没有修改原来的对象

4.6 Monitor概念

Java对象头

以32位虚拟机为例

  1. 普通对象
    在这里插入图片描述
  2. 数组对象
    在这里插入图片描述
    其中Mark Word结构为
    在这里插入图片描述

Monitor

Monitor被翻译为监视器管程
每个Java对象都可以关联一个Monitor对象,如果使用stnchronized给对象上锁(重量级)之后,该对象的MarkWord就被设置珍指向Monitor的指针
Monitor结构如下
在这里插入图片描述

  1. 刚开始Monitor中Owner为null
  2. 当Thread-2执行synchronized(obj)就会将Monitor的所有者Owner置为Thread-2,Monitor中只能有一个Owner
  3. 在Thread-2上锁的过程中,如果Thread-3,Thread-4,Thread-5也来执行synchronized(obj),就会进入EntryLIst BLOCKED
  4. Thread-2执行完同步代码块的内容,然后唤醒EntryList中等待的线程来竞争锁,竞争时是非公平的
  5. 途中的Thread-0,Thread-1是之前获得过锁,但条件不满足进入WAITING状态的线程,后面wait-notify时会分析
    注意:
  6. synchronized必须是进入同一个对象的monitor才能有效果
  7. 不加synchronized的对象不会关联监视器,不遵守以上规则

原理之synchronized

代码示例

public class Test2 {
    static final Object lock = new Object();
    static int counter = 0;
    public static void main(String[] args) {
        synchronized (lock){
            counter++;
        }
    }
}

其代码所对应的字节码为

0 getstatic #2 // <-lock引用(synchronized)开始
3 dup
4 astore_1 // lock引用 -> slot 1
5 monitorenter //将lock对象MarkWord置为Monitor的指针
6 getstatic #3 // <- i
9 iconst_1 // 准备常数1
10 iadd // +1
11 putstatic #3 //-> i
14 aload_1 // <- lock引用
15 monitorexit //将lock对象MarkWord重置,唤醒EntryList
16 goto 24 (+8) 
// 由异常表可知,若6-16行的代码出现异常则进入19行
19 astore_2 // 异常对象e -> slot 2
20 aload_1 // <- lock引用
21 monitorexit  //将lock对象MarkWord重置,唤醒EntryList
22 aload_2 // <- slot 2(e)
23 athrow // throw e
24 return

其对应的异常表为
在这里插入图片描述

4.7 synchronized优化原理

4.7.1 轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是synchronized(即会优先使用轻量级锁,当轻量级锁加锁失败时才会改变)
假设有两个方法同步块,利用同一个对象加锁

static final Object obj = new Object();
public static void method1(){
	synchronized(obj){
		//同步块A
		method2();
	}
}
public static void method2(){
	synchronized(obj){
		//同步块B
	}
}

加锁流程为

  1. 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word
    在这里插入图片描述
  2. 让锁记录中的Object reference指向锁对象,并尝试用cas替换Object的Mark Word,将Mark Word的值存入锁记录
    在这里插入图片描述
  3. 如果cas(compare and set)替换成功,对象头中存储了锁记录地址和状态00,表示由该线程给对象加锁,图示如下。
    在这里插入图片描述
  4. 如果cas失败,有两种情况
    a)如果是其他线程已经持有了该Object的轻量级锁,这时表明有竞争,进入锁膨胀过程
    b)如果是自己执行了synchronized锁重入(自己给自己加锁),那么再添加一条Lock Record作为重入的计数
    在这里插入图片描述
  5. 当退出synchronized代码块(解锁时)如果有取值为null的锁记录,表示有重入,这是重置锁记录,表示重入数量减一
    在这里插入图片描述
  6. 当退出synchronzied代码块(解锁时)锁记录不为null,这时使用cas将Mark Word的值回复给对象头
    a)成功,则恢复成功
    b)失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

4.7.2 锁膨胀

如果在尝试加轻量级锁的过程中,CAS操作无法成功,这是一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这是需要进行锁膨胀,将轻量级锁变成重量级锁

static Object obj = new Object();
public static void method1(){
	synchronized(obj){
		//同步块
	}
}
  1. 当Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁
    在这里插入图片描述

  2. 这时Thread-1 加轻量级锁失败,进入锁膨胀流程
    a)即为Object对象申请Monitor锁,让Object指向重量级锁地址
    b)然后自己进入Monitor的EntryList的BLOCKED
    在这里插入图片描述

  3. 当Thread-0退出同步块解锁时,使用cas将Mark Word的值回复给对象头,失败。此时会进入重量级解锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList中BLOCKED线程

4.7.3 自旋优化(多核CPU)

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞
自旋重试成功的情况
在这里插入图片描述
自旋重试失败的情况
在这里插入图片描述
总结:

  1. 在Java6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就会多自旋几次;反之,就少自旋甚至不自旋,总之比较智能
  2. 自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势
  3. Java7之后不能控制是否开启自旋功能

4.7.4 偏向锁

轻量级锁在没有竞争的时(就自己这个线程),每次重入仍然需要执行CAS操作
Java6中引入了偏向锁来进一步优化;只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有
例如:

static final Object obj = new Object();
public static void m1(){
	synchronized(obj){
		// 同步块A
		m2();
	}
}
public static void m2(){
	synchronized(obj){
		//同步块B
		m3();
	}
}
public static void m3(){
	synchronized(obj){
		//同步块C
	}
}

对比
在这里插入图片描述

在这里插入图片描述

偏向状态

一个对象创建时:

  1. 如果开启了偏向锁(默认开启),那么对象创建后,markword值为0x05即最后三位是101,这时他的thread,epoch,age都为0
  2. 偏向锁的开启默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加VM参数,XX:BiasedLockingStartupDealy=0来禁用延迟
  3. 如果没有开启偏向锁,那么对象创建后,markword值为0x01即最后三位为001,这时他的hashcode,age都为0,第一次用到hashcode时才会赋值
  4. 禁用偏向锁,添加VM参数,-XX:-UseBiasedLocking
  5. 调用对象的hashcode()方法后,会使偏向锁失效,因为在对象头中,hashcode要占用31位,会导致线程id无处存放

撤销-调用对象hashCode

调用了对象的hashCode,但偏向锁的对象MarkWord中存储的是线程id,如果调用hashCode会导致线程锁被撤销

  1. 轻量级锁会在锁记录中记录hashCode
  2. 重量级锁会在Monitor中记录hashCode
    在调用hashCode后使用偏向锁,要记得去掉-XX:-UseBiasedLocking

撤销-其他线程使用对象

当有其他线程使用偏向锁对象时,会将偏向锁升级成为轻量级锁

调用wait/notify

批量重偏向

如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重置对象的ThreadID
当撤销偏向锁阈值超过20次后,jvm会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程

批量撤销

当撤销偏向锁40次后,jvm会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,心间的对象也是不可偏向的。

4.7.5 锁消除

即时编译器(JIT)会在运行过程中判断代码块是否需要加锁,来进行锁消除操作,增加性能

4.8 wait notify

4.8.1 原理之wait/notify

在这里插入图片描述

  1. Owner线程发现条件不满足,调用wait方法,即可进入WaitSet变为WAITING状态
  2. BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片
  3. BLOCKED线程会在Owner线程释放锁时唤醒
  4. WAITING线程会在Owner线程调用notify或者notifyAll方法时唤醒,但唤醒后不以为着立刻获得锁,仍需进入EntryList重新竞争

API介绍

  1. obj.wait() 让进入object监视器的线程到waitSet等待
  2. obj.wait(time) 让进入object监视器的线程到waitSet等待一定时间,若时间到达没被唤醒会自动唤醒
  3. obj.notify()在object上正在waitSet等待的线程中挑一个唤醒
  4. obj.notifyAll()让object上正在waitSet等待的线程全部唤醒
    他们都是线程之间进行写作的手段,都属于object对象的方法。必须获得此对象的锁,才能调用这几个方法
@Slf4j
public class WaitTest {
    static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        lock.wait();
    }
}

以上代码会抛出异常Exception in thread “main” java.lang.IllegalMonitorStateException,因为还没有获取到他的锁
必须要获取到锁才能够正常运行,如下

@Slf4j
public class WaitTest {
    static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        synchronized (lock){
            lock.wait();
        }
    }
}

notify与notifyAll的区别

@Slf4j
public class WaitTest {
    static final Object obj = new Object();

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            synchronized (obj){
                log.debug("正在执行");
                try {
                    obj.wait(); //让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其他代码");
            }
        },"t1").start();

        new Thread(()->{
            synchronized (obj){
                log.debug("正在执行");
                try {
                    obj.wait();//让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其他代码");
            }
        },"t2").start();
        Thread.sleep(2000);
        log.debug("唤醒obj上其他线程");
        synchronized (obj){
            obj.notify(); //唤醒obj上一个线程
            //obj.notifyAll(); //唤醒obj上所有线程
        }
    }
}

notify()输出结果
在这里插入图片描述
notifyAll()输出结果
在这里插入图片描述

4.8.2 wait和notify的正确姿势

开始前先看看sleep(long n)和wait(long n)的区别

  1. sleep是Thread的方法,而wait是Object的方法
  2. sleep不需要强制和synchronized配合使用,但wait需要
  3. sleep在睡眠的同时,不会释放对象锁,但wait会释放
  4. 他们的状态都是TIMED_WAITING
@Slf4j
public class WaitNotifyStep1 {
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            synchronized (room){
                log.debug("没烟,先歇会");
                try {
                    room.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug("有烟没?[{}]",hasCigarette);
            if(hasCigarette){
                log.debug("可以开始干活了");
            }
        },"小南").start();
        for(int i = 0 ; i < 5 ; i++){
            new Thread(()->{
                synchronized (room){
                    log.debug("可以开始干活了");
                }
            },"其他人").start();
        }
        Thread.sleep(1000);
        new Thread(()->{
            synchronized (room){
                hasCigarette = true;
                log.debug("烟到了哦");
                room.notify();
            }
        },"送烟的").start();
    }
}

输出结果为
在这里插入图片描述

  1. 解决了其他干活的线程的阻塞问题
  2. 但如果有其他线程也在等待条件呢?
@Slf4j
public class WaitNotifyStep2 {
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;
    //虚假唤醒
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            synchronized (room){
                log.debug("有烟没?[{}]",hasCigarette);
                if(!hasCigarette){
                    log.debug("没烟,先歇会");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            log.debug("有烟没?[{}]",hasCigarette);
            if(hasCigarette){
                log.debug("可以开始干活了");
            }else{
                log.debug("没干成活");
            }
        },"小南").start();
        new Thread(()->{
            synchronized (room){
                Thread thread = Thread.currentThread();
                log.debug("外卖送到没?[{}]",hasTakeout);
                if(!hasTakeout){
                    log.debug("没外卖先歇会");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("外卖送到没?[{}]",hasTakeout);
                if(hasTakeout){
                    log.debug("可以开始干活了");
                }else{
                    log.debug("没干成活");
                }
            }
        },"小女").start();
        Thread.sleep(1000);
        new Thread(()->{
            synchronized (room){
                hasTakeout = true;
                log.debug("外卖到了哦");
                room.notify();
            }
        },"送外卖的").start();
    }
}

输出结果为
在这里插入图片描述
那么如何解决虚假唤醒呢

@Slf4j
public class WaitNotifyStep3 {
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;
    //虚假唤醒
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            synchronized (room){
                log.debug("有烟没?[{}]",hasCigarette);
                while(!hasCigarette){
                    log.debug("没烟,先歇会");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            log.debug("有烟没?[{}]",hasCigarette);
            if(hasCigarette){
                log.debug("可以开始干活了");
            }else{
                log.debug("没干成活");
            }
        },"小南").start();
        new Thread(()->{
            synchronized (room){
                Thread thread = Thread.currentThread();
                log.debug("外卖送到没?[{}]",hasTakeout);
                while(!hasTakeout){
                    log.debug("没外卖先歇会");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("外卖送到没?[{}]",hasTakeout);
                if(hasTakeout){
                    log.debug("可以开始干活了");
                }else{
                    log.debug("没干成活");
                }
            }
        },"小女").start();
        Thread.sleep(1000);
        new Thread(()->{
            synchronized (room){
                hasTakeout = true;
                log.debug("外卖到了哦");
                room.notifyAll();
            }
        },"送外卖的").start();
    }
}

只需要将判断条件改为while就可以了,这就是wait,notify使用的正确姿势
伪代码如下

synchronized(lock){
	while(条件不成立){
		lock.wait();
	}
	//干活
}
//另一个线程
synchronized(lock){
	lock.notifyAll();
}

同步模式之保护性暂停

即Guarded Suspension,用在一个线程等待另一个线程的执行结果

  1. 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个GuardedObject
  2. 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(后面说)
  3. JDK中,join的实现,Future的实现,采用的就是此模式
  4. 因为到等待另一方的结果,所以归类到同步模式
    在这里插入图片描述
    代码实现
@Slf4j
public class Test3 {
    public static void main(String[] args) {
        GuardedObject guardedObject = new GuardedObject();
        new Thread(()->{
            log.debug("等待结果");
            guardedObject.get();
            log.debug((String) guardedObject.get());
        },"t1").start();
        new Thread(()->{
            log.debug("执行下载");
            String test = "123";
            guardedObject.complete(test);
        },"t2").start();
    }
}
class GuardedObject{
    private Object response;
    public Object get(){
        synchronized (this){
            while(response == null){
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return response;
        }
    }
    public void complete(Object response){
        synchronized (this){
            this.response = response;
            this.notifyAll();
        }
    }
}

输出结果为
在这里插入图片描述
相比于join的优点

  1. 下载线程下载之后还可以干其他的事情,不需要一定等待他结束线程1才可以干其他的事情
  2. 等待的结果的变量可以为局部变量

功能增强(超时效果)

// 增强超时效果
    public Object get(long timeout){
        synchronized (this){
            //开始时间
            long begin = System.currentTimeMillis();
            //经历时间
            long passedTime = 0;
            while(response == null){
                // 这一轮循环应该等待的时间
                long waitTime = timeout - passedTime;
                //经历的时间超过了最大等待时间就退出循环
                if(waitTime <= 0){
                    break;
                }
                try {
                    // 防虚假唤醒
                    this.wait(waitTime);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 求得经历时间
                passedTime = System.currentTimeMillis() - begin;
            }
            return response;
        }
    }

扩展

途中Futures就好比居民楼每一层的信箱(每个信箱有房间编号),左侧的t0,t2,t4就好比等待邮件的居民,右侧的t1,t3,t5就好比邮递员
如果需要在多个类之间使用GuardedObject对象,作为参数传递不是很方便,因此设计一个用来解耦的中间件,这样不仅能够解耦【结果等待者】和【结果生产者】,还能够同时支持多个任务的管理
在这里插入图片描述
代码如下

@Slf4j
public class Test4 {
    public static void main(String[] args) throws InterruptedException {
        for(int i = 0 ; i < 3 ; i++){
            new People().start();
        }
        Thread.sleep(1000);
        for (Integer id : MailBoxes.getIds()) {
            new Postman(id,"内容" + id).start();
        }
    }
}
@Slf4j
class People extends Thread{
    @Override
    public void run() {
        // 收信
        GuardedObject1 guardedObject1 = MailBoxes.generateGuardedObject();
        log.debug("开始收信 id:{}",guardedObject1.getId());
        Object mail = guardedObject1.get(5000);
        log.debug("收到信 id{},内容:{}",guardedObject1.getId(),mail);
    }
}
@Slf4j
class Postman extends Thread{
    private int id;
    private String mail;
    public Postman(int id,String mail){
        this.id = id;
        this.mail = mail;
    }

    @Override
    public void run() {
        GuardedObject1 guardedObject = MailBoxes.getGuardedObject(id);
        guardedObject.complete(mail);
        log.debug("送信 id:{},内容:{}",id,mail);
    }
}
class MailBoxes{
    private static Map<Integer,GuardedObject1> boxes = new HashMap<>();

    // 产生唯一id
    private static int id = 1;
    private static synchronized int generateId(){
        return id++;
    }
    public static GuardedObject1 getGuardedObject(int id){
        return boxes.remove(id);
    }
    public synchronized static GuardedObject1 generateGuardedObject(){
        GuardedObject1 guardedObject1 = new GuardedObject1(generateId());
        boxes.put(guardedObject1.getId(),guardedObject1);
        return guardedObject1;
    }
    public static Set<Integer> getIds(){
        return boxes.keySet();
    }
}
class GuardedObject1{
    private int id;
    private Object response;
    public GuardedObject1(int id){
        this.id = id;
    }

    public int getId() {
        return id;
    }

    public Object get(){
        synchronized (this){
            while(response == null){
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return response;
        }
    }
    // 增强超时效果
    public Object get(long timeout){
        synchronized (this){
            //开始时间
            long begin = System.currentTimeMillis();
            //经历时间
            long passedTime = 0;
            while(response == null){
                // 这一轮循环应该等待的时间
                long waitTime = timeout - passedTime;
                //经历的时间超过了最大等待时间就退出循环
                if(waitTime <= 0){
                    break;
                }
                try {
                    // 防虚假唤醒
                    this.wait(waitTime);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 求得经历时间
                passedTime = System.currentTimeMillis() - begin;
            }
            return response;
        }
    }
    public void complete(Object response){
        synchronized (this){
            this.response = response;
            this.notifyAll();
        }
    }
}

输出结果为
在这里插入图片描述

4.9 异步模式之生产者/消费者

要点

  1. 与前面的保护性暂停中的GuardObject不同,不需要产生结果和消费结果的线程一一对应
  2. 消费队列可以用来平衡生产和消费的线程资源
  3. 生产者仅负责生产数据,不关心数据如何处理,而消费者专心处理消费结果数据
  4. 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
  5. JDK中各种阻塞队列,采用的就是这种模式
    在这里插入图片描述
    代码如下
public class Test5 {
    public static void main(String[] args) {
        MessageQueue queue = new MessageQueue(2);
        for(int i = 0 ; i < 3 ; i++){
            int id = i;
            new Thread(()->{
                queue.put(new Message(id,"值" + id));
            },"生产者" + i).start();
        }
        new Thread(()->{
            while(true){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                Message message = queue.take();
            }
        },"消费者").start();
    }
}
// 消息队列类,用于处理java线程之间的通信
@Slf4j
class MessageQueue{
    // 消息的队列集合
    private LinkedList<Message> list = new LinkedList<>();
    // 队列容量
    public MessageQueue(int capcity){
        this.capcity = capcity;
    }
    private int capcity;
    // 获取消息
    public Message take(){
        // 检查队列是否为空
        synchronized (list){
            while(list.isEmpty()){
                try {
                    log.debug("队列为空,消费者线程等待");
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 从队列的头部获取消息返回
            Message message = list.removeFirst();
            log.debug("已消费消息{}",message);
            list.notifyAll();
            return message;
        }
    }
    // 存入消息
    public void put(Message message){
        // 检查队列是否满
        synchronized (list){
            while(list.size() == capcity){
                try {
                    log.debug("队列已满,生产者线程等待");
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            list.addLast(message);
            log.debug("已生产消息:{}",message);
            list.notifyAll();
        }
    }
}
class Message{
    private int id;
    private Object value;
    public Message(int id,Object value){
        this.id = id;
        this.value = value;
    }
    public int getId() {
        return id;
    }
    public Object getValue() {
        return value;
    }
    @Override
    public String toString() {
        return "Message{" +
                "id=" + id +
                ", value=" + value +
                '}';
    }
}

输出结果为
在这里插入图片描述

4.10 Park & Unpark

基本使用

他们是LockSupport类中的方法

// 暂停当前线程
LockSupport.park();
// 回复某个线程的运行
LockSupport.unpark(暂停线程对象);

先park后unpark的基本使用

@Slf4j
public class ParkTest {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            log.debug("start...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("park...");
            LockSupport.park();
            log.debug("resume");
        });
        t1.start();
        Thread.sleep(2000);
        log.debug("unpark...");
        LockSupport.unpark(t1);
    }
}

输出结果为
在这里插入图片描述

特点

与Object的wait&notify相比

  1. wait,notify,notifyAll必须配合Object Monitor一起使用,而park,unpark不必
  2. park & unpark是以线程为单位来【阻塞】和【唤醒】线程,而notify只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】
  3. park&unpark可以先unpark,但是wait&notify不能先notify

原理之park & unpark

每个线程都有一个自己的一个Parker对象,由三部分组成,_counter,_cond,_mutex,打个比喻

  1. 线程就像一个旅人,Parker就像他随身携带的书包,条件变量就好比背包中的帐篷。_counter就好比背包中的备用干粮(0为耗尽,1为充足)
  2. 调用park就是看需不需要停下来休息
    a)如果备用干粮耗尽。那么钻进帐篷里休息
    b)如果备用干粮充足,那么不需停留,继续前进。
  3. 调用unpark,就好比令干粮充足
    a)如果这时线程还在帐篷,就唤醒他让他继续前进
    b)如果这时线程还在运行,那么他下次调用park时,仅是消耗掉备用干粮,不需停留继续前进
    c)因为背包空间有限,多次调用unaprk只会补充一份备用干粮
    在这里插入图片描述
    1 当前线程调用Unsafe.park()方法
    2 检查_counter.本情况为0,这时获得_mutex互斥锁
    3 线程进入_cond条件变量阻塞
    4 设置_couner为0
    在这里插入图片描述

1 调用Unsafe.unpark()方法,设置_counter为1
2 当前线程调用Unsafe.park()方法
3 检查_counter,本情况为1,这时线程无需阻塞,继续运行
4 设置_counter为0

4.11 重新理解线程状态转换

在这里插入图片描述
假设有线程Thread t

情况1 NEW --> RUNNABLE

当调用t.start()方法时,由NEW --> RUNNABLE

情况2 RUNNABLE <–> WAITING

t线程用synchronized(obj)获取了对象锁之后
调用obj.wait()方法时,t线程从RUNNABLE --> WAITING
调用obj.notify(),obj.notifyAll(),t.interrupt()时
a)竞争锁成功,t线程从WAITING --> RUNNABLE
b)竞争锁失败,t线程从 WAITING --> BLOCKED

情况3 RUNNABLE<–> WAITING

当前线程调用t.join()方法时,当前线程从RUNNABLE --> WAITING
注意是当前线程t线程对象的监视器上等待
t线程运行结束,或调用了当前线程的interrupt()时,当前线程从WAITING --> RUNNING

情况4 RUNNABLE <–> RUNNING

当前线程调用LockSupport.park()方法会让当前线程从 RUNNBALE --> RUNNING
调用LockSupport.unpark(目标线程)或调用了线程的interrupt(),会让目标线程从WAITING --> RUNNABLE

情况5 RUNNABLE <–> TIMED_WAITING

t线程用synchronized(obj)获取了对象锁之后
调用obj.wait(long n)方法时,t线程从RUNNABLE --> TIMED_WAITING
调用obj.notify(),obj.notifyAll(),t.interrupt()时
a)竞争锁成功,t线程从TIMED_WAITING --> RUNNABLE
b)竞争锁失败,t线程从 TIMED_WAITING --> BLOCKED

情况6 RUNNABLE <–> TIMED_WAITING

当前线程调用t.join(long n)方法时,当前线程从RUNNABLE --> TIMED_WAITING
注意是当前线程t线程对象的监视器上等待
当前线程等待时间超过n毫秒,或者t线程运行结束,或调用了当前线程的interrupt()时,当前线程从TIMED_WAITING–> RUNNABLE

情况7 RUNNABLE <–> TIMED_WAITING

当前线程调用Thread.sleep(long n),当前线程从RUNNBALE --> TIMED_WAITING
当前线程等待时间超过了n毫秒,当前线程从TIMED_WAITING --> RUNNABLE

情况8 RUNNABLE <–> TIMED_WAITING

当前线程调用LockSupport.parkNanos(long nanos)或LockSupport.parkUntil(long millis)时,当前线程从RUNNABLE --> TIMED_WAITING
调用LockSupport.unpark(目标线程)或调用了线程了interrupt(),或是等待超时,会让目标线程从TIMED_WAITING --> RUNNABLE

情况9 RUNNABLE <–> BLOCKED

t线程用synchronized(obj)获取了对象锁时如果竞争失败,从RUNNABLE --> BLOCKED
持obj锁线程的同步代码块执行完毕,会唤醒该对象上所有的BLOCKED的线程重新竞争,如果其中t线程竞争成功,从BLOCKED --> RUNNABLE,其他失败的线程仍然是 BLOCKED

情况10 RUNNABLE <–> TERMINATED

当前线程所有代码运行完毕,进入TERMINATED

4.11 多把锁

多把不相干的锁

一间大屋子有两个功能:睡觉,学习,互不相干
现在小南要学习,小女要睡觉,但如果只用一间屋子的话(一个对象锁)的话,那么并发度很低
解决方法是准备多个房间(多个对象锁)
例如

public class TestMutiLock {
    public static void main(String[] args) {
        BigRoom bigRoom = new BigRoom();
        new Thread(()->{
            try {
                bigRoom.sleepd();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();
        new Thread(()->{
            try {
                bigRoom.study();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();
    }
}
@Slf4j
class BigRoom{
    public void sleepd() throws InterruptedException {
        synchronized (this){
            log.debug("sleeping两个小时");
            Thread.sleep(2000);
        }
    }
    public void study() throws InterruptedException {
        synchronized (this){
            log.debug("学习一个小时");
            Thread.sleep(1000);
        }
    }
}

输出结果为
在这里插入图片描述
可以发现其并发度很低,如何改进呢,如下

@Slf4j
class BigRoom{
    private final Object bedRoom = new Object();
    private final Object studyRoom = new Object();
    public void sleepd() throws InterruptedException {
        synchronized (bedRoom){
            log.debug("sleeping两个小时");
            Thread.sleep(2000);
        }
    }
    public void study() throws InterruptedException {
        synchronized (studyRoom){
            log.debug("学习一个小时");
            Thread.sleep(1000);
        }
    }
}

现在的输出结果为
在这里插入图片描述
可以发现他们是同时进行的,将锁的粒度细分

  1. 好处是可以增强并发度
  2. 坏处是如果一个线程需要同时获得多把锁,就容易发生死锁

4.12 活跃性

死锁

有这样的情况:一个线程需要获得多把锁,这时就容易发生死锁
t1线程获得A对象的锁,接下来像获取B对象的锁
t2线程获得B对象的锁,接下来像获取A对象的锁
例如

@Slf4j
public class TestDeadLock {
    public static void main(String[] args) {
        test();
    }
    public static void test(){
        Object A = new Object();
        Object B = new Object();
        Thread t1 = new Thread(()->{
            synchronized (A){
                log.debug("lock A");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (B){
                    log.debug("lock B");
                    log.debug("操作");
                }
            }
        },"t1");
        Thread t2 = new Thread(()->{
            synchronized (B){
                log.debug("lock B");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (A){
                    log.debug("lock A");
                    log.debug("操作");
                }
            }
        },"t2");
        t1.start();
        t2.start();
    }
}

就会陷入死锁

定位死锁

检测死锁可以使用jconsole工具,或者使用jps定位进程id,再使用jstack定位死锁
我们先使用jps查看java进程ID
在这里插入图片描述
再使用jstack + 进程ID来查看进程信息
在这里插入图片描述
其输出的内容很多
在这里插入图片描述
这里可以发现两个线程的状态为BLOCKED,他们正在等待锁"waiting to lock < xxxx >"
往下翻一翻可以发现其为我们列出了死锁
在这里插入图片描述
并且还告诉了我们是哪一行代码发生了死锁

另一种方法是使用jconsole

我们可以在这里打开jconsole进行查看
在这里插入图片描述
在这里点击死锁就可以查看死锁在这里插入图片描述
可以发现其列出了死锁和对应的行数
在这里插入图片描述

经典问题 哲学家就餐问题

在这里插入图片描述
有五个哲学家,围坐在圆桌旁

  1. 他们只做两件事,思考和吃饭,思考一会吃个饭,吃完饭后接着思考
  2. 他们吃饭时要用两根筷子吃,桌上共有五个筷子,每位哲学家左右手边各有一个筷子
  3. 如果筷子被身边人拿着,自己就得等待
@Slf4j
public class PhilosopherProblem {
    public static void main(String[] args) {
        ChopSticks c1 = new ChopSticks("1");
        ChopSticks c2 = new ChopSticks("2");
        ChopSticks c3 = new ChopSticks("3");
        ChopSticks c4 = new ChopSticks("4");
        ChopSticks c5 = new ChopSticks("5");
        new Philosopher("张峰",c1,c2).start();
        new Philosopher("问候",c2,c3).start();
        new Philosopher("杨勇",c3,c4).start();
        new Philosopher("夏鑫宇",c4,c5).start();
        new Philosopher("王沛",c5,c1).start();
    }
}
@Slf4j
class Philosopher extends Thread{
    ChopSticks left;
    ChopSticks right;
    public Philosopher(String name,ChopSticks left,ChopSticks right){
        super(name);
        this.left = left;
        this.right = right;
    }

    @Override
    public void run() {
        while(true){
            synchronized (left){
                synchronized (right){
                    try {
                        eat();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    private void eat() throws InterruptedException {
        log.debug("eating");
        Thread.sleep(1000);
    }
}
class ChopSticks{
    String name;
    public ChopSticks(String name){
        this.name = name;
    }

    @Override
    public String toString() {
        return "筷子{" + name + "}";
    }
}

运行发现输出结果卡在这里不动了

使用jconsole检测死锁,发现

名称: 王沛
状态: org.example.Test.ChopSticks@4f6b6d07上的BLOCKED, 拥有者: 张峰
总阻止数: 1, 总等待数: 0
堆栈跟踪: 
org.example.Test.Philosopher.run(PhilosopherProblem.java:36)
   - 已锁定 org.example.Test.ChopSticks@1d99b39b
名称: 张峰
状态: org.example.Test.ChopSticks@59bd0fee上的BLOCKED, 拥有者: 问候
总阻止数: 2, 总等待数: 1
堆栈跟踪: 
org.example.Test.Philosopher.run(PhilosopherProblem.java:36)
   - 已锁定 org.example.Test.ChopSticks@4f6b6d07
名称: 问候
状态: org.example.Test.ChopSticks@618bfff2上的BLOCKED, 拥有者: 杨勇
总阻止数: 2, 总等待数: 2
堆栈跟踪: 
org.example.Test.Philosopher.run(PhilosopherProblem.java:36)
   - 已锁定 org.example.Test.ChopSticks@59bd0fee
名称: 杨勇
状态: org.example.Test.ChopSticks@74392739上的BLOCKED, 拥有者: 夏鑫宇
总阻止数: 10, 总等待数: 2
堆栈跟踪: 
org.example.Test.Philosopher.run(PhilosopherProblem.java:36)
   - 已锁定 org.example.Test.ChopSticks@618bfff2
名称: 夏鑫宇
状态: org.example.Test.ChopSticks@1d99b39b上的BLOCKED, 拥有者: 王沛
总阻止数: 2, 总等待数: 0
堆栈跟踪: 
org.example.Test.Philosopher.run(PhilosopherProblem.java:36)
   - 已锁定 org.example.Test.ChopSticks@74392739

这种线程没有按预期结束,执行不下去的情况,归类为【活跃性】问题,除了死锁以外,还有活锁和接着两个情况

活锁

活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,如

@Slf4j
public class TestLikeLock {
    static volatile int count = 10;
    public static void main(String[] args) {
        new Thread(()->{
            // 期望到0退出循环
            while(count > 0){
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count--;
                log.debug("count : {}",count);
            }
        },"t1").start();
        new Thread(()->{
            // 期望到20退出循环
            while(count < 20){
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count++;
                log.debug("count : {}",count);
            }
        },"t2").start();
    }
}

饥饿

很多教程把饥饿定义为,一个线程由于优先级太低,始终得不到CPU调度执行也,也不能够结束,饥饿的情况不易演示,讲读写锁时会涉及饥饿问题
下面我讲一下我遇到的一个饥饿线程的例子,先来看看使用顺序加锁的方式解决之前的死锁问题
在这里插入图片描述
顺序加锁的解决方案
在这里插入图片描述
但是这样容易发生饥饿现象,拿之前的哲学家问题举例,如果将第五个哲学家的c5,c1改成c1,c5就不会出现死锁

@Slf4j
public class PhilosopherProblem {
    public static void main(String[] args) {
        ChopSticks c1 = new ChopSticks("1");
        ChopSticks c2 = new ChopSticks("2");
        ChopSticks c3 = new ChopSticks("3");
        ChopSticks c4 = new ChopSticks("4");
        ChopSticks c5 = new ChopSticks("5");
        new Philosopher("张峰",c1,c2).start();
        new Philosopher("问候",c2,c3).start();
        new Philosopher("杨勇",c3,c4).start();
        new Philosopher("夏鑫宇",c4,c5).start();
        new Philosopher("王沛",c1,c5).start();
    }
}

但是输出结果基本上都是夏鑫宇哲学家在吃饭,而王沛哲学家几乎没吃过饭,就发生了饥饿现象
在这里插入图片描述

4.13 ReentrantLock

相对于synchronized他具备一下特点

  1. 可中断
  2. 可以设置超时时间
  3. 可以设置为公平锁
  4. 支持多个条件变量
    与synchronized一样,都支持可重入
    基本语法
//获取锁
reentarntLock.lock();
try{
	//临界区
}finally{
	//释放锁
	reentrantLock.unlock();
}

可重入

可重入是指同一个线程如果首次获得了这把锁,那么因为他是这把锁的拥有者,因此有权利再次获得这把锁
如果是不可重入锁,那么第二次获得锁时,自己也会被挡住
例如

@Slf4j
public class ReentrantLockTest {
    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        lock.lock();
        try {
            log.debug("enter main");
            m1();

        }finally {
            lock.unlock();
        }
    }
    public static void m1(){
        lock.lock();
        try {
            log.debug("enter m1");
            m2();
        }finally {
            lock.unlock();
        }
    }
    public static void m2(){
        lock.lock();
        try {
            log.debug("enter m2");
        }finally {
            lock.unlock();
        }
    }
}

输出结果为
在这里插入图片描述
证明其可以被同一个线程进入多次同一把锁

可打断

@Slf4j
public class LockInterruptly {
    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            try {
                // 如果没有竞争那么此方法就会获取lock对象锁
                // 如果有竞争就进入阻塞队列,可以被其他线程用interrupt方法打断
                log.debug("尝试获得锁");
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.debug("没有获得锁,返回");
                return;
            }
            try {
                log.debug("获得到锁");
            }finally {
                lock.unlock();
            }
        },"t1");
        lock.lock();
        t1.start();
        Thread.sleep(1000);
        log.debug("打断t1");
        t1.interrupt();
    }
}

输出结果为
在这里插入图片描述
可以发现线程t1中的锁被打断了,不会无限制的等待下去,这也是一种防止死锁的有效方式

锁超时

@Slf4j
public class ReentrantTryLock {
    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            log.debug("尝试获得锁");
            if(!lock.tryLock()){
                log.debug("获取不到锁");
                return;
            }
            try {
                log.debug("获得到锁");
            }finally {
                lock.unlock();
            }
        },"t1");
        lock.lock();
        log.debug("获得到锁");
        t1.start();
    }
}

tryLock也有携带参数的方法,表示最大等待时间
在这里插入图片描述

锁超时解决哲学家就餐问题

@Override
    public void run() {
        while(true){
            // 尝试获得左手筷子
            if(left.tryLock()){
                try {
                    // 尝试获得右手筷子
                    if(right.tryLock()){
                        try {
                            eat();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        } finally {
                            right.unlock();
                        }
                    }
                }finally {
                    left.unlock(); // 释放自己左手的筷子
                }
            }
        }
    }

使用tryLock成功解决哲学家就餐问题

公平锁

ReentrantLock默认是不公平(后来的也可以先运行)的,公平(排队)锁一般是没有必要的,会降低并发度,后面再进行具体分析

条件变量

synchronized中也有条件变量,就是我们讲原理时那个waitSet休息室,当条件不满足的时候进入waitSet等待
ReentrantLock的条件变量比synchronized强大之处在于,它是支持多个条件变量的,这就好比
a)synchronized是那些不满足条件的线程都在一间休息室休息
b)而ReentrantLock支持多间休息室,有专门等烟的休息室,专门等早餐的休息室,唤醒时也是根据休息室来唤醒
使用流程

  1. await前需要获得锁
  2. await执行后,会释放锁,进入conditionObject等待
  3. await的线程被唤醒(或打断,或超时)重新竞争lock锁
  4. 竞争lock锁成功后,从await后继续执行
    例如
public class ReentrantLockCondition {
    static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
        // 创建一个新的条件变量(休息室)
        Condition condition = lock.newCondition();
        Condition condition1 = lock.newCondition();
        lock.lock();
        // 进入休息室等待
        condition1.await();
        condition1.await(1000, TimeUnit.MINUTES);
        condition1.signal();
        condition1.signalAll();
    }
}

使用例子

@Slf4j
public class ReentrantLockConditionTest {
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;
    static ReentrantLock ROOM = new ReentrantLock();
    static Condition waitCigaretteSet = ROOM.newCondition();
    static Condition waitTakeoutSet = ROOM.newCondition();
    //虚假唤醒
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            ROOM.lock();
            try {
                log.debug("有烟没?[{}]",hasCigarette);
                while(!hasCigarette){
                    log.debug("没烟,先歇会");
                    try {
                        waitCigaretteSet.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("可以开始干活了");
            }finally {
                ROOM.unlock();
            }
        },"小南").start();
        new Thread(()->{
            ROOM.lock();
            try {
                log.debug("外卖送到没?[{}]",hasTakeout);
                while(!hasTakeout){
                    log.debug("没外卖先歇会");
                    try {
                        waitTakeoutSet.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("可以开始干活了");
            }finally {
                ROOM.unlock();
            }
        },"小女").start();
        Thread.sleep(1000);
        new Thread(()->{
            ROOM.lock();
            try {
                waitTakeoutSet.signal();
                hasTakeout = true;
            }finally {
                ROOM.unlock();
            }
        },"送外卖的").start();
        Thread.sleep(1000);
        new Thread(()->{
            ROOM.lock();
            try {	
                waitCigaretteSet.signal();
                hasCigarette = true;
            }finally {
                ROOM.unlock();
            }
        },"送烟的").start();
    }
}

同步模式之顺序控制

4.14固定运行顺序

比如必须先2后1打印

wait notify版打印

@Slf4j
public class Test6 {
    static final Object lock = new Object();
    // 表示t2是否运行过
    static boolean t2run = false;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (lock){
                while(!t2run){
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("1");
            }
        },"t1");
        Thread t2 = new Thread(()->{
            synchronized (lock){
                log.debug("2");
                t2run = true;
                lock.notifyAll();
            }
        },"t2");
        t1.start();
        t2.start();
    }
}

park unpark版打印

@Slf4j
public class Test7 {
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            LockSupport.park();
            log.debug("1");
        },"t1");
        Thread t2 = new Thread(()->{
            log.debug("2");
            LockSupport.unpark(t1);
        },"t2");
        t1.start();
        t2.start();
    }
}

交替输出

三个线程交替输出abc五次

wait notify版

@Slf4j
public class Test8 {
    public static void main(String[] args) throws InterruptedException {
        WaitNotify waitNotify = new WaitNotify(5,1);
        Thread t1 = new Thread(()->{
            try {
                waitNotify.print("a",1,2);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        },"t1");
        Thread t2 = new Thread(()->{
            try {
                waitNotify.print("b",2,3);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        },"t2");
        Thread t3 = new Thread(()->{
            try {
                waitNotify.print("c",3,1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        },"t3");
        t1.start();
        t2.start();
        t3.start();
    }
}
class WaitNotify{
    private int loopNumber;
    private int flag;
    public WaitNotify(int loopNumber,int flag){
        this.flag = flag;
        this.loopNumber = loopNumber;
    }
    public void print(String str,int waitFlag,int nextFlag) throws InterruptedException {
        for(int i = 0 ; i < loopNumber ; i++){
            synchronized (this){
                while(flag != waitFlag){
                    this.wait();
                }
                System.out.print(str);
                flag = nextFlag;
                this.notifyAll();
            }
        }
    }
}

await signal版

public class Test9 {
    public static void main(String[] args) throws InterruptedException {
        WaitSignal waitSignal = new WaitSignal(5);
        Condition a = waitSignal.newCondition();
        Condition b = waitSignal.newCondition();
        Condition c = waitSignal.newCondition();
        new Thread(()->{
            try {
                waitSignal.print("a",a,b);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(()->{
            try {
                waitSignal.print("b",b,c);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(()->{
            try {
                waitSignal.print("c",c,a);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        Thread.sleep(1000);
        waitSignal.lock();
        try {
            System.out.println("开始...");
            a.signal();
        }finally {
            waitSignal.unlock();
        }
    }
}
class WaitSignal extends ReentrantLock{
    private int loopNumber;
    public WaitSignal(int loopNumber){
        this.loopNumber = loopNumber;
    }
    public void print(String str, Condition current,Condition next) throws InterruptedException {
        for(int i = 0 ; i < loopNumber ; i++){
            lock();
            try {
                current.await();
                System.out.print(str);
                next.signal();
            }finally {
                unlock();
            }
        }
    }
}

park unpark版

public class Test10 {
    static Thread t1;
    static Thread t2;
    static Thread t3;
    public static void main(String[] args) throws InterruptedException {
        WaitUnpark waitUnpark = new WaitUnpark(5);
        t1 = new Thread(() -> {
            waitUnpark.print("a",t2);
        });
        t2 = new Thread(() -> {
            waitUnpark.print("b",t3);
        });
        t3 = new Thread(() -> {
            waitUnpark.print("c",t1);
        });
        t1.start();
        t2.start();
        t3.start();
        Thread.sleep(1000);
        LockSupport.unpark(t1);
    }
}
class WaitUnpark{
    private int loopNumber;
    public WaitUnpark(int loopNumber){
        this.loopNumber = loopNumber;
    }
    public void print(String str,Thread next){
        for(int i = 0 ; i < loopNumber ;i++){
            LockSupport.park();
            System.out.print(str);
            LockSupport.unpark(next);
        }
    }
}

本章小节

本章我们需要重点掌握的是

  1. 分析多线程访问共享资源时那么,哪些代码属于临界区
  2. 使用synchronized互斥解决临界区的线程安全问题
    a)掌握synchronized锁对象语法
    b)掌握synchronized加载成员方法和静态方法语法
    c)掌握wait/notify同步方法
  3. 使用lock互斥解决临界区的线程安全问题
    a)掌握lock的使用细节:可打断,锁超时,公平锁,条件变量
  4. 学会分析变量的线程安全性,掌握常见线程安全类的使用
  5. 了解线程活跃性问题:死锁,活锁,饥饿
  6. 应用方面
    a)互斥:使用synchronized或Lock达到共享资源互斥效果
    b)同步:使用wait/notify或Lock的条件变量达到线程间通信效果
  7. 原理方面
    a)monitor,synchronized,wait/notify原理
    b)synchronized进阶原理
    c)park & unpark原理
  8. 模式方面
    a)同步模式之保护性暂停
    b)异步模式之生产者消费者
    c)同步模式之顺序控制
  • 21
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值