本文源码:GitHub·点这里 || GitEE·点这里java
多线程并发访问同一个资源问题,假如线程A获取变量以后修改变量值,线程C在此时也获取变量值而且修改,两个线程同时并发处理一个变量,就会致使并发问题。git
这种并行处理数据库的状况在实际的业务开发中很常见,两个线程前后修改数据库的值,致使数据有问题,该问题复现的几率不大,处理的时候须要对整个模块体系有概念,才能容易定位问题。github
public class LockThread01 { public static void main(String[] args) { CountAdd countAdd = new CountAdd() ; AddThread01 addThread01 = new AddThread01(countAdd) ; addThread01.start(); AddThread02 varThread02 = new AddThread02(countAdd) ; varThread02.start(); } } class AddThread01 extends Thread { private CountAdd countAdd ; public AddThread01 (CountAdd countAdd){ this.countAdd = countAdd ; } @Override public void run() { countAdd.countAdd(30); } } class AddThread02 extends Thread { private CountAdd countAdd ; public AddThread02 (CountAdd countAdd){ this.countAdd = countAdd ; } @Override public void run() { countAdd.countAdd(10); } } class CountAdd { private Integer count = 0 ; public void countAdd (Integer num){ try { if (num == 30){ count = count + 50 ; Thread.sleep(3000); } else { count = count + num ; } System.out.println("num="+num+";count="+count); } catch (Exception e){ e.printStackTrace(); } } }
这里案例演示多线程并发修改count值,致使和预期不一致的结果,这是多线程并发下最多见的问题,尤为是在并发更新数据时。数据库
出现并发的状况时,就须要经过必定的方式或策略来控制在并发状况下数据读写的准确性,这被称为并发控制,实现并发控制手段也不少,最多见的方式是资源加锁,还有一种简单的实现策略:修改数据前读取数据,修改的时候加入限制条件,保证修改的内容在此期间没有被修改。编程
并发编程中一个最关键的问题,多线程并发处理同一个资源,防止资源使用的冲突一个关键解决方法,就是在资源上加锁:多线程序列化访问。锁是用来控制多个线程访问共享资源的方式,锁机制可以让共享资源在任意给定时刻只有一个线程任务访问,实现线程任务的同步互斥,这是最理想但性能最差的方式,共享读锁的机制容许多任务并发访问资源。安全
悲观锁,老是假设每次每次被读取的数据会被修改,因此要给读取的数据加锁,具备强烈的资源独占和排他特性,在整个数据处理过程当中,将数据处于锁定状态,例如synchronized关键字的实现就是悲观机制。多线程
悲观锁的实现,每每依靠数据库提供的锁机制,只有数据库层提供的锁机制才能真正保证数据访问的排他性,不然,即便在本系统中实现了加锁机制,也没法保证外部系统不会修改数据,悲观锁主要分为共享读锁和排他写锁。并发
排他锁基本机制:又称写锁,容许获取排他锁的事务更新数据,阻止其余事务取得相同的资源的共享读锁和排他锁。若事务T对数据对象A加上写锁,事务T能够读A也能够修改A,其余事务不能再对A加任何锁,直到T释放A上的写锁。ide
乐观锁相对悲观锁而言,采用更加宽松的加锁机制。悲观锁大多数状况下依靠数据库的锁机制实现,以保证操做最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务的开销很是的占资源,乐观锁机制在必定程度上解决了这个问题。高并发
乐观锁大可能是基于数据版本记录机制实现,为数据增长一个版本标识,在基于数据库表的版本解决方案中,通常是经过为数据库表增长一个version字段来实现。读取出数据时,将此版本号一同读出,以后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,若是提交的数据版本号等于数据库表当前版本号,则予以更新,不然认为是过时数据。乐观锁机制在高并发场景下,可能会致使大量更新失败的操做。
乐观锁的实现是策略层面的实现:CAS(Compare-And-Swap)。当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能成功更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知此次竞争中失败,并能够再次尝试。
悲观锁自己的实现机制就以损失性能为代价,多线程争抢,加锁、释放锁会致使比较多的上下文切换和调度延时,加锁的机制会产生额外的开销,还有增长产生死锁的几率,引起性能问题。
乐观锁虽然会基于对比检测的手段判断更新的数据是否有变化,可是不肯定数据是否变化完成,例如线程1读取的数据是A1,可是线程2操做A1的值变化为A2,而后再次变化为A1,这样线程1的任务是没有感知的。
悲观锁每一次数据修改都要上锁,效率低,写数据失败的几率比较低,比较适合用在写多读少场景。
乐观锁并未真正加锁,效率高,写数据失败的几率比较高,容易发生业务形异常,比较适合用在读多写少场景。
是选择牺牲性能,仍是追求效率,要根据业务场景判断,这种选择须要依赖经验判断,不过随着技术迭代,数据库的效率提高,集群模式的出现,性能和效率仍是能够两全的。
lock:执行一次获取锁,获取后当即返回;
lockInterruptibly:在获取锁的过程当中能够中断;
tryLock:尝试非阻塞获取锁,能够设置超时时间,若是获取成功返回true,有利于线程的状态监控;
unlock:释放锁,清理线程状态;
newCondition:获取等待通知组件,和当前锁绑定;
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class LockThread02 { public static void main(String[] args) { LockNum lockNum = new LockNum() ; LockThread lockThread1 = new LockThread(lockNum,"TH1"); LockThread lockThread2 = new LockThread(lockNum,"TH2"); LockThread lockThread3 = new LockThread(lockNum,"TH3"); lockThread1.start(); lockThread2.start(); lockThread3.start(); } } class LockNum { private Lock lock = new ReentrantLock() ; public void getNum (){ lock.lock(); try { for (int i = 0 ; i < 3 ; i++){ System.out.println("ThreadName:"+Thread.currentThread().getName()+";i="+i); } } finally { lock.unlock(); } } } class LockThread extends Thread { private LockNum lockNum ; public LockThread (LockNum lockNum,String name){ this.lockNum = lockNum ; super.setName(name); } @Override public void run() { lockNum.getNum(); } }
这里多线程基于Lock锁机制,分别依次执行任务,这是Lock的基础用法,各类API的详解,下次再说。
基于synchronized实现的锁机制,安全性很高,可是一旦线程失败,直接抛出异常,没有清理线程状态的机会。显式的使用Lock语法,能够在finally语句中最终释放锁,维护相对正常的线程状态,在获取锁的过程当中,能够尝试获取,或者尝试获取锁一段时间。
GitHub·地址 https://github.com/cicadasmile/java-base-parent GitEE·地址 https://gitee.com/cicadasmile/java-base-parent
推荐阅读:Java基础系列
序号 | 文章标题 |
---|---|
A01 | Java基础:基本数据类型,核心点整理 |
A02 | Java基础:特殊的String类,和相关扩展API |
B01 | Java并发:线程的建立方式,状态周期管理 |
B02 | Java并发:线程核心机制,基础概念扩展 |
B03 | Java并发:多线程并发访问,同步控制 |
B04 | Java并发:线程间通讯,等待/通知机制 |