JUC并发编程(一)

本文详细介绍了Java并发编程的基础知识,包括进程与线程的概念,线程的创建与运行,线程状态,同步与异步的概念,以及synchronized和ReentrantLock的使用。同时,文章通过实例探讨了线程安全问题,如线程安全变量、临界区与竞态条件,并提供了相关的多线程设计模式和实践案例。
摘要由CSDN通过智能技术生成

JUC并发编程


一、进程与线程
1 进程

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

当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器等),也有的程序只能启动一个实例进程 (例如网易云音乐、360 安全卫士等)。


2 线程

一个程序运行的时候,就开启了一个进程,一个进程当中也可以分为一到多个线程,一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行。

在Java 中,线程作为最小的调度单位,进程作为资源分配的最小单位。在 windows 中进程是不活动的,只是作为线程的容器。


3 进程与线程的对比
  • 进程基本上是相互独立的,而线程存在于进程中,是进程的一个子集;
  • 进程拥有共享的资源,如内存空间等,可以供它内部的线程共享;
  • 线程通信相对简单,它们可以共享进程内的内存,进程间通信较为复杂,同一台计算机的进程通信称为 IPC ,不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP ;

4 并行(一起执行)与并发(串行)

单核 cpu 下,线程实际上是串行执行的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows 下时间片最小约为 15 毫秒)分给不同的线程使用,只是由于 cpu 在线程间(时间片很短)的切换速度非常快,给人一种同时运行的错觉 。

  • 并发:线程轮流使用 CPU 的情况称为并发(concurrent)。
  • 并行:由于我们的个人电脑大多都是多核cpu,多个cpu同时运行多个线程的情况叫做并行。

举例说明:

  • 并发:家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人依次做这三件事,这就是并发。
  • 并行:家庭主妇雇了3个保姆,一个专门做饭、一个专门打扫卫生、一个专门喂奶,互不干扰,这就是并行。
  • 既有并发又有并行:家庭主妇只雇了一个保姆,她们一起做这些事,保姆扫地、家庭主妇给孩子喂奶(这时属于并行),当保姆去做饭时,家庭主妇也想要做饭就只能等待(这时属于并发)。

5 同步与异步

从方法调用的角度,如果:

  • 需要等待结果返回,才能继续往下执行,就是同步;
  • 不需要等待结果返回,直接往下执行,就是异步;

设计:

  • 多线程可以实现方法执行变为异步!!!

结论:

  • 比如在项目中,某一个业务操作花费时间较长,这时可以开一个新的线程去处理这个耗时的业务,避免阻塞主线程的执行;
  • tomcat 的异步 servlet 也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞 tomcat 的工作线程;
  • ui程序中,多线程进行其他操作,避免阻塞ui线程

二、Java线程
1 创建和运行线程
  • 方法一:直接使用Thread,创建Thread对象,然后重写run()方法,这个run()方法就是线程的执行体,最后调用Thread对象的start()启动线程。
// 创建线程对象
Thread t = new Thread() {
    public void run() {
        // 要执行的任务
    }
};
// 启动线程
t.start();

例如:

Thread t = new Thread(){
    @Override
    public void run() {
        log.info("running...");
    }
};
t.setName("线程t1");
t.start();
log.info("main===>running...");

  • 方法二:使用Runnable配合Thread,创建Runnable对象,并重写run()方法,然后再创建Thread对象,调用Thread对象的start()方法启动线程。

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

Thread代表线程;

Runnable代表可运行的任务;

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

例如:

Runnable runnable = new Runnable() {
    @Override
    public void run() {
        // 要执行的任务
        log.info("running...");
    }
};
// 创建线程对象
Thread t = new Thread(runnable,"线程t2");
// 启动线程
t.start();
log.info("main running...");

  • 方法三:使用FutureTask,传递一个Callable参数,然后重写run()方法,这个run()方法也是线程的执行体,最后创建一个Thread

    对象,调用Thread对象的start()方法启动线程。

// 创建任务对象
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
        log.debug("running...");
        Thread.sleep(2000);
        return 100;
    }
});

// 参数1是任务对象,参数2是线程名字
Thread t = new Thread(task, "线程3");
t.start();

2 查看进程线程的方法
  • windows

    • tasklist:查看进程
    • taskskill杀死进程
  • linux

    • ps -ef 查看所有进程
    • ps -ft -p 查看某个进程
    • kill 杀死进程

3 栈与栈帧

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

每个栈由多个栈帧(Frame) 组成,对应着每次方法调用时所占用的内存;

每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法;

线程上下文切换:

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

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

当上下文切换发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器,它的作用是记录下一条 jvm 指令的执行地址,是线程私有的。

4 线程中常见方法
  • start():启动一个新线程,在新的线程运行run方法中的代码;
  • run():新线程启动后调用的方法;
  • join():等待线程结束:用在线程间通信使用;
  • join(long n):最多等待n毫秒;
  • getId():获取线程长整型的id,唯一id;
  • getName():获取线程名;
  • setName(String str):修改线程名;
  • getPriority():获取线程优先级;
  • setPriority(int n):修改线程优先级,java中规定线程的优先级是1-10的整数,最大优先级是10,最小是1。默认优先级是5,公平竞争。
  • getState():获取线程状态,java中线程状态用6个enum表示:NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED;
  • isInterrupted():判断是否被打断;
  • interrupt():打断线程:如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除打断标记;如果打断正在运行的线程,则会设置打断标记;park 的线程被打断,也会设置打断标记。
  • 静态方法:
    • interrupted():判断当前线程是否被打断,会清除 打断标记;
    • currentThread():获取当前正在执行的线程;
    • sleep(long n):让当前执行的线程休眠n毫秒,休眠时让出cpu 的时间片给其它线程;

5 start()与run()

直接上测试案例演示:

  • run()方法
    public static void main(String[] args) {
        Thread t1 = new Thread("线程t1_testRun()") {
            @Override
            public void run() {
                log.info("当前线程:" + Thread.currentThread().getName());
                try {
                    FileReader file = new FileReader("D:\\BaiduNetdiskDownload\\壁纸.jpg");
                    System.out.println("file:" + file);
                } catch (Exception e) {
                    log.error("找不到文件");
                    e.printStackTrace();
                }
            }
        };
        System.out.println("线程状态:" + t1.getState());
        t1.run();
        System.out.println("线程状态:" + t1.getState());
        log.info("执行完毕!");
    }

结果:
在这里插入图片描述

  • start()方法
    public static void main(String[] args) {
        Thread t1 = new Thread("线程t1_testStart()") {
            @Override
            public void run() {
                log.info("当前线程:" + Thread.currentThread().getName());
                try {
                    FileReader file = new FileReader("D:\\BaiduNetdiskDownload\\壁纸.jpg");
                    System.out.println("file:" + file);
                } catch (Exception e) {
                    log.error("找不到文件");
                    e.printStackTrace();
                }
            }
        };
        System.out.println("线程状态:" + t1.getState());
        t1.start();
        System.out.println("线程状态:" + t1.getState());
        log.info("执行完毕!");
    }

结果:
在这里插入图片描述

结论:执行run()方法发现线程状态是NEW,处于新建的状态,当前线程名称是main,也就是线程并没有执行;执行start()方法,执行前后线程状态不一致,当前线程名称也是t1线程的名称,证明线程执行了。

6 sleep()与yield()
  • sleep()

    • 调用sleep会让当前线程从 RUNNABLE (可运行状态) 进入TIMED_WAITING(停止状态);
    • 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出InterruptedException;
    • 睡眠结束后的线程未必会立刻得到执行;
    • 建议用 Timelnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性;
  • yield()

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

7 线程优先级

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

案例:在没有利用 cpu 来进行计算时,不要让 while(true) 空转浪费 cpu,这时可以使用 yield 或 sleep 来让出 cpu 的使用权,让给其它程序。

此处我在我的阿里云服务器进行效果演示,服务器为1核2G,1核容易看出效果:先注释掉Thread.sleep(50)

    public static void main(String[] args) {
        while (true) {
            try {
                 // Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

使用 top 命令发现运行这个java造成cpu短时间占满!
在这里插入图片描述
取消注释后,就恢复了正常:
在这里插入图片描述

8 join()方法

join方法用于等待线程结束,用在线程间通信使用。

观察如下代码块,猜想执行结果:

@Slf4j
public class test01 {

    static int r = 0;

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

    public static void test() throws InterruptedException {
        log.info("开始");
        Thread t1 = new Thread(() -> {
            log.info("开始");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("结束");
            r = 10;
        });
        t1.setName("线程t1");
        t1.start();
        log.info("结果为:{}",r);                 // 最终的结果为多少?
        log.info("结束");
    }
}

发现线程t1的执行体内,休眠了100毫秒,因此当t1.start();启动线程后,不会去等待t1休眠,而是直接往下执行,因此结果为 0。
在这里插入图片描述

对代码块做出改造,加入join()方法。

public class test01 {

    static int r = 0;

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

    public static void test() throws InterruptedException {
        log.info("开始");
        Thread t1 = new Thread(() -> {
            log.info("开始");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("结束");
            r = 10;
        });
        t1.setName("线程t1");
        t1.start();
        t1.join();                                     // 加入join()方法
        log.info("结果为:{}",r);                       // 结果还是0吗?  
        log.info("结束");
    }
}

此时,当t1.start();启动线程过后,t1.join()会先等待t1执行,因此100毫秒后,才会打印结果,此时r应为10。
在这里插入图片描述


案例升级,若等待多个线程,会发生什么?

public class test01 {

    static int r = 0;
    static int r1 = 0;
    static int r2 = 0;

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

    public static void test() throws InterruptedException {

        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r1 = 10;
        });
        t1.setName("线程t1");

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

        long start = System.currentTimeMillis();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        long end = System.currentTimeMillis();
        log.info("r1:{}, r2:{}, cost:{}", r1, r2, end-start);     // 最后的打印结果是多少?特别是cost
        log.info("结束");
    }
}

对代码分析,main线程执行-> t1线程启动 -> t2也启动-> t1线程等待 -> t2线程也等待,最后打印结果。在t1线程等待时,t2也在等待,t1等待1000毫秒后,执行了r1 = 10; 此时t2线程也等待了1000毫秒,因此t2此时只需再等待1000毫秒就执行r2 = 20;最后的结果为 r1=10,r2=20,cost=2000毫秒左右(执行其它代码也需要时间,因此不可能等于固定的2000)。

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

若先执行t2.join(); 再执行t1.join();呢?

此处代码块省略,直接分析,由于先等待t2线程,但此时t1线程也在同步等待,2000毫秒后,t2线程结束,但在1000毫秒时t1线程早就结束了,因此,最终的cost也是2000毫秒左右,不会有什么变化。

再次思考,若join()方法传递参数呢?比如上述代码块,取消t1.join(), t2.join()变为t2.join(1500),也就是只等待1500毫秒,但t2线程的睡眠时间是2000毫秒。这样的最终结果就是,在等待了1500毫秒后,main主线程继续往下执行,打印结果cost为1500左右。

9 interrupt()方法

interrupt()方法会打断 sleep、wait(join的底层原理其实就是wait)、join的线程。

public class test01 {
    
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                while (true) {
                    boolean interrupted = Thread.currentThread().isInterrupted();
                    if (interrupted) {
                        log.info("被打断了,退出循环");
                        break;
                    }
                }
            }
        };
        Thread t1 = new Thread(runnable);
        t1.start();
        Thread.sleep(100);
        log.info("打断线程");
        t1.interrupt();
    }
}

结果:
在这里插入图片描述

10 使用到interrupt()的多线程设计模——两阶段终止模式

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

错误思路:

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

11 过时不推荐的方法

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

  • stop() :停止线程运行
  • suspend():挂起线程运行
  • resume():恢复线程运行

12 主线程与守护线程

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

例如:

public class test01 {

    public static void main(String[] args) {
        log.debug("主线程,开始运行...");
        Thread t1 = new Thread(() -> {
            log.debug("守护线程,开始运行...");
            try {
                sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("守护线程,运行结束...");
        },"守护线程t1");
        // 设置该线程为守护线程
        t1.setDaemon(true);
        t1.start();
        try {
            sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("主线程,运行结束...");
    }
}

结果:
在这里插入图片描述
没等守护线程执行完毕,主线程就已经结束了。

注意:

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

13 线程状态-五种

网络上对于线程状态众说纷纭,有说5种的,有说6种的。现对于这两种不同的说法做出解释。

从操作系统层面-五种。
在这里插入图片描述

  • 初始状态:仅是在语言层面创建了线程,还未与操作系统线程关联。
  • 可运行状态:指该线程已经被创建,与操作系统线程关联,可以由CPU调度执行。
  • 运行状态:指获取了cpu时间片运行中的状态,当cpu时间片用完,会从运行状态转移至可运行状态。
  • 阻塞状态:如果运行中的线程调用了阻塞api,比如读写一些文件,这时该线程实际不会用到cpu,会导致进入阻塞状态;等读写完毕,操作系统会唤醒阻塞的线程,转移至可运行状态。
  • 终止状态:表示线程已经执行完毕,生命周期已经结束,不会再转移为其它状态。

14 线程状态-六种

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

public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}
  • NEW:线程刚被创建,但是还没有调用 start() 方法。
  • RUNNABLE:当调用了 start() 方法之后进入运行状态,注意,JavaAPI层面的 RUNNABLE 状态涵盖了操作系统层面的[可运行状态]、[运行状态]和[阻塞状态] (由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)。
  • BLOCKED:
  • WAITING:
  • TIMED_WAITING:
  • TERMINATED:

三、共享模型之管程
1 线程安全问题

案例:两个线程对初始值为0的静态变量一个做自增,一个做自减,各做 5000 次,结果是0吗?

public class test01 {

    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.info("counter:{}",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 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:
在这里插入图片描述
如果以单线程按顺序执行,不会出现任何问题,如果是多个线程,有可能会出现问题。

2 临界区与竞态条件

一个程序运行多个线程本身是没有问题的,问题出在多个线程访问共享资源。

  • 多个线程读共享资源其实也没有问题;
  • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题;

临界区:一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为 临界区。

例如,下面代码中的临界区

	static int counter = 0;
	static void increment() {
	    // 临界区
	    counter ++;
	}
	static void decrement() {
	    // 临界区
	    counter --;
	}

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


3 synchronized 解决方案

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

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

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

注意

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

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码。
  • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点。

synchronized 语法

synchronized(对象) {
	临界区
}

解决上述案例:

    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.info("counter:{}",counter);
    }

加了synchronized 锁过后,得到解决:
在这里插入图片描述

思考:synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。
为了加深理解,思考以下问题:

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

    • 加了 synchronized 锁后,实际上是保证了以下4条指令的原子操作。
    getstatic   i           // 获取静态变量i的值
    iconst_1                // 准备常量1
    iadd                    // 白增
    putstatic   i           // 将修改后的值存入静态变量i
    
    • 如果把 synchronized 加在for循环外面,也就保证了 4* 5000=20000条指令的原子操作,也就是完成5000次累加后再去完成累减。
  • 2、如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会发生什么?

    • 得不到解决,要想保证最终的结果,synchronized 必须传入同一个对象。
  • 3、如果 t1 synchronized(obj) 而 t2 没有加锁发生什么? 如何理解?

    • 也得不到解决,对于临界区的代码,多个线程都需要加锁。

4 对上述案例改造为面向对象方法编写
    static class Room {
        private int counter = 0;

        public void incr() {
            synchronized (this) {
                counter++;
            }
        }

        public void decr() {
            synchronized (this) {
                counter--;
            }
        }

        public int getCounter() {
            synchronized (this) {
                return this.counter;
            }
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        Room room = new Room();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                room.incr();
            }
        },"t1");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                room.decr();
            }
        },"t2");

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.info("counter:{}", room.getCounter());
    }

或者直接加在方法上:

    static class Room {
        private int counter = 0;

        public synchronized  void incr() {
            counter++;
        }

        public synchronized void decr() {
        	counter--;
        }

        public synchronized int getCounter() {
        	return this.counter;
    	}
    }

5 练习题之线程八锁——帮助理解 synchronized

问题1:

     static class Number {
        public synchronized void a() {
            log.info("1");
        }

        public synchronized void b() {
            log.info("2");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Number n1 = new Number();
        new Thread(() -> {
            n1.a();
        },"线程t1").start();

        new Thread(() -> {
            n1.b();
        },"线程t2").start();
    }

答案:先打印1,再打印2 或 先打印2,再打印1.

分析:只创建了一个Number 对象n1,但是开启了两个线程,线程t1调用a方法,线程t2调用b方法,由于这两个方法都加了 synchronized 锁,因此如果cpu先调度线程 t1,则先打印1,再打印2;如果cpu先调度线程 t2,则先打印2,再打印1。

问题2:在问题1的基础上,对方法a加入了休眠。

     static class Number {
        public synchronized void a() throws InterruptedException {
            sleep(1000);
            log.info("1");
        }

        public synchronized void b() {
            log.info("2");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Number n1 = new Number();
        new Thread(() -> {
            n1.a();
        },"线程t1").start();

        new Thread(() -> {
            n1.b();
        },"线程t2").start();
    }

答案:先打印1,再打印2 或 先打印2,再打印1.

分析:只创建了一个Number 对象n1,但是开启了两个线程,线程t1调用a方法,线程t2调用b方法,并且这两个方法都加了 synchronized 锁。因此如果cpu先调度线程 t1,就算要先休眠1秒,但由于加了synchronized 互斥锁,因此会等待线程t1执行完a方法后,线程t2才会执行b方法,因此结果是先打印1,再打印2。如果cpu先调度线程 t2,这时方法b没有休眠,因此结果就是直接先打印2,再打印1。


问题3:在问题2的基础上,再加入方法c。
static class Number {
        public synchronized void a() {
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("1");
        }

        public synchronized void b() {
            log.info("2");
        }

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

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

        new Thread(() -> {
            n1.b();
        },"线程t2").start();

        new Thread(() -> {
            n1.c();
        },"线程t3").start();
    }

答案:先打印3,再打印1,2 或 先打印3,再打印2,1 或先打印2,再打印3,1。

分析:由于方法c没有加锁,方法b加了锁但没有休眠,因此cpu会先调度线程t3或线程t2,如果先调度线程t3,率先执行方法c后,对于线程t1和线程t2执行方法a,b,由于都加了锁,因此当cpu先调度线程t1,则最后结果为先打印3,然后打印1,2;如果cpu先调度线程t2,则最后结果为先打印3,然后打印2,1。如果一开始cpu就先调度线程t2,执行完方法b后,由于线程t1执行a需要休眠1秒,因此不会先执行a方法,而是直接执行没有加锁的c方法,因此最后结果为先打印2,再打印3,1。


问题4:

     static class Number {
        public synchronized void a()  {
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("1");
        }

        public synchronized void b() {
            log.info("2");
        }
    }

    public static void main(String[] args) {
        Number n1 = new Number();
        Number n2 = new Number();
        new Thread(() -> {
            n1.a();
        },"线程t1").start();

        new Thread(() -> {
            n2.b();
        },"线程t2").start();
    }

答案:总数先打印2,后打印1.

分析:创建了两个 Number 对象,一个n1,一个n2,由于n1休眠1秒属于阻塞状态,因此总是会优先打印2,再打印1.


问题5:方法a改为静态方法

     static class Number {
        public static synchronized void a()  {
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("1");
        }

        public synchronized void b() {
            log.info("2");
        }
    }

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

        new Thread(() -> {
            n1.b();
        },"线程t2").start();
    }

答案: 先打印2,再打印1

由于方法a是一个加了 synchronized 的静态锁,因此锁住的是 Number 这个类对象;而方法b不是静态的,因此锁住的还是 this对象,也就是n1对象,这样就导致方法a、b锁住的是不同的对象,因此这两个方法没有互斥条件,由于方法a会休眠1秒,因此先打印2,再打印1。


问题6:方法a、b都是静态方法

     static class Number {
        public static synchronized void a()  {
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("1");
        }

        public static synchronized void b() {
            log.info("2");
        }
    }

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

        new Thread(() -> {
            n1.b();
        },"线程t2").start();
    }

答案:先打印1,再打印2或先打印2,再打印1.

分析,由于方法a,b都是静态的,都加有 synchronized 锁,因此方法a,b锁住的都是 Number 这个类对象,也就是锁住的是同一个对象,所以存在互斥条件,因此结果为要么先打印1,后打印2或者先打印2,再打印1。


问题7:

     static class Number {
        public static synchronized void a()  {
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("1");
        }

        public synchronized void b() {
            log.info("2");
        }
    }

    public static void main(String[] args) {
        Number n1 = new Number();
        Number n2 = new Number();
        new Thread(() -> {
            n1.a();
        },"线程t1").start();

        new Thread(() -> {
            n2.b();
        },"线程t2").start();
    }

答案:先打印2,再打印1.

分析,由于方法a是静态的,但有两个 Number 对象n1和n2,方法a锁住的是 Number 类对象,而方法b锁住的是 n2 这个对象,锁住的不是同一个对象,因此没有互斥条件,因此会优先打印2,再打印1.


问题8:
     static class Number {
        public static synchronized void a()  {
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("1");
        }

        public static synchronized void b() {
            log.info("2");
        }
    }

    public static void main(String[] args) {
        Number n1 = new Number();
        Number n2 = new Number();
        new Thread(() -> {
            n1.a();
        },"线程t1").start();

        new Thread(() -> {
            n2.b();
        },"线程t2").start();
    }

答案:先打印2,再打印1或先打印1,再打印2。

分析:虽然方法a和方法b都是静态的,因此方法a和b都是对 Number 这个类对象加锁,因此存在互斥条件。如果cpu先调度线程t1,则先打印1,后打印2;如果cpu先调度线程t2,则先打印2,再打印1。


6 变量的线程安全问题分析
  • 1、成员变量和静态变量是否线程安全?

    • 如果它们没有共享,则线程安全;
    • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况。如果只有读操作,则线程安全,如果有读写操作,则这段代码是临界区,需要考虑线程安全。

  • 2、局部变量是否线程安全?

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

7 Monitor-对象头

java对象头:通常一个对象,在内存中由两部分组成:对象头和成员变量。
以32位虚拟机为例,一个Object Header为对象头、Mark Word为成员变量、:Klass Word为类的类型(如Student类、Teacher类)。
普通对象:对象头为64位(8个字节)。
在这里插入图片描述
数组对象:对象头为96位(12个字节)。
在这里插入图片描述
其中Mark Word结构为:
在这里插入图片描述

Monitor工作原理:每个Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针,Monitor 结构如下:
在这里插入图片描述
刚开始 代码块的Monitor 中 Owner 为 null,当Thread-1 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-1,Monitor中也只能有一个Owner。在 Thread-1 上锁的过程中,如果 Thread-2,Thread-3,也来执行 synchronized(obj),就会进入EntryList 进入阻塞状态,当Thread-1 执行完同步代码块的内容后,就会唤醒 EntryList 中等待的线程来竞争锁,竞争的时候是非公平的, WaitSet 中是之前获得过锁,但条件不满足进入 WAITING 状态的线程,待条件满足过后,又会重新进入EntryList ,参与下一轮获取锁。

EntryList 和 WaitSet 都是阻塞状态,不占用cpu时间片。


8 常用方法介绍
  • obj.wait() 让进入 object 监视器的线程到 WaitSet 等待;

  • obj.wait(long timeout) 让进入 object 监视器的线程到 WaitSet 等待,时间为timeout;

  • obj.notify() 在 object 上正在 WaitSet 等待的线程中挑一个唤醒;

  • obj.notifyAll() 让 object上正在 WaitSet 等待的线程全部唤醒;


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

10 ReentrantLock

相对于 synchronized 它具备如下特点:

  • 可中断。
  • 可以设置超时时间。
  • 可以设置为公平锁(先进先出)。
  • 支持多个条件变量。

与synchronized一样,都支持可重入。

ReentrantLock基本语法:

	ReentrantLock reentrantLock = new ReentrantLock();
	// 获取锁
	reentrantLock.lock();
	try {
	    // 临界区
	} finally {
	    // 释放锁
	    reentrantLock.unlock();  
	}

ReentrantLock条件变量:

synchronized 中也有条件变量,就是上述讲解的 WaitSet 休息室,当条件不满足时进入 WaitSet 等待,ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比 synchronized 那些不满足条件的线程都在一间休息室等消息,而 ReentrantLock 支持多间休息室,唤醒时也是按休息室来唤醒。

一、可重入特性:

可重入是指同一个线程如果首次获得了这把锁,由于它是这把锁的拥有者,因此有权利再次获取这把锁,锁中有个变量记录重入的次数,如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。

    private static ReentrantLock reentrantLock = new ReentrantLock();
    public static void main(String[] args) {
        // 获取锁
        reentrantLock.lock();
        try {
            log.info("进入主方法");
            m1();
        } finally {
            // 释放锁
            reentrantLock.unlock();
        }
    }

    public static void m1() {
        // 获取锁
        reentrantLock.lock();
        try {
            log.info("进入m1方法");
            m2();
        } finally {
            // 释放锁
            reentrantLock.unlock();
        }
    }

    public static void m2() {
        // 获取锁
        reentrantLock.lock();
        try {
            log.info("进入m2方法");
        } finally {
            // 释放锁
            reentrantLock.unlock();
        }
    }

成功获取了3次锁。
在这里插入图片描述


二、可打断特性:

在获取锁时,如果长时间未获取到锁,可以进行打断,但是加锁方式要改变,不能再使用 reentrantLock.lock() 进行加锁,而是使用 reentrantLock.lockInterruptibly() 进行加锁。


    private static ReentrantLock reentrantLock = new ReentrantLock();
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            try {
                // 如果没有竞争则会获取对象锁,如果有竞争或进入阻塞队列,可以被其它线程用 interrupt 打断
                log.info("尝试获取锁");
                reentrantLock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.info("没有获取到锁,返回");
                return;
            }
            try {
                log.info("获取到锁");
            } finally {
                // 释放锁
                reentrantLock.unlock();
            }
        }, "线程t1");
        reentrantLock.lock();
        t1.start();
        sleep(1000);
        log.info("打断线程t1");
        t1.interrupt();
    }

打断过后不再继续获取锁!!!
在这里插入图片描述


三、锁超时特性:

避免无期限的等待,因此采用 reentrantLock.tryLock() 尝试获取锁,获取到就返回 true ,获取不到就返回false。

    private static ReentrantLock reentrantLock = new ReentrantLock();
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.info("尝试获取锁");
            boolean isLock = reentrantLock.tryLock();
            if (!isLock) {
                log.warn("没有获取到锁,返回");
                return;
            }
            try {
                log.info("获取到锁");
            } finally {
                // 释放锁
                reentrantLock.unlock();
            }
        }, "线程t1");
        log.info("主线程先获取到锁");
        reentrantLock.lock();
        t1.start();
        t1.interrupt();
    }

由于主线程先获取到了锁,因此线程t1会获取失败:
在这里插入图片描述


还可以设置超时时间:

    private static ReentrantLock reentrantLock = new ReentrantLock();
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.info("尝试获取锁");
            boolean isLock = false;
            try {
                isLock = reentrantLock.tryLock(1, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.warn("没有获取到锁,发生了异常,返回");
                return;
            }
            if (!isLock) {
                log.warn("没有获取到锁,返回");
                return;
            }
            try {
                log.info("获取到锁");
            } finally {
                // 释放锁
                reentrantLock.unlock();
            }
        }, "线程t1");
        log.info("主线程先获取到锁");
        reentrantLock.lock();
        t1.start();
    }

结果:此时没有打断锁,而是超时自动返回了。
在这里插入图片描述


再次演示,主线程设置2秒后释放锁,而线程t1等待4秒:

    private static ReentrantLock reentrantLock = new ReentrantLock();
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.info("尝试获取锁");
            boolean isLock = false;
            try {
                isLock = reentrantLock.tryLock(4, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.warn("没有获取到锁,发生了异常,返回");
                return;
            }
            if (!isLock) {
                log.warn("没有获取到锁,返回");
                return;
            }
            try {
                log.info("获取到锁");
            } finally {
                // 释放锁
                reentrantLock.unlock();
            }
        }, "线程t1");
        log.info("主线程先获取到锁");
        reentrantLock.lock();
        t1.start();
        sleep(2000);
        reentrantLock.unlock();
        log.info("主线程先释放了锁");
    }

结果:主线程先加锁,2秒后才释放锁,由于线程t1一直在等待,在主线程释放锁的一瞬间,线程t1立马获取锁成功。
在这里插入图片描述


四、锁公平性特性:

ReentrantLock 默认是不公平的,即等待锁的线程,并不是按顺序排队,有可能后来的线程会先获取到锁。但是 ReentrantLock 可以通过构造方法设置是否公平。

    // ReentrantLock 底层构造方法
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
	ReentrantLock reentrantLock = new ReentrantLock(true);

(续)

由于篇幅过长,因此对本笔记进行了拆分,其余部分随此链接查看:
https://blog.csdn.net/weixin_44780078/article/details/130753056

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值