【并发】java知识点总结

知识脉络

并发问题的根源:保证线程安全的三要素

  1. 可见性:CPU缓存引起
    一个线程对共享变量的修改,另一个线程能立即看见。

现象:一个线程1修改一个变量,会先把变量放到cpu缓存中,但这时,另一个线程(对应的cpu不相同!so 缓存不同)需要读取这个变量,从内存中读取出来的结果不一样。

解决方案:1 JMM(java内存模型)提供来volatile 2 synchronized,lock

volatile: 当一个共享变量被volatile修饰时,保证修改的值立即被更新到主存。

synchronized, lock的方法:锁释放前,进程会将对变量的修改更新到主存。
  1. 有序性:重排序引起
    程序执行的顺序按照代码的先后顺序执行。(允许jvm对指令重新排序)

解决方案:as-if-seria(线程内):指令重排序,但不会对存在数据以来的操作重新排序,保证排序后结果不变。
happends-before(多线程)
volatile, synchronized, lock都可以用来保证有序性。

  1. 原子性:分时复用引起
    一个操作要么都完成,要么都不完成。现在只有基本的读取和赋值是原子操作,因此需要结合锁。

方案:悲观锁:synchronized关键字,jvm级别锁。
乐观锁:自旋锁+CAS。无法保证可见性,一般配合volatile使用。

JMM java内存模型

本质:规范了JVM如何按需禁用缓存 和 如何编译优化

方法:volatile, synchronized, final ; happends-before规则。

volatile, synchronized, final

volatile

使用条件:

  • 只有状态独立与程序内其他内容才能使用volatile.
  • 变量没有包含在具有其他变量的不变式中。
  • 状态独立于程序内其他内容
  1. volatile实现可见性

基于内存屏障实现的

添加了vilatile关键字的变量,在修改时,jvm产生一个lock前缀的指令发送给该cpu1,这个指令会将当前处理器缓存行的数据写回到内存。内存被写会后,其他的处理器中的缓存也会失效,因此会从内存中重新读取新的数据。(缓存一致性协议:处理器在总线上传播的数据检查自己的缓存是否过期,过期则回重新从内存读取新的数据),

lock前缀的指令:锁定的是缓存。
  1. volatile实现有序

基于 happens-before实现

happens-before规则:对volatile变量的写必须 发生在 任意后续中对volatile变量的读。
(提供一个内存屏障)

volatile禁止重排的方法: StoreStore, StoreLoad, LoadLoad, LoadStore

  • volatile写操作:前面有一个StoreStore防止上面的写操作和下面的volatile写重新排序。后面有一个StoreLoad:防止后面的写操作和上面的volatile写重排序

  • volatile读操作:后面有一个LoadLoad防止后面的读和上面的volatile读重排序。后面还有一个LoadStore防止后面的写和上面的volatile读重排序

  1. volatile不能实现原子性
synchronized

tips: 方法正常执行or抛出异常,都会释放锁

1 使用方法

  • this方法 或者 手动指定锁
// this方法
public void run(){
    // 代码块同步  this, 两个线程运行run,使用的同一个锁,线程1等待0完成后再执行
    synchronized(this){
        try{
            Thread.sleep(3000);
        }catch(InterruptedException e){
            e.printStackTrace();
        }
    }
}

Thread t1 = new Thread(instence); // instence是上文中声明的构造器
Thread t2 = new Thread(instence);
t1.start();
t2.start();

// 手动设置锁
Object block1 = new Object();
Object block2 = new Object();

public void run(){
    synchronized(block1){

    }
    synchronized(block2){

    }
}

public static void main(String[] args){
    Thread t1 = new Thread(instence);
    Thread t2 = new Thread(instence);
    t1.start();
    t2.start();
}

当两个thread分别传入不同的instence时候:
Thread t1 = new Thread(instence1);以及Thread t1 = new Thread(instence2);

锁用在普通方法,默认锁就是this,无法实现锁的功能(因为是两个锁);修饰静态方法,默认的锁就是当前在的Class类,这就是一个锁,才能实现不同进程中这个方法的同步。

public void run(){
    method();
}
public synchronized void method(){

}
public void run(){
    method();
}

2 synchronized原理

保持monitor计数器。当计数器为0,说明没有人获得锁,线程就会申请得到锁,然后将计数器+1。每当有新进程想要获得锁,计数器就加1。释放则-1

  1. synchronized实现可见性的原理:
public class MonitorDemo{
    public synchronized void writer(){

    }
    public synchronized void reader(){

    }
}

3 synchronized的优化

之前是基于mutex lock实现的,涉及了用户态和内核态的切换,代价很高。因此需要被优化。

  • 锁粗化:减少不必要的连在一起的锁。
  • 锁消除:
  • 轻量级锁:CAS原子命令代替悲观锁
  • 适应性自旋:CAS失败后进入忙则等待,之后再次尝试,尝试失败的次数超过阈值则调用阻塞。避免阻塞, 减少了从用户态切换到内核台的消费。

一些问题

举例说明线程和进程

在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,main函数再的线程就是进程的主线程

多线程带来的问题

内存泄露、死锁、线程不安全

死锁:

条件:互斥、资源不可剥夺、请求并保持(一次性申请完所有资源)、循环等待(按照什么顺序申请资源,就按照什么顺序释放资源)

sleep(), wait()方法的区别

sleep(): 没有释放锁

wait():释放了锁

wait经常用于线程之间的交互、通信。调用之后,线程不会自动苏醒,需要别的线程调用notify()

sleep通常用于暂停执行。调用之后,线程会自动苏醒。

通信方式

线程共享进程的方法区资源
但每个线程有自己的程序计数器虚拟机栈本地方法栈

  • 堆:存放新创建的对象

  • 方法区:已被加载的类信息,常量、静态变量、即时编译器编译后的代码

  • 虚拟机栈:为虚拟机执行java方法而服务,存放局部变量表、操作数栈、常量池等信息

  • 本地方法栈:为虚拟机使用到的native方法服务。

进程通信:

- socket通信(基于tcp/udp的通信方式,不同主机之间通过网络进行通信)
- 信号量:用在解决进程之间的同步。
- 消息队列:存放在内核里,只有内核重启(操作系统重启)或者显示地删除消息队列时,消息队列才被真正的删除
- 共享内存:依赖互斥锁和信号量等等。
- 管道(具有亲缘关系的父子进程间or兄弟进程之间的通信)

线程间通信:

-互斥锁,读写锁:mutex, 比如java中的synchronized关键词和各种lock
-信号量:
-事件Event: Wait/Notify: 通过通知操作的方式保持多线程同步。方便的实现多线程优先级的比较操作。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值