因为以后的我的项目中因为多线程操作同一个实体,会呈现数据笼罩的问题,后保留的实体把先保留的实体的数据给笼罩了。

于是查找了锁的实现的几种形式。

但写到最初发现,其实本人能够写sql 更新须要更新的字段即可,这个操作放在文章尾部。

先来说一下实现锁的几种形式。

对办法加锁

不同于对数据库数据加锁,这种形式是对类中的某个办法加锁

synchronized

java中曾经有了内置锁:synchronized, 它的特点是应用简略,所有交给JVM去解决,不须要显示开释

示例:

public class SynchronizedMethod {    public synchronized void method() {        System.out.println("Hello World!");    }}

如代码可见,只有办法上加上synchronized关键字就能够了。

当初来测试一下

不应用synchronized时

测试: test字段初始化为0,通过new Thread测试100个线程高并发量下test每次 + 1。

controller:

public void batchSave() {        for (int i = 0; i < 100; i++) {            int finalI = i;            new Thread(() -> {                logService.test();            }).start();        }

service:

 @Override  public void test() {    Client client = clientRepository.findById(9L).get();    client.setTest(client.getTest() + 1);    clientRepository.save(client);  }

测试后果:为15

阐明在高并发量的状况下,呈现了数据笼罩的状况,线程并没有期待上一个线程实现再进行操作,100个线程进行+1操作,最终只加了15.

应用synchronized时

service:

 @Override  public synchronized void test() {    Client client = clientRepository.findById(9L).get();    client.setTest(client.getTest() + 1);    clientRepository.save(client);  }

测试后果:

测试后果为100合乎预期,高并发量下,每个线程都排队进行拜访批改,100个线程进行+1操作,最终加了100.

原理:从语法上讲,Synchronized能够把任何一个非null对象作为"锁",这个锁的名字是monitor。

当synchronized作用在实例办法时,监视器锁(monitor)便是对象实例(this);当synchronized作用在静态方法时,监视器锁(monitor)便是对象的Class实例,因为Class数据存在于永恒代,因而静态方法锁相当于该类的一个全局锁;当synchronized作用在某一个对象实例时,监视器锁(monitor)便是括号括起来的对象实例;

简略来说,该办法的monitor只能被一个线程占用,如果其余线程曾经占用了monitor,则该线程进入阻塞状态,直到monitor不被占用。这就实现了锁的实现。

ReentrantLock

java.util.concurrent.locks包提供的ReentrantLock加锁。

ReentrantLock示例:

private final Lock lock = new ReentrantLock();    public void add() {        lock.lock();        try {          // 代码        } finally {            lock.unlock();        }    }

加锁之后,咱们须要在finally中开释锁

这种锁的其实就是在类中设置一个变量,咱们能够对它加锁和解锁。当锁定状态时,其余线程须要期待。

相比synchronized,显示锁能够用非阻塞的形式获取锁,能够响应程序中断,能够设定程序的阻塞工夫,领有更加灵便的操作。相干具体操作能够去查问,在此不过多展现。

例如: 能够设置tryLock,如果1秒未获取到锁则不执行

if (lock.tryLock(1, TimeUnit.SECONDS)) {    try {        ...    } finally {        lock.unlock();    }}

ReentrantLock有两个构造方法。

public ReentrantLock()public ReentrantLock(boolean fair)

参数fair示意是否保障偏心,在不指定的状况下默认值为false,示意不保障偏心。

偏心示意:等待时间最长的线程优先获取锁。


事务和锁产生的异样

留神:这两种锁在在和@Transational同一个办法一起应用的状况下会呈现,锁曾经去除,然而事务还没提交的状况,造成脏读和数据不一致性等状况.

因为事务的提交在办法运行完结之后,并且事务真正启动在执行到它们之后的第一个操作 InnoDB 表的语句的时候

所以会呈现这种状况

例如上一个线程还没来得及提交事务,所以以后线程拜访到的数据库还是原来的数据,这就造成了读取的库存数据不是最新的。

解决:能够把@Transational和业务逻辑的代码独自提到一个service里。

public void test() {    lock.lock();    try {      service.update    } finally {      lock.unlock();    }  }
   @Transactional    public void update(int id) {        /*          业务代码         */    }

总结:

锁的实现:
Synchronized是依赖于JVM实现的,而ReenTrantLock是JDK实现的,有什么区别,说白了就相似于操作系统来管制实现和用户本人敲代码实现的区别。前者的实现是比拟难见到的,后者有间接的源码可供浏览。

性能的区别:在Synchronized优化后,其实性能差不多。

ReenTrantLock独有的能力:
ReenTrantLock能够指定是偏心锁还是非偏心锁。而synchronized只能是非偏心锁。还能够应用tryLock等操作。

对这两种来说,我比拟举荐用ReenTrantLock,更灵便,并且代码可读性更高。如果用synchronized,他人读代码可能容易疏忽。

对数据库数据加锁

相比下面的对办法加锁来说,这种形式是对数据库加锁。对于多个不同的service都操作同一数据的话,咱们就须要对数据库上锁了。

又分为乐观锁和乐观锁:

乐观锁

乐观锁顾名思义就是乐观的认为本人操作的数据都会被其余线程操作,所以就必须本人独占这个数据,能够了解为”独占锁“。下面提到的中synchronized和ReentrantLock等锁就是乐观锁,数据库中表锁、行锁、读写锁等也是乐观锁。

实现原理很简略: 在 where语句前面加上for update就行了。这样就能锁住这条数据。不过要留神的是,留神查问条件必须要是索引列(这里设置的是id),如果不是索引就会变成表锁,把整个表都锁住。

@Query(value = "select * from client a where a.id = :id for update", nativeQuery = true)Optional<Client> findClientForUpdate(Long id);

JPA有提供一个更简洁的形式,就是@Lock注解

    /**     * 查问时加上乐观锁     * 在咱们没有将其提交事务之前,其余线程是不能获取批改的,须要期待     * @param id clientId     * @return     */    @Lock(value = LockModeType.PESSIMISTIC_WRITE)    @Query("select a from Client a where a.id = :id")    Optional<Client> findClientByIdWithPessimisticLock(Long id);

原理还是for update,只不过是JPA帮咱们加了而已。

具体测试乐观锁的过程能够看我这篇文章:https://segmentfault.com/a/11...

乐观锁

原理就是在实体中加一个字段当作版本号,比方咱们加个字段version。

@Data@Entitypublic class Client {    ...    private Long version;}

当咱们client进行更新的时候,例如咱们拿到version是1,操作实现之后要插入的时候,发现version变成2了,哎这就不对了,必定是有其他人更改了数据,那这时候我就不能插入了。

@Query(value = "update client set name = :name, version = version + 1 where id = :id and version = :version", nativeQuery = true)int updateClinetWithVersion(Long id, String naem, Long version);

能够看到update的where有一个判断version的条件,并且会set version = version + 1。这就保障了只有当数据库里的版本号和要更新的实体类的版本号雷同的时候才会更新数据。

这个返回值代表更新了的数据库行数,如果值为0的时候没有更新,阐明版本号产生谬误。

与乐观锁雷同,jpa也提供了乐观锁的实现形式。

@Data@Entitypublic class Article {    ...        @Version    private Long version;}

应用了@version之后,咱们不须要再本人写仓库层, 失常应用findById,save办法即可。

public void update(Long id, String name) {    Client client = repository.findById(id).get();    client.setName(name);    repository.save(client);}

实现乐观锁之后,如果没有胜利更新数据则抛出异样回滚保证数据

乐观锁适宜写少读多的场景,写多的状况会常常回滚,耗费性能。

乐观锁适宜写多读少的场景,应用的时候该线程会独占这个资源。

参考文章:https://segmentfault.com/a/11...

回到结尾

结尾讲到,数据笼罩的时候不肯定须要应用锁,也能够本人写sql,更新须要更新的字段即可。

例如一个线程更新的是age字段,一个是name字段,那咱们能够写更新语句,只更新实体的名字。

@Modifying@Query (value = "update client set name = :name where id = :id")public void updateNameById(String name, int id);
public update(int id){    Client client = repository.findById(id);   client.setName("name");//留神不要这么写   repository.updateNameById("name",id);    

留神这里不要应用set,否则发现数据还是会被笼罩。

这是jpa本人的个性,自动更新

当实体对象属于托管状态下时,往这个对象外面的某个属性set新的值,jpa检测到有变动,就会自动更新entity 的数据到db中。

具体能够查看这篇文章:https://blog.csdn.net/janet11...