美文网首页
3 synchronized

3 synchronized

作者: lijiaccy | 来源:发表于2018-06-15 16:50 被阅读0次

synchronized是java的关键字,这个关键字有什么作用呢?
看看下面的例子

public class Test implements Runnable{

    static int sum =0;
    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new Test());
            thread.start();
        }
        Thread.sleep(5000);
        System.out.println("result: " + sum);
    }

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            sum++;
        }
    }
}

开启10个线程,每个线程都执行10000次+1,那么结果应该是100000,但是不管怎么计算,结果都达不到100000.是什么原因呢?

之前我们说的java之间线程通信用的是共享内存。那么有可能出现问题的是内存可见性问题重排序问题,进一步知道了happens-before规则。
线程运行时有自己的栈空间,每个会在自己的栈空间运行,如果多线程间没有共享的数据也就是说多线程间并没有协作完成一件事情,那么就不会有任何问题,有点类似于ThreadLocal
但是涉及到共享变量了,比如上面的sum。就会出现问题,那么怎么去解决上面的问题呢。能想到的肯定是排队去取(也就是同步)。这样就不会有任何数据安全的问题,因为每个线程所操作的都是当前最新的版本数据。
java关键字synchronized就具有使每个线程依次排队操作共享变量的功能。很显然,这种同步机制效率很低(最新的版本中也不一定)。

synchronized实现原理

使用synchronized可是使用在代码块和方法中,根据Synchronized用的位置可以有这些使用场景,锁住的对象不同:


注意:
如果锁的是类对象的话,尽管new多个实例对象,但他们仍然是属于同一个类依然会被锁住,即线程之间保证同步关系。

对象锁(monitor)机制

在上面代码的run方法里面加上synchronized

    public void run() {
        synchronized (Test.class){
        for (int i = 0; i < 10000; i++) {
            sum++;
        }
        }
    }

执行代码,会得到正确的结果。那么我们进去看看它为什么会保证正确。
使用javap -v Test.class去看字节码。找到run方法,会发现出现了monitorentermonitorexit两个指令。

通过分析之后可以看出,使用Synchronized进行同步,其关键就是必须要对对象的监视器monitor进行获取,当线程获取monitor后才能继续往下执行,否则就只能等待。而这个获取的过程是互斥的,即同一时刻只有一个线程能够获取到monitor。
上图中只看到一个monitorenter,但是有两个monitorexit,这就是锁的重入性,即在同一锁程中,线程不需要再次获取同一把锁。Synchronized先天具有重入性。每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一。
下图表现了对象,对象监视器,同步队列以及执行线程状态之间的关系:

对象,对象监视器,同步队列和线程状态的关系

该图可以看出,任意线程对Object的访问,首先要获得Object的监视器,如果获取失败,该线程就进入同步状态,线程状态变为BLOCKED,当Object的监视器占有者释放后,在同步队列中得线程就会有机会重新获取该监视器。

synchronized的happens-before关系

  public class MonitorDemo {
    private int a = 0;

    public synchronized void writer() {     // 1
        a++;                                // 2
    }                                       // 3

    public synchronized void reader() {    // 4
        int i = a;                         // 5
    }                                      // 6
}

该代码的happens-before关系如图所示:

synchronized的happens-before关系

在图中每一个箭头连接的两个节点就代表之间的happens-before关系,黑色的是通过程序顺序规则推导出来,红色的为监视器锁规则推导而出:线程A释放锁happens-before线程B加锁,蓝色的则是通过程序顺序规则和监视器锁规则推测出来happens-befor关系,通过传递性规则进一步推导的happens-before关系。现在我们来重点关注2 happens-before 5,通过这个关系我们可以得出什么?

根据happens-before的定义中的一条:如果A happens-before B,则A的执行结果对B可见,并且A的执行顺序先于B。线程A先对共享变量A进行加一,由2 happens-before 5关系可知线程A的执行结果对线程B可见即线程B所读取到的a的值为1。

锁获取和锁释放的内存语义

线程A写共享变量

从上图可以看出,线程A会首先先从主内存中读取共享变量a=0的值然后将该变量拷贝到自己的本地内存,进行加一操作后,再将该值刷新到主内存,整个过程即为线程A 加锁-->执行临界区代码-->释放锁相对应的内存语义。

线程B读共享变量

线程B获取锁的时候同样会从主内存中共享变量a的值,这个时候就是最新的值1,然后将该值拷贝到线程B的工作内存中去,释放锁的时候同样会重写到主内存中。

从整体上来看,线程A的执行结果(a=1)对线程B是可见的,实现原理为:释放锁的时候会将值刷新到主内存中,其他线程获取锁时会强制从主内存中获取最新的值。另外也验证了2 happens-before 5,2的执行结果对5是可见的。

从横向来看,这就像线程A通过主内存中的共享变量和线程B进行通信,A 告诉 B 我们俩的共享数据现在为1啦,这种线程间的通信机制正好吻合java的内存模型正好是共享内存的并发模型结构。

锁优化

synchronized最大的特征就是在同一时刻只有一个线程能够获得对象的监视器(monitor),从而进入到同步代码块或者同步方法之中,即表现为互斥性(排它性)。所以表现出来的就是效率低。那么怎么可以提高效率呢?先了解两个知识点CAS,和Java对象头

CAS

使用锁时,线程获取锁是一种悲观锁策略,即假设每一次执行临界区代码都会产生冲突,所以当前线程获取到锁的时候同时也会阻塞其他线程获取该锁。而CAS操作(又称为无锁操作)是一种乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。因此,线程就不会出现阻塞停顿的状态。那么,如果出现冲突了怎么办?无锁操作是使用CAS(compare and swap)又叫做比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。

CAS比较交换的过程可以通俗的理解为CAS(V,O,N),包含三个值分别为:V 内存地址存放的实际值;O 预期的值(旧值);N 更新的新值。当V和O相同时,也就是说旧值和内存中实际的值相同表明该值没有被其他线程更改过,即该旧值O就是目前来说最新的值了,自然而然可以将新值N赋值给V。反之,V和O不相同,表明该值已经被其他线程改过了则该旧值O不是最新版本的值了,所以不能将新值N赋给V,返回V即可。当多个线程使用CAS操作一个变量是,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择挂起线程。CAS的实现需要硬件指令集的支撑,在JDK1.5后虚拟机才可以使用处理器提供的CMPXCHG指令实现。

元老级的Synchronized(未优化前)最主要的问题是:在存在线程竞争的情况下会出现线程阻塞和唤醒锁带来的性能问题,因为这是一种互斥同步(阻塞同步)。而CAS并不是武断的间线程挂起,当CAS操作失败后会进行一定的尝试,而非进行耗时的挂起唤醒的操作,因此也叫做非阻塞同步。这是两者主要的区别。

CAS的应用场景

Atomic**下面几乎用到的都是CAS原理,Lock中也有CAS操作state变量等等

那么CAS的缺点呢

3.1.4 CAS的问题

  1. ABA问题:因为CAS会检查旧值有没有变化,这里存在这样一个有意思的问题。比如一个旧值A变为了成B,然后再变成A,刚好在做CAS时检查发现旧值并没有变化依然为A,但是实际上的确发生了变化。解决方案可以沿袭数据库中常用的乐观锁方式,添加一个版本号可以解决。原来的变化路径A->B->A就变成了1A->2B->3C。java这么优秀的语言,当然在java 1.5后的atomic包中提供了AtomicStampedReference来解决ABA问题,解决思路就是这样的。

  2. 自旋时间过长:使用CAS时非阻塞同步,也就是说不会将线程挂起,会自旋(无非就是一个死循环)进行下一次尝试,如果这里自旋时间过长对性能是很大的消耗。如果JVM能支持处理器提供的pause指令,那么在效率上会有一定的提升。

  3. 只能保证一个共享变量的原子操作:当对一个共享变量执行操作时CAS能保证其原子性,如果对多个共享变量进行操作,CAS就不能保证其原子性。有一个解决方案是利用对象整合多个共享变量,即一个类中的成员变量就是这几个共享变量。然后将这个对象做CAS操作就可以保证其原子性。atomic中提供了AtomicReference来保证引用对象之间的原子性。

Java对象头

在同步的时候是获取对象的monitor,即获取到对象的锁。那么对象的锁怎么理解?无非就是类似对对象的一个标志,那么这个标志就是存放在Java对象的对象头。Java对象头里的Mark Word里默认的存放的对象的Hashcode,分代年龄和锁标记位。32为JVM Mark Word默认存储结构为:

Mark Word存储结构

如图在Mark Word会默认存放hasdcode,年龄值以及锁标志位等信息。
Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。对象的MarkWord变化为下图:

Mark Word状态变化

各种锁的比较

各种锁的对比

相关文章

网友评论

      本文标题:3 synchronized

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