网站建设业务培训资料,网站构成的作用,济南 网站建设 域名注册,北京市建设厅门户网站6一、Volatile底层原理
volatile是轻量级的同步机制#xff0c;volatile保证变量对所有线程的可见性#xff0c;不保证原子性。
当对volatile变量进行写操作的时候#xff0c;JVM会向处理器发送一条LOCK前缀的指令#xff0c;将该变量所在缓存行的数据写回系统内存。由于缓… 一、Volatile底层原理
volatile是轻量级的同步机制volatile保证变量对所有线程的可见性不保证原子性。
当对volatile变量进行写操作的时候JVM会向处理器发送一条LOCK前缀的指令将该变量所在缓存行的数据写回系统内存。由于缓存一致性协议每个处理器通过嗅探在总线上传播的数据来检查自己的缓存是不是过期了当处理器发现自己缓存行对应的内存地址被修改就会将当前处理器的缓存行置为无效状态当处理器对这个数据进行修改操作的时候会重新从系统内存中把数据读到处理器缓存中。 来看看缓存一致性协议是什么。 缓存一致性协议当CPU写数据时如果发现操作的变量是共享变量即在其他CPU中也存在该变量的副本会发出信号通知其他CPU将该变量的缓存行置为无效状态因此当其他CPU需要读取这个变量时就会从内存重新读取。 volatile关键字的两个作用
保证了不同线程对共享变量进行操作时的可见性即一个线程修改了某个变量的值这新值对其他线程来说是立即可见的。禁止进行指令重排序。 指令重排序是JVM为了优化指令提高程序运行效率在不影响单线程程序执行结果的前提下尽可能地提高并行度。Java编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止处理器重排序。插入一个内存屏障相当于告诉CPU和编译器先于这个命令的必须先执行后于这个命令的必须后执行。对一个volatile字段进行写操作Java内存模型将在写操作后插入一个写屏障指令这个指令会把之前的写入值都刷新到内存。 volatile变量的使用
public class Main extends Thread {private volatile boolean keepRunning true;public void run() {System.out.println(Thread started);while (keepRunning) {try {System.out.println(Going to sleep);Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread stopped);}public void stopThread() {this.keepRunning false;}public static void main(String[] args) throws Exception{Main v new Main();v.start();Thread.sleep(3000);System.out.println(Going to set the stop flag to true);v.stopThread();}
}
二、synchronized的用法有哪些?
修饰普通方法作用于当前对象实例进入同步代码前要获得当前对象实例的锁修饰静态方法作用于当前类进入同步代码前要获得当前类对象的锁synchronized关键字加到static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁修饰代码块指定加锁对象对给定对象加锁进入同步代码库前要获得给定对象的锁
synchronized的作用有哪些
原子性确保线程互斥的访问同步代码可见性保证共享变量的修改能够及时可见其实是通过Java内存模型中的 “对一个变量unlock操作之前必须要同步到主内存中如果对一个变量进行lock操作则将会清空工作内存中此变量的值在执行引擎使用此变量前需要重新从主内存中load操作或assign操作初始化变量值” 来保证的有序性有效解决重排序问题即 “一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”
synchronized 底层实现原理
synchronized 同步代码块的实现是通过 monitorenter 和 monitorexit 指令其中 monitorenter 指令指向同步代码块的开始位置monitorexit 指令则指明同步代码块的结束位置。当执行 monitorenter 指令时线程试图获取锁也就是获取 monitor的持有权。 monitor对象存在于每个Java对象的对象头中 synchronized 锁便是通过这种方式获取锁的也是为什么Java中任意对象可以作为锁的原因 其内部包含一个计数器当计数器为0则可以成功获取获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后将锁计数器设为0 表明锁被释放。如果获取对象锁失败那当前线程就要阻塞等待直到锁被另外一个线程释放为止
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令取得代之的确实是ACC_SYNCHRONIZED 标识该标识指明了该方法是一个同步方法JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法从而执行相应的同步调用。
关键字synchronized示例
public class Main {private static int myValue 1;public static void main(String[] args) {Thread t new Thread(() - {while (true) {updateBalance();}});t.start();t new Thread(() - {while (true) {monitorBalance();}});t.start();}public static synchronized void updateBalance() {System.out.println(start: myValue);myValue myValue 1;myValue myValue - 1;System.out.println(end: myValue);}public static synchronized void monitorBalance() {int b myValue;if (b ! 1) {System.out.println(Balance changed: b);System.exit(1); }}
}
上面的代码生成以下结果 volatile和synchronized的区别
volatile只能使用在变量上而synchronized可以在类变量方法和代码块上。volatile至保证可见性synchronized保证原子性与可见性。volatile禁用指令重排序synchronized不会。volatile不会造成阻塞synchronized会。
Synchronized总共有三种用法
当synchronized作用在实例方法时监视器锁monitor便是对象实例this当synchronized作用在静态方法时监视器锁monitor便是对象的Class实例因为Class数据存在于永久代因此静态方法锁相当于该类的一个全局锁当synchronized作用在某一个对象实例时监视器锁monitor便是括号括起来的对象实例
ReentrantLock和synchronized区别
使用synchronized关键字实现同步线程执行完同步代码块会自动释放锁而ReentrantLock需要手动释放锁。synchronized是非公平锁ReentrantLock可以设置为公平锁。ReentrantLock上等待获取锁的线程是可中断的线程可以放弃等待锁。而synchonized会无限期等待下去。ReentrantLock 可以设置超时获取锁。在指定的截止时间之前获取锁如果截止时间到了还没有获取到锁则返回。ReentrantLock 的 tryLock() 方法可以尝试非阻塞的获取锁调用该方法后立刻返回如果能够获取则返回true否则返回false。
wait()和sleep()的异同点
相同点
使当前线程暂停运行把机会交给其他线程任何线程在等待期间被中断都会抛出InterruptedException
不同点
wait()是Object超类中的方法而sleep()是线程Thread类中的方法对锁的持有不同wait()会释放锁而sleep()并不释放锁唤醒方法不完全相同wait()依靠notify或者notifyAll、中断、达到指定时间来唤醒而sleep()到达指定时间被唤醒调用wait()需要先获取对象的锁而Thread.sleep()不用
Runnable和Callable有什么区别
Callable接口方法是call()Runnable的方法是run()Callable接口call方法有返回值支持泛型Runnable接口run方法无返回值。Callable接口call()方法允许抛出异常而Runnable接口run()方法不能继续上抛异常。
守护线程是什么
守护线程是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。在 Java 中垃圾回收线程就是特殊的守护线程。
线程间通信方式
volatile
volatile 使用共享内存实现线程间相互通信。多个线程同时监听一个变量当这个变量被某一个线程修改的时候其他线程可以感知到这个变化。
wait和 notify
wait/notify为Object对象的方法调用wait/notify需要先获得对象的锁。对象调用wait()之后线程释放锁将线程放到对象的等待队列当通知线程调用此对象的notify()方法后等待线程并不会立即从wait()返回需要等待通知线程释放锁通知线程执行完同步代码块等待队列里的线程获取锁获取锁成功才能从wait()方法返回即从wait()方法返回前提是线程获得锁。
join
当在一个线程调用另一个线程的join()方法时当前线程阻塞等待被调用join方法的线程执行完毕才能继续执行。join()是基于等待通知机制实现的。
三、AQS原理
AQSAbstractQueuedSynchronizer抽象队列同步器定义了一套多线程访问共享资源的同步器框架许多并发工具的实现都依赖于它如常用的ReentrantLock/Semaphore/CountDownLatch。
AQS使用一个volatile的int类型的成员变量state来表示同步状态通过CAS修改同步状态的值。当线程调用 lock 方法时 如果 state0说明没有任何线程占有共享资源的锁可以获得锁并将 state加1。如果 state不为0则说明有线程目前正在使用共享变量其他线程必须加入同步队列进行等待。
private volatile int state;//共享变量使用volatile修饰保证线程可见性同步器依赖内部的同步队列一个FIFO双向队列来完成同步状态的管理当前线程获取同步状态失败时同步器会将当前线程以及等待状态独占或共享构造成为一个节点Node并将其加入同步队列并进行自旋当同步状态释放时会把首节点中的后继节点对应的线程唤醒使其再次尝试获取同步状态。
四、ReentrantLock 是如何实现可重入性的?
ReentrantLock内部自定义了同步器sync在加锁的时候通过CAS算法将线程对象放到一个双向链表中每次获取锁的时候会检查当前占有锁的线程和当前请求锁的线程是否一致如果一致同步状态加1表示锁被当前线程获取了多次。
源码如下
final boolean nonfairTryAcquire(int acquires) {final Thread current Thread.currentThread();int c getState();if (c 0) {if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}else if (current getExclusiveOwnerThread()) {int nextc c acquires;if (nextc 0) // overflowthrow new Error(Maximum lock count exceeded);setState(nextc);return true;}return false;
}ReentrantLock 示例
public class ReentrantLockDemo01 implements Runnable {private Lock lock new ReentrantLock();private int tickets 200;Overridepublic void run() {while (true) {lock.lock(); // 获取锁try {if (tickets 0) {TimeUnit.MILLISECONDS.sleep(100);System.out.println(Thread.currentThread().getName() tickets--);} else {break;}} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock(); // 释放所}}}public static void main(String[] args) {ReentrantLockDemo01 reentrantLockDemo new ReentrantLockDemo01();for (int i 0; i 10; i) {Thread thread new Thread(reentrantLockDemo, thread i);thread.start();}}
}
reentrantlock用于替代synchronized
需要注意的是必须要必须要必须要手动释放锁重要的事情说三遍使用syn锁定的话如果遇到异常jvm会自动释放锁但是lock必须手动释放锁因此经常在finally中进行锁的释放使用reentrantlock可以进行“尝试锁定”tryLock这样无法锁定或者在指定时间内无法锁定线程可以决定是否继续等待使用ReentrantLock还可以调用lockInterruptibly方法可以对线程interrupt方法做出响应在一个线程等待锁的过程中可以被打断
五、锁的分类
公平锁与非公平锁
按照线程访问顺序获取对象锁。synchronized是非公平锁Lock默认是非公平锁可以设置为公平锁公平锁会影响性能。
public ReentrantLock() {sync new NonfairSync();
}public ReentrantLock(boolean fair) {sync fair ? new FairSync() : new NonfairSync();
}共享式与独占式锁
共享式与独占式的最主要区别在于同一时刻独占式只能有一个线程获取同步状态而共享式在同一时刻可以有多个线程获取同步状态。例如读操作可以有多个线程同时进行而写操作同一时刻只能有一个线程进行写操作其他操作都会被阻塞。
悲观锁与乐观锁
悲观锁每次访问资源都会加锁执行完同步代码释放锁synchronized和ReentrantLock属于悲观锁。
乐观锁不会锁定资源所有的线程都能访问并修改同一个资源如果没有冲突就修改成功并退出否则就会继续循环尝试。乐观锁最常见的实现就是CAS。
适用场景
悲观锁适合写操作多的场景。乐观锁适合读操作多的场景不加锁可以提升读操作的性能。
六、乐观锁有什么问题?
乐观锁避免了悲观锁独占对象的问题提高了并发性能但它也有缺点:
乐观锁只能保证一个共享变量的原子操作。长时间自旋可能导致开销大。假如CAS长时间不成功而一直自旋会给CPU带来很大的开销。ABA问题。CAS的原理是通过比对内存值与预期值是否一样而判断内存值是否被改过但是会有以下问题假如内存值原来是A 后来被一条线程改为B最后又被改成了A则CAS认为此内存值并没有发生改变。可以引入版本号解决这个问题每次变量更新都把版本号加一。
七、什么是CAS
CAS全称Compare And Swap比较与交换是乐观锁的主要实现方式。CAS在不使用锁的情况下实现多线程之间的变量同步。ReentrantLock内部的AQS和原子类内部都使用了CAS。
CAS 操作包含三个操作数 —— 内存位置V、预期原值A和新值(B)。 如果内存位置的值与预期原值相匹配那么处理器会自动将该位置值更新为新值 。否则处理器不做任何操作。无论哪种情况它都会在 CAS 指令之前返回该位置的值。在 CAS 的一些特殊情况下将仅返回 CAS 是否成功而不提取当前 值。CAS 有效地说明了“我认为位置 V 应该包含值 A如果包含该值则将 B 放到这个位置否则不要更改该位置只告诉我这个位置现在的值即可。” 通常将 CAS 用于同步的方式是从地址 V 读取值 A执行多步计算来获得新 值 B然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改则 CAS 操作成功。 类似于 CAS 的指令允许算法执行读-修改-写操作而无需害怕其他线程同时 修改变量因为如果其他线程修改变量那么 CAS 会检测它并失败算法可以对该操作重新计算。
CAS的目的
原子操作是利用类似的特性完成的。整个JUC都是建立在CAS之上的因此对于synchronized阻塞算法JUC在性能上有了很大的提升。
参考Java并发八股文第二弹 - 腾讯云开发者社区-腾讯云