Синхронизированная блокировка не является потокобезопасной при управлении транзакциями Spring?

Java

предисловие

Только лысая голова может стать сильнее.

Текст был включен в мой репозиторий GitHub, добро пожаловать, звезда:GitHub.com/Zhongf UC очень…

На второй день нового года друг задал мне технический вопрос (мой друг очень хочет учиться, я восхищаюсь!)

Источник проблемы знает (проблема синхронизированной блокировки):

Откройте 10 000 потоков, и каждый поток добавляет 1 в поле денег таблицы сотрудников [начальное значение равно 0]. Пессимистические блокировки и оптимистичные блокировки не используются, но в метод бизнес-уровня добавляется ключевое слово synchronized. Проблема в том, что БД выполняется после выполнения кода.Поле денег в не 10000, а меньше 10000 В чем проблема?

Код сервисного уровня:

代码

Код SQL (без пессимистической/оптимистической блокировки):

SQL代码(没有加悲观/乐观锁)

Запустите код с 1000 потоков:

用1000个线程跑代码:

Проще говоря: многопоточность запускает один с использованиемsynchronizedМетод с измененным ключевым словом работает с базой данных.По нормальной логике конечное значение должно быть 1000, но после многих тестов результатниже чем1000. Почему это?

1. Мое мышление

Так как результат теста ниже 1000, значит этот кодне потокобезопасныйиз. Это не потокобезопасно, так в чем проблема? Как мы все знаем, синхронизированный метод может гарантировать изменение代码块、方法гарантировать有序性、原子性、可见性.

Чтобы быть разумным, приведенный выше код работает, проблемаServiceслоистыйincreaseMoney()да有序的、原子的、可见的,такутверждатьС синхронизацией должно быть все в порядке.

(Обратитесь к примечаниям по блокировке синхронизации, которые я написал ранее:Механизм блокировки Java для понимания)

Поскольку на уровне Java причину найти не удается, давайте проанализируем уровень базы данных (потому что база данных работает внутри метода). существуетincreaseMoney()добавлен перед методом@TranscationalАннотация, указывающая, что этот метод сделаиз. Транзакции гарантируют, что SQL-запросы в одной и той же группе будут успешными или ошибочными одновременно. Чтобы быть разумным, если нет ошибки, каждый поток должен выполнять денежное значение+1. Теоретически результат должен быть 1000.

(Ссылаясь на транзакции Spring, которые я писал ранее:Эта статья поможет вам разобраться в весенних делах!)

Основываясь на приведенном выше анализе, я подозреваю, чтоСпрашивающий плохо проверил(hhhh, побег), так что я тоже побежал его тестировать и обнаружил, что он использовался на пути вопрошающегодействительно проблема.

Сначала опубликуйте мой тестовый код:


@RestController
public class EmployeeController {

    @Autowired
    private EmployeeService employeeService;

    @RequestMapping("/add")
    public void addEmployee() {
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> employeeService.addEmployee()).start();
        }
    }


}

@Service
public class EmployeeService {

    @Autowired
    private EmployeeRepository employeeRepository;


    @Transactional
    public synchronized void addEmployee() {

        // 查出ID为8的记录,然后每次将年龄增加一
        Employee employee = employeeRepository.getOne(8);
        System.out.println(employee);
        Integer age = employee.getAge();
        employee.setAge(age + 1);

        employeeRepository.save(employee);
    }

}

Просто печатайте значение сотрудника, полученное каждый раз, и получайте порядок выполнения SQL следующим образом (отправьте небольшую часть):

SQL执行的顺序

Из ситуации с печатью можно сделать вывод, что в случае многопоточности инет серийного номеравоплощать в жизньaddEmployee()метод. Это приводит к выполнениюповторение, поэтому окончательное значение меньше 1000.

Во-вторых, причина появления диаграммы

найдено неСинхронизироватьКазнен, так что я подозреваюsynchronizedОпределенно существует конфликт между ключевыми словами и Spring. Поэтому я искал эти два ключевых слова и нашел проблему.

Мы знаем, что нижний уровень транзакции Spring — это Spring AOP, а нижний уровень Spring AOP — технология динамического прокси. Давайте рассмотрим динамический прокси вместе с вами:


    public static void main(String[] args) {

        // 目标对象
        Object target ;

        Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), Main.class, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                // 但凡带有@Transcational注解的方法都会被拦截

                // 1... 开启事务

                method.invoke(target);

                // 2... 提交事务

                return null;
            }
            
        });
    }

(Подробности см. в динамическом прокси, который я написал ранее:Объясни своей девушке, что такое прокси-режим)

Фактически обработка, выполняемая Spring, такая же, как и в приведенной выше идее.Мы можем посмотреть на класс TransactionAspectSupport.invokeWithinTransaction():

Spring事务管理是如何实现的

метод вызовавпередОткрытая транзакция, метод вызоваЗаднийсовершить транзакцию

Spring事务和synchronized锁互斥问题

В многопоточной среде это может произойти:После того, как метод выполнен (выполнен синхронизированный блок кода), и транзакция не была отправлена, другие потоки могут войти в метод, измененный с помощью synchronized.При повторном чтении считываются данные транзакции, которая не была отправлена, и эти данные не последние., поэтому возникает эта проблема.

事务未提交,别的线程读取到旧数据

3. Решить проблему

Из вышеизложенного мы видим, что проблема в том, что@Transcationalаннотации иsynchronizedиспользуется вместе,Объем блокировки не включает всю транзакцию. Итак, мы можем сделать это:

Создайте новый класс с именем SynchronizedService и позвольте ему вызыватьaddEmployee()метод, весь код выглядит следующим образом:


@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();
        }
    }
}

// 新建的Service类
@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() {

        // 查出ID为8的记录,然后每次将年龄增加一
        Employee employee = employeeRepository.getOne(8);
        System.out.println(Thread.currentThread().getName() + employee);
        Integer age = employee.getAge();
        employee.setAge(age + 1);

        employeeRepository.save(employee);

    }
}

Мы будем синхронизировать область блокировкиВключено во всю транзакцию Spring, не будет проблем с безопасностью потоков. Во время теста мы можем обнаружить, что запущено 1000 потоков.гораздо медленнее, чем раньше, конечно, наши данные верны:

正确的数据

Наконец

Можно обнаружить, что, хотя нам очень удобно использовать транзакции Spring, если мы не понимаем некоторые детали транзакций Spring, часто возникают ошибки, которые трудно понять. Все еще нужно продолжать усердно работать~~~

рад вывестигалантерейные товарыОбщедоступный номер технологии Java: Java3y. В паблике более 200 статейоригинальныйТехнические статьи, обширные видеоресурсы, красивые карты мозга — идите сюдаобрати внимание нанемного!

帅的人都关注了

Я думаю, что моя статья хорошо написана, пожалуйста, нажмитеотличный!