线程同步之锁、synchronized、volatile

线程同步

    线程的同步是为了防止多个线程访问一个数据对象时,对数据的写操作造成数据错误。锁就是Java实现同步的方法。

    锁是用来控制多个线程访问共享资源的方式,可以防止多个线程同时操作线程而出错。
死锁
    死锁产生的原因就是在多线程操作时,互相等待对方的资源,一旦产生死锁程序就会死掉。

乐观锁、悲观锁

    悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。synchronized关键字和Lock的实现类都是悲观锁。
    乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据(一般会使用“数据版本机制”或“CAS操作”来实现)。如果这个数据没有被更新,当前线程将自己修改的数据成功写入;如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。在Java中乐观锁的使用是无锁编程,常常采用的是CAS算法,例如原子类就是通过CAS自旋实现原子操作的更新。

数据版本机制

    数据版本机制一般有两种,第一种是使用版本号(添加一个修改数据就自增的版本号,根据版本号的比对判断数据是否改变),第二种是使用时间戳。

CAS操作

    CASCompare And Swap(比较与交换),是一种无锁算法。在不使用锁的情况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。
    CAS算法涉及到三个操作数:需要读写的内存值 V、进行比较的值 A、要写入的新值 B。当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。
CAS的三大问题

  • ABA问题。如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号。JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。
  • 循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。
  • 只能保证一个共享变量的原子操作,无法保证对多个共享变量操作的原子性的。Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。

自旋锁、适应性自旋锁

    如果同步资源的锁定时间很短,线程挂起和恢复现场消耗的时间有可能比用户代码执行的时间还要长。如果物理机器能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁,而为了让当前线程稍等一下,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销,这就是自旋锁。自旋锁也是根据CAS实现的。
    自旋虽然避免了线程切换的开销,但会循环占用CPU。如果锁被占用的时间很短,效果就会非常好,自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。
    自旋锁在JDK4中引入,使用-XX:+UseSpinning来开启。JDK6中变为默认开启,并且引入了适应性自旋锁。
    自适应意味着自旋的次数不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

公平锁、非公平锁

    公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会等待很长时间。缺点是CPU每次都需要唤醒阻塞线程,整体开销比非公平锁大。
    非公平锁是多个线程加锁时直接尝试获取锁,如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会等很久才会获得锁。Synchronized,ReetrantLock都是非公平锁(也可以通过构造函数指定为公平锁)。

可重入锁

    又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class)。ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

独享锁、共享锁

    独享锁也叫排他锁,是指该锁一次只能被一个线程所持有。synchronized和JUC中Lock的实现类就是独享锁。
    共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,获得共享锁的线程只能读数据。
    独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
    ReentrantReadWriteLock有两把锁:ReadLock和WriteLock一个读锁一个写锁,合称“读写锁”。读锁和写锁的锁主体都是Sync,但读锁是共享锁,写锁是独享锁。读锁的共享锁可保证并发读非常高效,而读写、写读、写写的过程互斥,因为读锁和写锁是分离的。所以ReentrantReadWriteLock的并发性相比一般的互斥锁有了很大提升。

无锁、偏向锁、轻量级锁、重量级锁

    这四种锁是指锁的状态,专门针对synchronized的,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁。锁状态只能升级不能降级。

偏向锁

    偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,来提高性能。
    当一个线程访问同步代码块并获取锁时,会在Java头里存储锁偏向的线程ID,在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测Java头里是否存储着指向当前线程的偏向锁。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。
    引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令即可。
    在JDK6默认启用偏向锁,但是会在程序启动几秒后启动。

  • 关闭延迟:-XX:BiasedLockingStartupDelay=0
  • 关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。

轻量级锁

    是指当锁是偏向锁的时候被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
    在代码进入同步块的时候,如果同步对象锁状态为无锁状态,虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,然后拷贝对象头复制到锁记录中。拷贝成功后,虚拟机将使用CAS操作尝试将对象的对象头更新为指向Lock Record的指针,并将Lock Record里的owner指针指向对象的对象头。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象的对象头中锁标志位标记为轻量级锁定状态。
    如果轻量级锁的更新操作失败了,虚拟机首先会检查对象的对象头是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁。若当前只有一个等待线程,则该线程通过自旋进行等待。

重量级锁

    当竞争偏向锁的线程自旋超过一定的次数,或者一个线程在持有锁,一个线程在自旋,又有第三个线程来访时,轻量级锁升级为重量级锁。此时对象头中存储的是指向重量级锁的指针,等待锁的线程都会进入阻塞状态。

synchronized

    Java的关键字,能够保证在同一时刻最多只有一个线程执行该段代码,用来修饰方法或者代码块。
    Java中每个对象的头里面都有都会存储锁相关的数据,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,这个就是synchronized实现的原理
    synchronized (1) 可重入:在同一个线程中外层函数获取到锁之后,内层函数可以直接使用该锁。(2)不可中断:如果这个锁被a线程获取,如果b线程想要获取这把锁,只能选择等待或者阻塞,直到a线程释放这把锁。
    synchronized不够灵活:只有代码执行完毕或者异常才会释放锁,每个锁只能锁住一个对象,并且无法判断是否成功获得锁。

  • 同步代码块
synchronized(obj){
	// 其中obj称为同步监视器,同步监视器只能是对象
	//obj不能是空
}
  • 同步方法
public synchronized …方法名(参数列表){
	//普通同步方法的同步监视器为当前对象this
	//静态同步方法的同步监视器为当前类的Class对象
}

volatile

    volatile是轻量级的synchronized,volatile修饰的成员变量在每次被线程访问时,都强制从共享内存中重新读取该成员变量的值。而且,当成员变量发生变化时,会强制线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
    一个volatile对象引用可能是null。

public class MyRunnable implements Runnable {
    private volatile boolean active;
    public void run() {
        active = true;
        while (active){ // 第一行
            // 代码
        }
    }
    public void stop() {
        active = false; // 第二行
    }
}

    通常情况下,在一个线程调用 run() 方法(在 Runnable 开启的线程),在另一个线程调用 stop() 方法。 如果 第一行 中缓冲区的 active 值被使用,那么在 第二行 的 active 值为 false 时循环不会停止。
    但是以上代码中我们使用了 volatile 修饰 active,所以该循环会停止。

  • 保证可见性
        当修改一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去,这个操作会导致其他线程中的缓存无效。
  • 禁止指令重排
  • 不保证原子性
        volatile不适合复合操作,例如,inc++不是一个原子性操作,可以由读取、加、赋值3步组成,所以结果可能会出错。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值