剑指JUC原理-4.共享资源和线程安全性

news2024/11/16 12:31:37

共享问题

小故事

老王(操作系统)有一个功能强大的算盘(CPU),现在想把它租出去,赚一点外快

在这里插入图片描述

小南、小女(线程)来使用这个算盘来进行一些计算,并按照时间给老王支付费用

但小南不能一天24小时使用算盘,他经常要小憩一会(sleep),又或是去吃饭上厕所(阻塞 io 操作),有
时还需要一根烟,没烟时思路全无(wait)这些情况统称为(阻塞)

在这里插入图片描述

在这些时候,算盘没利用起来(不能收钱了),老王觉得有点不划算

另外,小女也想用用算盘,如果总是小南占着算盘,让小女觉得不公平

于是,老王灵机一动,想了个办法 [ 让他们每人用一会,轮流使用算盘 ](分时系统)

这样,当小南阻塞的时候,算盘可以分给小女使用,不会浪费,反之亦然

最近执行的计算比较复杂,需要存储一些中间结果,而学生们的脑容量(工作内存)不够,所以老王申请了
一个笔记本(主存),把一些中间结果先记在本上

计算流程是这样的

在这里插入图片描述

但是由于分时系统,有一天还是发生了事故

小南刚读取了初始值 0 做了个 +1 运算,还没来得及写回结果

老王说 [ 小南,你的时间到了,该别人了,记住结果走吧 ],于是小南念叨着 [ 结果是1,结果是1…] 不甘心地
到一边待着去了(上下文切换)

老王说 [ 小女,该你了 ],小女看到了笔记本上还写着 0 做了一个 -1 运算,将结果 -1 写入笔记本

这时小女的时间也用完了,老王又叫醒了小南:[小南,把你上次的题目算完吧],小南将他脑海中的结果 1 写
入了笔记本

在这里插入图片描述

小南和小女都觉得自己没做错,但笔记本里的结果是 1 而不是 0

Java的体现

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

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);
}
问题分析

以上的结果可能是正数、负数、零。为什么呢?因为 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

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

synchronized 解决方案

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

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

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

synchronized

语法

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

解决

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

为了加深理解,请思考下面的问题

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

其实很好回答,就是相当于保证了整个for循环的原子性。

2.如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎样运作?

相当于没有作用,因为想要保护共享资源,需要让多个资源保护同一把锁

3.如果 t1 synchronized(obj) 而 t2 没有加会怎么样?如何理解?

由于线程2没有加锁,根据上图所示,第一次上下文切换的时候,就不会出现获取锁被阻塞的情况,所以就会直接写入,导致出现问题。

面向对象的改进

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

class Room {
 int value = 0;
 public void increment() {
 synchronized (this) {
 value++;
 }
 }
 public void decrement() {
 synchronized (this) {
 value--;
 }
 }
 public int get() {
 synchronized (this) {
 return value;
 }
 }
}
@Slf4j
public class Test1 {
 
 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());
 }
}

这里其实需要关注的是为什么get方法也上锁了,其实是为了保证获取值是一个准确的结果,而不是一个中间的结果,需要加锁。

方法上的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) {
 
 }
 }
}

所谓的线程八锁

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

情况1:
class Number{
 public synchronized void a() {
 log.debug("1");
 }
 public synchronized void b() {
 log.debug("2");
 }
}
public static void main(String[] args) {
 Number n1 = new Number();
 new Thread(()->{ n1.a(); }).start();
 new Thread(()->{ n1.b(); }).start();
}

先1后2 或者 先2后1 原因是因为 同一个对象实例,都是同一把锁,谁先抢到锁谁先输出

情况2:
class Number{
 public synchronized void a() {
 sleep(1);
 log.debug("1");
 }
 public synchronized void b() {
 log.debug("2");
 }
}
public static void main(String[] args) {
 Number n1 = new Number();
 new Thread(()->{ n1.a(); }).start();
 new Thread(()->{ n1.b(); }).start();
}

1s后1 2 或者 2 1s后 1 情况与情况1一致

情况3:
class Number{
 public synchronized void a() {
 sleep(1);
 log.debug("1");
 }
 public synchronized void b() {
 log.debug("2");
 }
 public void c() {
 log.debug("3");
 }
}
public static void main(String[] args) {
 Number n1 = new Number();
 new Thread(()->{ n1.a(); }).start();
 new Thread(()->{ n1.b(); }).start();
 new Thread(()->{ n1.c(); }).start();
}

先 3 1s后 1 2 或者 2 3 1s 后 1 或者 3 2 1s 后 1 这个总体上还是和情况1一致

情况4:
class Number{
 public synchronized void a() {
 sleep(1);
 log.debug("1");
 }
 public synchronized void b() {
 log.debug("2");
 }
}
public static void main(String[] args) {
 Number n1 = new Number();
 Number n2 = new Number();
 new Thread(()->{ n1.a(); }).start();
 new Thread(()->{ n2.b(); }).start();
}

2 1s 后 1 之所以会出现这样的情况是因为,因为 synchronized(this)锁住的是 实例对象,而main中有两个不一样的实例,那么其实就相当于没加锁,所以自然是这个执行顺序

情况5:
class Number{
 public static synchronized void a() {
 sleep(1);
 log.debug("1");
 }
 public synchronized void b() {
 log.debug("2");
 }
}
public static void main(String[] args) {
 Number n1 = new Number();
 new Thread(()->{ n1.a(); }).start();
 new Thread(()->{ n1.b(); }).start();
}

2 1s 后 1 这个情况其实和前面说的一样,实际上是关于锁对象的概念,就是如果给静态方法加锁,实际上的锁对象是类本身,而不是实例对象,而给普通方法加锁,锁对象是实例对象,所以自然是这个结果

情况6:
class Number{
 public static synchronized void a() {
 sleep(1);
 log.debug("1");
 }
 public static synchronized void b() {
 log.debug("2");
 }
}
public static void main(String[] args) {
 Number n1 = new Number();
 new Thread(()->{ n1.a(); }).start();
 new Thread(()->{ n1.b(); }).start();
}

1s 后12, 或 2 1s后 1 实际上和情况1类似,只不过 锁对象由 实例对象 替换成了 类对象

情况7:
class Number{
 public static synchronized void a() {
 sleep(1);
 log.debug("1");
 }
 public synchronized void b() {
 log.debug("2");
 }
}
public static void main(String[] args) {
 Number n1 = new Number();
 Number n2 = new Number();
 new Thread(()->{ n1.a(); }).start();
 new Thread(()->{ n2.b(); }).start();
}

2 1s 后 1 这个情况也非常好分析,锁对象不同

情况8:
class Number{
 public static synchronized void a() {
 sleep(1);
 log.debug("1");
 }
 public static synchronized void b() {
 log.debug("2");
 }
}
public static void main(String[] args) {
 Number n1 = new Number();
 Number n2 = new Number();
 new Thread(()->{ n1.a(); }).start();
 new Thread(()->{ n2.b(); }).start();
}

1s 后12, 或 2 1s后 1 该情况和前面情况重复,不在介绍

变量的线程安全分析

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

如果它们没有共享,则线程安全

如果它们被共享了,根据它们的状态是否能够改变,又分两种情况

  • 如果只有读操作,则线程安全
  • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量是否线程安全?

局部变量是线程安全的

但局部变量引用的对象则未必

  • 如果该对象没有逃离方法的作用访问,它是线程安全的
  • 如果该对象逃离方法的作用范围,需要考虑线程安全

局部变量线程安全分析

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

每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享

在这里插入图片描述

局部变量的引用稍有不同

先看一个成员变量的例子

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);
 }
}

执行

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

其中一种情况是,如果线程2 还未 add,线程1 remove 就会报错:

Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0 
 at java.util.ArrayList.rangeCheck(ArrayList.java:657) 
 at java.util.ArrayList.remove(ArrayList.java:496) 
 at cn.itcast.n6.ThreadUnsafe.method3(TestThreadSafe.java:35) 
 at cn.itcast.n6.ThreadUnsafe.method1(TestThreadSafe.java:26) 
 at cn.itcast.n6.TestThreadSafe.lambda$main$0(TestThreadSafe.java:14) 
 at java.lang.Thread.run(Thread.java:748) 

分析:

  • 无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量
  • method3 与 method2 分析相同

在这里插入图片描述

将 list 修改为局部变量

class ThreadSafe {
 public final 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);
 }
}

那么就不会有上述问题了

分析:

  • list 是局部变量,每个线程调用时会创建其不同实例,没有共享
  • 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象
  • method3 的参数分析与 method2 相同

在这里插入图片描述

方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会代理线程安全问题?

情况1:有其它线程调用 method2 和 method3,有可能被其他线程调用到,此时的list不一定是局部变量了

情况2:在 情况1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法,即

class ThreadSafe {
 public final 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");
}
 public void method3(ArrayList<String> list) {
 list.remove(0);
 }
}
class ThreadSafeSubClass extends ThreadSafe{
 @Override
 public void method3(ArrayList<String> list) {
 new Thread(() -> {
 list.remove(0);
 }).start();
 }
}

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

常见的线程安全类

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

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

Hashtable table = new Hashtable();
new Thread(()->{
 table.put("key", "value1");
}).start();
new Thread(()->{
 table.put("key", "value2");
}).start();

它们的每个方法是原子的

但注意它们多个方法的组合不是原子的

线程安全类方法的组合

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

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

在这里插入图片描述

不可变类线程安全性

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

有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安
全的呢?

进入replace源码查看:

public String replace(char oldChar, char newChar) {
        if (oldChar != newChar) {
            int len = value.length;
            int i = -1;
            char[] val = value; /* avoid getfield opcode */

            while (++i < len) {
                if (val[i] == oldChar) {
                    break;
                }
            }
            if (i < len) {
                char buf[] = new char[len];
                for (int j = 0; j < i; j++) {
                    buf[j] = val[j];
                }
                while (i < len) {
                    char c = val[i];
                    buf[i] = (c == oldChar) ? newChar : c;
                    i++;
                }
                return new String(buf, true);
            }
        }
        return this;
    }

其实可以看到最终的返回是 return 了一个 新的String对象,并不是改变值。

以一个例子来举例:

public class Immutable{
 private int value = 0;
 public Immutable(int value){
 this.value = value;
 }
 public int getValue(){
 return this.value;
 }
}

如果想增加一个增加的方法呢?

public class Immutable{
 private int value = 0;
 public Immutable(int value){
 this.value = value;
 }
 public int getValue(){
 return this.value;
 }
 
 public Immutable add(int v){
 return new Immutable(this.value + v);
 } 
}

实例分析

例1:
// 是否安全?
 Map<String,Object> map = new HashMap<>();
 // 是否安全?
 String S1 = "...";
 // 是否安全?
 final String S2 = "...";
 // 是否安全?
 Date D1 = new Date();
 // 是否安全?
 final Date D2 = new Date();

其中map不是线程安全的,map的线程安全由其他方式实现

s1 和 s2都是线程安全的

D1 不是线程安全的

D2 虽然加了final,但是日期是可变类,里面的值是可以改变的

例2:
public class MyServlet extends HttpServlet {
 // 是否安全?
 private UserService userService = new UserServiceImpl();
 
 public void doGet(HttpServletRequest request, HttpServletResponse response) {
 userService.update(...);
 }
}
public class UserServiceImpl implements UserService {
 // 记录调用次数
 private int count = 0;
 
 public void update() {
 // ...
 count++;
 }
}

并不是线程安全的,原因是 impl中有 临界区资源没被控制

例3:
@Aspect
@Component
public class MyAspect {
 // 是否安全?
 private long start = 0L;
 
 @Before("execution(* *(..))")
 public void before() {
 start = System.nanoTime();
 }
 
 @After("execution(* *(..))")
 public void after() {
 long end = System.nanoTime();
 System.out.println("cost time:" + (end-start));
 }
}

还是存在线程安全的,spring是单例,所以临界区资源要被共享,如果想保证线程安全,那么可以使用环绕通知来实现,此时 start 和 after都是局部变量,那么此时就不存在线程安全问题了。

例4:
public class MyServlet extends HttpServlet {
 // 是否安全
 private UserService userService = new UserServiceImpl();
 
 public void doGet(HttpServletRequest request, HttpServletResponse response) {
 userService.update(...);
 }
}
public class UserServiceImpl implements UserService {
 // 是否安全
 private UserDao userDao = new UserDaoImpl();
 
 public void update() {
 userDao.update();
 }
}
public class UserDaoImpl implements UserDao { 
 public void update() {
 String sql = "update user set password = ? where username = ?";
 // 是否安全
 try (Connection conn = DriverManager.getConnection("","","")){
 // ...
 } catch (Exception e) {
 // ...
 }
 }
}

本质上是线程安全的,没有成员变量,Conn 属于一个方法内部的局部变量,每个线程得到的都是一个新的。

例5:
public class MyServlet extends HttpServlet {
 // 是否安全
 private UserService userService = new UserServiceImpl();
 
 public void doGet(HttpServletRequest request, HttpServletResponse response) {
 userService.update(...);
 }
}
public class UserServiceImpl implements UserService { 
 public void update() {
 UserDao userDao = new UserDaoImpl();
 userDao.update();
 }
}
public class UserDaoImpl implements UserDao {
 // 是否安全
 private Connection = null;
 public void update() throws SQLException {
 String sql = "update user set password = ? where username = ?";
 conn = DriverManager.getConnection("","","");
 // ...
 conn.close();
 }
}

其中虽然UserDao 线程不安全,但是 UserServiceImpl它在局部变量中,不存在线程不安全

例7:
public void bar() {
 // 是否安全
 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
 foo(sdf);
}
 
 public abstract foo(SimpleDateFormat sdf);
 
 
 public static void main(String[] args) {
 new Test().bar();
 }

其中还是有可能存在线程安全的问题,原因是因为,是局部变量还需要看看会不会暴漏给其他线程,也要看对象的引用是否泄露了

其中 foo 的行为是不确定的,可能导致不安全的发生,被称之为外星方法

public void foo(SimpleDateFormat sdf) {
 String dateStr = "1999-10-11 00:00:00";
 for (int i = 0; i < 20; i++) {
 new Thread(() -> {
 try {
 sdf.parse(dateStr);
 } catch (ParseException e) {
 e.printStackTrace();
 }
 }).start();
 }
}

其实在这里面也就可以理解了,String不设置为null,其子类就有可能覆盖其中的一些行为,导致线程不安全性。

卖票问题

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;
 }
 }
}

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;
 }

这段代码中是存在线程安全的问题的,但是线程安全的问题不好复现,可以加上sleep来方便复现问题。

通过分析代码,发现是sell方法里面的临界资源出现了 线程不安全的问题

解决方法就是再方法上加入 synchronized。

但是其实代码中还是存在很多细节的:

比如说,再线程中有一个sellCount.add的操作,这个操作其实是多线程操作,每一个线程都需要执行,而普通的list集合是线程不安全的,所以代码中使用的是 Vector,具体再java中源码如下:

ArrayList
public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    
Vector
public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }

而 后面还有一个list.add 这个没有用线程安全的集合实现,原因就是 因为它是执行在主线程中,本身就不存在线程安全的问题。

转账问题

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

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);
 	}
 }
}

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;
 }

其实根据卖票问题,代码中也是存在一个线程不安全问题的。但是不能像卖票问题一样,加一个锁就可以了,仔细分析一下发现 transfer中存在两个共享变量,money 和 target,如果对方法加synchronized ,实际上只对money这个共享变量上锁了,而 target并没有,解决办法其实就是对他们两个的父类上锁,也就是 锁对象是Account.class。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1141124.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何优化工业5G网关的网络信号

工业5G网关&#xff0c;通常是指支持5G网络&#xff0c;具有高速率、低时延、广接入等特点的高性能工业物联网智能网关&#xff0c;这类网关具有强大的设备接入能力、通信协议转换、运算处理能力、联动控制能力&#xff0c;有助于提升工业物联网整体通信效率&#xff0c;实现生…

tooltip实现悬停内容高亮及格式化

一: 通过highlight.js项目实现对json字符串的染色高亮 此项目是jsp文件,并且引用了element-ui/highlight.js的组件&#xff0c;对tooltip中的json文本&#xff08;理论上支持highlight所支持的所有项目&#xff09;进行高亮并格式化 二: 实现效果 三: 代码实现 关键点在于成功…

树莓派 qt 调用multimedia、multimediawidgets、serialport、Qchats

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、测试11.命令安装出现错误 二、测试21. 安装 Qt Charts&#xff1a;2. 安装 Qt Multimedia 和 Qt MultimediaWidgets&#xff1a;3. 安装 Qt SerialPort&…

postgis ST_CoverageInvalidEdges用法

官方文档 概要 geometry ST_CoverageInvalidEdges(geometry winset geom, float8 tolerance 0); 描述 一个窗口函数&#xff0c;用于检查窗口分区中的多边形是否形成有效的多边形覆盖范围。 它返回线性指示器&#xff0c;显示每个多边形中无效边&#xff08;如果有&#x…

C++项目——云备份-⑥-服务端热点管理模块的设计与实现

文章目录 专栏导读1.热点管理类设计2.热点管理类的实现与整理 专栏导读 &#x1f338;作者简介&#xff1a;花想云 &#xff0c;在读本科生一枚&#xff0c;C/C领域新星创作者&#xff0c;新星计划导师&#xff0c;阿里云专家博主&#xff0c;CSDN内容合伙人…致力于 C/C、Linu…

解放工程师双手帮助网工做运维

✍ SNMP为什么被誉为“网管神器”&#xff1f; ✍ SNMP不同版本有何区别&#xff1f; ✍ SNMP有哪些问题及Telemetry有何优势&#xff1f; telnet, ssh远程登录到设备&#xff1a; 简单网络管理协议&#xff1a;SNMP 集中式管理&#xff1a; 华为&#xff1a;e-sight 华三…

计算机毕设 基于CNN实现谣言检测 - python 深度学习 机器学习

文章目录 1 前言1.1 背景 2 数据集3 实现过程4 CNN网络实现5 模型训练部分6 模型评估7 预测结果8 最后 1 前言 Hi&#xff0c;大家好&#xff0c;这里是丹成学长&#xff0c;今天向大家介绍 一个深度学习项目 基于CNN实现谣言检测 1.1 背景 社交媒体的发展在加速信息传播的…

使用NATAPP内网穿透详细步骤

在开发过程中&#xff0c;避免不了前端和后端不在一个局域网下&#xff0c;这时候&#xff0c;前后端联调的时候&#xff0c;前端访问不到后端的服务器&#xff0c;使用穿透就可以解决这个问题。 1、打开网址https://natapp.cn/2、进行注册&#xff0c;然后登录 3、击购买渠道…

大数据-Storm流式框架(五)---DRPC

DRPC 概念 分布式RPC&#xff08;DRPC&#xff09;背后的想法是使用Storm在运行中并行计算真正强大的函数。 Storm拓扑接收函数参数流作为输入&#xff0c;并为每个函数调用发送结果的输出流。 DRPC并不是Storm的一个特征&#xff0c;因为它基于Storm的spouts&#xff0c;bo…

推荐一个高效测试用例工具:XMind2TestCase..

一、背景 软件测试的核心是什么&#xff1f;毫无疑问是测试分析和测试用例设计&#xff0c;也是日常测试投入最多时间的工作内容之一。 然而&#xff0c;传统的测试用例设计过程有很多痛点&#xff1a; 1、使用Excel表格进行测试用例设计&#xff0c;虽然成本低&#xff0c;但…

FL Studio音乐编曲软件好不好用?要不要购买

音乐编曲软件的出现使得音乐创作者能够克服时间和空间的限制&#xff0c;随时随地进行创作。随着信息时代的发展&#xff0c;使用编曲软件已成为音乐创作领域的主流。那么编曲软件哪个好用呢&#xff1f;我推荐这三款。 在业内&#xff0c;常用的音乐编曲软件包括Cubase、Logi…

增强常见问题解答搜索引擎:在 Elasticsearch 中利用 KNN 的力量

在快速准确的信息检索至关重要的时代&#xff0c;开发强大的搜索引擎至关重要。 随着大型语言模型和信息检索架构&#xff08;如 RAG&#xff09;的出现&#xff0c;在现代软件系统中利用文本表示&#xff08;向量/嵌入&#xff09;和向量数据库已变得越来越流行。 在本文中&am…

scratch接钻石 2023年9月中国电子学会图形化编程 少儿编程 scratch编程等级考试三级真题和答案解析

目录 scratch接钻石 一、题目要求 1、准备工作 2、功能实现 二、案例分析

postgis ST_ClipByBox2D用法

官方文档 概述 geometry ST_ClipByBox2D(geometry geom, box2d box); 描述 以快速且宽松但可能无效的方式通过 2D 框剪切几何体。 拓扑上无效的输入几何图形不会导致抛出异常。 不保证输出几何图形有效&#xff08;特别是&#xff0c;可能会引入多边形的自相交&#xff09;…

FL Studio21.2中文版多少钱?值得下载吗

水果&#xff0c;全称Fruity Loop Studio&#xff0c;简称FL Studio。是一款全能的音乐制作软件&#xff0c;经过二十多年的演化更迭&#xff0c;其各项功能非常的先进。其开创性的Pat\song模式&#xff0c;也为初学者的学习提供了便利。那么水果音乐制作软件需要多少钱呢&…

鸡尾酒学习——沧海桑田

1、材料&#xff1a;冰块&#xff08;或者雪莲&#xff09;、蓝橙力娇酒、伏特加、橙汁、柠檬、雪碧/气泡水&#xff1b; 2、口感&#xff1a;酸甜口味&#xff0c;下层感觉是再喝橙汁&#xff0c;上层在喝有些度数的雪碧&#xff0c;可能是昨天的长岛冰茶过于惊艳&#xff0c;…

机器学习(四十九):高斯混合模型

补充一个聚类算法:高斯混合模型 假设有一组需要根据它们的相似性分组到几个部分或簇中的数据点。在机器学习中,这被称为聚类。有几种可用的聚类方法: K均值聚类分层聚类高斯混合模型在这篇文章中,我们将讨论高斯混合模型。 文章目录 正态或高斯分布期望最大化(EM)算法期…

微信 macOS 版迎来 3.8.4.20 更新,新功能一览

微信 macOS 版迎来 3.8.4.20 更新&#xff0c;增加了多个新功能&#xff0c;包括可将某个聊天在独立窗口中显示、可在聊天中搜索表情等。 附更新信息如下&#xff1a; 可将某个聊天在独立窗口中显示&#xff1b; ・可在聊天中搜索表情&#xff1b; ・新增 「看一看」&#…

Umijs创建一个antd+Ts项目环境

上文搭建Umijs环境并创建一个项目 介绍基本操作中 我们构建了一个Umijs环境的环境 但也只创建了一个页面 真正开发来讲 也不可能只创建几个界面这么简单 这里面的创建 还是非常完整的 这里 我创建一个文件夹 主要是做我们的项目目录 然后 我们在终端输入命令 然后 打开目录终…

VScode 自定义主题各参数解析

参考链接&#xff1a; vscode自定义颜色时各个参数的作用(史上最全)vscode编辑器&#xff0c;自己喜欢的颜色 由于 VScode 搜索高亮是在是太不起眼了&#xff0c;根本看不到此时选中到哪个搜索匹配了&#xff0c;所以对此进行了配置&#xff0c;具体想增加更多可配置项可参考…