美文网首页java基础
线程的三特性

线程的三特性

作者: isLJli | 来源:发表于2021-05-23 12:21 被阅读0次

    内存模型

    由前一遍文章https://www.jianshu.com/p/623cf38cc4c7讲解了内存模型,但也带来线程的三个问题:原子性、可见性、有序性

    内存模型
    因为CPU的运行速度特别快,而主存的运行的速度跟不上CPU的速度,造成CPU在读取主存的数据要等待很久时间,所以CPU增加了的高速缓存区把需要数据存起来,CPU需要数据的时候就从高速缓存区中获取数据的副本,虽然高速缓存区运行速度很快,但也很昂贵。高速缓存区的出现提高了CPU的执行效率,但在多线程中也随之出现数据的原子性、可见性问题

    我们模仿两核CPU的执行多线程对同一数据的读取:

    CPU与主存的数据读取
    CPU1和CPU2分别从主存中获取a的副本,两个高速缓存区a=0,线程A通过CPU1的运算后把a赋值为3,然后把a=3的结果缓存到高速缓存区中,但是并没来的急把结果返回到主存时,线程B通过CPU2也运算操作a++,从CPU2的高速缓存区中拿到的a=0的值+1,而不是3+1,所以内存模型在多线程中会造成数据的不同步。也带出线程的三大特性:原子性、可见性、可序性

    线程原子性:在多线程的情况下,数据可能同时被多个线程上同时执行,这样就造成运算结果的不一致性,线程的原子性就是数据在被一个线程执行的时候,其他线程不可以同时再运行此数据。java中可以使用synchronized可以解决线程的原子性问题。

    线程可见性:原子性解决了多线程同时访问数据的问题,但是CPU的高速缓存区会并不能执行后数据立刻的更新到主存中,这样会导致其他的线程对另一个线程的计算结果不可见。在java中volatile可以接口线程的可见性问题。

    线程可序性:这个也是指令重排序问题,就是CPU为了内部的处理器单元的充分利用,会在对单线程结果正确的情况下,对代码指令进行非顺序执行。

    原子性——Synchronized

    Synchronized:每一个对象都有一个锁,在执行Synchronized就会为线程获取其锁,其他线程再访问Synchronized的内容时,如果没有对象的锁就不能访问。所以保证了多线程的原子性。

    通过一个Bean数据类学习Synchronized的使用:

    public class StudentBean {
      // 每个对象都有一个锁
    
      private String name;
      private String age;
      private String sex;
    
      // 1.在方法中加锁
      public synchronized void setNameAndAge(String name, String age) {
          this.name = name;
          this.age = age;
      }
    
      // 2.在代码块中加锁
      public void setName(String name) {
          synchronized (this) {
              this.name = name;
          }
      }
    
      // 3.在静态方法中加锁,这里锁的是"类的对象,即Class的对象"
      public static synchronized void start() {
          System.out.println("start");
      }
    
      // 执行完synchronized方法或代码块后,就会释放对象锁
      
      // 没有获取对象锁的线程,可以访问没有被synchronized的方法或代码块
      public void setAge(String age) {
          this.age = age;
      }
    
    }
    

    Synchronized可以修饰在方法、代码块、静态方法中,其中方法、代码块锁的是java对象,静态方法锁的是Class对象。执行完锁的方法后,拥有锁的线程就会释放对象锁。其他没有锁的线程就要么在CPU等一会,要么进入阻塞状态。

    Synchronized原理
    看看synchronized代码块的通过反编译后的字节码是怎样的。

    public class SynchronizedTest {
      public static void main(String[] args) {
          //通过synchronized修饰代码块
          synchronized (SynchronizedTest.class) {
              System.out.println("this is in synchronized");
          }
      }
    }
    
    反编译字节码

    同步代码块在monitorenter之后开始,同步代码块在monitorenter结束。
    monitor:锁可以理解为对象锁,它是java虚拟机实现的,底层依赖操作系统的Mutex Lock实现,每一个java对象都有都拥有monitor锁。

    monitorenter:执行monitorenter时,会先尝试获取锁,如果monitor没有被锁,或者已经拥有的monitor锁,锁的计数器就会+1,并开始执行同步代码。

    monitorexit:执行monitorexit时,锁的计数器就会-1,如果计数器为0时,线程就会释放monitor锁,其他线程就可以获取monitor锁。

    1. 静态方法:
    public class SynchronizedTest {
    
    
      public static void main(String[] args) {
          doSynchronizedTest();
      }
      //通过synchronized修饰方法
      public static synchronized void doSynchronizedTest(){
          System.out.println("this is in synchronized");
      }
    }
    
    反编译字节码
    静态方法并没有出现monitorenter和monitorexit,而是执行了ACC_SYNCHRONIZED,ACC_SYNCHRONIZED在执行同步代码块之前会获取monitor锁,再执行完成同步方法后会释放monitor锁。
    1. JDK1.6后的锁优化
      1.6之前,如果其他线程获取不了锁,就会进入阻塞状态,阻塞状态就会涉及上下文切换,这将消耗很多的时间,所以1.6后优化了锁频繁的上下文切换问题。


      对象头的锁信息

    首先线程进来获取锁时,会先获取偏向锁,偏向锁记录着线程Id,如果偏向锁已经有线程锁定,那就就会进入轻量级锁,在轻量级锁的线程就会在CPU执行回旋操作,如果已经获取的锁的线程很快的执行完成,那么就到轻量级锁执行,如果等待的时间久了,就进入重量级锁,那么线程就进入阻塞状态。所以这就优化了线程没有获取锁就直接阻塞问题。

    可见性——volatile

    被Volatile修饰的变量,在变量会修改后的值,对于其他的线程来说一样是及时可见的。

    private var volatile hasLoadingHeader = false
    

    它是如何实现的,被volatile修饰的变量在修改之后,会从高速缓存立即更新到主存中,并让主存发送一个通知给其他线程,告诉其他线程当前的变量已经更新,你高速缓存区的变量已经失效了,如果你要使用的话,请到主存拿去最新的变量值。

    相关文章

      网友评论

        本文标题:线程的三特性

        本文链接:https://www.haomeiwen.com/subject/pxnhjltx.html