做外汇看的网站,网络建设方案论文,重庆通信管理局网站,基于网站优化的搜索引擎推广方法前言
之前两篇文章介绍了线程的基本概念和锁的基本知识#xff0c;本文主要是学习同步机制#xff0c;包括使用synchronized关键字、ReentrantLock等#xff0c;了解锁的种类#xff0c;死锁、竞争条件等并发编程中常见的问题。
一、关键字synchronized
synchronied关键…前言
之前两篇文章介绍了线程的基本概念和锁的基本知识本文主要是学习同步机制包括使用synchronized关键字、ReentrantLock等了解锁的种类死锁、竞争条件等并发编程中常见的问题。
一、关键字synchronized
synchronied关键字可以把任意一个非null的对象当做锁属于独占式的悲观锁。同时属于可重入锁早期的的synchronized属于重量级的锁效率低下因为监视器是依赖底层的操作系统Lock实现的从6之后java对sychronized进行了优化jdk1.6以后还引入了 大量的优化比如自旋锁适应性锁锁消除锁粗化偏向锁轻量级锁等。
1.synchronized用法
常用来保证代码的原子性主要有三种使用方法
修饰实例作用于当前的对象实例加锁进入同步代码前获得当前对象实例的锁。
synchronized void method() {
//业务代码
}修饰静态方法 也就是给当前类加锁会作用于该类所有的对象实例。如果线程A调用一个实例对象的非静态synchronized方法而线程B需要调用这个实例对象所属类的静态synchronized方法是允许的不会发生互斥现象因为静态synchronized方法是占用的锁是当前类的锁而访问非静态synchronized方法占用的锁是当前实例对象的锁。
synchronized void staic method() {
//业务代码
}修饰代码块 指定加锁对象对给定的对象/类加锁synchronizedthis object表示进入同步前要获得给定对象的锁synchronized类.class表示进入同步前要获得给定类class的锁
synchronized(this) {
//业务代码
}2. synchronized实现原理
使用synchronized是不用我们去加锁和释放lockunlock是jvm已经代替去做了synchronized修饰代码块的时候jvm是使用monitorenter和monitorexit两个指令实现的监视器 当修饰同步方法jvm采用ACC_SYNCHRONIZED标记符来实现的同步 这个标识表面了这是一个同步方法 3.synchronized锁住的原理
monitorentermonitorexitACC_SYNCHRONIZED都是基于monitor监视器 所谓的Monitor其实是一种同步工具也可以说是一种同步机制。在Java虚拟机HotSpot中Monitor是由 ObjectMonitor实现的可以叫做内部锁或者Monitor锁。 ObjectMonitor的工作原理 ObjectMonitor有两个队列WaitSet、EntryList用来保存ObjectWaiter 对象列表。 _owner获取 Monitor 对象的线程进入 _owner 区时 _count 1。如果线程调用了wait() 方法此时会释放Monitor 对象 _owner 恢复为空 _count - 1。同时该等待线程进入 _WaitSet 中等待被唤醒。 -同步是锁住的
monitorenter在判断拥有同步标识 ACC_SYNCHRONIZED 抢先进入此方法的线程会优先拥有 Monitor 的owner 此时计数器1。monitorexit当执行完退出后计数器-1归 0 后被其他进入的线程获得
4.除了原子性synchronized的可见性和有序性可重入性怎么实现
可见性线程加锁前将清空工作内存中共享变量的值从而使用共享变量的时候需要从主内存中重新读取最新的值。线程加锁后其他线程无法获得主内存中的共享变量的值线程解锁前必须把共享变量的最新值刷新到主内存中。有序性synchronized同步的代码块具有排他性一次只能被一个线程拥有所以可以保证同一个时刻代码是单线程执行的因为as-if-serial存在单线程语句是能够保证最终结果是有序的但是不保证不会进行指令重排所以synchronized是保证有序是执行结果的有序而不是防止指令重排的有序性。可重入性synchronized是可重入锁也就说允许一个线程二次请求自己持有的锁的临界资源这种情况就是可重入锁锁对象有个计数器会记录线程获取锁的次数当执行完对应的代码后计数器就会减去1只有归零就会释放锁。之所以可以重入就是因为这个计数器。
5.synchronized和ReentrantLock的区别
可从锁的实现、功能特点、性能维度等分析 锁的实现synchronized是通过jvm实现是java的关键字而reentrantlock是通过jdk层面的api实现的的一般是lock和unlock方法配合try/catch/finally语句实现 性能jdk1.6前synchronized性能比较差应该都是要通过底层调用但是1.6以后增加了适应性自旋锁消除等两者性能差不多。 功能特点- ReentrantLock比synchronized增加了一些高级功能如等待中断可实现公平锁可实现选择性通知synchronized只能是非公平锁内部锁- ReentrantLock可以指定是公平还是非公平公平锁就是先等待的线程先获得锁synchronized与wait()和notify()/notifyAll()方法结合实现等待/通知机制ReentrantLock类借助Condition接口与newCondition()方法实现。ReentrantLock需要手工声明来加锁和释放锁一般跟finally配合释放锁。而synchronized不用手动释放锁 二、锁类型
锁可以分为
悲观、乐观锁独享、共享锁互斥锁、读写锁可重入锁公平锁、非公平锁分段锁偏向锁轻量级锁、重量级锁自旋锁
以上是锁的名词有的是指锁的状态有的是锁特性或者设计。
1.乐观锁、悲观锁
乐观锁和悲观锁并不是两种特定类型锁是人们定义的概念或者思想。主要是指人们看待同步的角度。
乐观锁顾名思义就是乐观的认为每次取数据别人都不会修改所以不上锁但是在更新的时候会去判断在此期间别人有没有取更新这个数据可以使用版本号等机制乐观锁适用于多读的应用程序这样可以提高吞吐量在java中原子变量类就是使用了乐观锁的一种实现方式CAScompare and swap 比较并交换来实现的悲观锁总是假设每次去获取数据都认为别人会修改所以每次拿取数据都会进行上锁这样别人拿取数据就会阻塞直到拿到锁才行比如Java里面的关键字synchronized实现就是悲观锁悲观锁适合写操作多的场景。
①. 乐观锁乐观锁适合读多的场景不加锁会代理大量的性能提升 在java编程中是无锁编程常常采用的是CAS算法。典型的例子就是原子类通过CAS自旋实现原子的更新操作。 乐观锁更新判断其他线程有没有更新共享变量 一般采用数据版本机制或者CAS操作实现 1数据版本机制一般两种方式一种是使用版本号另一个是使用时间戳方式。 版本号方式一般是在数据表上加上一个数据版本号version字段表示更新的次数当数据被更新的时候计数加一当线程A更新数据时候会在读取数据的同时也会读取version字段在更新提交的时候若刚才的读取的version和数据库中的version相等才会更新否则会重新进行更新操作。直到更新成功。
update table set xxx#{xxx}, versionversion1 where id#{id} and version#{version};2CAS操作当多个线程尝试使用CAS同时更新一个变量时候只有一个线程能够更新变量其他线程并不会被挂起会收到通知失败并可以再次尝试。 CAS需要三个字段值1.需要读写的内存位置V、进行比较的预期原值A和拟写入的新值B如果内存位置的V值和预期原值A想匹配那么就会更新B否则不做变动。
②悲观锁悲观锁认为对于同一个数据的并发操作一定会发生修改的哪怕没有修改也会认为修改。因此对于同一份数据的并发操作悲观锁采取加锁的形式。悲观的认为不加锁并发操作一定会出问题。在对任意记录进行修改前先尝试为该记录加上排他锁exclusive locking。如果加锁失败说明该记录正在被修改那么当前查询可能要等待或者抛出异常。具体响应方式由开发者根据实际需要决定。如果成功加锁那么就可以对记录做修改事务完成后就会解锁了。期间如果有其他对该记录做修改或加排他锁的操作都会等待我们解锁或直接抛出异常。
2.独享锁、共享锁
独享锁是指该锁只能被一个线程获取共享锁是指该锁可以被多个线程持有。 对于java而言ReentrantLock是独享锁但是对于另一个lock的实现类ReadWriterLock来说其读是共享锁其写是独占锁。独享锁和共享锁是通过AQS来实现的通过不同的方法来实现独享或者共享synchronized是独占锁
AQSAbstractQueueSynchronized抽象同步队列简称AQS它是java并发包的基础并发的锁就是基于Aqs实现的。 AQS是基于一个FIFO的双向队列其内部定义了一个node节点类node节点内部的SHARED用来标记该线程是获取共享变量时被阻挂起后放入AQS队列的EXCLUSIVE用来标记线程是独占资源时被挂起放入AQS队列。 AQS使用一个volatile修饰的int类型的成员变量state来表示同步状态修改同步状态成功表示获得锁volatile保证了变量在线程之间的可见性修改state通过CAS机制来保证修改的原子性。 获取state方式有两种独占和共享。一个线程使用了独占的方式那么其他线程就失败会被阻塞一个线程使用共享时获取资源另一个线程还可以通过CAS的方式进行获取。 如果共享资源被占用需要一定的阻塞等待唤醒机制来保证锁的分配AQS会将获取共享资源失败的线程添加到一个变体的CLH中。 AQS中的ClH变体等待队列特性 AQs中队列是个双链表也是符合FIFO先进先出的特性。 通过head、tail两个头尾节点来组成队列结构通过volatile来保证可见性。 Head指向的节点本身已经获得了锁是一个虚拟节点节点本身不具备具体线程 获取不到同步状态会将节点进行自旋获取锁自旋一定次数失败后会将线程阻塞相对于CLH队列性能较好。
3.互斥锁/读写锁
讲的独享锁/共享锁就是一种广义的说法互斥锁/读写锁就是具体的实现。互斥锁在Java中的具体实现就是ReentrantLock。读写锁在Java中的具体实现就是ReadWriteLock对资源读取和写入的时候拆分为2部分处理读的时候可以多线程一起读写的时候必须同步地写。
4.可重入锁可重入锁又名递归锁是指在同一个线程在外层获锁的时候在进入内存自动获取锁。也就是在执行对象中所有的同步方法不用再次获取锁。 对于Java ReetrantLock而言从名字就可以看出是一个重入锁其名字是Reentrant Lock 重新进入锁。对于Synchronized而言也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。
synchronized void setA() throws Exception{Thread.sleep(1000);setB();
}synchronized void setB() throws Exception{Thread.sleep(1000);
}
上面的代码就是一个可重入锁的一个特点。如果不是可重入锁的话setB可能不会被当前线程执行可能造成死锁。4. 公平锁和非公平锁
公平锁是指多个线程按照锁的申请顺序来获取锁按等待时间来获取锁等待时间长的线程有优先获取锁的权利。非公平锁就是不是获取锁的顺序不是按照申请锁的顺序有可能后申请的先执行有可能会造成优先级反转或者饥饿现象。对于Java ReetrantLock而言通过构造函数指定该锁是否是公平锁默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。对于Synchronized而言也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度所以并没有任何办法使其变成公平锁。
5.分段锁
分段锁是一种设计并不是具体的一种锁对于ConcurrentHashMap而言是最好的例子其并发就是通过分段式锁来实现的
ConcurrentHashMap实现原理内部分为了若干的小的hashmap称为段segment默认情况下一个ConcurrenthashMap 分为16段即就是锁的并发度如果需要在ConcurrenthashMap中添加key-value并不是将整个都加锁而是 首先根据hashcode计算出key-value应该存放在那个段中然后对该段加锁并完成put操作在多线程操作中如果多个线程进行put操作只要被加入的key-value不在同一个段中则线程就可以实现真正的并行。线程安全ConcurrentHashMap 是一个 Segment 数组 Segment 通过继承ReentrantLock 来进行加锁所以每次需要加锁的操作锁住的是一个 segment这样只要保证每个 Segment 是线程安全的也就实现了全局的线程安全 6.偏向锁/轻量锁/重量级锁
java每个对象都可以作为锁锁有四种基本无锁、偏向锁、轻量级锁、重量级锁并且锁可以进行升级不能下降这三种是指锁的状态并且是针对synchronized的是在java5jdk1.6后引入实现高效升级synchronized,这三种锁通过对象监视器在对象头中的字段表明。
偏向锁是指一段同步代码一直被一个线程所访问那么该线程会自动获取锁。降低获取锁的代价。 在一段时间内锁不存在多线程竞争 而是总是由同一个线程多次获得为了让线程获取锁的代价更低就引入了偏向锁的概念。怎么理解偏向锁呢 当一个线程访问加了同步锁的代码块时会在对象头中存储当前线程的 ID后续这个线程进入和退出这段加了同步锁的代码块时不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁。如果相等表示偏向锁是偏向于当前线程的就不需要再尝试获得锁了。轻量级锁是指当锁是偏向锁的时候被另一个线程所访问偏向锁就会升级为轻量级锁其他线程会通过自旋的形式尝试获取锁不会阻塞提高性能。 -重量级锁是指当锁为轻量级锁的时候另一个线程虽然是自旋但自旋不会一直持续下去当自旋一定次数的时候还没有获取到锁就会进入阻塞该锁膨胀为重量级锁。重量级锁会让他申请的线程进入阻塞性能降低。
7.自旋锁 自旋锁是一种技术是为了让线程等待我们只需要让线程执行一个忙循环。自旋锁是指尝试获取锁的线程不会立即阻塞而是采用循环的方式获取锁这样好处是减少线程上下文切换的消耗缺点是循环会消耗cpu。 自旋锁是一种非阻塞锁核心就是自旋两个字即用自旋代替阻塞操作某一个线程尝试获取锁的时候如果该锁已经被另一个线程占用那么这个这个线程将不断循环进行检查该锁是否被释放默认次数是10可以使用-XX:PreBlockSpinsh参数设置该值而不是让此线程挂起或者睡眠一旦另一个线程释放锁那么此线程就会立即获得锁。自旋是一种忙等待状态过程会一直消耗cpu的时间片。
8.可中断锁
在等待锁的过程中可以中断。
9.死锁
死锁是一种现象程A持有资源x线程B持有资源y线程A等待线程B释放资源y线程B等待线程A释放资源x两个线程都不释放自己持有的资源则两个线程都获取不到对方的资源就会造成死锁。 死锁不能自行打破所以线程死锁后线程不能进行响应所以要注意线程的使用并发场景。
死锁形成条件
互斥条件指线程对已经获取到的资源进行排他性使用。请求并持有指一个线程已经持有了最少一个资源但是有提出来新的资源请求而新资源已经被其他线程给占用所以当前线程会被阻塞但阻塞的同时不会释放自己持有的资源。不可剥夺条件指线程获取到的资源在自己使用完成之前不能被其它线程抢占只能是自己在使用完毕后由自己进行释放。环路等待条件指发生死锁的时候必然形成了一个线程------资源的环形链。
如何破坏避免形成死锁呢
条件1互斥条件肯定不能破坏只能是下面三个条件进行破坏请求并持有我们可以一次性请求所有的数据。对于不可剥夺条件占用部分资源进一步申请其他资源的时候如果申请不到可以主动释放它占有的资源这样不可抢占这个条件就失效了。对于环路等待条件可以按顺序进行申请资源来预防。
如何排查 可以使用jdk自带的工具排查
1.使用jps查找运行的java进程jsp -12.使用jstack查看线程堆栈信息3.可以利用图形化工具Jconsole出现死锁点击面板就能看见
三、 总结