前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java并发机制的底层实现原理--Java并发编程的艺术

Java并发机制的底层实现原理--Java并发编程的艺术

原创
作者头像
猎户星座1
修改2020-06-22 16:38:13
5230
修改2020-06-22 16:38:13
举报
文章被收录于专栏:Java Study

主要通过两个 Java 并发中常用的关键字volatile 及 synchronied 来将当Java 使用这两个关键字时对计算机的cpu的影响来说明。

1.volatile关键字

首先volatile关键字不会引起上下文的切换。

当volatile 修饰的共享变量时,在进行写操作时,查看Java程序经 编译 解释为机器语言,汇编语言时,发现多了一个lock 的前缀。

接下来因为这个lock前缀的出现,cpu相对正常的线程执行多了一下两个很重要的操作。

(1)将当前处理器(针对多cpu)中的缓存的数据同步到系统内存中。 (缓存指定是如下图中的L1,L2,L3)

(2)这个同步内存的操作,会让其他cpu的缓存该共享数据的数据失效。

书中解释:

为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部

缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的 变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据

写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操

作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一

致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当

处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状

态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存

里。

image.png
image.png

2.volatile的使用优化

增大对象所占内存(追加字节),提供并发的速度?查看原文

此处不整理了

synchronized 关键字

先来看下利用synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表现 为以下3种形式。

·对于普通同步方法,锁是当前实例对象。

·对于静态同步方法,锁是当前类的Class对象。

·对于同步方法块,锁是Synchonized括号里配置的对象。

当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。

那么锁到底存在哪里呢?锁里面会存储什么信息呢?

从JVM规范中可以看到Synchonized在JVM里的实现原理,JVM基于进入和退出Monitor对 象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter 和monitorexit指令实现的,而方法同步是使用另外一种方式实现的,细节在JVM规范里并没有

详细说明。但是,方法的同步同样可以使用这两个指令来实现。

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

使用 syn 来实现 大三上学习 操作系统课时,当时没有用代码研究比较明白的 生产者-消费者问题

代码语言:javascript
复制
package pv;

import java.util.*;

public class ProudectAndCoummer {
     //定义一个常量 表示生产者 生产到10会停止生产 等 消费者拿走小于10后 再进行生产
    final static  int MAX_VALUE = 10;
    //将生产出产品 存入到一个 队列中 
   volatile static Queue<String> produceStack  = new LinkedList<>();

    public static void main(String[] args) {

       Thread t1  = new Thread(){
                //生产者
             @Override
             public void run() {


                 while (true){
                     synchronized (produceStack){
                     if(produceStack.size()==MAX_VALUE){
                         try {
                             System.out.println("生产者:已经生产到最大值,线程暂停,等待拿走后再继续生产");
                             produceStack.wait();


                         } catch (InterruptedException e) {
                             e.printStackTrace();
                         }
                     }else {

                             try {
                             //生产者 的sleep 时间为50 消费者为 200 可以转化为,多个消费者对一个生产者的
                                 Thread.sleep(50);
                                 produceStack.add("产品");
                                 System.out.println("生产者:生产一个产品放到队列上,当前有"+produceStack.size()+"个产品");
                                 produceStack.notify();
                             } catch (InterruptedException e) {
                                 e.printStackTrace();
                             }
                         }


                     }


             }

             }
         };
        Thread t2   = new Thread(){
           @Override
           public void run() {

               while (true){

                   synchronized (produceStack){
                   if(produceStack.size()==0){
                       System.out.println("消费者:已经没有产品了通知生产者补货");
                       produceStack.notify();
                   }else {


                           try {
                               Thread.currentThread().sleep(200);
                               System.out.println("消费者:拿走一个货物,当前有"+produceStack.size()+"个产品");
                               produceStack.poll();
                           } catch (InterruptedException e) {
                               e.printStackTrace();
                           }

                       }

                   }

               }



           }
       };
  //两个线程开始运行,因为设置的 循环结束时间为while (true) 因此 理论上达到,两者一直在工作,
  // 生产者 生产产品到10后停止, 等待消费者去消费,当产品为0后 ,消费者唤醒线程因为只有两个线程,抛去主线程,
  // 通知生产者去生产产品
  t1.start();
  t2.start();
    }


控制台输出如下:
消费者:拿走一个货物,当前有3个产品
生产者:生产一个产品放到队列上,当前有3个产品
生产者:生产一个产品放到队列上,当前有4个产品
生产者:生产一个产品放到队列上,当前有5个产品
生产者:生产一个产品放到队列上,当前有6个产品
消费者:拿走一个货物,当前有6个产品
生产者:生产一个产品放到队列上,当前有6个产品
生产者:生产一个产品放到队列上,当前有7个产品
生产者:生产一个产品放到队列上,当前有8个产品
生产者:生产一个产品放到队列上,当前有9个产品
生产者:生产一个产品放到队列上,当前有10个产品
生产者:已经生产到最大值,线程暂停,等待拿走后再继续生产
消费者:拿走一个货物,当前有10个产品
消费者:拿走一个货物,当前有9个产品
消费者:拿走一个货物,当前有8个产品
消费者:拿走一个货物,当前有7个产品

Java对象头

synchronized用的锁是存在Java对象头里的。如果对象是数组类型,则虚拟机用3个字宽 (Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,1字宽 等于4字节,即32bit。

锁升级 无锁、偏向锁、轻量级锁,重量级锁。(synchronized 锁才有的)

实际因为发现,多个线程竞争同一把锁,锁总是被同一个线程获得,这肯定是不是设计并发时 想要看到的。

注意,锁可以升级,但不可以 降级。

为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并 获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出 同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需 要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则 使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。(疑惑点)

明天再写啦,发出去,防止自己不写了,今天再看 Java核心技术36讲,感觉好多都能看, 老朋友的感觉了,哈哈哈哈

-------------------------------------------------------华丽不分割线-------------------------------------------------------------------------------------------

今天带有自己学习到的归纳一下知识。

ReentrantLock锁

翻译为重入锁,它比Synchronized 提供了很多使用,且明确的方法,能够做到比syn更加的直观,syn 是jvm进行操控,它的获取锁和 释放锁都是隐式进行的,是,通过方法wait(time)和sleep(time second) 方法可以控制线程的暂停和等待,wait() 释放 锁,notify()随机唤醒一个处在wait中的线程 和notifyAll()唤醒所有的等待睡眠的线程。

reentrantlock 可以通过lock及 unlock 进行 锁的上锁和获取,比较明显的对资源进行操控,因此在使用,r.lock()进行加锁,及使用r.unlock(),进行 释放锁,因此最终该线程的一次执行过程最后 需要用finally{ r.unlock(); } 去包裹住将锁释放,防止因为异常的出现导致 锁无法进行释放。

代码语言:java
复制
        ReentrantLock r=new ReentrantLock(); //构造方法中添加 Boolean 值,默认为 非公平锁
    //    public ReentrantLock() {
    //    sync = new NonfairSync();
   // }
   
    fnal ReentrantLock lock = this.lock;
    
    
    try{
        //进行的操作 
    
    }
    finally{  //最后使用finally 把锁的所有权释放掉,防止因为 try 捕获到异常,导致锁 无法释放
      lock.unlock();
    }
  
   
  // Condition则是将wait、notify、notifyAll等操作转化为相 应的对象,将复杂而晦涩的同步操作转变为直观可控的对象行为。

        Condition condition = r.newCondition(); //通过condition 来使用 锁的状态进一步修饰
        condition.await();
        condition.signal();

ReentrantLock 相比synchroized ,因为可以像普通对象一样使用,所有可以利用其提供的各种便利方法,进行精细的同步操作

实现比synchroized 来说不能做的操作。

  1. 带超时的获取锁尝试。
  2. 可以判断是否有线程,或者某个特定线程,在排队等待获取锁。
  3. 可以响应中断请求。

通过signal/await的组合,完成了条件判断和通知等待线程,非常顺畅就完成了状态流转。注意,signal和await成对调用非常重要,不然假设只有await动作,线程会一直等待直到 被打断(interrupt)。看到signal 和await ,想到当时 操作系统老师讲的pv操作,起初我以为应该用 reentrantlock 来 用代码表示 生产者消费者 ,因为使用 reentrantlock 来操作锁 的获取 和释放很直观,且signal() 和await ()操作和操作系统老师讲的很像。侧面说明了 使用 reentrantlock的易懂。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 主要通过两个 Java 并发中常用的关键字volatile 及 synchronied 来将当Java 使用这两个关键字时对计算机的cpu的影响来说明。
  • 1.volatile关键字
  • 2.volatile的使用优化
  • 增大对象所占内存(追加字节),提供并发的速度?查看原文
  • synchronized 关键字
    • Java对象头
    • 锁升级 无锁、偏向锁、轻量级锁,重量级锁。(synchronized 锁才有的)
    • ReentrantLock锁
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档