前言
此为我在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 线程运行原理
-
栈与栈帧
JVM由栈、堆、方法区所组成,其中栈内存由线程使用,每个线程启动后,虚拟机就会为其分配一块栈内存。
-
每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存
-
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
-
-
线程上下文切换(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() | 获取线程长整型的 id | id 唯一 | |
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()实现)
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 里无法区分,仍然认为
是可运行)BLOCKED
,WAITING
,TIMED_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