Java并发-volatile,synchronized关键字,happends-before原则,Lock锁和CAS操作

volatile

volatile是轻量级的synchronized,它保证了共享变量的可见性(当一个线程修改一个共享变量时,另一个线程可以读到这个修改的值),但不保证原子性。如果volatile变量修饰符使用的恰当,比synchronize的使用和执行成本更低。

synchronized

synchronized是重量级锁,他不仅保证了可见性,还保证了原子性。

利用synchronized实现同步的具体表现为3种形式:
1.对于同步方法,锁是当前实例对象。
2.对于静态同步方法,锁是当前类的Class对象。
3.对于同步方法块,锁是Synchronized括号里配置的对象。

Sychronized支持重入锁, Sychronized A里面可以调用Sychronized B方法,重入锁另外一种情形,子类锁调用父类锁。
被sychronized锁定的代码块越少同步效率就越高.

使用Synchronized进行同步,首先需要获取对象的监视器monitor,只有获取监视器后才可以继续执行,否则只能等待.这个操作是互斥的,只有一个线程可以获取.

锁的重入性:即在同一锁程中,线程不需要再次获取同一把锁。Synchronized先天具有重入性。每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一。

public class SynchronizedDemo {
    public static void main(String[] args) {
        //执行同步代码块之前要先执行monitorenter指令 下面第4行
        synchronized (SynchronizedDemo.class) {
        }//退出的时候执行monitorexit指令 下面第6行

        //而该方法为静态方法,该方法的对象仍锁着该类对象,就需要执行monitorenter指令,
        //因为上面已经获取了该对象的监视器,所以不必再次获取
        method();
    }

    private static void method() {
    }
    /**
     *  public static void main(java.lang.String[]);
     *     descriptor: ([Ljava/lang/String;)V
     *     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     *     Code:
     *       stack=2, locals=3, args_size=1
     *          0: ldc           #2     // class threadTest/SynchronizedDemo
     *          2: dup
     *          3: astore_1
     *          4: monitorenter      
     *          5: aload_1
     *          6: monitorexit
     *          7: goto          15
     *         10: astore_2
     *         11: aload_1
     *         12: monitorexit
     *         13: aload_2
     *         14: athrow
     *         15: invokestatic  #3     // Method method:()V
     *         18: return
     */
}

访问过程:

—Monitor.Enter—>Monitor(监视器)—Monitor.Enter成功—>对象(Objject)—Monitor.Exit–>

如果线程A先Enter,获取了Object的监视器,后面来的线程都会进入同步状态(BLOCKED),进入同步队列,当Object的监视器占有者释放后,同步队列中的线程就可以有机会获得监视器.

Synchronized关键字会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高

其他:
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
程序运行中如果碰到异常,那么锁默认会被释放.所以在高并发处理的过程中,有异常要考虑清楚
wait/notify/notifyall 必须作用于同一个对象才能达到目的,调用wait会进入等待并且释放锁,调用notify则不会。
调用wait前必须进行Synchronized操作,要么会抛异常 IllegalMonitorStateException(非法监控状态异常)
在并发量较高的情况下,适用Synchronized.

Concurrent.locks.Lock

JUC(Java.utils.Concurrent)包下的类
方法:

void lock() 获取该锁
void unlock() 释放该锁

Lock的实现类Concurrent.locks.ReentrantLock

构造器
ReentrantLock() 构建一个可以被用来保护临界区的重入锁
ReentrantLock(boolean fair) 构建一个带有公平策略的锁

具体加锁步骤

Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
//1.加锁
lock.lock();
//业务逻辑
     //2.条件不满足等待
     condition.await();
     //3.通知其他
     condition.signalAll();
//4.释放锁
lock.unlock();

Synchronized和Lock的区别
1.Synchronized是java关键字.Lock是java类
2.Synchronized无法判断锁的状态.Lock可以判断是否获取了锁
3.Synchronized会自动释放锁.Lock必须手动释放,如果不释放会造成 死锁!
4.Synchronized 线程1获取,线程2会一直阻塞.Lock锁不会一直等待下去
5.Synchronized可重入锁,不可以中断的,非公平.Lock可重入锁,可以中断,默认非公平(可设为公平)
6.Synchronized适合少量代码同步.Lock适合大量同步代码!

happends-before原则

cup的运行速度特别快,但读取主存对于cpu来说就有点慢,在读取主存的过程中cpu一直处于空闲状态(没有数据可以运行),对资源造成了极大的浪费.慢慢的cpu演变成了多级cache结构,cpu在读cache的速度比读主存快了n倍。

当线程在执行时,会保存临界资源的副本到私有work memory中,这个memory在cache中,修改这个临界资源会更新work memory但并不一定立刻刷到主存中

而且编译器为了提高指令执行效率,是可以对指令重排序的,重排序后指令的执行顺序不一样,有可能线程2读取某个变量时,线程1还未进行写入操作。

i = 1; //线程A执行
j = i ; //线程B执行
假定线程A的操作(i = 1)happens-before线程B的操作(j = i)。 那么可以确定线程B执行后j = 1 一定成立。 如果他们不存在happens-before原则,那么j = 1 不一定成立

即使代码是先执行j=1,然后执行j=i,也不一定j=1,主要看是否符合happens-before.

happens-before

如果操作1 happens-before 操作2,那么第操作1的执行结果将对操作2可见,而且操作1的执行顺序排在第操作2之前。
两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。
如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。

如何判断是否为 happens-before?

程序次序规则: 在一个单独的线程中,按照程序代码的执行流顺序,(时间上)先执行的操作happen—before(时间上)后执行的操作(同一个线程中前面的所有写操作对后面的操作可见)

管理锁定规则: 一个unlock操作happen—before后面(时间上的先后顺序)对同一个锁的lock操作。
(如果线程1解锁了monitor a,接着线程2锁定了a,那么,线程1解锁a之前的写操作都对线程2可见(线程1和线程2可以是同一个线程))

volatile变量规则:对一个volatile变量的写操作happen—before后面(时间上)对该变量的读操作。
(如果线程1写入了volatile变量v(临界资源),接着线程2读取了v,那么,线程1写入v及之前的写操作都对线程2可见(线程1和线程2可以是同一个线程))

线程启动规则: Thread.start()方法happen—before调用用start的线程前的每一个操作。
(假定线程A在执行过程中,通过执行ThreadB.start()来启动线程B,那么线程A对共享变量的修改在接下来线程B开始执行前对线程B可见。
注意:线程B启动之后,线程A在对变量修改线程B未必可见。)

线程终止规则: 线程的所有操作都happen—before对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
(线程t1写入的所有变量,在任意其它线程t2调用t1.join(),或者t1.isAlive() 成功返回后,都对t2可见。)

线程中断规则: 对线程interrupt()的调用 happen—before 发生于被中断线程的代码检测到中断时事件的发生。
(线程t1写入的所有变量,调用Thread.interrupt(),被打断的线程t2,可以看到t1的全部操作)

对象终结规则: 一个对象的初始化完成(构造函数执行结束)happen—before它的finalize()方法的开始。
(对象调用finalize()方法时,对象初始化完成的任意操作,同步到全部主存同步到全部cache。)

传递性: 如果操作A happen—before操作B,操作B happen—before操作C,那么可以得出A happen—before操作C。
(A h-b B , B h-b C 那么可以得到 A h-b C)

在程序运行过程中,所有的变更会先在寄存器或本地cache中完成,然后才会被拷贝到主存以跨越内存栅栏(本地或工作内存到主存之间的拷贝动作),此种跨越序列或顺序称为happens-before。
注:happens-before本质是顺序,重点是跨越内存栅栏 通常情况下,写操作必须要happens-before读操作,即写线程需要在所有读线程跨越内存栅栏之前完成自己的跨越动作,其所做的变更才能对其他线程可见

CAS(Compare And Swap)

Java对并发控制的最常见方法就是锁,可以实现线程安全,锁虽然有效,但采用的是一种悲观的策略。它假设每一次对临界区资源的访问都会发生冲突,当有一个线程访问资源,其他线程就必须等待,所以锁是会阻塞线程执行的。

CAS:比较和交换(对资源的访问没有冲突,所有的线程执行不需要等待)
参数: V  A  B 
V : 表示要读写的主存位置
A : 表示旧的预期值
B : 表示新值

CAS指令执行时,当且仅当V的值等于预期值A时,才会将V的值改为B,如果V的值与A不同,说明其他线程作了更新,那么当前线程什么都不做,最后CAS返回的是V的真实值.
在多线程的情况下,多个线程用CAS操作一个变量时,只有一个会成功并更新新值,其余线程都会失败,但不会被挂起,而是循环重试.
1.主存中存储着值为10的变量
2.线程1想操作该值,将其+1. 此时 预期值A=10 新值B为11
3.在线程1提交数据之前,线程2先行一步进行+1操作,将V变为了11 ( v = 11 )
4.线程1提交,将A=10与V=11进行比较结果不相等 操作失败
5.线程1重新获取主存V的值 11, 并重新计算想要修改的值.此时预期值A=11 , 新值B=12,这个重新尝试的过程被称为自旋.
6.线程1提交,将A=11和V=11进行比较,结果相等,操作成功
7.将V的值更新为V=12

CAS的缺点:
1.CPU开销大
在并发量高的情况下,若线程反复尝试更新某一个变量,却一直失败,循环会给CPU带来很大压力
2.不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性 的更新,就不得不使用Synchronized了。
因为它本身就只是一个锁住总线的原子交换操作啊。两个CAS操作之间并不能保证没有重入现象。

3.ABA问题
线程1准备用CAS修改变量值A,在此之前,其它线程将变量的值由A替换为B,又由B替换为A,然后线程1执行CAS时发 现变量的值仍然为A,所以CAS成功。但实际上这时的现场已经和最初不同了.

解决ABA最简单的方案就是给值加一个修改版本号,每次值变化,都会修改它版本号,CAS操作时都对比此版本号。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值