Why Combining synchronized with @Transactional Causes Thread‑Safety Issues in Spring and How to Resolve Them
The article explains why using the synchronized keyword together with Spring's @Transactional annotation can lead to lost updates when many threads increment a database field, analyzes the underlying transaction‑proxy interaction, and presents a refactored solution that moves the lock to a separate service to ensure correct results.
In response to a Zhihu question about a synchronized method that increments a database column but yields a final value lower than the expected total, the author investigates the root cause of the thread‑safety problem.
Initial reasoning assumes that the synchronized method guarantees ordering, atomicity, and visibility, and that the @Transactional annotation should ensure each increment is committed, so the final count should equal the number of threads.
Through testing with 1,000 concurrent threads, the author discovers that the addEmployee() method is not executed serially; multiple threads interleave their database reads and writes, causing lost updates.
The deeper analysis reveals that Spring’s transaction management is implemented via dynamic proxies (AOP). When a method annotated with @Transactional is also synchronized, the lock only protects the Java method body, while the transaction commit occurs after the lock is released. Consequently, another thread can enter the synchronized method before the previous transaction is committed, reading stale data and causing inconsistent updates.
To fix the issue, the author proposes moving the synchronized block to a separate service that wraps the transactional call, ensuring the lock covers the entire transaction lifecycle. The revised code is shown below:
@RestController
public class EmployeeController {
@Autowired
private SynchronizedService synchronizedService;
@RequestMapping("/add")
public void addEmployee() {
for (int i = 0; i < 1000; i++) {
new Thread(() -> synchronizedService.synchronizedAddEmployee()).start();
}
}
}
// New service that holds the lock
@Service
public class SynchronizedService {
@Autowired
private EmployeeService employeeService;
public synchronized void synchronizedAddEmployee() {
employeeService.addEmployee();
}
}
@Service
public class EmployeeService {
@Autowired
private EmployeeRepository employeeRepository;
@Transactional
public void addEmployee() {
Employee employee = employeeRepository.getOne(8);
System.out.println(Thread.currentThread().getName() + employee);
Integer age = employee.getAge();
employee.setAge(age + 1);
employeeRepository.save(employee);
}
}With this arrangement, the synchronized lock now spans the whole transaction, eliminating the lost‑update problem; however, the execution becomes slower due to the serialized access, which is expected.
The article concludes by noting that while Spring transactions are convenient, developers must understand their interaction with Java synchronization to avoid subtle bugs.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Captain
Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
