并发编程学习笔记

前言

​ 此为我在B站看传智播客的并发编程视频教程的学习笔记,个人目前感觉还不错,点此可前往观看

1.进程和线程

1.1 进程和线程

  • 进程

​ 进程是用来加载指令、管理内存、管理IO的,在Java中,进程是资源分配的最小单位;在windows中,进程是作为线程的容器存在的,一个进程包含多个线程。

  • 线程

​ 线程可以看成一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行,在Java中,线程是最小调度单位

  • 二者对比
    • 进程基本上相互独立的,线程存在于进程之内,是进程的一个子集
    • 进程拥有共享的资源,如内存空间等,供内部的线程共享
    • 进程间通信较为复杂
    • 线程间通信较为简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
    • 线程更轻量,线程上下文切换成本一般比进程上下文切换低

1.2 并发和并行

  • 并发是同一时间应对多件事情的能力

    单核CPU轮流执行多个线程,由于切换线程时间太短,人们无法感觉得到,可以看成是同一时间执行多个线程,这就是并发。

  • 并行是同一时间动手做多件事情的能力

    多核CPU同时处理多个线程,多个线程是同一时刻同时被处理的,这就是并行。

2.Java线程

2.1创建和运行线程

2.1.1 直接使用Thread(匿名内部类)
//创建线程对象
Thread t = new Thread(){
    public void run(){
        //要执行的任务
    }
}//启动线程
t.start();
2.1.2 使用Runnable和Thread相结合的方式
Runnable runnable = new Runnable() {
public void run(){
// 要执行的任务
}
};
// 创建线程对象
Thread t = new Thread( runnable );
// 启动线程
t.start();

/*****************************************************************************************************************/
/* 因为Runnable接口只有一个抽象方法,因此可以使用lambda表达式简化,JDK中会将只有只有一个抽象方法的接口使用@FunctionalInterface注解,因此以后看到@FunctionalInterface注解即可使用lambda表达式简化(注意:lambda表达式在JDK8之后才支持)*/

Runnable runnable = () -> { 要执行的方法 };
/ 创建线程对象
Thread t = new Thread( runnable );
// 启动线程
t.start();

  • 方法1是把线程和任务合并到一起,方法2是把线程和任务分开
  • 用Runnable更容易与线程池等高级API配合
  • 用Runnable让任务类脱离Thread继承体系,更灵活
2.1.3 FutureTask配合Thread

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

FutureTask<Integer> task3 = new FutureTask<>(new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
        log.debug("running...");
        return 100;
    }
});
// 参数1 是任务对象; 参数2 是线程名字,推荐
new Thread(task3, "t3").start();
// 主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task3.get();
log.debug("结果是:{}", result);

​ 输出结果:

00:45:01 [t3] c.Test2 - running...
00:45:01 [main] c.Test2 - 结果是:100

​ 底层原理解释:FutureTask实现了RunnableFuture接口,RunnableFuture接口继承了Runnable接口

//底层源码
//FutureTask的构造函数之一
/**
     * Creates a {@code FutureTask} that will, upon running, execute the
     * given {@code Callable}.
     *
     * @param  callable the callable task
     * @throws NullPointerException if the callable is null
     */
public FutureTask(Callable<V> callable) {
    if (callable == null)
        throw new NullPointerException();
    this.callable = callable;
    this.state = NEW;       // ensure visibility of callable
}


//Callable接口
@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

2.2 操作线程进程

window

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

linux

  • ps -fe 查看所有进程

  • ps -ft -p 查看某个进程(PID)的所有线程

  • kill 杀死进程

  • top 按大写H切换是否显示线程

  • top -H -p 查看某个进程(PID)的所有线程

Java

  • jps命令查看所有Java进程

  • jstack 查看某个Java进程(PID)的所有线程状态

    工具

  • 使用 JDK自带的监控工具 Jconsole(Java Monitoring and Management Console)
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

2.3 线程运行原理

  1. 栈与栈帧

    JVM由栈、堆、方法区所组成,其中栈内存由线程使用,每个线程启动后,虚拟机就会为其分配一块栈内存。

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

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

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

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

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

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

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

2.4 线程常见方法

方法名static功能说明注意
start()启动一个新线程,在新的线程运行 run 方法中的代码start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的start方法只能调用一次,如果调用了多次会出现IllegalThreadStateException
run()新线程启动后会调用的方法如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为
join()等待线程运行结束
join(long n)等待线程运行结束,最多等待 n毫秒
getId()获取线程长整型的 idid 唯一
getName()获取线程名
setName(String)修改线程名
getPriority()获取线程优先级
setPriority(int)修改线程优先级java中规定线程优先级是1~10 的整数,较大的优先级能提高该线程被 CPU 调度的机率
getState()获取线程状态Java 中线程状态是用 6 个 enum 表示,分别为:NEW, RUNNABLE, BLOCKED, WAITING,TIMED_WAITING, TERMINATED
isInterrupted()判断是否被打断不会清除打断标记
isAlive()线程是否存活(还没有运行完毕)
interrupt()打断线程如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除 打断标记;如果打断的正在运行的线程,则会设置 打断标记 ;park 的线程被打断,也会设置打断标记
interrupted()static判断当前线程是否被打断会清除打断标记(清除打断标记的意思是如果打断标记为true,则会把打断标记设置为false)
currentThread()static获取当前正在执行的线程
sleep(long n)static让当前执行的线程休眠n毫秒,休眠时让出 cpu的时间片给其它线程
yield()static提示线程调度器让出当前线程对CPU的使用主要是为了测试和调试

​ 注:使用了setPriority(int)设置线程优先级之后,线程优先级会提示调度器优先调度该线程,但它仅仅只是一个提示,调度器可以忽略它,如果cpu比较忙,那么优先级高的线程会获得更多的时间片,但线程比较闲时,优先级几乎没作用。在Linux单核操作系统中可以使用sleep()防止程序死循环时cpu占用100%(适用于无需锁同步的场景)。

2.5 两阶段终止模式(使用interrupt()实现)

无异常
有异常
while(true)
有没有被打断?
料理后事
睡眠2s
结束循环
执行监控记录
设置打断标记

2.6 守护线程

定义:守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。

创建方法:守护线程和普通线程一样,只是在调用start()方法前,调用setDaemon(true)把该线程标记为守护线程即可。示例:

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

应用示例

  • 垃圾回收器线程就是一种守护线程;

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

2.7 线程的五种状态(操作系统层面描述的)

在这里插入图片描述

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

2.8 线程的六种状态(Java API层面进行描述的)

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

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

3.共享模型之管程

3.1 共享带来的问题

Java案例:

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

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test5")
public class Test5 {
    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);
    }
}

​ 输出

12:01:30 [main] c.Test5 - -226

问题分析

以上的结果可能是正数、负数、零。为什么呢?因为 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 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:
在这里插入图片描述
如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题:
在这里插入图片描述
但多线程下这 8 行代码可能交错运行:
出现负数的情况:
在这里插入图片描述
出现正数的情况:
在这里插入图片描述
临界区 Critical Section

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

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

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

竞态条件 Race Condition

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

3.2 synchronized 解决方案

应用之互斥

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

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

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

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

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

synchronized

语法

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

解决方案:

package cn.shentianlan;

import lombok.extern.slf4j.Slf4j;

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

在这里插入图片描述

你可以做这样的类比:

  • synchronized(对象) 中的对象,可以想象为一个房间(room),有唯一入口(门)房间只能一次进入一人
    进行计算,线程 t1,t2 想象成两个人
  • 当线程 t1 执行到 synchronized(room) 时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行
    count++ 代码
  • 这时候如果 t2 也运行到了 synchronized(room) 时,它发现门被锁住了,只能在门外等待,发生了上下文切
    换,阻塞住了
  • 这中间即使 t1 的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦),
    这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才
    能开门进入
  • 当 t1 执行完 synchronized{} 块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥
    匙给他。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的 count-- 代码

用图来表示
在这里插入图片描述
synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切
换所打断。

面向对象改进

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

package cn.shentianlan;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test5")
public class Test5 {
    public static void main(String[] args) throws InterruptedException {
        Room room = new Room();
        Thread t1 = new Thread(() -> {
            for (int j = 0; j < 5000; j++) {
                room.increment();
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int j = 0; j < 5000; j++) {
                room.decrement();
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("count: {}",room.get());
    }
}

class Room {
    int value = 0;

    public void increment() {
        synchronized (this) {
            value++;
        }
    }

    public void decrement() {
        synchronized (this) {
            value--;
        }
    }

    public int get() {
        synchronized (this) {
            return value;
        }
    }
}

3.3 方法上的 synchronized

class Test{
    public synchronized void test() {
    }
}
等价于
class Test{
    public void test() {
        synchronized(this) {
        }
    }
}

/***************************************************************************************************/

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

3.4 线程八锁

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

情况1:12 或 21

package cn.shentianlan;

import lombok.extern.slf4j.Slf4j;

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

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

情况2:1s后12,或 2 1s后 1

package cn.shentianlan;

import lombok.extern.slf4j.Slf4j;

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

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

情况3:3 1s 12 或 23 1s 1 或 32 1s 1

package cn.shentianlan;

import lombok.extern.slf4j.Slf4j;

import static java.lang.Thread.sleep;

public class Test6 {
    public static void main(String[] args) {
        Number n1 = new Number();
        new Thread(()->{
            try {
                n1.a();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(()->{ n1.b(); }).start();
        new Thread(()->{ n1.c(); }).start();
    }
}

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

情况4:2 1s 后 1

package cn.shentianlan;

import lombok.extern.slf4j.Slf4j;

import static java.lang.Thread.sleep;

@Slf4j(topic = "c.Number")
public class Test6 {
    public static void main(String[] args) {
        Number n1 = new Number();
        Number n2 = new Number();
        new Thread(()->{
            try {
                n1.a();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(()->{ n2.b(); }).start();
    }
}

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

情况5:2 1s 后 1

package cn.shentianlan;

import lombok.extern.slf4j.Slf4j;

import static java.lang.Thread.sleep;

@Slf4j(topic = "c.Number")
public class Test6 {
    public static void main(String[] args) {
        Number n1 = new Number();
        new Thread(()->{
            try {
                n1.a();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(()->{ n1.b(); }).start();
    }
}

@Slf4j(topic = "c.Number")
class Number{
    //因为是静态方法,synchronized相当于synchronized(Number.class)
    public static synchronized void a() throws InterruptedException {
        sleep(1);
        log.debug("1");
    }
    public synchronized void b() {
        log.debug("2");
    }
}

情况6:1s 后12, 或 2 1s后 1

package cn.shentianlan;

import lombok.extern.slf4j.Slf4j;

import static java.lang.Thread.sleep;

@Slf4j(topic = "c.Number")
public class Test6 {
    public static void main(String[] args) {
        Number n1 = new Number();
        new Thread(()->{
            try {
                n1.a();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(()->{ n1.b(); }).start();
    }
}

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

情况7:2 1s 后 1

package cn.shentianlan;

import lombok.extern.slf4j.Slf4j;

import static java.lang.Thread.sleep;

@Slf4j(topic = "c.Number")
public class Test6 {
    public static void main(String[] args) {
        Number n1 = new Number();
        Number n2 = new Number();
        new Thread(()->{
            try {
                n1.a();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(()->{ n2.b(); }).start();
    }
}

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

情况8:1s 后12, 或 2 1s后 1

package cn.shentianlan;

import lombok.extern.slf4j.Slf4j;

import static java.lang.Thread.sleep;

@Slf4j(topic = "c.Number")
public class Test6 {
    public static void main(String[] args) {
        Number n1 = new Number();
        Number n2 = new Number();
        new Thread(()->{
            try {
                n1.a();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(()->{ n2.b(); }).start();
    }
}

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

3.5 变量线程安全分析

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

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

局部变量是否线程安全?

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

常见线程安全类

  • String
  • Integer
  • StringBuffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent 包下的类

这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。需要注意的是:

它们的每个方法是原子的,但它们多个方法的组合不是原子的。

不可变类线程安全性

String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的

3.6 习题分析

3.6.1 卖票

​ 测试下面代码是否存在线程安全问题,并尝试改正

package cn.shentianlan;

import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Vector;

@Slf4j(topic = "c.ExerciseSell")
public class ExerciseSell {
    public static void main(String[] args) {
        TicketWindow ticketWindow = new TicketWindow(2000);
        List<Thread> list = new ArrayList<>();
        // 用来存储卖出去多少张票
        List<Integer> sellCount = new Vector<>();
        for (int i = 0; i < 2000; i++) {
            Thread t = new Thread(() -> {
                // 分析这里的竞态条件
                int count = ticketWindow.sell(randomAmount());
                sellCount.add(count);
            });
            list.add(t);
            t.start();
        }
        list.forEach((t) -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        // 卖出去的票求和
        log.debug("selled count:{}", sellCount.stream().mapToInt(c -> c).sum());
        // 剩余票数
        log.debug("remainder count:{}", ticketWindow.getCount());
    }

    // Random 为线程安全
    static Random random = new Random();

    // 随机 1~5
    public static int randomAmount() {
        return random.nextInt(5) + 1;
    }
}

class TicketWindow {
    private int count;

    public TicketWindow(int count) {
        this.count = count;
    }

    public int getCount() {
        return count;
    }

    public  int sell(int amount) {
        if (this.count >= amount) {
            this.count -= amount;
            return amount;
        } else {
            return 0;
        }
    }
}

答案:给TicketWindow类的sell(int amount)加锁,即

public synchronized int sell(int amount) {
    if (this.count >= amount) {
        this.count -= amount;
        return amount;
    } else {
        return 0;
    }
}

问题:用下面的代码行不行,为什么?

List<Integer> sellCount = new ArrayList<>();

答案:不行,因为ArrayList不是线程安全的

测试脚本

for /L %n in (1,1,10) do java -cp ".;C:\Users\manyh\.m2\repository\ch\qos\logback\logback-
classic\1.2.3\logback-classic-1.2.3.jar;C:\Users\manyh\.m2\repository\ch\qos\logback\logback-
core\1.2.3\logback-core-1.2.3.jar;C:\Users\manyh\.m2\repository\org\slf4j\slf4j-
api\1.7.25\slf4j-api-1.7.25.jar" cn.itcast.n4.exercise.ExerciseSell
3.6.2 转账

​ 测试下面代码是否存在线程安全问题,并尝试改正

package cn.shentianlan;

import lombok.extern.slf4j.Slf4j;

import java.util.Random;

@Slf4j(topic = "c.ExerciseTransfer")
public class ExerciseTransfer {
    public static void main(String[] args) throws InterruptedException {
        Account a = new Account(1000);
        Account b = new Account(1000);
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                a.transfer(b, randomAmount());
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                b.transfer(a, randomAmount());
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        // 查看转账2000次后的总金额
        log.debug("total:{}", (a.getMoney() + b.getMoney()));
    }

    // Random 为线程安全
    static Random random = new Random();

    // 随机 1~100
    public static int randomAmount() {
        return random.nextInt(100) + 1;
    }
}

class Account {
    private int money;

    public Account(int money) {
        this.money = money;
    }

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

    public void transfer(Account target, int amount) {
        if (this.money > amount) {
            this.setMoney(this.getMoney() - amount);
            target.setMoney(target.getMoney() + amount);
        }
    }
}

答案:给Account类的transfer()加锁,锁住Account类对象,代码如下所示

public void transfer(Account target, int amount) {
    synchronized (Account.class){
        if (this.money > amount) {
            this.setMoney(this.getMoney() - amount);
            target.setMoney(target.getMoney() + amount);
        }
    }
}

不可这样写

public synchronized void transfer(Account target, int amount) {
    if (this.money > amount) {
        this.setMoney(this.getMoney() - amount);
        target.setMoney(target.getMoney() + amount);
    }
}

//因为上面的代码相当于
public void transfer(Account target, int amount) {
    synchronized (this){
        if (this.money > amount) {
            this.setMoney(this.getMoney() - amount);
            target.setMoney(target.getMoney() + amount);
        }
    }
}
//这样只是锁住一个共享变量money,还有一个共享变量amount没被锁住

3.7 Monitor概念

3.7.1 Java对象头

​ 以 32 位虚拟机为例

​ 普通对象
在这里插入图片描述
数组对象
在这里插入图片描述
其中 Mark Word 结构为在这里插入图片描述
64 位虚拟机 Mark Word
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值