浅谈Java多线程<最通俗易懂的讲解>

一、浅谈Java多线程CAS原理

二、多线程锁的升级原理是什么? 

三、synchronized 和 volatile 的区别是什么?

一、浅谈Java多线程CAS原理

(一)、锁

实现线程同步最直观的策略是加锁,如使用synchronizeed关键字进行加锁。

悲观锁:悲观锁在一个线程进行加锁操作后使得该对象变为该线程的独有对象,其它的线程都会被悲观锁阻拦在外,无法操作。

悲观锁的缺陷:

1、一个线程获得悲观锁后其它线程必须阻塞。

2、线程切换时要不停的释放锁和获取锁,开销巨大。

3、当一个低优先级的线程获得悲观锁后,高优先级的线程必须等待,导致线程优先级倒置问题,synchronized加锁是一种典型的悲观锁。
乐观锁:

乐观锁与悲观锁不同,他乐观的认为对一个对象的操作不会引发冲突,所以每次操作都不进行加锁,只是在最后提交更改时验证是否发生冲突,如果冲突则再试一遍知道成功为止,这个尝试的过程被称为自旋。乐观锁其实并没有加锁,但乐观锁也引入了诸如ABA、自旋次数过多等问题。

(二)、CAS操作

1、在JDK1.5之前,Java中所有锁都是重量级的悲观锁,1.5中引入了java.util.concurrent包,这个包中提供了乐观锁的使用,而整个JUC包实现的基石就是CAS操作。

2、CAS(compare and swap)即比较和替换,在juc包中进程会看多诸如此类的代码:

unsafe.compareAndSwapInt(this, valueOffset, expect, update);这便是使用CAS操作。

3、CAS操作的过程为判断某个内存地址的值是否为给定的原值,如果是则修改为心智并返回成功,否则返回该地址的值。CAS操作有三个参数:内存地址、原值、新值。当内存地址中存放的对象不等于提供的原值时则将其替换为新值。

4、以AtomicInteger的修改为例查看使用CAS时如何无锁并安全的修改某个值的:

    public final int getAndUpdate(IntUnaryOperator updateFunction) {
        int prev, next;
        do {
            prev = get();
            next = updateFunction.applyAsInt(prev);
        } while (!compareAndSet(prev, next));
        return prev;
    }

先获得要修改的原值prev和要改成的新值next,当使用CAS替换新值不成功时,自旋,重新获得原值和新值再试一次直到成功为止。

5、这段代码中可以发现两个问题:

① compareAndSet操作必须是原子性的,即操作中间无法被打断。

② 获取原值时要保证这个原值对本线程可见

这两个问题是CAS操作的前提条件。

compareAndSet其实是调用了JNI,使用本地方法来保证原子性。

JNI(Java native interface),通过使用Java本地接口书写程序,可以确保代码在不同的平台上方便移植。

(三)、CAS带来的问题

1、ABA问题

CAS操作的流程为:

①读取原值。

②通过原子操作比较和替换。

虽然比较和替换是原子性的,但是读取原值和比较替换这两步不是原子性的,期间原值可能被其它线程修改。

ABA问题有些时候对系统不会产生问题,但是有些时候却也是致命的。

如:你在自动售货机上买了瓶饮料花了5块钱,你的账户原有10块钱,此时售货机发出扣款CAS请求:如果是10元变为5元。但是由于网络问题,这个请求发了两次,如果第一次执行正常将账户变为5元了,那么第二次请求时无法执行的,但是在第一次扣款操作执行完之后有人给你转账5元了,此时你的账户再次变为10元,那么第二遍的扣款请求是可以执行成功的,显然在这种情况下你多花了5块钱。
ABA问题的解决方法是对该变量增加一个版本号,每次修改都会更新其版本号。JUC包中提供了一个类AtomicStampedReference,这个类中维护了一个版本号,每次对值的修改都会改动版本号。

2、自旋次数过多

CAS操作在不成功时会重新读取内存值并自旋尝试,当系统的并发量非常高时即每次读取新值之后该值又被改动,导致CAS操作失败并不断的自旋重试,此时使用CAS并不能提高效率,反而会因为自旋次数过多还不如直接加锁进行操作的效率高。

3、只能保证一个变量的原子性

当对一个变量操作时,CAS可以保证原子性,但同时操作对个变量CAS就无能为力了。

Java中提供了atomicReference类来保证引用对象之间的原子性,就可以把多个变量放到一个对象里进行CAS操作,或者直接使用锁来操作多个对象。

二、多线程锁的升级原理是什么? 

锁的级别:

无锁->偏向锁->轻量级锁->重量级锁

锁分级别原因:

没有优化前,sychroniezed是重量级锁(悲观锁),使用wait、notify、notifyAll来切换线程状态非常消耗系统资源,线程的挂起和唤醒间隔很短暂,这样很浪费资源,影响性能。所以JVM对sychronized关键字进行了优化,把锁分为无锁、偏向锁、轻量级锁、重量级锁。

1、无锁

没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其它修改失败的线程会不断重试直到修改成功。

2、偏向锁

对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续执行中自动获取锁,降低获取锁带来的性能开销。偏向锁,指的是偏向第一个加锁线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争偏向锁才会被释放。

偏向锁的撤销,需要在某个时间点上没有字节码正在执行时,先暂停偏向锁的线程,然后判断锁对象是否处于被锁定状态,如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁。

如果线程处于活动状态,升级为轻量级锁的状态。

3、轻量级锁

轻量级锁是指当锁是偏向锁的时候,被第二个线程B访问,此时偏向锁就会升级为轻量级锁,线程B会通过自旋的形式尝试获取锁,线程不会阻塞,从er提升性能。

当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定次数时,轻量级锁边会升级为重量级锁,当一个线程已持有锁,另一个线程在自旋,而此时第三个线程来访时,轻量级锁也会升级为重量级锁。

注:自旋是什么?

自旋(spinlock)是指当一个线程获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

4、重量级锁

指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。

重量级锁通过对象内部的监听器(monitor)实现,而其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。

5、锁状态对比:

 偏向锁轻量级锁重量级锁
使用场景只有一个线程进入同步块虽然很多线程,但没有冲突,线程进入时间错开因而并未争抢锁发生了锁争抢的情况,多条线程进入同步块争用锁
本质取消同步操作CAS操作代替互斥同步互斥同步
优点不阻塞,执行效率高(只有第一次获取偏向锁时需要CAS操作,后面只是比对ThreadId)不会阻塞不会空耗CPU
缺点

适用场景太局限。若竞争产生,会有额外的偏向锁撤销的消耗

长时间获取不到锁空耗CPU阻塞,上下文切换,重量级操作,消耗操作系统资源

6、CAS是什么呢?

CAS即即比较和替换,当使用CAS替换新值不成功时,自旋,重新获得原值和新值再试一次直到成功为止。

CAS通过无锁操作提高了系统的吞吐率,高效的解决了原子操作问题。

三、synchronized 和 volatile 的区别是什么?

1、notify() 和 notifyAll() 有什么区别?

先解释两个概念:

等待池:假设一个线程调用了wait方法,线程就会释放该对象的锁,进入到该对象的等待池

锁池:只有获得了对象的锁,线程才会执行对象的synchronizeed代码,对象的锁每次只有一个线程可以获得,其它线程只能在锁池中等待

notify()方法随机唤醒对象等待池中的一个线程,进入锁池。

notifyAll()唤醒对象的等待池中的所有线程,进入锁池。

2、execute()和submit()有什么区别?

线程任务分两类,一类是实现了runnable接口,一类是实现了callable接口。

execute(Runnable x)没有返回值,可以执行任务,但无法判断任务是否成功完成,实现runnable接口

submit(Runnable x)返回一个future。可以用这个future来判断任务是否成功完成,实现Callable接口

3、sleep() 和 wait() 有什么区别? 

①sleep()是thread类的静态本地方法

wait()是Obejct类的成员本地方法

②sleep()方法可以在任何地方使用

wait()方法只能在同步方法或同步代码块中使用

③sleep()会休眠当前线程指定时间,释放CPU资源,不释放对象锁,休眠时间到自动苏醒继续执行

wait()方法放弃持有的对象锁,进入等待队列,当该对象被调动notify()或notifyAll()方法后才有机会竞争获取对象锁,进行运行状态

④均需捕获interruptedException异常

4、synchronized 和 volatile 的区别是什么?

作用:

synchronized表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其它线程。

volatile表示变量在CPU寄存器中是不确定的,必须从主存中读取,保证多线程环境下变量的可见性和有序性。

区别:

synchronized可以作用于方法、变量;volatile只能作用于变量。

synchronized可以保证线程间的有序性、原子性和可见性;volatile纸包装了可见性和有序性,无法保证原子性。

synchronized线程阻塞,volatile线程不阻塞。

 

素小暖讲Java@目录

转载于:https://my.oschina.net/u/4006148/blog/3069575

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值