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

  • 👏作者简介:大家好,我是爱吃芝士的土豆倪,24届校招生Java选手,很高兴认识大家
  • 📕系列专栏:Spring源码、JUC源码
  • 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦
  • 🍂博主正在努力完成2023计划中:源码溯源,一探究竟
  • 📝联系方式:nhs19990716,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬👀

共享问题

小故事

老王(操作系统)有一个功能强大的算盘(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。

相关推荐博客

👉👉👉 剑指JUC原理-1.进程与线程-CSDN博客

👉👉👉 剑指JUC原理-2.线程-CSDN博客

👉👉👉 剑指JUC原理-3.线程常用方法及状态-CSDN博客

👉👉👉 剑指JUC原理-5.synchronized底层原理-CSDN博客

👉👉👉 剑指JUC原理-6.wait notify-CSDN博客

👉👉👉 剑指JUC原理-7.线程状态与ReentrantLock-CSDN博客

👉👉👉 剑指JUC原理-8.Java内存模型-CSDN博客

👉👉👉 剑指JUC原理-9.Java无锁模型-CSDN博客

👉👉👉 剑指JUC原理-10.并发编程大师的原子累加器底层优化原理(与人类的优秀灵魂对话)-CSDN博客

👉👉👉 剑指JUC原理-11.不可变设计-CSDN博客

👉👉👉 剑指JUC原理-12.手写简易版线程池思路-CSDN博客

👉👉👉 剑指JUC原理-13.线程池-CSDN博客

👉👉👉 剑指JUC原理-14.ReentrantLock原理-CSDN博客

👉👉👉 剑指JUC原理-15.ThreadLocal-CSDN博客

👉👉👉 剑指JUC原理-16.读写锁-CSDN博客

👉👉👉 剑指JUC原理-17.CompletableFuture-CSDN博客

👉👉👉 剑指JUC原理-18.同步协作-CSDN博客

👉👉👉 剑指JUC原理-19.线程安全集合-CSDN博客

👉👉👉 剑指JUC原理-20.并发编程实践-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱吃芝士的土豆倪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值