Java并发编程实战

并发编程 专栏收录该内容
1 篇文章 0 订阅

最近在看java并发编程实战,打算记录一些自己认为经常面试用的知识点。


1.Volatile变量

1.1 作用:它用来确保将变量的更新操作通知到其他线程。
volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。
1.保证可见性、不保证原子性
2.禁止指令重排序
可见性的实现:
(1)修改volatile变量时会强制将修改后的值刷新的主内存中。
(2)修改volatile变量后会导致其他线程工作内存中对应的变量值失效。因此,再读取该变量值的时候就需要重新从读取主内存中的值。

从汇编角度起步理解实现原理:
volatile变量修饰的共享变量进行写操作的代码转变为汇编代码,会有Lock前缀的指令
Lock前缀指令在多核处理器中会引发两件事:
(1)(volatile写的内存语义)将当前线程的工作内存中关于这个共享变量的数据写回主内存;
(2)(volatile读的内存语义)这个写回主内存的操作会使其他线程的工作内存缓存这个共享变量的数据无效(过期了);
详细说明:
为了提高处理速度,线程一般会直接先从主内存中复制一份到自己的工作内存中,然后在工作内存中进行操作,然后会选择一个时间,将工作内存中的数据写回主内存中(这个时间不确定)。如果声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,会将这个变量缓存在线程工作内存中的数据写回主内存中。剩下要处理的就是如何将其他线程缓存该变量的值进行更新?在多处理器的情况下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改了,就会将当前处理器的缓存行设置成无状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读取到处理器缓存里。

有序性的实现:
volatile限制操作系统进行指令重排序(编译器重排序和处理器重排序)。而实现volatile可见性和happen-befor的语义是由JVM通过一组叫做内存屏障的处理器指令完成的,它可以实现对内存顺序的限制。
对于原子性:
volatile只能保证读/写操作单个的原子性,如果一个操作包含了读和写之类的复合操作,volatile不能保证其原子性。

1.2 Volatile变量是比锁更加轻量级的一种同步机制。
线程在访问volatile变量的时候不会执行加锁操作,所以也不会执行线程阻塞,因此volatile是一种比synchronized更加轻量级的同步机制。

1.3 volatile变量对可见性的影响比volatile变量本身更加重要。
当线程A首先写入一个volatile变量并且线程B随后读取到了该变量后,对写入volatile变量之前对A可见的所有变量值,在B读取了volatile变量后。对B也是可见的。从内存可见的角度来看,写入volatile变量相当于推出同步代码块,而读取volatile变量就相当于进入同步代码块。

1.4 使用volatile变量的条件
(1)对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值;
(2)该变量不会与其他状态变量一起纳入不变性条件中;
(3)在访问变量时不需要锁。
比如在第一个条件中,遇到要保证递增操作(i++)的原子性时候,就不能只靠使用volatile变量,因为volatile只能确保可见性,不能确保原子性。
对于第二个条件,如果程序中要求保存当前的值和上一个值,如果都用volatile变量修饰,就有两个volatile变量。但是在保存当前的值的时候,不能原子性地保存上一个值,这样就会造成在保存这两个值的间隙可能会被其他线程修改值,产生程序不正常运行。


2.JVM内存模型

这里写图片描述
Java内存模型将内存分为了 主内存和工作内存 。类的状态,也就是类之间共享的变量,是存储在主内存中的,每个线程都有一个自己的工作内存(相当于CPU高级缓冲区,这么做的目的还是在于进一步缩小存储系统与CPU之间速度的差异,提高性能),每次Java线程用到这些主内存中的变量的时候,会读一次主内存中的变量,并让这些内存在自己的工作内存中有一份拷贝,运行自己线程代码的时候,用到这些变量,操作的都是自己工作内存中的那一份。在线程代码执行完毕之后,然后在某个时间点上再将最新的值更新到主内存中去。
这样导致的问题是,如果线程1对某个变量进行了修改,线程2却有可能看不到线程1对共享变量所做的修改。


3.什么是CAS ?

CAS,全称为Compare and Set,即比较-设置。假设有三个操作数: 内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true,否则什么都不做并返回false 。当然CAS一定要volatile变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值A对某条线程来说,永远是一个不会变的值A,只要某次CAS操作失败,永远都不可能成功。


4.什么是乐观锁和悲观锁

(1)乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将 比较-设置 这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。

​ 乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。

(2)悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,然后再操作资源。


5.多线程中一些方法

5.1Object的方法
1.wait()
方法wait的作用是使当前执行代码的线程进行等待,将当前线程置入“预执行队列”中,并且wait所在的代码停止执行,直到接到通知或被中断。
(在调用wait()方法之前,线程必须获得该对象的对象锁,因此只能在同步方法/同步代码块中调用wait方法。
执行了wait方法后,当前线程会释放锁。)

2.notify()
notify方法的作用是,用来通知那些可能等待该对象的对象锁的其他线程。如果有多个线程等待,那么线程等待,则由线程规划器随机挑选出一个wait状态的线程,对其发出通知的notify,并使它等待获取该对象的对象锁
(注意等待获取该对象的对象锁,即使执行了notify方法,当前线程也不会马上释放该对象所,wait状态的线程也不能马上获取该对象锁,要等到执行notify方法的线程将程序执行完,退出synchronized代码块以后,当前线程才会释放锁,wait状态的线程才能获取到该对象锁。
notify方法也必须在同步方法或者同步代码块中使用。
notify方法不立即释放对象锁。)

总结:wait使线程停止运行,而notify使停止的线程继续运行。

3.notifyAll
notifyAll方法可以所有正在等待队列中等待同一共享资源的全部线程,让它们从等待状态,进入可运行状态。

5.2Thread类的实例方法
1.start()
start方法的作用就是通知线程规划器,此线程可以运行了,正在等待CUP调用线程对象。
(start方法执行了,线程不是立即就会执行,需要等待CPU调用。
start方法的执行顺序不代表线程启动的顺序,线程启动的顺序具有不确定性。)

2.run()
线程开始执行,虚拟机调用的是线程run()方法中的内容。
(只有run方法而不调用start方法来启动线程是没有多线程的意义的,只是一个普通方法,属于调用run方法的线程。)

3.isAlive()
该方法的作用是判断当前线程是否处于活动状态。只要线程启动了且没有终止,返回的就是true。

4.getPriority()和setPriority()
这两个方法是获取和设置线程的优先级。
(优先级高的线程更容易被CPU选择执行,得到的CPU资源也更多)

5.interrupt()
该方法的作用并不是中断线程,而是在线程阻塞的时候给线程一个中断标识,表示线程中断。
一般可以与wait()方法一起使用,特别用interru方法打断wait方法,会抛出异常中断线程的阻塞状态。

6.join()
join方法的作用是等待线程对象毁灭。
(模拟场景:主线程中包含一个子线程,子线程执行了,并调用了join方法。
join方法会使调用join方法的线程(也就是子线程)所在的线程(也就是主线程)无限阻塞,直到调用join方法的线程(子线程)毁灭,再继续执行子线程以后的代码。
join方法内部通过使用wait方法实现等待。
join方法执行后,当前线程的锁立即被释放(因为内部使用wait,wait方法执行后会释放锁);sleep方法却不是释放锁)

5.3Thread中的静态方法
1.currentThread()
该方法返回的是对当前正在执行线程对象的引用。比如Thread.currentThread()…..
(线程类的构造方法,静态代码块是被main线程调用,而线程类的run方法才是线程自己调用的)

2.sleep()
该方法的作用是在指定的毫秒数内让当前“正在执行的线程”休眠(暂停执行)。
(这个“当前执行的线程”就是指Thread.currentThread()返回的线程。
sleep方法并不释放锁,如果sleep代码上下文如果被加锁了,锁依旧在,但是CPU资源会让给其他线程)

3.yield()
暂停当前的执行的线程对象,并执行其他线程。这个暂停是会放弃CPU资源的,并且放弃的CPU资源的时间不确定,有可能刚放弃,就获得了资源,也有可能很久才会获得CPU资源。


6.线程的状态
线程共有六种状态:
NEW状态,线程实例化后海未执行start()方法时的状态;
RUNNABLE状态,线程进入运行的状态;
BLOCKED状态,线程在等待锁时的状态;
WAITING状态,线程执行了Object.wait()方法的状态,需要其他线程进行如notify的方法才能继续执行;
TIMED_WAITING状态,线程执行了Thread.sleep()方法,呈等待状态,等待时间到达,就可以继续执行;
TERMINATED状态,线程被销毁的状态。


wait释放锁,notify不释放,notifyall是让所有wait的线程状态改为可运行。
interrupt让wait的线程抛出异常,interrupt有点迷惑性,它并不能中断线程,


JMM java内存模型

  • 3
    点赞
  • 0
    评论
  • 4
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2020 CSDN 皮肤主题: 像素格子 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值