Synchronized详解

一、先看一个简单的多线程例子

public class Test {
    static int count=0;
    public static void main(String[] args) throws InterruptedException {
        for(int i=0;i<100;i++){
            new Thread(){
                @Override
                public void run() {
                    Test.count++;
                }
            }.start();
        }
        while (Thread.activeCount()>2){
        }
        System.out.println(Test.count);
    }
}

运行100个线程累加count,结果有可能不是100。很显然是出现了线程不安全。

线程不安全的因素主要在count++这行

    count++ 操作,计算机需要分成三步来执行。

1、读取 i 的值。

2、把 i 加 1.

3、把 最终 i 的结果写入内存之中。

(1)假如线程 A 读取了 i 的值为 i = 0,
(2)这个时候线程 B 也读取了 i 的值 i = 0。
(3)接着 A把 i 加 1,然后写入内存,此时 i = 1。
(4)紧接着,B也把 i 加 1,此时线程B中的 i = 1,然后线程B 把 i 写入内存,此时内存中的 i = 1。

也就是说,线程 A, B 都对 i 进行了自增,但最终的结果却是1,不是 2。

那么解决方法有很多种,本次主要讲解Synchronized,所以只要在Test.count++;代码块上加上Synchronized即可。

二、Synchronized原理

1、Synchronized总共有三种用法

修饰实例方法

public synchronized void eat(){
    .......
  .......
}

修饰静态方法

public static synchronized void eat(){
    .......
  .......
}

修饰代码块

public void eat(){
   synchronized(this){
       .......
     .......
   }
}

 

public void eat(){
   synchronized(Eat.class){
       .......
     .......
   }
}

其中第一种和第三种对等,第二种和第四种对等

注意:

(1)选用一个锁对象,可以是任意对象

(2)锁对象锁的是同步代码块,并不是自己

(3)不同类型的多个 Thread 如果有代码要同步执行,锁对象要使用所有线程共同持有的同一个对象

(4)需要同步的代码放到大括号中。需要同步的意思就是需要保证原子性、可见性、有序性中的任何一种或多种。不要放不需要同步的代码进来,影响代码效率

2、锁升级

在JDK的早期,synchronized叫做重量级锁,因为申请锁资源必须通过kernel,系统调用,从用户态 -> 内核态的转换,效率比较低,JDK1.6 之后做了一些优化,为了减少获得锁和释放锁带来的性能开销,引入了偏向锁、轻量级锁的概念。

在 synchronized 中,锁存在四种状态分别是:无锁、偏向锁、轻量级锁、重量级锁;

synchronized锁的是对象,对象就是Object,Object在heap中的布局如下图

前面8个字节就是markword,后面4个字节是class pointer就是这个对象属于哪个类的,People就是People.class,Cat类就是Cat.class,在后面实例数据就是看你类里面字段的具体大小了,int age就是4个字节,string name就是英文1个字节, 中文2个字节(String的中文字节数要看用的编码集合,如果是utf-8类型的,那么中文占2到3个字节,如果是GBK类型的,那么中文占2个字节),最后前面三项加起来不能被8整除的,就是补齐到能够被8整除。

(1)偏向锁

hotspot虚拟机的作者经过调查发现,大部分情况下,加锁的代码总是由同一个线程多次获得。所以基于这样一个概率,我们一开始加锁上的是偏向锁,当一个线程访问加了同步锁的代码块时,首先会尝试通过CAS操作在对象头中存储当前线程的ID。

i)如果成功markword则存储当前线程ID,接着执行同步代码块

ii)如果是同一个线程加锁的时候,不需要争用,只需要判断线程指针是否同一个,可直接执行同步代码块

iii)如果有其他线程已经获得了偏向锁,这种情况说明当前锁存在竞争,需要撤销已获得偏向锁的线程,并且把它持有的锁升级为轻量级锁(这个操作需要等到全局安全点,也就是没有线程在执行字节码)才能执行

(2)轻量级锁

撤销偏向锁,升级轻量级锁,每个线程在自己的线程栈生成LockRecord,用CAS操作将markword设置为指向自己这个线程的LR的指针,设置成功者得到锁。轻量级锁在加锁过程中,用到了自旋锁,自旋锁的使用,其实也是有一定条件的,如果一个线程执行同步代码块的时间很长,那么这个线程不断的循环反而会消耗 CPU 资源。

i)默认情况下自旋的次数是 10 次,可以通过-XX:PreBlockSpin来修改,或者自旋线程数超过CPU核数的一半

ii)在 JDK1.6 之后,引入了自适应自旋锁,自适应意味着自旋的次数不是固定不变的,而是根据前一次在同一个锁上自旋的时间以及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源

满足以上这两种情况之一后升级为重量级锁

(3)重量级锁

java中每个对象都关联了一个监视器锁monitor,当monitor被占用时就会处于锁定状态。线程执行monitorenter 指令时尝试获取monitor的所有权,过程如下:

i)如果monitor的进入数为 0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor 的所有者。

ii)如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加 1。

iii)如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为 0,再重新尝试获取monitor的所有权。

monitor 依赖操作系统的mutexLock(互斥锁)来实现的,线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值