предисловие
Перечислите некоторые из наиболее распространенных ошибок параллелизма, которые вы обычно допускаете в своей работе, все из которых являются яркими примерами, встречающимися в реальном коде проекта, и надеюсь, что они будут вам полезны.
First Blood
Всегда появляется строка:ERROR 1062 (23000) Duplicate entry 'xxx' for key 'yyy', давайте посмотрим на рассматриваемый код:
UserBindInfo info = selectFromDB(userId);
if(info == null){
info = new UserBindInfo(userId,deviceId);
insertIntoDB(info);
}else{
info.setDeviceId(deviceId);
updateDB(info);
}
существуетпараллелизмВниз,Первый шаг - решить, что все пусто, будет 2 или более потоков для ввода операции вставки в базу данных,В настоящее время один и тот же идентификатор был вставлен несколько раз.
Правильная осанка:
insert into UserBindInfo values(#{userId},#{deviceId}) on duplicate key update deviceId=#{deviceId}多次的情况,导致插入失败。
В общем, вы можете использоватьinsert...on duplicate key update...решить эту проблему.
Уведомление:Если таблица UserBindInfo существуетПервичный ключ и более одного уникального индекса, в случае параллелизма использование вставки... на дублирующемся ключе может привести к взаимоблокировке (Mysql5.7), которую можно обработать следующим образом:
try{
UserBindInfoMapper.insertIntoDB(userBindInfo);
}catch(DuplicateKeyException ex){
UserBindInfoMapper.update(userBindInfo);
}
Double Kill
Будьте осторожны с вашими глобальными переменными, как в следующем коде:
public class GlobalVariableConcurrentTest {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 100, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(1000));
while (true){
threadPoolExecutor.execute(()->{
String dateString = sdf.format(new Date());
try {
Date parseDate = sdf.parse(dateString);
String dateString2 = sdf.format(parseDate);
System.out.println(dateString.equals(dateString2));
} catch (ParseException e) {
e.printStackTrace();
}
});
}
}
}
Вы можете видеть, что выбрасывается исключение
SimpleDateFormat глобальной переменной имеет проблемы с безопасностью в случае параллелизма, и Статут Ali Java явно требует, чтобы его использовали с осторожностью.
В дополнение к SimpleDateFormat, на самом деле, много раз перед лицом глобальных переменных нам нужно учитывать, есть ли проблема с параллелизмом, как показано ниже.
@Component
public class Test {
public static List<String> desc = new ArrayList<>();
public List<String> getDescByUserType(int userType) {
if (userType == 1) {
desc.add("普通会员不可以发送和查看邮件,请购买会员");
return desc;
} else if (userType == 2) {
desc.add("恭喜你已经是VIP会员,尽情的发邮件吧");
return desc;
}else {
desc.add("你的身份未知");
return desc;
}
}
}
Поскольку desc является глобальной переменной, в параллельной ситуации запрос метода getDescByUserType может не дать желаемого результата.
Trible Kill
Допустим, есть такое дело: частота управления одним и тем же пользователем доступа к определенному интерфейсу не может быть меньше 5 секунд. Обычно легко думатьИспользуйте операцию setnx redis для управления одновременным доступом, поэтому есть следующий код:
if(RedisOperation.setnx(userId, 1)){
RedisOperation.expire(userId,5,TimeUnit.SECONDS));
//执行正常业务逻辑
}else{
return “访问过于频繁”;
}
Предположим, что после выполнения операции setnx значение expireTime не было установлено до того, как компьютер перезагрузится или внезапно выйдет из строя, и возникнет тупиковая ситуация. Идентификатор пользователя, последующее выполнение setnx всегда будет ложным,это может стоить вам этого пользователя навсегда.
Итак, как решить эту проблему, вы можете рассмотреть возможность использованияSET key value NX EX max-lock-time, это способ реализации блокировок в Redis, это атомарная операция, она не будет выполняться в два этапа, как приведенный выше код, сначала устанавливается, а затем истекает, этозавершить за один шаг.
Клиент выполняет приведенную выше команду:
- Если сервер возвращает OK, то этот клиент получает блокировку.
- Если сервер возвращает NIL , клиенту не удалось получить блокировку, и он может повторить попытку позже.
- По истечении установленного времени истечения блокировка будет автоматически снята.
Quadra Kill
Давайте взглянем на фрагмент кода ConcurrentHashMap следующим образом:
//全局变量
Map<String, Integer> map = new ConcurrentHashMap();
Integer value = count.get(k);
if(value == null){
map.put(k,1);
}else{
map.put(k,value+1);
}
Предположим, что оба потока входятvalue==null, На этом этапе результат станет меньше? Хорошо, приглашенный офицер сначала делает перерыв, закрывает глаза и отдыхает некоторое время, давайте проверим это, пожалуйста, посмотрите демонстрацию:
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
testConcurrentMap();
}
}
private static void testConcurrentMap() {
final Map<String, Integer> count = new ConcurrentHashMap<>();
ExecutorService executorService = Executors.newFixedThreadPool(2);
final CountDownLatch endLatch = new CountDownLatch(2);
Runnable task = ()-> {
for (int i = 0; i < 5; i++) {
Integer value = count.get("k");
if (null == value) {
System.out.println(Thread.currentThread().getName());
count.put("k", 1);
} else {
count.put("k", value + 1);
}
}
endLatch.countDown();
};
executorService.execute(task);
executorService.execute(task);
try {
endLatch.await();
if (count.get("k") < 10) {
System.out.println(count);
}
} catch (Exception e) {
e.printStackTrace();
}
На первый взгляд, беговые результаты должны быть равны 10, верно? Хорошо, давайте посмотрим на беговые результаты. :
Текущий результат показывает 5, поэтому в этой реализации есть проблема параллелизма, так какова правильная поза реализации?
Map<K,V> map = new ConcurrentHashMap();
V v = map.get(k);
if(v == null){
v = new V();
V old = map. putIfAbsent(k,v);
if(old != null){
v = old;
}
}
может рассмотреть возможность использованияputIfAbsentрешить эту проблему
(1) Если ключ является новой записью, на карту будет добавлена пара ключ-значение, и будет возвращено значение null.
(2) Если ключ уже существует, существующее значение не будет перезаписано, и будет возвращено существующее значение.
Давайте взглянем на следующий код и результаты выполнения:
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
testConcurrentMap();
}
}
private static void testConcurrentMap() {
ExecutorService executorService = Executors.newFixedThreadPool(2);
final Map<String, AtomicInteger> map = Maps.newConcurrentMap();
final CountDownLatch countDownLatch = new CountDownLatch(2);
Runnable task = ()-> {
AtomicInteger oldValue;
for (int i = 0; i < 5; i++) {
oldValue = map.get("k");
if (null == oldValue) {
AtomicInteger initValue = new AtomicInteger(0);
oldValue = map.putIfAbsent("k", initValue);
if (oldValue == null) {
oldValue = initValue;
}
}
oldValue.incrementAndGet();
}
countDownLatch.countDown();
};
executorService.execute(task);
executorService.execute(task);
try {
countDownLatch.await();
System.out.println(map);
} catch (Exception e) {
e.printStackTrace();
}
}
Penta Kill
Существующие бизнес-сценарии таковы: у пользователя на руках есть денежный купон, который можно обменять на соответствующие денежные средства.
пример ошибки один
if(isAvailable(ticketId){
1、给现金增加操作
2、deleteTicketById(ticketId)
}else{
return “没有可用现金券”
}
Разобрать:Предполагая, что есть два потока A и B для обмена наличными, последовательность выполнения выглядит следующим образом:
- 1. Тема А плюс наличные
- 2. Поток B плюс наличные
- 3. Поток A удаляет флаг заявки
- 4. Поток B удаляет флаг заявки
Очевидно,Это проблема, я уже два раза добавлял деньги пользователю.
Пример ошибки 2
if(isAvailable(ticketId){
1、deleteTicketById(ticketId)
2、给现金增加操作
}else{
return “没有可用现金券”
}
В случае параллелизма, если потоку не удастся удалить deleteTicketById на первом этапе, будет добавлено больше денег.
Правильное решение
if(deleteAvailableTicketById(ticketId) == 1){
1、给现金增加操作
}else{
return “没有可用现金券”
}
Личный публичный аккаунт
- Если вы хороший ребенок, который любит учиться, вы можете подписаться на мой официальный аккаунт, чтобы вместе учиться и обсуждать.
- Если вы считаете, что в этой статье есть какие-либо неточности, вы можете прокомментировать или подписаться на мой официальный аккаунт, пообщаться со мной в частном порядке, и все смогут учиться и прогрессировать вместе.