线程的三特性

内存模型

由前一遍文章www.jianshu.com/p/623cf38cc…讲解了内存模型,但也带来线程的三个问题:原子性、可见性、有序性
内存模型
因为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修饰的变量,在变量会修改后的值,对于其他的线程来说一样是及时可见的。
它是如何实现的,被Volatile修饰的变量在修改之后,会从高速缓存立即更新到主存中,并让主存发送一个通知给其他线程,告诉其他线程当前的变量已经更新,你高速缓存区的变量已经失效了,如果你要使用的话,请到主存拿去最新的变量值。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享