JUC高并发

----------------------------并发编程-------------------------------------------

1. 概览

这门课中的【并发】一词涵盖了在 Java 平台上的

  • 进程
  • 线程
  • 并发
  • 并行

以及 Java 并发工具、并发问题以及解决方案,同时我也会讲解一些其它领域的并发

JUC高并发_第1张图片

 2. 进程与线程

2.1 进程与线程

进程
  • 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO
  • 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
  • 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)
线程
  • 一个进程之内可以分为一到多个线程。
  • 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
  • Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作为线程的容器
二者对比
  • 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
  • 进程拥有共享的资源,如内存空间等,供其内部的线程共享
  • 进程间通信较为复杂,同一台计算机的进程通信称为 IPCInter-process communication
  • 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
  • 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同个共享变量
  • 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低

3. Java 线程

3.1 创建和运行线程

3.1.1 直接使用 Thread

package cn.itcast.test;

import lombok.extern.slf4j.Slf4j;

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

        Thread t = new Thread("thread") {
            @Override
            public void run() {
                log.debug("thrad is running");
            }
        };
        t.setName("threadT");
        t.start();
    }
}

3.1.2 使用 Runnable 配合 Thread

把【线程】和【任务】(要执行的代码)分开
  • Thread 代表线程
  • Runnable 可运行的任务(线程要执行的代码)
package cn.itcast.test;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test2")
public class Test2 {
    public static void main(String[] args) {
        //代码执行体
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                log.debug("thrad is running");
            }
        };
        //创建线程
        Thread t = new Thread(runnable, "thread");
        t.setName("threadT");
        t.start();
    }
}
原理之 Thread Runnable 的关系
  • 方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了
  • Runnable 更容易与线程池等高级 API 配合
  • Runnable 让任务类脱离了 Thread 继承体系,更灵活

3.1.3 FutureTask 配合 Thread

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

package cn.itcast.test;

import lombok.extern.slf4j.Slf4j;

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

@Slf4j(topic = "c.Test1")
public class Test3{
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask task = new FutureTask(new Callable() {
            @Override
            public Integer call() throws Exception {
                log.debug("thrad is running");
                Thread.sleep(1000);
                return 100;
            }
        });
        Thread t = new Thread(task, "threadT");
        t.start();
        log.debug("{}", task.get());
    }
}

3.2 观察多个线程同时运行

主要是理解
  • 交替执行,由底层的任务调度器决定
  • 谁先谁后,不由我们控制

3.3 查看进程线程的方法
windows
  • 任务管理器可以查看进程和线程数,也可以用来杀死进程
  • tasklist 查看进程
  • taskkill 杀死进程
linux
  • ps -fe 查看所有进程
  • ps -fT -p 查看某个进程(PID)的所有线程
  • kill 杀死进程
  • top 按大写 H 切换是否显示线程
  • top -H -p 查看某个进程(PID)的所有线程
Java
  • jps 命令查看所有 Java 进程
  • jstack 查看某个 Java 进程(PID)的所有线程状态
  • jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)
jconsole 远程监控配置
  • 需要以如下方式运行你的 java
  • 修改 /etc/hosts 文件将 127.0.0.1 映射至主机名
java -Djava.rmi.server.hostname=`ip地址` -Dcom.sun.management.jmxremote -
Dcom.sun.management.jmxremote.port=`连接端口` -Dcom.sun.management.jmxremote.ssl=是否安全连接 -
Dcom.sun.management.jmxremote.authenticate=是否认证 java类
如果要认证访问,还需要做如下步骤
  • 复制 jmxremote.password 文件
  • 修改 jmxremote.password jmxremote.access 文件的权限为 600 即文件所有者可读写
  • 连接时填入 controlRole(用户名),R&D(密码)

3.4 * 原理之线程运行

Java Virtual Machine Stacks Java 虚拟机栈)
我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟 机就会为其分配一块栈内存。
  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

JUC高并发_第2张图片

线程上下文切换( Thread Context Switch
因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码
  • 线程的 cpu 时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了 sleepyieldwaitjoinparksynchronizedlock 等方法
Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态, Java 中对应的概念就是程序计数器(Program Counter Register ),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的
  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
  • Context Switch 频繁发生会影响性能

JUC高并发_第3张图片

 3.5 常见方法

JUC高并发_第4张图片

JUC高并发_第5张图片JUC高并发_第6张图片

 3.6 start 与 run

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

3.7 sleep yield

sleep
  • 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
  •  其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
  •  睡眠结束后的线程未必会立刻得到执行
  • 建议用 TimeUnit sleep 代替 Thread sleep 来获得更好的可读性
package cn.itcast.test;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test7")
public class Test7 {

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread("t1") {
            @Override
            public void run() {
                log.debug("enter sleep...");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    //休眠线程被打断这时 sleep 方法会抛出 InterruptedException
                    log.debug("wake up...");
                    e.printStackTrace();
                }
            }
        };
        t1.start();
        //主线程休眠,t1线程加入
        Thread.sleep(1000);
        log.debug("interrupt...");
        //其它线程可以使用 interrupt 方法打断正在睡眠的线程
        t1.interrupt();
    }
}
yield
  • 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
  • 具体的实现依赖于操作系统的任务调度器
package cn.itcast.test;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test9")
public class Test9 {

    public static void main(String[] args) {
        Runnable task1 = () -> {
            int count = 0;
            for (; ; ) {
                System.out.println("---->1 " + count++);
            }
        };
        Runnable task2 = () -> {
            int count = 0;
            for (; ; ) {
                //调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
                Thread.yield();
                System.out.println("              ---->2 " + count++);
            }
        };
        Thread t1 = new Thread(task1, "t1");
        Thread t2 = new Thread(task2, "t2");
        t1.start();
        t2.start();
    }
}
线程优先级
  • 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
  • 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 空闲时,优先级几乎没作用
package cn.itcast.test;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test9")
public class Test9 {

    public static void main(String[] args) {
        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.MIN_PRIORITY);
        t2.setPriority(Thread.MAX_PRIORITY);
        t1.start();
        t2.start();
    }
}

 3.8 join 方法详解

下面的代码执行,打印 r 是什么?

package cn.itcast.test;

import lombok.extern.slf4j.Slf4j;

import static cn.itcast.n2.util.Sleeper.sleep;

@Slf4j(topic = "c.Test10")
public class Test10 {
    static int r = 0;
    public static void main(String[] args) throws InterruptedException {
        test1();
    }
    private static void test1() throws InterruptedException {
        log.debug("开始");
        Thread t1 = new Thread(() -> {
            log.debug("开始");
            sleep(1);
            log.debug("结束");
            r = 10;
        },"t1");
        t1.start();
        //t1线程加入主线程,主线程等待t1线程结束在结束
        //t1.join();
        //打印结果为0,因为主线程代码不会等待其他线程先执行
        log.debug("结果为:{}", r);
        log.debug("结束");
    }
}
分析
  • 因为主线程和线程 t1 是并行执行的,t1 线程需要 1 秒之后才能算出 r=10
  • 而主线程一开始就要打印 r 的结果,所以只能打印出 r=0
解决方法
  • join,加在 t1.start() 之后即可

应用之同步

JUC高并发_第7张图片

3.9 interrupt 方法详解

打断 sleepwaitjoin 的线程

  • 这几个方法都会让线程进入阻塞状态
  • 打断 sleep 的线程, 会清空打断状态,以 sleep 为例
package cn.itcast.test;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test11")
public class Test11 {

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            log.debug("sleep...");
            try {
                Thread.sleep(5000); // wait, join一样
            } catch (InterruptedException e) {
                //被阻塞的线程被打断会报InterruptedException异常
                e.printStackTrace();
            }
        },"t1");

        t1.start();
        Thread.sleep(1000);
        log.debug("interrupt");
        //打断标记,让线程停止
        t1.interrupt();
        //打断 sleep 的线程, 会清空打断状态,isInterrupted()的值为false
        log.debug("打断标记:{}", t1.isInterrupted());
    }
}

输出:

JUC高并发_第8张图片

打断正常运行的线程

打断正常运行的线程 , 不会清空打断状态
package cn.itcast.test;

import lombok.extern.slf4j.Slf4j;

/**
 * intercupt方法的使用
 */
@Slf4j(topic = "c.Test12")
public class Test12 {

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (true) {
                //获取当前线程的打断标记
                boolean interrupted = Thread.currentThread().isInterrupted();
                //未阻塞线程,手动处理打断标记
                if (interrupted) {
                    log.debug("被intercupt方法打断");
                    break;
                }
            }
        }, "t");
        t.start();
        //让主线程等待一会儿,不然t线程未执行调用intercupt方法会报错
        Thread.sleep(1000);
        log.debug("intercupt");
        //调用该方法时,会把调用线程的该方法返回值设置为true,但是不会直接终止线程,要终止线程则需要我们手动编码实现
        t.interrupt();
    }
}
模式之两阶段终止
打断 park 线程
package cn.itcast.test;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.LockSupport;

import static cn.itcast.n2.util.Sleeper.sleep;

@Slf4j(topic = "c.Test14")
public class Test14 {

    private static void test4() {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                log.debug("park...");
                //让线程t1阻塞
                LockSupport.park();
                log.debug("打断状态:{}", Thread.interrupted());
            }
        });
        t1.start();
        
        sleep(1);
        t1.interrupt();
    }

    private static void test3() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            log.debug("park...");
            //让线程t1阻塞
            LockSupport.park();
            log.debug("unpark...");
            log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
        }, "t1");
        t1.start();

        sleep(1);
        t1.interrupt();

    }

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

提示:可以使用 Thread.interrupted() 清除打断状态

3.10 不推荐的方法

JUC高并发_第9张图片

3.11 主线程与守护线程

package cn.itcast.test;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test15")
public class Test15 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (true) {
                if (Thread.currentThread().isInterrupted()) {
                    break;
                }
            }
            log.debug("结束");
        }, "t1");
        //t1设置为守护线程,当主线程结束,守护线程一起结束
        t1.setDaemon(true);
        t1.start();

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

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

3.12 五种状态

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

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

 javaAPI的六种状态代码演示

package cn.itcast.n3;

import lombok.extern.slf4j.Slf4j;

import java.io.IOException;

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

        Thread t2 = new Thread("t2") {
            @Override
            public void run() {
                //while-true一直处于运行状态
                while (true) { // runnable

                }
            }
        };
        t2.start();

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

        Thread t4 = new Thread("t4") {
            @Override
            public void run() {
                synchronized (TestState.class) {
                    try {
                        //有时间约束等待
                        Thread.sleep(1000000); // timed_waiting
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t4.start();

        Thread t5 = new Thread("t5") {
            @Override
            public void run() {
                try {
                    //无时间约束等待
                    t2.join(); // waiting
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        t5.start();

        Thread t6 = new Thread("t6") {
            @Override
            public void run() {
                //t4线程会先拿到TestState.class这把锁,但是没有释放,所以是blocked阻塞
                synchronized (TestState.class) { // blocked
                    try {
                        Thread.sleep(1000000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t6.start();

        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        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());
        System.in.read();
    }
}

JUC高并发_第12张图片

 3.14 习题

比如,想泡壶茶喝。当时的情况是:开水没有;水壶要洗,茶壶、茶杯要洗;火已生了,茶叶也有了。怎么办?
  • 办法甲:洗好水壶,灌上凉水,放在火上;在等待水开的时间里,洗茶壶、洗茶杯、拿茶叶;等水开了,泡茶喝。
  • 办法乙:先做好一些准备工作,洗水壶,洗茶壶茶杯,拿茶叶;一切就绪,灌水烧水;坐待水开了,泡茶喝。
  • 办法丙:洗净水壶,灌上凉水,放在火上,坐待水开;水开了之后,急急忙忙找茶叶,洗茶壶茶杯,泡茶喝。

JUC高并发_第13张图片

package cn.itcast.test;

import cn.itcast.n2.util.Sleeper;
import lombok.extern.slf4j.Slf4j;

import static cn.itcast.n2.util.Sleeper.sleep;

@Slf4j(topic = "c.Test16")
public class Test16 {

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.debug("洗水壶");
            sleep(1);
            log.debug("烧开水");
            sleep(5);
        },"老王");

        Thread t2 = new Thread(() -> {
            log.debug("洗茶壶");
            sleep(1);
            log.debug("洗茶杯");
            sleep(2);
            log.debug("拿茶叶");
            sleep(1);
            try {
                t1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("泡茶");
        },"小王");

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

JUC高并发_第14张图片

4. 共享模型之管程

4.1 共享带来的问题

小故事
JUC高并发_第15张图片

 JUC高并发_第16张图片

JUC高并发_第17张图片

Java 的体现

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

JUC高并发_第18张图片  Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:

JUC高并发_第19张图片

如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题:

JUC高并发_第20张图片

但多线程下这 8 行代码可能交错运行:
出现负数的情况:
JUC高并发_第21张图片

出现正数的情况

JUC高并发_第22张图片

临界区 Critical Section
  •  一个程序运行多个线程本身是没有问题的
  • 问题出在多个线程访问共享资源
    • 多个线程读共享资源其实也没有问题
    • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
  • 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
例如,下面代码中的临界区 
static int counter = 0;
static void increment() 
// 临界区
{ 
 counter++;
}
static void decrement() 
// 临界区
{ 
 counter--;
}
竞态条件 Race Condition
多个线程在临界区内执行,由于代码的执行序列不同 而导致结果无法预测,称之为发生了竞态条件

4.2 synchronized 解决方案

为了避免临界区的竞态条件发生,有多种手段可以达到目的。
  • 阻塞式的解决方案:synchronizedLock
  • 非阻塞式的解决方案:原子变量
synchronized ,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
JUC高并发_第23张图片
类比:
  • synchronized(对象) 中的对象,可以想象为一个房间(room),有唯一入口(门)房间只能一次进入一人进行计算,线程 t1t2 想象成两个人
  • 当线程 t1 执行到 synchronized(room) 时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行count++ 代码
  • 这时候如果 t2 也运行到了 synchronized(room) 时,它发现门被锁住了,只能在门外等待,发生了上下文切换,阻塞住了
  • 这中间即使 t1 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦),这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才
  • 能开门进入当 t1 执行完 synchronized{} 块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥匙给他。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的 count-- 代码

JUC高并发_第24张图片

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

4.3 方法上的 synchronized

JUC高并发_第25张图片

所谓的线程八锁

 4.4 变量的线程安全分析

成员变量和静态变量是否线程安全?
  • 如果它们没有共享,则线程安全
  • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
    • 如果只有读操作,则线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全
局部变量是否线程安全?
  • 局部变量是线程安全的
  • 但局部变量引用的对象则未必
    • 如果该对象没有逃离方法的作用访问,它是线程安全的
    • 如果该对象逃离方法的作用范围,需要考虑线程安全

MarkWord存放monitor的地址指针

Owner:哪个线程是这把锁的持有者

EntryList:阻塞队列,进入BLOCKED状态

4.6 Monitor 概念

普通对象头

JUC高并发_第26张图片

数组对象头

JUC高并发_第27张图片

Java 对象头
JUC高并发_第28张图片

Monitor 原理
Monitor 被翻译为 监视器 或管程,每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针
JUC高并发_第29张图片

 Obj对象在加锁时:会清除在MarkWord里记录的hashcode、age、biased_lock,然后记录Monitor的指针地址(占30位),加锁状态由01(Normal)改为10(Heavyweight Locked

JUC高并发_第30张图片

 Monitor 结构如下

JUC高并发_第31张图片

  1. 刚开始 Monitor Owner null
  2. Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2Monitor中只能有一 个 Owner
  3. Thread-2 上锁的过程中,如果 Thread-3Thread-4Thread-5 也来执行 synchronized(obj),就会进入 EntryList ,变为BLOCKED阻塞状态
  4. Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的
  5. 图中 WaitSet 中的 Thread-0Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲wait-notify 时会分析

 synchronized 原理

static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
     synchronized (lock) {
         counter++;
     }
}
对应的字节码为

JUC高并发_第32张图片

synchronized 原理进阶

1. 轻量级锁
轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是 synchronized
假设有两个方法同步块,利用同一个对象加锁
JUC高并发_第33张图片

JUC高并发_第34张图片

JUC高并发_第35张图片

JUC高并发_第36张图片

JUC高并发_第37张图片

当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一

JUC高并发_第38张图片 2. 锁膨胀

如果在尝试加轻量级锁的过程中, CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
JUC高并发_第39张图片

JUC高并发_第40张图片

JUC高并发_第41张图片

3. 自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
JUC高并发_第42张图片

JUC高并发_第43张图片

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

 4. 偏向锁

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

JUC高并发_第45张图片 JUC高并发_第46张图片

偏向状态

JUC高并发_第47张图片

一个对象创建时:
  • 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的thread、epochage 都为 0
  • 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数
  • -XX:BiasedLockingStartupDelay=0 来禁用延迟
  • 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值

JUC高并发_第48张图片

JUC高并发_第49张图片 JUC高并发_第50张图片

 撤销 - 调用对象 hashCode

调用了对象的 hashCode ,但偏向锁的对象 MarkWord 中存储的是线程 id ,如果调用 hashCode 会导致偏向锁被撤销
  • 轻量级锁会在锁记录中记录 hashCode
  • 重量级锁会在 Monitor 中记录 hashCode
在调用 hashCode 后使用偏向锁,记得去掉 - XX: - UseBiasedLocking
撤销 - 其它线程使用对象
当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
撤销 - 调用 wait/notify

 批量重偏向

  1. 如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID
  2. 当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程
批量撤销
当撤销偏向锁阈值超过 40 次后, jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的

 5. 锁消除

4.7 wait notify

wait notify 原理
JUC高并发_第51张图片
  • Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
  • BLOCKED WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
  • BLOCKED 线程会在 Owner 线程释放锁时唤醒
  • WAITING 线程会在 Owner 线程调用 notify notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争

 API 介绍

  • obj.wait() 让进入 object 监视器的线程到 waitSet 等待
  • obj.notify() object 上正在 waitSet 等待的线程中挑一个唤醒
  • obj.notifyAll() object 上正在 waitSet 等待的线程全部唤醒
wait() 方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到 notify 为止
wait(long n) 有时限的等待 , n 毫秒后结束等待,或是被 notify

4.8 wait notify 的正确姿势

sleep(long n) wait(long n) 的区别
  • sleep Thread 方法,而 wait Object 的方法
  • sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
  • sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
  • 它们状态 TIMED_WAITING

JUC高并发_第52张图片

 

你可能感兴趣的:(java)