• 初识synchronized


    初识synchronized

    • 线程安全问题
    • 什么是synchronized
    • synchronized几种使用方式
    • synchronized特性

    线程安全问题

    首先得知道什么是线程安全问题
    这里我们打个比方
    在同一个时间段我们使用两个线程对同一个数据进行++操作,这个被操作数据就可能会出现线程安全问题,假如说这个数据是0,两个线程同时++,我们想要得到的数据是2,但其实最后是1,原因是线程之间是不可见的。
    第一个线程在读这个数的时候是0,假如第二个线程在第一个线程写回前读了这个数,那么它读到的也是0,这个时候第一个线程写回也就是++操作,这个时候这个数是1,而第二个线程对读到的数进行++操作,写回的时候也是1。
    我们看代码:

    public class test4 {
    
    public static class MyRunna implements Runnable {
    
    public static int count;
    
    @Override
    public void run() {
    count++;
    }
    }
    
    public static void main(String[] args) throws InterruptedException {
    for (int i = 0; i < 1000; i++) {
    new Thread(new MyRunna()).start();
    }
    Thread.sleep(500);
    System.out.println("循环1000次以后count为" + MyRunna.count);
    }
    }
    

    执行结果为:

    循环1000次以后count为996
    

    这个数字不太好996
    不过我们可以观察到它并没有++到1000
    你多执行几次可以发现它每次的结果都不一样
    这就是通俗的理解线程安全问题

    什么是synchronized

    我们刚明白了线程安全问题大致是什么意思,那么肯定得有解决办法
    还是按照上面的代码为例子,这里暂且不理会写法,后面会细说

    public class test4 {
    
    public static class MyRunna implements Runnable {
    
    public static int count;
    
    public static Object object = new Object();
    
    @Override
    public void run() {
    synchronized (object) {
    count++;
    }
    }
    }
    
    public static void main(String[] args) throws InterruptedException {
    for (int i = 0; i < 1000; i++) {
    new Thread(new MyRunna()).start();
    }
    Thread.sleep(500);
    System.out.println("循环1000次以后count为" + MyRunna.count);
    }
    }
    
    

    执行结果为

    循环1000次以后count为10000
    

    我们使用了synchronized关键字,我们给操作的对象上了一把锁,这里引用一下百度知道对synchronized的解释

    synchronized 关键字,代表这个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B(或者C 、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。它包括两种用法:synchronized 方法和 synchronized 块。

    我们大致解释了一下synchronized是什么,下面将会解释它的一些基本使用方式和写法

    synchronized几种使用方式

    通过this锁定
    如果每次我们都要去定义一个锁的对象,也就是上面的Object object,每次都要new一个对象出来,那样加锁就过于繁琐了,有一个简单的方式,synchronized(this)也就是锁定当前对象
    贴一点部分代码出来

    public void run() {
    synchronized (this) {
    count++;
    }
    }
    

    锁定方法
    我们也可以直接在方法上面进行上锁

    public synchronized void run() {
    count++;
    }
    

    这和上面的方式其实是等价的

    锁定静态方法
    我们知道静态方法static是没有this对象的,你可以不用new一个对象来调用这个方法,假如这个方法上加一个synchronized就是相当于对当前类上锁,我们看代码
    假如这个类名字叫T,那么直接加synchronized等价于synchronized(T.class)

    public synchronized static void run() {
    count++;
    }
    

    总结一下其实也就是三种方式

    1.普通同步方法,锁定当前对象
    2.static静态方法,锁定class类
    3.同步方法块,锁定括号里的对象,对给对象加锁,进入同步代码库前要获得给定对象的锁

    锁优化

    一般有两种,一种是粗粒度,一种是细粒度
    其实也就是锁对象和代码块,有时候一个对象里的代码并不是都需要锁,假如这个时候给整个对象上锁就不太合适,我们可以把锁细化,只锁定我们需要锁的代码块
    假如说这整个对象里面的各个地方都用到锁,就没必要细化了,直接给整个对象加锁就好了

    synchronized特性

    synchronizd有那么几种特性
    1.可重入
    2.可见性
    3.出现异常自动释放

    可重入

    什么是可重入

    如果一个同步方法调用另外一个同步方法,一个方法加了锁另外一个方法也加了锁,加的是同一把锁也是同一线程,那这个时候申请仍然会得到该对象的锁。怎么解释可重入呢,比如有一个方法m1是synchronized的,有个方法m2也是synchronized的,这个时候m1方法里面是可以调用m2方法的。当一个线程调用m1的时候获得了这个把锁,然后在m1里面调用m2的时候,这个时候m2发现是同一个线程,因为m2和m1是同一个线程调用的那么允许它获得这把锁,这就是可重入。

    public class test6<main> {
    synchronized void m1(){
    System.out.println("m1执行了");
    try {
    TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    m2();
    }
    
    synchronized void m2(){
    System.out.println("m2执行了");
    }
    
    public static void main(String[] args) {
    new test6().m1();
    }
    }
    

    执行结果

    m1执行了
    m2执行了
    

    可见性

    可见性在上面已经说过了,所以就不细说了
    多线程之间对方法调用是不可见的,当线程A读一个数的时候,线程B也读了,最后A写入的时候,B有没有写入A是不知道的,这就是线程不可见,而synchronized具有可见性,用于确保写线程更新变量后,读线程再去访问的时候可以读到该变量的最新值。
    上面已经有代码实现过了这里就不演示了。

    出现异常自动释放锁

    程序在执行过程中,如果出现异常,默认情况下锁会被释放,所以,在并发过程中异常的处理很重要,不然如果异常处理的不合适,在第一个线程中抛出异常,接下来的线程中就会进入同步代码块,那么就有可能读到异常时的数据。
    我们看一下代码

    public class test7 {
    synchronized void erroTest() {
    for (int i = 0; i < 10; i++) {
    System.out.println("线程"+Thread.currentThread().getName()+"循环第" + i);
    try {
    TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    if (i == 5) {
    int i1 = i / 0;//此处抛出异常,锁将被释放,要想不被释放,可以在这里进行 catch,然后让循环继续
    }
    }
    }
    
    public static void main(String[] args) {
    test7 test7 = new test7();
    Runnable runnable = new Runnable() {
    @Override
    public void run() {
    test7.erroTest();
    }
    };
    new Thread(runnable,"r1").start();
    
    try {
    TimeUnit.SECONDS.sleep(3);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    new Thread(runnable,"r2").start();
    }
    
    }
    

    代码可能不是很严谨,但是并不影像我们观察这个特性

    synchronized锁升级

    • jdk早期的时候,synchronized的底层实现是重量级的,需要找操作系统去申请锁,效率非常低。
    • 后来经过改进,有了现在synchronized锁升级的概念
      锁升级经历这么几个过程
      偏向锁 -> 自旋锁(轻量级锁,无锁) -> 重量级锁

    然后我们依次介绍这几个锁是什么意思

    偏向锁

    偏向锁就是偏向第一个获得它的线程,在接下来的执行过程中,假如该锁没有被其它线程获取,那么下一次这个线程在来执行的时候就不需要在上锁,也就是说这个锁偏向第一个线程。在此线程执行过程中,中途退出或者加入并不在需要去进行加锁和解锁的操作。
    这里面有个markword记录了这个线程的id,具体比较细致,后面讲

    自旋(CAS)

    所谓的自旋就是指当一个线程来竞争锁的时候,这个线程会在原地转圈,也就是循环,而不是直接把线程阻塞,锁在原地循环的时候是消耗cpu的,就相当于是在执行一个什么都没有的空循环(jdk1.6默认是循环10次)。这就自旋,也叫轻量级锁,也叫无锁。

    重量级锁

    重量级锁就是依赖于对象内部的monitor实现的,而monitor又是依赖于操作系统的MutexLock实现的,所以重量级锁也叫做互斥锁。

    这里先简单介绍下一些基础概念,关于锁升级和对象头的相关基础知识请看这篇文章https://www.cnblogs.com/ccsert/p/12381817.html

  • 相关阅读:
    sql注入之payload
    cve2019-0708漏洞复现
    xss学习
    kernel panic not syncing
    nodeJS爬虫
    JS刷题自制参考知识
    HTML Cookie
    点击按钮触发div颜色改变的几种写法
    Bootstrap4布局(简要)
    jQuery实现论坛发帖Demo
  • 原文地址:https://www.cnblogs.com/ccsert/p/12381459.html
Copyright © 2020-2023  润新知