• synchronized原理、流程、异常处理



    synchronized原理

    synchronized 是重量级锁,通过调用底层操作系统来实现

    早期的synchronized运行很慢,后来jdk对它进行了优化,引入很多新的概念

    当一个线程A过来请求一个锁资源时:

    1. 偏向锁:A首先判断一下这个线程是否持有这个锁。在每个对象头里都有一个markword区域,里面记录了目前获取锁的线程id,如果这个线程再次请求了这个锁,就不会再执行lock和unlock操作从而来提高性能
    1. 自旋锁:A如果判断不是,就会锁膨胀为自旋锁,默认10次。自旋锁,就是持有cpu继续请求这个锁,并不会直接加入到等待队列当中,自旋锁适合锁定的代码操作不是很多,就是经常会释放和获取的场景。对于比较耗时的操作,不经常释放的锁,如果使用自旋锁会导致浪费cpu。
    1. 如果自旋锁没有获取到锁,这个时候,才用将当前线程A放到等待队列wait中
    1. wait队列中的线程还没有具备竞选锁的权力。因为再高并发的场景下,会有大量线程对我们的wait队列进行cas操作,如果从wati队列取一个线程获取锁容易出错。
    1. 假如现在获取锁的线程是B,当B比执行unlock的时候,就会从wait队列中挑选一部分线程放到一个新的等待队列static中,并从static中选取一个(默认是第一个)给它举行一次竞争的资格,注意是举行竞争,并不是说它就是下一个获取锁的。加入我们的A线程进入了static
    1. 第一个线程进行一次竞争,加入A线程竞争上了,这个时候A算是正式获取了锁,然后就是正常的流程sleep呀或者是wait或者是unlock。

    加锁之所以慢,是因为需要操作系统实现,真正意义的加锁,都是需要底层的操作系统(windows、linux)实现的,像轻量级锁 偏向锁,乐观锁等等都是后来人们经过研究,发现的在一些特定的条件下不需要加锁,从而取消了一部分加锁的操作,从而提高效率

    为什么需要操作系统就慢

    因为需要从用户态到内核态操作,用户态和内核态之间的切换是很消耗时间的,操作系统加锁的方式是在指定的代码前后分别加上标识(英文怎么说我不记得了)

    jvm并没有规定如何实现synchronized,在每个对象头上都有一个markword 里面有两位用来标记是否被锁定。

    脏读

    当只是对写进行加锁的时候,容易出现脏读,如果我们项目不允许出现脏读,则需要注意避免

    
    
    /**
     * @program: solution
     * @description: 脏读
     * @author: Wang Hai Xin
     * @create: 2022-10-27 10:48
     **/
    public class Synchronization  {
        int count = 100 ;
        String name ="张三";
        public static void main(String[] args) {
            Synchronization t = new Synchronization();
            new Thread(t::set,"t1").start();
            new Thread(t::read,"t2").start();
        }
        private synchronized  void set() {
            try{
                System.out.println("开始修改");
                Thread.sleep(1000);
            }catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            this.count = 1000;
            System.out.println("修改完毕");
            read();
        }
    
        public /*synchronized*/ void read(){
            System.out.println(name + ":" + count);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    运行结果:

    开始修改
    张三:100
    修改完毕
    张三:1000
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    解决办法: 对读操作也加锁处理

    可重入锁

    一个同步方法可以调用另一个同步方法,一个线程获取到某个对象的锁之后,再次请求的时候仍然可以获得该对象的锁。也就是说 synchronization是可重入锁

    synchronized 是可重入锁,如下代码,我们启动一个线程调用一个用 synchronized修饰的方法, 在方法内调用父类用synchronized的方法依然可以访问到,这是因为一个synchronized是可重入锁。否则的话就形成了死锁

    package 线程同步synchronization;
    
    /**
     * @program: solution
     * @description: synchronized 是可重入锁
     * @author: Wang Hai Xin
     * @create: 2022-11-02 16:08
     **/
    public class synchronizedT {
        public static void main(String[] args) {
            mm mm = new mm();
            new Thread(mm::read,"t1").start();
        }
    }
     class m{
    public synchronized void read(){
        System.out.print("hello ");
    }
    }
    class mm extends m{
        public synchronized void read(){
            super.read();
            System.out.println("world");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    运行结果

    hello world
    
    • 1

    synchronized遇到异常会释放

    默认情况下 锁在遇到异常时,会释放

    因此在多线程的情况下,要格外注意异常的处理,不然可能会导致一个线程意外释放锁资源
    导致数据不一致
    如在一个程序中多个线程请求同一个资源,如果第一个线程遇到异常意外释放锁资源,
    其它线程就会进入修改数据。
    如下代码,正常情况下不应该有第二个线程进入。

    package 线程同步synchronization;
    
    /**
     * @program: solution
     * @description: 锁在遇到错误时默认会自动释放
     * @author: Wang Hai Xin
     * @create: 2022-11-02 16:23
     **/
    public class synchronizedT1 {
        int count  = 0;
        public static void main(String[] args) {
            synchronizedT1 synchronizedT1 = new synchronizedT1();
            new Thread(synchronizedT1:: m,"t1").start();
            new Thread(synchronizedT1::m,"t2").start();
        }
        public void m(){
            System.out.println(Thread.currentThread().getName() + "start");
            while (true){
                count++;
                System.out.println("count:" + count);
                if (count == 5) {
                    int i = 10/0;//制造异常
                }
                if (10 == count) {
                    break;
                }
            }
    
        }
    }
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    运行结果

    t1start
    count:1
    count:2
    count:3
    count:4
    count:5
    t2start
    count:6
    count:7
    count:8
    count:9
    count:10
    Exception in thread "t1" java.lang.ArithmeticException: / by zero
    	at 线程同步synchronization.synchronizedT1.m(synchronizedT1.java:23)
    	at java.lang.Thread.run(Thread.java:748)
    
    进程已结束,退出代码0
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    那些情况一下不用synchronized

    synchronized不要用在 Integer 、Lone等基本数据类型 和string常量。上
    Integer 、Lone等基本数据类型和String常量 在我们常量池里只有一份,假设我们将其锁定,其它调用我们这个jar包的很容易出现死锁,另外基本数据类型,变化的时候很容易产生新的对象,导致失效。所以最好不要用来锁。

  • 相关阅读:
    dpdk pdump 原理浅析
    WebSocket
    ​7.1 项目1 学生通讯录管理:文本文件增删改查(C++版本)(自顶向下设计+断点调试) (A)​
    Python A 组 G 题,全排列的价值 (AC)
    【TypeScript】中的函数详解
    中科磐云—2022广西逆向解析思路
    Verilog刷题[hdlbits] :Always nolatches
    Rust Yew应用开发的事件初探
    nVisual网络可视化管理平台功能和价值点
    自动化测试1
  • 原文地址:https://blog.csdn.net/qq_44850489/article/details/127646315