多线程-线程安全、synchronized 、锁的升级机制

线程安全

什么是线程安全呢?为什么要保证线程安全?
线程安全是指先多个线程访问同一资源(数据)进行修改时,要保证数据的一致性。这也叫保证数据的一致性。

为什么要保证线程安全呢?
举个例子,现在我们定义了一个整形数据100,此时我们定义两个线程,线程A和线程B,都对它进行+1操作,请问结果是多少呢?不清楚,有可能是101,也有可能是102,因为我们不清楚这个线程是什么时候开始,当它拿到数据时到底是100,还是101,此时线程进行操作就会有意想不到的后果了。
所以当我们有多个线程操作同一资源时,我们必须要有同步的操作,要保证数据的一致性,让资源在同一时刻只有一个线程在进行操作。

我们将同一时刻只能有一个线程访问的资源叫做临界资源

将临界资源的代码段,叫做临界区

临界区特点:某一时刻如果有一个线程正在访问代码段,其他线程想要访问,只有等待当前的线程离开该代码段,你才可以访问,这样保证了线程安全。

Synchronized

为了保证我们的线程安全,引入了Synchronized关键字。
Synchronized关键字中放入代码段或者定义成方法。此时这个方法就定义为同步方法。
(tips:单独对某一行代码定义为同步代码没有任何意义)

例如:
两个线程都顺序打印数字,一个线程答应1-5,另一个线程答应6-10
(如果不对线程访问加锁,是无法做到的)

 public static void main(String[] args) {
       ThreadTestA A = new ThreadTestA();
       ThreadTestA B = new ThreadTestA();
       new Thread(A).start();
       new Thread(B).start();
    }
    
class ThreadTestA implements Runnable {
    private static  int number = 1;

    @Override
    public void run() {
        //较好系统访问的是类信息number,我们可以对这个类加锁
        synchronized(ThreadTestA.class){
          for(int i = 0; i < 5;i++){
              System.out.println(Thread.currentThread().getName()+"打印"+(number++));
          }
        }
    }

}

结果如下:
在这里插入图片描述
又或者我们对某一个方法进行加锁

class ThreadTestA implements Runnable {
    private static  int number = 1;

    public synchronized void prints(){
        for(int i = 0; i < 5;i++){
            System.out.println(Thread.currentThread().getName()+"打印"+(number++));
        }
    }
    @Override
    public void run() {
        prints();
    }

}

此时就有人吹毛求疵,这把锁太重了,范围太大了。那么我们就对方法体加锁,Synchronized(对象),这个对象可以是类中的Object。此时我们获取这个Object的锁也就相当于获得了对象的锁,因为这个Object就在这个对象中,所以就相当于要获得这个ThreadTestA类的实例对象的锁。

class ThreadTestA implements Runnable {
    private static  int number = 1;
    Object object = new Object();
    
    public  void prints(){
        synchronized(object) {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + "打印" + (number++));
            }
        }
    }
    
    @Override
    public void run() {
        prints();
    }

}

底层如何实现锁的获取

那么Java的底层是如何获取到锁的呢?获取到什么锁?

首先我们要理解一个获得锁这个概念,不是获取代码段,或者方法的锁,而是获取对象的锁。

所有的对象天生就有一把锁,而这把锁叫做monitor锁,也就是监视器锁。

每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
1)如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2)如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1
3)如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

执行monitorexit的线程必须是object ref所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

从反编译的结果来看,方法的同步并没有通过指令 monitorenter 和monitorexit 来完成(理论上其实也可以通过这两条指令来实现),不过同步方法相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。


Java对象头中的markword以及锁升级

首先我们了解下对象头的结构
Java对象头的结构分别是:markword,类型指针。
一个数组的对象的对象头是:markword,类型指针,数组长度。
JVM中的markword图如下
在这里插入图片描述
我们能看到锁并不是一次就让其他线程阻塞的,因为让其他线程阻塞是需要OS(操作系统的调度),如果线程太多,那么系统的调度就会很慢了,这与我们想要的快速不符。所以我们在中间进行过度。

那么我们就来了解一下Synchronized锁的升级吧。

概述:
无锁:现在这个对象没有任何线程占用它,所以不需要上锁,先来先得。

偏向锁:此时线程A要获取锁,一看没有人占用这把锁,那就拿来用,此时这把锁被A标记,这就是我A的锁,锁升级为偏向锁,偏向A。下次A再获得锁的时候,直接拿就行了,锁已经被A标记了,
此时线程B也想要锁,但是现在锁被A标记了,那不行B就抢(通过CAS的方式),抢成功了。那么锁就被B标记,此时还是偏向锁,但是偏向了B,下次B来的时候就不用抢了,这就B的了;抢失败了,现在锁发现有人再抢自己,锁烦死了,直接升级为轻量级锁。

轻量级锁:B抢失败之后,B就想那不行,不能放弃,B再抢(此时锁变成为自旋锁),抢了10次!此时发现B还抢这把锁,锁开始恼怒了,你不能再抢我了,你给我原地站着。此时轻量级锁升级为重量级锁,由OS调度。

重量级锁:此时线程A获取到锁,其他线程,例如线程B,直接阻塞。

详细:
无锁:当这个对象Object的对象头中markword的锁标志位位01,并且偏向锁的标记为0,那就证明此时的锁状态就是无锁。此时线程先来先得,谁先拿到Object的锁,这个锁就升级为偏向锁,并且偏向它,偏向的线程用线程ID号来保存。

偏向锁:现在线程A首先抢到了Object的锁使用权,此时锁的偏向锁的状态为置为1,线程ID保存的就是线程A的ID。A线程要执行这个锁关联的任何代码,不需要再做任何检查和切换,这种竞争不激烈的情况下,效率非常高。
此时线程B的也要抢锁的使用权,就使用CAS尝试抢夺锁。抢夺成功,那就直接把线程ID改为自己的(B的)线程ID;抢夺失败,此时锁发现自己在被其他线程争抢,升级为轻量级锁。

轻量级锁:锁标志位置为00.当锁升级为轻量级锁的时候,线程B还在不断的抢夺(抢夺的方法还是CAS)。那么锁就变成自旋锁,(JDK1.6版本是B再抢夺10次),当自旋次数内A释放了该锁的资源,那么此时B就进去占用,如果此时B没结束,线程A又来了,接着进入自旋状态抢夺锁,一直到自旋锁结束,或者B释放锁的资源。
此时自选锁结束了,但是还是没抢到锁的资源。那就升级为重量级锁,让老大哥OS去操作。

重量级锁:锁标志位置为10。此时线程A和线程B还有线程C要获取锁的对象,由OS去调度,A获取了,那么B和C就阻塞,等待A的释放,此时A释放了,再由OS调度,B和C谁来获得锁,其他线程都阻塞。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值