Java--并发总结

并发总结

并行和并发的区别:
并行是指多个事件在同一时刻发生(例如多核处理器可同时处理不同的任务);而并发是指多个事件在同一时间间隔发生

JVM

JVM是可运行Java代码的假想计算机,包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收,堆 和 一个存储方法域。JVM是运行在操作系统之上的,它与硬件没有直接的交互。

运行过程:Java源文件—->编译器—->字节码文件—->JVM—->机器码

Java内存模型
Java内存模型中规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的变量到主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要在主内存来完成,线程、主内存和工作内存的交互关系如下图所示:
注:此处的变量指实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,后者是线程私有的,不会被共享。
这里写图片描述

volatile

作用:一是保证共享变量对所有线程的可见性;二是禁止指令重排序优化

重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段,遵守一定规则
1.重排序操作不会对存在数据依赖关系的操作进行重排序。
2.重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变
若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序

实现:加入volatile关键字的共享变量编译所生成的汇编代码会多出一个lock前缀指令,它会引发两个事件

  • Lock前缀指令会强制将对缓存的修改操作立即写入主存;
  • 缓存回写到内存会导致其他CPU的缓存无效 ;
instance = new Singleton(); // instance是volatile变量
//转变成汇编代码,如下。
0x01a3de1d: movb $0×0,0×1104800(%esi);
0x01a3de24: lock addl $0×0,(%esp); 

使用场景:volatile关键字无法保证操作的原子性,使用volatile必须具备以下2个条件:

  • 对变量的写操作不依赖于当前值
  • 该变量没有包含在具有其他变量的不变式中

和synchronized 比较:
volatile轻量级,只能修饰变量。synchronized重量级,还可修饰方法;
volatile只能保证数据的可见性,不能用来同步,synchronized不仅保证可见性,而且还保证原子性;

synchronized

作用:用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。

  1. 确保线程互斥的访问同步代码
  2. 保证共享变量的修改能够及时可见
  3. 有效解决重排序问题

使用场景:

  • 修饰一个代码块,作用的对象是调用这个代码块的对象;
  • 修饰一个方法,作用的对象是调用这个方法的对象;
  • 修饰一个静态的方法,作用的对象是这个类的所有对象;

实现:
JVM基于进入和退出Monitor对象来实现同步的,monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。

synchronized 和 Lock 的区别:

  1. synchronized是Java语言关键字,Lock是一个类,通过这个类可以同步访问;
  2. synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
  3. Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
  4. 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
  5. Lock可以提高多个线程进行读操作的效率。

Java 锁

synchronized的缺点:
如果获取锁的线程被阻塞,其他线程只能等待,效率低; 读写操作时候,多线程读不需要等待可以同时读取,使用synchronized的时候只能一个线程读取 ,其他线程等待;

Lock

java.util.concurrent.locks包中的Lock是一个接口

public interface Lock {
    
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

lock()方法是最常用获取锁的方法,如果锁已被其他线程获取,则进行等待;
tryLock()方法是有返回值的,它表示用来尝试获取锁,成功返回true,失败返回false,这个方法无论结果如何都会直接返回值,不会一直等待;
tryLock(long time, TimeUnit unit)方法和上面的类似,区别在于这个方法在拿不到锁时会等待 一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
lockInterruptibly()方法当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断。(interrupt()方法只能中断阻塞线程)

ReentrantLock
可重入锁,ReentrantLock是唯一实现了Lock接口的类
ReentrantReadWriteLock
ReentrantReadWriteLock类实现了ReadWriteLock接口.主要方法
Lock readLock(); Lock writeLock();
在读多于写的情况,该锁效率较好,支持锁重入和锁降级(锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程);

锁的介绍

  1. 可重入锁
    锁具备可重入性,像synchronized和ReentrantLock都是可重入锁;
// 例如 method1方法可以调用 另一个synchronized 的 method2方法,不需要重新申请锁
class MyClass {
    public synchronized void method1() {
        method2();
    }
    public synchronized void method2() {
         
    }
}
  1. 可中断锁
    synchronized就不是可中断锁,而Lock是可中断锁。lockInterruptibly()的用法就是Lock的可中断性;
  2. 读写锁
    ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。
    可以通过readLock()获取读锁,通过writeLock()获取写锁。
  3. 公平锁和非公平锁
    公平锁的作用就是严格按照线程启动的顺序来执行的,不允许其他线程插队执行的;而非公平锁是允许插队的。默认情况下都是按照非公平锁进行同步的,创建ReentrantLock对象时,可以设置参数是true来实现公平锁。
    当使用非公平锁的时候,会立刻尝试配置状态,成功了就会插队执行,失败了就会和公平锁的机制一样,调用acquire()方法,以排他的方式来获取锁,成功了立刻返回,否则将线程加入队列,知道成功调用为止。

锁的基本概念和实现

对象在内存中的分布:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding);具体可以看以下的图片。

这里写图片描述

对象头分两部分:

  1. 对象自身运行时的数据,如hash码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID等,官方称Mark Word;
  2. 类型指针,对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

实例数据:对象真正存储的有效信息;
对齐填充:HotSpot虚拟机要求对象的起始地址必须是8字节的整数倍因此,当对象实例数据部分没有对齐的时候,就需要通过对齐填充来补全。

常见的锁

锁的状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。锁可以升级不可以降级。
偏向锁
—>大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。当一个线程访问同步块并获取锁时,会使用CAS操作在对象头Mark Word里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行任何同步操作和CAS操作。
—>偏向锁的撤销:偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁

1、偏向锁的撤销,它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着
2、如果线程不处于活动状态,则将对象头设置成无锁状态,然后重新偏向其它线程
3、如果线程仍然活动着,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁。如果不存在使用了,则重新变为无锁状态,然后重新偏向。

—>关闭偏向锁,通过jvm的参数-XX:UseBiasedLocking=false,则默认会进入轻量级锁。
注意:偏向锁在锁竞争激烈的场合没有太强的优化效果,大量竞争导致线程不停切换。

轻量级锁
轻量锁是在有锁竞争出现时升级为重量锁,而一般偏向锁是在有不同线程申请锁时升级为轻量锁;
—>轻量级锁加锁:线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
—>轻量级锁解锁的过程也是用CAS操作的,如下:
1、把栈帧中的锁记录与对象头用CAS换回来,
2、替换成功,则同步周期完成
3、替换失败,则说明有其他线程在竞争这个锁,那轻量级锁就会膨胀为重量级锁,释放锁的时候要唤醒挂起的线程。

重量级锁
当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。

这里写图片描述

CAS

Compare and Swap 比较并交换,是一种乐观锁的模式。CAS只适合于线程冲突较少的情况使用。
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
CAS缺点:

  1. ABA问题,如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。在变量前面加版本号来解决如 1A 2B 3A
  2. 循环时间长开销大,自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
  3. 只能保证一个共享变量的原子操作

concurrent包的源代码实现,会发现一个通用化的实现模式:
首先,声明共享变量为volatile;
然后,使用CAS的原子条件更新来实现线程之间的同步;
同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。

并发工具类

CountDownLatch

CountDownLatch允许一个或多个线程等待其他线程完成操作。
CountDownLatch的构造函数接收一个int类型的参数作为计数器,如果你想等待N个点完成,这里就传入N。
调用CountDownLatch的countDown方法时,N就会减1,CountDownLatch的await方法会阻塞当前线程,直到N变成零。

CyclicBarrier

CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)多用于多线程计数器
作用:让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。
用法:CyclicBarrier c = new CyclicBarrier(2); c.await();

两者区别
CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置。所以CyclicBarrier能处理更为复杂的业务场景。

Semaphore

Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以
保证合理的使用公共资源。
用法:
首先线程使用Semaphore的acquire()方法获取一个许可证,使用完之后调用release()方法归还许可证。

https://mp.weixin.qq.com/s/ksv0jmAu_7NNELfixajlTA

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值