线程安全其实就是让多个线程对共享数据的访问顺序不乱套

线程安全其实就是让多个线程对共享数据的访问顺序不乱套。举个极端的例子:Bank 的账户余额本来是 0,现在有两个线程同时运行——一个想往里面存 100 元,另一个想把这 100 元取走。要是这两个操作恰好都进到了修改余额的那行代码(也就是“临界区”),会发生什么?如果取钱的线程先拿到执行权,余额会变成 -100,可它立马就会被存钱的线程的结果给覆盖掉,账户就这么莫名其妙地“复活”了;反过来如果存钱的线程先执行,余额变成 100 后取钱的线程接着动,账户又会瞬间“归零”。这样的结果对谁有利?根本说不清楚!多线程同步的核心目的,就是让读写操作一个接一个地进行,保证任何时候只有一个线程能改共享数据,从而避免出现指针乱飞或者余额变成负数这种奇怪的状态。 关于同步的写法主要有两种。一种是直接在方法上加上 synchronized 关键字,这是种简单直接的“全包围式”锁。Java 的每个对象本身自带一把内置锁,谁调用了带这个关键字的方法,谁就先把这把锁拿在手里。比如写一段简单的“银行”代码:public class Bank 里面有个 private int count = 0 代表账户余额;存钱的方法 addMoney 和取钱的方法 subMoney 都被 synchronized 修饰了。在 addMoney 里把 count 加上传进来的 money,然后用 System.currentTimeMillis() 打印一下时间;在 subMoney 里先判断一下余额够不够减,不够就直接返回不再改数,够了就减掉并打印结果。不过在取钱逻辑里还加了个 try/catch 块,如果调用 Thread.sleep(1000) 时遇到 InterruptedException 就用 e.printStackTrace() 把堆栈信息打出来。在主函数里启动两个线程:一个存 100 一个取 100。运行结果能清楚地看到所有读写操作都被强制按顺序排好队执行,余额一直是对的。要注意的是静态同步方法锁的是整个类而不是单个对象,用的时候要小心。 另一种写法是用 synchronized 关键字包裹住具体的代码块来进行同步操作。如果方法体很大但只有一小段关键代码需要锁住就可以用这种细粒度的方式。还是刚才那个 Bank 类的例子:把存钱方法里的关键逻辑放进 synchronized (this) 块里去执行就行了。取钱的方法也可以改写成同步块的形式,不过这里不再重复写那段代码了。好处是锁的范围变小了能让更多线程同时干活;坏处是得手动去管理锁的获取和释放,如果忘记写 finally 块去释放锁就很容易造成死锁。比较稳妥的做法是把锁对象定义成私有的 final 变量,这样既能保证它是唯一的又方便传递给别的地方用。 同步虽然能解决问题但也会带来一些副作用。最常见的就是死锁:两个或者更多的线程互相拿着对方想要的资源不放。解决思路通常是避免嵌套锁、或者让所有线程按照同一个顺序去抢资源。还有可能会出现饥饿或活锁的情况:有些线程老是拿不到锁一直干等着导致“忙等”。这时候可以试试 Lock 接口提供的 tryLock 方法或者使用公平锁来缓解一下。性能损耗也是一个问题:频繁切换上下文会把 CPU 的使用率给推上去。所以要评估清楚是不是真的有必要进行同步:如果是读操作多写操作少的时候可以考虑用“无锁设计”或者“乐观并发控制”这种方法。 最后总结一下:多线程同步不是万能的灵丹妙药但绝对是让程序稳定运行的一块“压舱石”。在处理共享数据时先想清楚哪些线程应该同时修改它然后再决定用哪种锁或者怎么去锁。只有把串行化做得足够优雅程序才能在并发的世界里站稳脚跟。