多线程并发操作共享可变对象不同字段:线程安全吗?是合理实践吗?
多线程并发更新/读取共享对象的不同字段是否线程安全?是否属于良好实践?
注意:假设任何时刻都不会有两个或更多线程对该共享可变对象的同一字段/属性进行操作(读取或写入)
示例场景
以下Java代码中,t1、t2、t3线程分别操作共享对象emp的salary、exp、id字段,这种并发操作是否线程安全?是否可作为常规实践?
package concurrency; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; public class SharedObjectApp { public static void main(String args[]){ new App().doSomething(); } public class App{ Employee emp = new Employee(001, 100000, 32); public void doSomething(){ Thread t1 = new Thread(new Runnable(){ @Override public void run(){ emp.setSalary(emp.getSalary() + 1000); } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { emp.setExp(emp.getExp() + 1); } }); Thread t3 = new Thread(new Runnable() { @Override public void run() { for(int i = 0; i < 10; i ++){ System.out.println(emp.getId()); } } }); t1.start(); // 更新salary t2.start(); // 更新exp t3.start(); // 读取id } } @NoArgsConstructor @AllArgsConstructor @Getter @Setter public static class Employee { int id; double salary; double exp; } }
一、线程安全性结论
在给定的严格前提(同一时刻无多线程操作同字段)下,这种操作是线程安全的:
- Java中基本类型的单个读/写操作是原子性的(
long和double的非volatile操作在JMM规范中允许拆分,但现代JVM大多会保证其原子性,实际场景中很少出问题)。 - 不同字段的内存操作完全独立,不存在数据竞争——没有线程同时访问同一字段,不会出现脏读、数据损坏的情况。
- 示例中的三个线程操作完全隔离,t1只碰
salary、t2只碰exp、t3只碰id,彼此不会干扰。
但要警惕:一旦后续代码打破这个前提(比如新增线程操作salary),emp.setSalary(emp.getSalary() + 1000)这种读-改-写复合操作立刻会引发线程安全问题,因为它本身不是原子性的。
二、是否属于良好实践?
绝对不算。核心问题在于这种做法的可维护性极差,具体原因:
- 约定依赖脆弱:线程安全完全依赖“不同线程操作不同字段”的人为约定,后续代码迭代中很容易被打破——比如新增功能时不小心让另一个线程操作了
salary,或者修改现有线程逻辑引入跨字段操作,此时问题会隐蔽出现,排查难度极大。 - 缺乏可见性保障:如果字段未声明为
volatile,一个线程对字段的修改,其他线程可能无法及时读到最新值(示例中id是不变的所以没问题,但如果是可变字段的跨线程读取,就会出现可见性问题)。 - 违反封装原则:共享对象的状态被外部线程直接修改,对象本身无法管控自己的状态变更,不符合面向对象的封装思想。
推荐替代方案
- 拆分对象:将不同线程操作的字段拆分为独立对象,每个线程仅操作自己负责的对象,从根源上消除共享。
- 使用原子类:如果必须共享单个字段,用
AtomicInteger、AtomicDouble等原子类替代基本类型,保证操作的原子性和可见性。 - 统一同步控制:如果对象状态需要整体一致性,用
synchronized块或Lock保护所有操作,虽然牺牲少量性能,但能彻底保证线程安全。 - ThreadLocal存储:如果线程间不需要共享状态,直接用
ThreadLocal让每个线程持有自己的对象实例。
内容的提问来源于stack exchange,提问作者Tintin
相关产品推荐
相关产品推荐

