一、共享带来的问题
1.1 小故事
老王(操作系统)有一个功能强大的算盘(CPU),现在想把它租出去,赚一点外快
小南、小女(线程)来使用这个算盘来进行一些计算,并按照时间给老王支付费用
但小南不能一天24小时使用算盘,他经常要小憩一会(sleep),又或是去吃饭上厕所(阻塞 io 操作),有 时还需要一根烟,没烟时思路全无(wait)这些情况统称为(阻塞)
在这些时候,算盘没利用起来(不能收钱了),老王觉得有点不划算
另外,小女也想用用算盘,如果总是小南占着算盘,让小女觉得不公平
于是,老王灵机一动,想了个办法 [ 让他们每人用一会,轮流使用算盘 ]
这样,当小南阻塞的时候,算盘可以分给小女使用,不会浪费,反之亦然
最近执行的计算比较复杂,需要存储一些中间结果,而学生们的脑容量(工作内存)不够,所以老王申请了 一个笔记本(主存),把一些中间结果先记在本上
计算流程是这样的
但是由于分时系统,有一天还是发生了事故
小南刚读取了初始值 0 做了个 +1 运算,还没来得及写回结果
老王说 [ 小南,你的时间到了,该别人了,记住结果走吧 ],于是小南念叨着 [ 结果是1,结果是1...] 不甘心地 到一边待着去了(上下文切换)
老王说 [ 小女,该你了 ],小女看到了笔记本上还写着 0 做了一个 -1 运算,将结果 -1 写入笔记本
这时小女的时间也用完了,老王又叫醒了小南:[小南,把你上次的题目算完吧],小南将他脑海中的结果 1 写 入了笔记本
小南和小女都觉得自己没做错,但笔记本里的结果是 1 而不是 0
1.2 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 //java准备常量1 isub //自减 putstatic i //将修改后的值存入静态变量i
而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换
如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题:
但多线程下这 8 行代码可能交错运行:
出现负数的情况:
出现正数的情况:
1.3 临界区 Critical Section
-
一个程序运行多个线程本身是没有问题的
-
问题出在多个线程访问共享资源
-
多个线程读共享资源其实也没有问题
-
在多个线程对共享资源读写操作时发生指令交错,就会出现问题
-
-
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
例如,下面代码中的临界区
static int counter = 0; static void increment() //临界区 { counter++; } static void decrement() //临界区 { counter--; }
1.4 竞态条件 Race Condition
多个线程在临界区内执行,由于代码的执行序列不同
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
-
阻塞式的解决方案:
synchronized
,Lock
-
非阻塞式的解决方案:原子变量
本次课使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
虽然 java中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
-
互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
-
同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
2.1 synchronized
语法
synchronized(对象) // 线程1, 线程2(blocked) { //临界区 }
-
-
当线程 t1 执行到
synchronized(room)
时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行count++
代码 -
这时候如果 t2 也运行到了
synchronized(room)
时,它发现门被锁住了,只能在门外等待,发生了上下文切 换,阻塞住了 -
这中间即使 t1 的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦), 这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才 能开门进入
-
当 t1 执行完
synchronized{}
块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥 匙给他。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的count--
时序图
思考:
synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切 换所打断
为了加深理解,请思考下面的问题
-
如果把
synchronized(obj)
放在 for 循环的外面,如何理解?-- 原子性-
整个for循环体作为整个原子体
-
-
如果 t1
synchronized(obj1)
而 t2synchronized(obj2)
会怎样运作?-- 锁对象-
不会保护临界区代码,因为拿的不是同一个锁对象
-
保护共享资源,多个线程要保证锁住的是同一个对象
-
-
如果 t1
synchronized(obj)
而 t2 没有加会怎么样?如何理解?-- 锁对象
面向对象改进
把需要保护的共享变量放入一个类
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()); } }
方法上的 synchronized (**)
synchronized不能锁方法,本质上是锁的对象
1.加在成员方法上,锁的是this对象
class Test{ public synchronized void test() { } } //等价于 class Test{ public void test() { synchronized(this) { } } }
2.加在静态方法上,锁的是类对象
class Test{ public synchronized static void test() { } } //等价于 class Test{ public static void test() { synchronized(Test.class) { } } }
不加 synchronized 的方法
不加 synchronzied 的方法就好比不遵守规则的人,不去老实排队(好比翻窗户进去的)
线程八锁(**)
其实就是考察 synchronized 锁住的是哪个对象
注意:sleep()会释放cpu资源,但是不会释放锁;wait()会释放锁
情况1:1->2 or 2->1
//a()、b()都加锁了,因为在同一个类下,所以都是给this加的锁 @Slf4j(topic = "c.Number") 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(); }
情况2:1s后1->2 or 2->1s后1
@Slf4j(topic = "c.Number") 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(); }
情况3:3 -> 1s后1->2 or 2->3 -> 1s后1 or 3->2 -> 1s后1
3不可能在1之后
c()
未加锁,与a()、b()
不会有互斥的效果,并行执行;而a()、b()
会有互斥效果
@Slf4j(topic = "c.Number") 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(); }
情况4:2 -> 1s 后 1
因为锁的不是同一对象,因此两者不互斥,加上t1休眠1s
@Slf4j(topic = "c.Number") 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(); }
情况5:2 -> 1s 后 1
static synchronized 锁住了类对象,类对象和this对象不是一个对象
a()、b()锁住的是不同的对象,中间没有互斥
@Slf4j(topic = "c.Number") 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(); }
情况6:1s后1 -> 2 or 2 -> 1s后1
a()
、b()
都是静态,锁住的是Number类对象,因此n1.a()
且n1.b()
是互斥的
@Slf4j(topic = "c.Number") 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(); }
情况7:2 -> 1s后1
锁住的仍然不是同一个对象
@Slf4j(topic = "c.Number") 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(); }
情况8:1s后1 -> 2 or 2 -> 1s后1
a()
、b()
都是静态,锁住的是Number类对象,因此n1.a()
且n2.b()
是互斥的,虽然是不同对象,但是是一个类的
@Slf4j(topic = "c.Number") 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(); }
-
如果它们没有共享,则线程安全
-
如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
-
如果只有读操作,则线程安全
-
如果有读写操作,则这段代码是临界区,需要考虑线程安全
-
3.2 局部变量是否线程安全?
-
局部变量是线程安全的(局部变量是线程私有的)
-
但局部变量引用的对象则未必
-
如果该对象没有逃离方法的作用访问,它是线程安全的
-
如果该对象逃离方法的作用范围,需要考虑线程安全
-
public static void test1() { int i = 10; i++; }
每个线程调用 test1()
方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享
public static void test1(); descriptor: ()V flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=0 0: bipush 10 2: istore_0 3: iinc 0, 1 6: return LineNumberTable: line 10: 0 line 11: 3 line 12: 6 LocalVariableTable: Start Length Slot Name Signature 3 4 0 i 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)
分析:
-
-
method3 与 method2 分析相同
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 相同
方法内的对象,发生了栈逃逸,所以会在堆内存空间创建list对象,没有逃逸的话是在栈内存分配list对象的内存
逃逸分析:方法内部对象没有被外部引用或代码结束仍在方法内部,这种属于没有逃逸
方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会代来线程安全问题?
-
情况1:有其它线程调用 method2 和 method3
-
其他线程直接调用method2 和 method3传过来的 list 与method1传进去的不是同一个,因此不会有问题
-
-
情况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"); } private 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(); } }
ThreadSafeSubClass extends ThreadSafe
,重写了父类method3,开辟了新线程,共享list,即出现了子类与父类共享资源,因此出现问题。
不能控制子类的行为,造成了线程安全的问题
访问修饰符在一定程度上,保护了线程安全
3.3 常见线程安全类(**)
-
String
-
Integer
-
-
Random
-
Vector
-
Hashtable
-
java.util.concurrent 包下的类
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为
-
它们的每个方法是原子的
-
但注意它们多个方法的组合不是原子的(不是线程安全的),见后面分析
线程安全类方法的组合
Hashtable table = new Hashtable(); // 线程1,线程2 if( table.get("key") == null) { table.put("key", value); }
不可变类线程安全性
String、Integer
等都是不可变类(final类),因为其内部的状态不可以改变,因此它们的方法都是线程安全的
有同学或许有疑问,String
有 replace
,substring
等方法可以改变值啊,那么这些方法又是如何保证线程安 全的呢?
1.subString()
源码
public String substring(int beginIndex) { if (beginIndex < 0) { throw new StringIndexOutOfBoundsException(beginIndex); } int subLen = value.length - beginIndex;//截取长度 = 总长度 - 索引下标 if (subLen < 0) { throw new StringIndexOutOfBoundsException(subLen); } //若索引为0?返回本身:创建新的字符串对象 return (beginIndex == 0) ? this : new String(value, beginIndex, subLen); }
2.String
构造器源码
//value为char数组 public String(char value[], int offset, int count) { if (offset < 0) { throw new StringIndexOutOfBoundsException(offset); } if (count <= 0) { if (count < 0) { throw new StringIndexOutOfBoundsException(count); } if (offset <= value.length) { this.value = "".value; return; } } // Note: offset or count might be near -1>>>1. if (offset > value.length - count) { throw new StringIndexOutOfBoundsException(offset + count); } //创建value字符串时,在原有字符串的基础上进行复制,赋值给新字符串(没有改动原有对象属性,直接创建新的) this.value = Arrays.copyOfRange(value, offset, offset+count); } 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
Servlet运行在tomcat环境下,只有一个实例,可以被多个线程共享使用
public class MyServlet extends HttpServlet { // 是否安全?否 HashMap不是线程安全的 Map<String,Object> map = new HashMap<>(); // 是否安全?是 String S1 = "..."; // 是否安全?是(final) final String S2 = "..."; // 是否安全?否(Date类不是) Date D1 = new Date(); // 是否安全?否(日期对象D2引用值是final,但是new Date()可变,即引用属性是可变的,因此不安全) final Date D2 = new Date(); public void doGet(HttpServletRequest request, HttpServletResponse response) { // 使用上述变量 } }
例2
public class MyServlet extends HttpServlet { // 是否安全?否(userService成员变量在Servlet是唯一的,多个线程共享) 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++; } }
例3
Spring AOP
Spring没有加@Scope(...)说明多例,则会默认为单例,即默认被共享,其成员变量默认被共享
@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)); } } //解决方案:做成环绕通知,将原有成员变量内嵌为局部变量
例4
三层结构的典型调用
/*----------自顶向下分析-----------*/ public class MyServlet extends HttpServlet { // 是否安全 是(UserService中的成员变量UserDao是私有,而且自身也是安全的) private UserService userService = new UserServiceImpl(); public void doGet(HttpServletRequest request, HttpServletResponse response) { userService.update(...); } } public class UserServiceImpl implements UserService { // 是否安全 是(虽然userdao是成员变量,但是内部无成员变量,无状态) private UserDao userDao = new UserDaoImpl(); public void update() { userDao.update(); } } public class UserDaoImpl implements UserDao { //无成员变量,因此update()线程安全 public void update() { String sql = "update user set password = ? where username = ?"; // 是否安全 是(conn是方法内的局部变量) try (Connection conn = DriverManager.getConnection("","","")){ // ... } catch (Exception e) { // ... } } }
例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 { // 是否安全 否(成员变量,且内部方法不安全) private UserDao userDao = new UserDaoImpl(); public void update() { userDao.update(); } } public class UserDaoImpl implements UserDao { // 是否安全 否(成员变量被共享,对比例4) private Connection conn = null; public void update() throws SQLException { String sql = "update user set password = ? where username = ?"; conn = DriverManager.getConnection("","",""); // ... conn.close(); } }
Servlet只有一份,导致了UserServiceImpl、UserDaoImpl只有一份,是多线程共享的。Connection是成员变量,也被多个线程所共享
例4、例5分析可知,实际编程中,要避免conn定义为成员变量
如:线程1实例化conn,线程2释放conn,那完犊子了
例6
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(); } }
例7
public abstract class Test { 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 的行为是不确定的,可能导致不安全的发生,被称之为外星方法
//子类中 重写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(); } }
请比较 JDK 中 String 类的实现
如果不设计成final,则继承String类的子类可能会覆盖掉父类的行为,造成线程不安全
String类的设计完美体现了Java的闭合原则
售票实例联系
测试下面代码是否存在线程安全问题,并尝试改正
public class ExerciseSell { public static void main(String[] args) throws InterruptedException { // 模拟多人买票 总共1000张票 TicketWindow window = new TicketWindow(1000); // 所有线程的集合 List<Thread> threadList = new ArrayList<>(); // 卖出的票数统计 Vector线程安全的 List<Integer> amountList = new Vector<>(); //启动线程 for (int i = 0; i < 2000; i++) { Thread thread = new Thread(() -> { // 买票 int amount = window.sell(random(5)); // 统计买票数 amountList.add(amount); }); threadList.add(thread); thread.start(); } // 主线程等待所有的线程结束,之后进行统计 for (Thread thread : threadList) { thread.join(); } // 统计卖出的票数和剩余票数 log.debug("余票:{}",window.getCount()); log.debug("卖出的票数:{}", amountList.stream().mapToInt(i-> i).sum()); } // Random 为线程安全 static Random random = new Random(); // 随机 1~5 public static int random(int amount) { return random.nextInt(amount) + 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; } } }
改进,在售票函数上加上synchronized:
//售票 临界区:共享变量有读写操作 public synchronized int sell(int amount) { if (this.count >= amount) { this.count -= amount; return amount; } else { return 0; } }
银行转账实例
public class ExerciseTransfer { public static void main(String[] args) throws InterruptedException { //两个账户,初始值为1000 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); } } }
总金额变多了?改进策略还是先去找共享变量,找临界区。可以这样改进吗?
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); } } }
只是保护了this对象,target对象没有保护,可以在对target加锁,但这样可能会造成死锁,可以写成这样:
public void transfer(Account target, int amount) { synchronized(Account.class) { if (this.money >= amount) { this.setMoney(this.getMoney() - amount); target.setMoney(target.getMoney() + amount); } } }
this和target共享了Account类,Account类对其所有对象都是共享的。
问题还是存在,银行这一时刻只有一笔交易了。