2-线程共享synchronized(内置锁、隐式锁)关键字的用法

1、synchronized的三种应用方式

  • 修饰实例方法
    作用于当前实例加锁,进入同步代码前要获得当前实例的锁
  • 修饰静态方法
    作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
  • 修饰代码块
    指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
    以上3个都说明sync是一种对象锁,它锁住的是对象,

1:类锁, 如果在在类的static方法上加synchronized关键字,那么锁住的就是类对象

比如:
    public class Test  extends Thread{
    @Override
    public void run() {
        synClass();
    }
    //类锁,实际是锁类的class对象
    private static synchronized void synClass(){
     
        System.out.println("synClass going...");
      
        System.out.println("synClass end");
    }
    public static void main(String[] args) {
        //新建2个实例,但run()方法里面调用static sync方法,锁住的是类对象,拿到锁的线程执行完后,才会执行第二个线程
    	Test  synClzAndInst = new Test  ();
    	Thread t1 = new Thread(synClzAndInst);
        Test  synClzAndInst1 = new Test  ();
        Thread t2 = new Thread(synClzAndInst1);
        t2.start();
    	t1.start();
    
    }
 }

2:如果在非static 方法上加锁,那么必须锁住同一个实例对象才有效

@Override
    public void run() {
        instance(); //非staic 方法
    }
    public static void main(String[] args) {
    	Test  synClzAndInst = new Test  ();
    	Thread t1 = new Thread(synClzAndInst);
        Thread t2 = new Thread(synClzAndInst);
        t2.start();
    	t1.start();
    }

synchronized作用于实例方法

public class Sy1 implements Runnable {

    //共享资源(临界资源)
    static int i=0;

    /**
     * synchronized 修饰实例方法
     */
    public synchronized void add(){
        i++;
    }

    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            add();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Sy1 instance=new Sy1();
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}
输出结果 2000000

上述代码开启了2个线程,但传的都是同一个实例对象,当前线程的锁便是实例对象instance。

还有一点需要注意:
当一个线程正在访问一个对象的synchronized 实例方法,那么其他线程不能访问该对象的其他 synchronized 方法,因为一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized实例方法,但是其他线程还是可以访问该实例对象的其他非synchronized方法。

synchronized作用于静态方法

public class Sy2 implements Runnable {
    //共享资源(临界资源)
    static int i=0;

    /**
     * 作用于静态方法,锁是当前class对象,也就是
     */
    public static synchronized void add(){
        i++;
    }

    /**
     * 非静态,访问时锁不一样不会发生互斥
     */
    public synchronized void add1(){
        i++;
    }

    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            add();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(new Sy2());
        Thread t2=new Thread(new Sy2());
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}
输出 2000000

synchronized关键字修饰的是静态increase方法,与修饰实例方法不同的是,其锁对象是当前类的class对象。注意代码中的add1方法是实例方法,其对象锁是当前实例对象,如果别的线程调用该方法,将不会产生互斥现象,毕竟锁对象不同。

synchronized作用于代码块

public class Sy3 implements Runnable {
    static Sy3 instance=new Sy3();
    static int i=0;
    @Override
    public void run() {
        //省略其他耗时操作....
        //使用同步代码块对变量i进行同步操作,锁对象为instance
        synchronized(instance){
            for(int j=0;j<1000000;j++){
                i++;
            }
        }
        //this,当前实例对象锁,和实列一相同
        /*synchronized(this){
            for(int j=0;j<1000000;j++){
                i++;
            }
        }*/

        //class对象锁,和实列二相同
        /*synchronized(Sy3.class){
            for(int j=0;j<1000000;j++){
                i++;
            }
        }*/
    }
    public static void main(String[] args) throws InterruptedException {
        Sy3 sy3 = new Sy3();
        Thread t1=new Thread(sy3);
        Thread t2=new Thread(sy3);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

2、synchronized底层含义

synchronized代码块底层含义

将下面代码用javap -c -s -v -l Test.class反编译

public class Test {
    public int i;
    public void syncTask(){
        synchronized (this){
            i++;
        }
    }
}

得到

Classfile /D:/myFile/code/thread/target/classes/com/common/clockbone/synchronize
dclass/Test.class
  Last modified 2019-1-24; size 535 bytes
  MD5 checksum 95d2a6f48a1d0f42cba99908bd7c68eb
  Compiled from "Test.java"
public class com.common.clockbone.synchronizedclass.Test
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   //……
  public void syncTask();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: aload_0
         5: dup
         6: getfield      #2                  // Field i:I
         9: iconst_1
        10: iadd
        11: putfield      #2                  // Field i:I
        14: aload_1
        15: monitorexit
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit
        22: aload_2
        23: athrow
        24: return
      Exception table:
         from    to  target type
             4    16    19   any
            19    22    19   any
      LineNumberTable:
        line 8: 0
        line 9: 4
        line 10: 14
        line 11: 24
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      25     0  this   Lcom/common/clockbone/synchronizedclass/Test
;
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 19
          locals = [ class com/common/clockbone/synchronizedclass/Test, class ja
va/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4
}
SourceFile: "Test.java"

从字节码中可知同步语句块的实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置。

synchronized方法底层含义

将下面代码用javap反编译

public class Test {
    public int i;
    public synchronized void syncTask(){
        i++;
    }
}

得到

Classfile /D:/myFile/code/thread/target/classes/com/common/clockbone/synchronize
dclass/Test.class
  Last modified 2019-1-24; size 423 bytes
  MD5 checksum b2ddc55c47f9d590976420a19692368a
  Compiled from "Test.java"
public class com.common.clockbone.synchronizedclass.Test
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   //……
  public synchronized void syncTask();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field i:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field i:I
        10: return
      LineNumberTable:
        line 8: 0
        line 9: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/common/clockbone/synchronizedclass/Test
;
}
SourceFile: "Test.java"

synchronized同步块代码:使用monitorenter和monitorexit指令实现的
monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处
synchronized方法:则是则 是依靠方法修饰符上的ACC_SYNCHRONIZED来完成的。无论采用哪种方式,其本质是对一 个对象的监视器(monitor)进行获取,而这个获取过程是排他的,也就是同一时刻只能有一个 线程获取到由synchronized所保护对象的监视器。

任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用 时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获 取到监视器(执行该方法)的线程将会被阻塞在同步块和同步方法的入口处,进入BLOCKED 状态。
synchronized用的锁是存在Java对象头里的。
在这里插入图片描述
任意线程对Object的访问,首先要获得 Object的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED。获得了锁的线程释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新 尝试对监视器的获取。

synchronized和 java.util.concurrent.locks.Lock 的异同

相同点:lock能完成synchronized所实现的功能
不同点:lock有更精确的线程语义和更好的性能
sync自动释放锁,lock需手动在finally块中显示释放锁,lock的trylock方法可非阻塞的方式去拿锁

sync和cas不同,为什么大部分情况下cas比sync性能要高

sync加锁实现,多线程争夺锁的情况下有会有上下文切换
上下文切换:多线程在执行sync时,最终只有一个线程执行,其它线程等待,cpu会把这其它线程挂起,当线程行完sync方法释放锁后,cpu会唤醒其它等待的线程,这些线程又会争夺锁…… cpu将线程被挂起和唤醒的(移出cpu和移进cpu)过程就是上下文切换。
cas :通过 获取旧值-》修改旧值-》比较主存旧值和工作内存旧值是否相等-》相等将新值写回主存-》不等再循环一次。cas 是通过比较和循环的方式实现的没有上下文切换,所以cas一般比sync快。

什么情况下cas比sync慢?
很多线程竞争的情况下,cas while循环开销也较高

当一个线程要获取锁,这个锁没有被别的线程获取过,那么这个锁就偏向这个线程

同一个线程再次去获取这个对象,会比较 是否偏向这个线程 如果是 那么直接调

执行完成

3、synchronized 锁四种状态及膨胀过程

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

无锁 不可偏向

2种情况 不可偏向, 这个对象计算了hashcode
如果计算过hashcode , 那么线程id存不下了,那么 就不可能再偏向了

无锁 可偏向

如果对象是无锁状态
对象包括:对象头 实例数据 对齐填充
对象头: gc 年龄 , hashcode , 锁状态

偏向锁----记录当前获取锁Id-----这个锁只有从头到尾一个线程访问

当一个线程要获取锁,这个锁没有被别的线程获取过,那么这个锁就偏向这个线程

同一个线程再次去获取这个对象,会比较 是否偏向这个线程 如果是 那么直接调

执行完成

轻量锁-----

没有资源竞争 -----多个线程 交替执行(线程A 获取锁–执行—释放锁;线程 B 才开始获取锁,线程B不会在 A还持有锁 就去获取锁,) 轻量锁性能比 偏向锁 要低,因为它要进行一个compareSwap 替换,轻量锁会把获取锁的线程信息 存起来,再来一个线程t2,因为t1已经把锁升级成轻量锁,它们没有资源竞争, t2获取锁后,会把线程信息修改成t2。

重量锁

如果t2在t1还未释放锁的时候 去获取锁,那么它们间会有资源竞争,就是升级成重量锁

升级和膨胀不一样
锁的膨胀是不可逆的,但实际是可以的,但条件比较苛刻

4、synchronized Interger问题

当Synchronized遇到这玩意儿,有个大坑,要注意!

public class SynchronizedTest {
    public static void main(String[] args) {
        Thread why = new Thread(new TicketConsumer(10), "why");
        Thread mx = new Thread(new TicketConsumer(10), "mx");
        why.start();
        mx.start();
    }
}

class TicketConsumer implements Runnable {

    private volatile static Integer ticket;

    public TicketConsumer(int ticket) {
        this.ticket = ticket;
    }

    @Override
    public void run() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + "开始抢第" + ticket + "张票,对象加锁之前:" + System.identityHashCode(ticket));
            synchronized (ticket) {
                System.out.println(Thread.currentThread().getName() + "抢到第" + ticket + "张票,成功锁到的对象:" + System.identityHashCode(ticket));
                if (ticket > 0) {
                    try {
                        //模拟抢票延迟
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "抢到了第" + ticket-- + "张票,票数减一");
                } else {
                    return;
                }
            }
        }
    }
}

运行上面代码:可能会得到结果
在这里插入图片描述
一开始抢第10张票的时候,只有一个线程能获到锁。但第9张票时,why、mx同时抢到了锁。

1.为什么 synchronized 没有生效?
2.为什么锁对象 System.identityHashCode 的输出是一样的?

why 线程执行了ticket--操作,ticket 变成了 9,但是此时 mx 线程被锁住的 monitor 还是 ticket=10 这个对象,它还在 monitor 的 _EntryList 里面等着的,并不会因为 ticket 的变化而变化。
所以,当 why 线程释放锁之后,mx 线程拿到锁继续执行,发现 ticket=9。
而 why 也搞到一把新锁,也可以进入 synchronized 的逻辑,也发现 ticket=9。
好家伙,ticket 都是 9, System.identityHashCode 能不一样吗?

当执行了ticket–后,ticket对象变了。
于是改成如下:

synchronized (TicketConsumer.class)

4.1、tikcet-- 为什么会导致integer对象发生变化

我们的程序里面,会涉及到拆箱和装箱的过程,这个过程中会调用到 Integer.valueOf 方法。具体其实就是 ticket-- 的这个操作。
对于 Integer,当值在缓存范围内的时候,会返回同一个对象。当超过缓存范围,每次都会 new 一个新对象出来。

4.2、他知道 Integer 不能做为锁对象,但是他的需求又似乎必须把 Integer 作为锁对象。

在这里插入图片描述
标号为① 的程序段,目的:先从缓存中获取,如果缓存中没有则从数据库获取,然后再放到缓存里面去。
如果并发的场景下,如果有多个线程同一时刻都来获取同一个 id,但是这个 id 对应的数据并没有在缓存里面,那么这些线程都会去执行查询数据库并维护缓存的动作。
于是想用标号 ② 的程序段 来解决这个问题。用 synchronized 来把 id 锁一下,不幸的是,id 是 Integer 类型的。
在标号为 ③ 的地方他自己也说了:不同的 Integer 对象,它们并不会共享锁,那么 synchronized 也没啥卵用。

其实他这句话也不严谨,经过前面的分析,我们知道在缓存范围内的 Integer 对象,它们还是会共享同一把锁的,这里说的“共享”就是竞争的意思。
但是很明显,他的 id 范围肯定比 Integer 缓存范围大。

如何解决上面问题? 如果是我们自己怎么加锁 防止并发。可能会想到redis分布式锁吧。那就没有上面那个问题。如果不用分布式锁呢?
如果你真的必须用 Integer 作为锁,那么你需要搞一个 Map 或 Integer 的 Set,通过集合类做映射,你就可以保证映射出来的是你想要的明确的一个实例。而这个实例,就可以拿来做锁。
然后他给出了这样的代码片段:
在这里插入图片描述
就是用 ConcurrentHashMap 然后用 putIfAbsent 方法来做一个映射。
比如多次调用 locks.putIfAbsent(200, 200),在 map 里面也只有一个值为 200 的 Integer 对象,这是 map 的特性保证的,无需过多解释。
其实上面目的就是 构建一个结果缓存。
还有其它解决思路。

4.2、构建高效且可伸缩的结果缓存

四个方案如下,逐步优化:
案例1 使用HashMap和同步机制开初始化缓存
案例2 用ConcurrentHashMap替换HashMap
案例3 基于FutureTask的Memoizing封装器
案例4 Memoizer的最终实现
可以参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值