基本原則をマルチスレッド-1

基本原則をマルチスレッド-1

問題からの感想

スレッドの使用の合理化は、プログラムの処理性能を向上させることができ、二つの主要な政党があります 直面します。
  最初は、マルチコアCPUであり、ハイパースレッディング・テクノロジーライン実現するために利用することができる 並列実行処理と、
  第二は、同期の実行に比べ実行の非同期スレッドである ライン、非同期実行はよく処理性能向上プログラムを最適化することができ、 スループットを送ります
同時に、それはまた、簡単な例を与え、多くの問題をもたらします。

によって引き起こされるマルチスレッド共有変数アクセスのセキュリティ問題のため

変数iは、スレッドが変数にアクセスする場合は、この修正される 問題なく修正及びアクセスデータのための時間を。しかし、複数の場合 のスレッドが、このために同じ変数を変更するには、データのセキュリティがあります 問題 のスレッドの安全性については、基本的にデータ状態、この状態とこの通常共有されている変数へのアクセスを管理します、。共有することを意味 データ変数が複数のスレッドによってアクセスされてもよい;変数は、この変数の値を参照し 、そのライフサイクルを変化させることができます。
オブジェクトかどうか、それは複数のスレッドがあるかどうかに応じて、スレッドセーフで アクセス、およびプログラムは、このオブジェクトを使用する方法です。のであれば、 複数のスレッドが同じ共有オブジェクトにアクセスし、追加の同期と転送 側のコードの場合は、他の共有オブジェクトの状態がコーディネート行いません必要はありません (まだ正しい結果である正しさを意味しているのオブジェクト私たちは期待します
一貫して結果が)、それは、オブジェクトがスレッドセーフであることを意味します。
パブリック クラスデモ{
     プライベート 静的 int型のカウント= 0 ;
    公共の 静的な 無効INC(){
      しようと{ 
        のThread.sleepを( 1 ); 
      } キャッチ(InterruptedExceptionある電子){ 
        e.printStackTrace(); 
      } 
      カウント ++ ; 
  } 
  パブリック 静的 ボイドメイン(文字列[]引数)がスロー InterruptedExceptionあるが{ 
  
    ためint型 i = 0; iは<1000; iは++ ){       新しいスレッド(() - >Demo.inc())スタート();.     }     のThread.sleep( 3000 );     System.out.printlnは( "実行結果" + COUNT); // <1000年   } }

 

 
➢のJava結果として並列にスレッドによるデータのセキュリティを解決する方法を尋ねる タイトルにそれを?

並行して、データのセキュリティスレッドを確保するためだと思う方法

私たちは、問題の本質は、共有データの訪問の存在によって複雑に考えることができます 尋ねます。私たちは、シリアルに並列スレッドを作ることができる方法があれば、 それは問題がまだ存在していないではないのですか?
すべての既存の知識によると、最初に考えたのは、ロックバーでなければなりません。 結局のところ、これはシーンモードではない、と私たちはデータベースを扱っているとき、 あなたはあまりにも悲観的ロック、オプティミスティックロックの概念を理解しています。ロックとは何ですか?それが処理され 、我々は前に目標を達成する必要性について話している場合は、同期手段を作った 、そしてロックは、特定の相互に排他的な特性を実現する必要があります。 Javaはロック方法がSynchroinzedキーワードで提供します。

同期の基本的な理解

マルチスレッドプログラミングでは、ベテランの役割となって、非常に同期した 多くの人々はヘビー級のロック、それを呼び出します。ただし、Java SE 1.6 DUIで いくつかのケースでは、それはありませんが、後にさまざまな最適化を実施し、同期 させるためにロックを取得し、解放するためにセックスを減少させるためにとても重く、Java SEの1.6 エネルギー消費量とバイアスロックと光の導入をミドルロック。
  同期の基本的な構文
synchronized 有三种方式来加锁,分别是
1. 修饰实例方法,作用于当前实例加锁,进入同步代码前 要获得当前实例的锁
2. 静态方法,作用于当前类对象加锁,进入同步代码前要 获得当前类对象的锁
3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同 步代码库前要获得给定对象的锁。 不同的修饰类型,代表锁的控制粒度
  synchronized 的应用
修改前面的案例,使用 synchronized 关键字后,可以达到 数据安全的效果
public class Demo{
  private static int count=0;
  public static void inc(){
    synchronized (Demo.class) {
      try {
        Thread.sleep(1);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      count++;
    }
  }
  public static void main(String[] args) throws InterruptedException {
    for(int i=0;i<1000;i++){
      new Thread(()->Demo.inc()).start();
    }
    Thread.sleep(3000);
    System.out.println("运行结果"+count);
  }
}

 

思考锁是如何存储的

可以思考一下,要实现多线程的互斥特性,那这把锁需要 哪些因素?
1. 锁需要有一个东西来表示,比如获得锁是什么状态、无 锁状态是什么状态
2. 这个状态需要对多个线程共享
那么我们来分析,synchronized 锁是如何存储的呢?观察 synchronized 的整个语法发现,synchronized(lock)是基于 lock 这个对象的生命周期来控制锁粒度的,那是不是锁 存储和这个 lock 对象有关系呢? 于是我们以对象在 jvm 内存中是如何存储作为切入点,去 看看对象里面有什么特性能够实现锁 对象在内存中的布局 在 Hotspot 虚拟机中,对象在内存中的存储布局,可以分 为三个区域:对象头(Header)、实例数据(Instance Data)、对 齐填充(Padding)
 
  探究 Jvm 源码实现
当我们在 Java 代码中,使用 new 创建一个对象实例的时 候,(hotspot 虚拟机)JVM 层面实际上会创建一个 instanceOopDesc 对象。
Hotspot 虚拟机采用 OOP-Klass 模型来描述 Java 对象实 例,OOP(Ordinary Object Point)指的是普通对象指针, Klass 用来描述对象实例的具体类型。Hotspot 采用 instanceOopDesc 和 arrayOopDesc 来 描述对象 头, arrayOopDesc 对象用来描述数组类型
instanceOopDesc 的定义在 Hotspot 源 码 中 的 instanceOop.hpp 文件中,另外,arrayOopDesc 的定义对应 arrayOop.hpp
class instanceOopDesc : public oopDesc {
 public:
  // aligned header size.
  static int header_size() { return sizeof(instanceOopDesc)/HeapWordSize; }

  // If compressed, the offset of the fields of the instance may not be aligned.
  static int base_offset_in_bytes() {
    // offset computation code breaks if UseCompressedClassPointers
    // only is true
    return (UseCompressedOops && UseCompressedClassPointers) ?
             klass_gap_offset_in_bytes() :
             sizeof(instanceOopDesc);
  }

  static bool contains_field_offset(int offset, int nonstatic_field_size) {
    int base_in_bytes = base_offset_in_bytes();
    return (offset >= base_in_bytes &&
            (offset-base_in_bytes) < nonstatic_field_size * heapOopSize);
  }
};

#endif // SHARE_VM_OOPS_INSTANCEOOP_HPP

从 instanceOopDesc 代码中可以看到 instanceOopDesc继承自 oopDesc,oopDesc 的定义载 Hotspot 源码中的oop.hpp 文件中在普通实例对象中,oopDesc 的定义包含两个成员,分别是 _mark 和 _metadata_mark 表示对象标记、属于 markOop 类型,也就是接下来要讲解的 MarkWorld,它记录了对象和锁有关的信息_metadata 表示类元信息,类元信息存储的是对象指向它的类元数据(Klass)的首地址,其中 Klass 表示普通指针、_compressed_klass 表示压缩类指针

  MarkWord 

在 Hotspot 中,markOop 的定义在 markOop.hpp 文件中,代码如下
class markOopDesc: public oopDesc {
 private:
  // Conversion
  uintptr_t value() const { return (uintptr_t) this; }

 public:
  // Constants
  enum { age_bits                 = 4, //分代年龄
         lock_bits                = 2, //锁标识
         biased_lock_bits         = 1, //是否为偏向锁
         max_hash_bits            = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
         hash_bits                = max_hash_bits > 31 ? 31 : max_hash_bits, //对象的哈希值
         cms_bits                 = LP64_ONLY(1) NOT_LP64(0),
         epoch_bits               = 2   //偏向锁时间戳
  };
....

Mark word 记录了对象和锁有关的信息,当某个对象被synchronized 关键字当成同步锁时,那么围绕这个锁的一系列操作都和 Mark word 有关系。Mark Word 在 32 位虚拟机的长度是 32bit、在 64 位虚拟机的长度是 64bit。Mark Word 里面存储的数据会随着锁标志位的变化而变化,

Mark Word 可能变化为存储以下 5 中情况

为什么任何对象都可以实现锁

1. 首先,Java 中的每个对象都派生自 Object 类,而每个Java Object 在 JVM 内部都有一个 native 的 C++对象 oop/oopDesc 进行对应。
2. 线程在获取锁的时候,实际上就是获得一个监视器对象 (monitor) ,monitor 可以认为是一个同步对象,所有的 Java 对象是天生携带 monitor。在 hotspot 源码的 markOop.hpp 文件中,可以看到下面这段代码。
ObjectMonitor* monitor() const {
    assert(has_monitor(), "check");
    // Use xor instead of &~ to provide one extra tag-bit check.
    return (ObjectMonitor*) (value() ^ monitor_value);
  }

多个线程访问同步代码块时,相当于去争抢对象监视器修改对象中的锁标识,上面的代码中ObjectMonitor这个对象和线程争抢锁的逻辑有密切的关系

synchronized 锁的升级

在分析 markword 时,提到了偏向锁、轻量级锁、重量级 锁。在分析这几种锁的区别时,我们先来思考一个问题? 使用锁能够实现数据的安全性,但是会带来性能的下降。 不使用锁能够基于线程并行提升程序性能,但是却不能保 证线程安全性。这两者之间似乎是没有办法达到既能满足
性能也能满足安全性的要求。
hotspot 虚拟机的作者经过调查发现,大部分情况下,加锁的代码不仅仅不存在多线程竞争,而且总是由同一个线程 多次获得。所以基于这样一个概率,是的 synchronized 在 JDK1.6 之后做了一些优化,为了减少获得锁和释放锁带 的性能开销,引入了偏向锁、轻量级锁的概念。因此大家
会发现在 synchronized 中,锁存在四种状态 分别是:无锁、偏向锁、轻量级锁、重量级锁, 根据竞争激烈的程度从低到高不断升级。

  偏向锁的基本原理

前面说过,大部分情况下,锁不仅仅不存在多线程竞争, 而是总是由同一个线程多次获得,为了让线程获取锁的代 价更低就引入了偏向锁的概念。怎么理解偏向锁呢? 当一个线程访问加了同步锁的代码块时,会在对象头中存 储当前线程的 ID,后续这个线程进入和退出这段加了同步
锁的代码块时,不需要再次加锁和释放锁。而是直接比较 对象头里面是否存储了指向当前线程的偏向锁。如果相等 表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁

  偏向锁的获取和撤销逻辑

1. 首先获取锁 对象的 Markword,判断是否处于可偏向状 态。(biased_lock=1、且 ThreadId 为空)
2. 如果是可偏向状态,则通过 CAS 操作,把当前线程的 ID 写入到 MarkWord
  a) 如果 cas 成功,那么 markword 就会变成这样。 表示已经获得了锁对象的偏向锁,接着执行同步代码
  b) 如果 cas 失败,说明有其他线程已经获得了偏向锁, 这种情况说明当前锁存在竞争,需要撤销已获得偏向 锁的线程,并且把它持有的锁升级为轻量级锁(这个 操作需要等到全局安全点,也就是没有线程在执行字 节码)才能执行
3. 如果是已偏向状态,需要检查 markword 中存储的 ThreadID 是否等于当前线程的 ThreadID
  a) 如果相等,不需要再次获得锁,可直接执行同步代码
  b) 如果不相等,说明当前锁偏向于其他线程,需要撤销 偏向锁并升级到轻量级锁

  偏向锁的撤销

偏向锁的撤销并不是把对象恢复到无锁可偏向状态(因为 偏向锁并不存在锁释放的概念),而是在获取偏向锁的过程 中,发现 cas 失败也就是存在线程竞争时,直接把被偏向 的锁对象升级到被加了轻量级锁的状态。对原持有偏向锁的线程进行撤销时,原获得偏向锁的线程 有两种情况:
  1. 原获得偏向锁的线程如果已经退出了临界区,也就是同 步代码块执行完了,那么这个时候会把对象头设置成无 锁状态并且争抢锁的线程可以基于 CAS 重新偏向但前 线程
  2. 如果原获得偏向锁的线程的同步代码块还没执行完,处 于临界区之内,这个时候会把原获得偏向锁的线程升级 为轻量级锁后继续执行同步代码块。
在我们的应用开发中,绝大部分情况下一定会存在 2 个以 上的线程竞争,那么如果开启偏向锁,反而会提升获取 的资源消耗。所以可以通过 jvm 参数 UseBiasedLocking 来设置开启或关闭.
偏向锁流程图分析

轻量级锁的基本原理

  轻量级锁的加锁和解锁逻辑

锁升级为轻量级锁之后,对象的 Markword 也会进行相应 的的变化。升级为轻量级锁的过程:
1. 线程在自己的栈桢中创建锁记录 LockRecord。
2. 将锁对象的对象头中的MarkWord复制到线程的刚刚创 建的锁记录中。
3. 将锁记录中的 Owner 指针指向锁对象。
4. 将锁对象的对象头的 MarkWord替换为指向锁记录的指 针。

  自旋锁

轻量级锁在加锁过程中,用到了自旋锁
所谓自旋,就是指当有另外一个线程来竞争锁时,这个线 程会在原地循环等待,而不是把该线程给阻塞,直到那个 获得锁的线程释放锁之后,这个线程就可以马上获得锁的。
注意,锁在原地循环的时候,是会消耗 cpu 的,就相当于 在执行一个啥也没有的 for 循环。
所以,轻量级锁适用于那些同步代码块执行的很快的场景, 这样,线程原地等待很短的时间就能够获得锁了。
自旋锁的使用,其实也是有一定的概率背景,在大部分同 步代码块执行的时间都是很短的。所以通过看似无异议的 循环反而能提升锁的性能。
但是自旋必须要有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,那么这个线程不断的循环反而 会消耗 CPU 资源。默认情况下自旋的次数是 10 次, 可以通过 preBlockSpin 来修改。
在 JDK1.6 之后,引入了自适应自旋锁,自适应意味着自旋 的次数不是固定不变的,而是根据前一次在同一个锁上自 旋的时间以及锁的拥有者的状态来决定。 如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并 且持有锁的线程正在运行中,那么虚拟机就会认为这次自 旋也是很有可能再次成功,进而它将允许自旋等待持续相 对更长的时间。如果对于某个锁,自旋很少成功获得过, 那在以后尝试获取这个锁时将可能省略掉自旋过程,直接 阻塞线程,避免浪费处理器资源

  轻量级锁的解锁

轻量级锁的锁释放逻辑其实就是获得锁的逆向逻辑,通过 CAS 操作把线程栈帧中的 LockRecord 替换回到锁对象的 MarkWord 中,如果成功表示没有竞争。如果失败,表示 当前锁存在竞争,那么轻量级锁就会膨胀成为重量级锁
流程图分析

  重量级锁的基本原理

当轻量级锁膨胀到重量级锁之后,意味着线程只能被挂起 阻塞来等待被唤醒了。重量级锁的 monitor
创建一个类如下
public class SynchronizeDome {
    private static int i;
    public static void main(String[] args) {
        synchronized (SynchronizeDome.class){
            i++;
        }
        addOper();
    }
    public  static synchronized void    addOper(){
        i++;
    }
}

 

运 行 以 后 通 过 java p 工 具 查 看 生 成 的 class 文 件 信 息 分 析 sync hron ized 关 键 字 的 实 现 细节 javap -v  app. class
public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #2                  // class com/zgw/threadState/SynchronizeDome
         2: dup
         3: astore_1
         4: monitorenter
         5: getstatic     #3                  // Field i:I
         8: iconst_1
         9: iadd
        10: putstatic     #3                  // Field i:I
        13: aload_1
        14: monitorexit
        15: goto          23
        18: astore_2
        19: aload_1
        20: monitorexit
        21: aload_2
        22: athrow
        23: invokestatic  #4                  // Method addOper:()V
        26: return

 

加了同步代码块以后,在字节码中会看到一个 monitorenter 和 monitorexit。
每一个 JAVA 对象都会与一个监视器 monitor 关联,我们 可以把它理解成为一把锁,当一个线程想要执行一段被 synchronized 修饰的同步方法或者代码块时,该线程得先 获取到 synchronized 修饰的对象对应的 monitor。 monitorenter 表示去获得一个对象监视器。monitorexit 表
示释放 monitor 监视器的所有权,使得其他被阻塞的线程 可以尝试去获得这个监视器 monitor 依赖操作系统的 MutexLock(互斥锁)来实现的, 线
程被阻塞后便进入内核(Linux)调度状态,这个会导致系 统在用户态与内核态之间来回切换,严重影响锁的性能。
  重量级锁的加锁的基本流程
任意线程对 Object(Object 由 synchronized 保护)的访 问,首先要获得 Object 的监视器。如果获取失败,线程进 入同步队列,线程状态变为 BLOCKED。当访问 Object 的 前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻 塞在同步队列中的线程,使其重新尝试对监视器的获取。

回顾线程的竞争机制

再来回顾一下线程的竞争机制对于锁升级这块的一些基本 流程。方便大家更好的理解, 假如有这样一个同步代码块,存在 Thread#1、Thread#2 等 多个线程
synchronized (lock) {
// do something
}

 

情况一:只有 Thread#1 会进入临界区;
情况二:Thread#1 和 Thread#2 交替进入临界区,竞争不激 烈;
情况三:Thread#1/Thread#2/Thread3… 同时进入临界区, 竞争激烈
偏向锁
此时当 Thread#1 进入临界区时,JVM 会将 lockObject 的 对象头 Mark Word 的锁标志位设为“01”,同时会用 CAS 操作把 Thread#1 的线程 ID 记录到 Mark Word 中,此时进 入偏向模式。所谓“偏向”,指的是这个锁会偏向于 Thread#1, 若接下来没有其他线程进入临界区,则 Thread#1 再出入 临界区无需再执行任何同步操作。也就是说,若只有 Thread#1 会进入临界区,实际上只有 Thread#1 初次进入 临界区时需要执行 CAS 操作,以后再出入临界区都不会有 同步操作带来的开销。
轻量级锁
偏向锁的场景太过于理想化,更多的时候是 Thread#2 也 会尝试进入临界区, 如果 Thread#2 也进入临界区但是 Thread#1 还没有执行完同步代码块时,会暂停 Thread#1 并且升级到轻量级锁。Thread#2 通过自旋再次尝试以轻量 级锁的方式来获取锁
重量级锁
如果 Thread#1 和 Thread#2 正常交替执行,那么轻量级锁 基本能够满足锁的需求。但是如果 Thread#1 和 Thread#2 同时进入临界区,那么轻量级锁就会膨胀为重量级锁,意 味着 Thread#1 线程获得了重量级锁的情况下,Thread#2 就会被阻塞
Synchronized 结合 Java Object 对象中的wait,notify,notifyAll 
前面我们在讲 synchronized 的时候,发现被阻塞的线程什 么时候被唤醒,取决于获得锁的线程什么时候执行完同步 代码块并且释放锁。那怎么做到显示控制呢?我们就需要 借 助 一 个 信 号 机 制 : 在 Object 对 象 中 , 提 供 了 wait/notify/notifyall,可以用于控制线程的状态
wait/notify/notifyall 基本概念
wait:表示持有对象锁的线程 A 准备释放对象锁权限,释 放 cpu 资源并进入等待状态。
notify:表示持有对象锁的线程 A 准备释放对象锁权限,通 知 jvm 唤 醒 某 个 竞 争 该 对 象 锁 的 线 程 X 。 线 程 A synchronized 代码执行结束并且释放了锁之后,线程 X 直 接获得对象锁权限,其他竞争线程继续等待(即使线程 X 同 步完毕,释放对象锁,其他竞争线程仍然等待,直至有新 的 notify ,notifyAll 被调用)。
notifyAll:notifyall 和 notify 的区别在于,notifyAll 会唤醒 所有竞争同一个对象锁的所有线程,当已经获得锁的线程 A 释放锁之后,所有被唤醒的线程都有可能获得对象锁权 限 需要注意的是:三个方法都必须在 synchronized 同步关键 字 所 限 定 的 作 用 域 中 调 用 , 否 则 会 报 错 java.lang.IllegalMonitorStateException ,意思是因为没有 同步,所以线程对对象锁的状态是不确定的,不能调用这 些方法。
另外,通过同步机制来确保线程从 wait 方法返回时能够感 知到感知到 notify 线程对变量做出的修改
wait/notify 的基本使用
参见源码
wait/notify 的基本原理

おすすめ

転載: www.cnblogs.com/qlsem/p/11477354.html