解决线程安全问题——加锁(synchronized)

本文围绕Java中synchronized展开,介绍其特性,如互斥和可重入;阐述使用方式,包括锁对象类型、锁竞争等待和修饰方法;说明其能解决线程安全问题,如多线程修改同一变量和原子性问题;还解释了如何解决可重入问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

synchronized的特性

        1.互斥

        synchronized会起到互斥效果,某个线程执⾏到某个对象的synchronized中时,其他线程如果也执⾏到同⼀个对象synchronized就会阻塞等待。

        进入synchronized修饰的代码块,相当于加锁

        退出synchronized修饰的代码块,相当于解锁

        2.可重入

        在Java中synchronized同步块对同⼀条线程来说是可重⼊的,不会出现⾃⼰把⾃⼰锁死的问题

        Object lock = new Object();
        synchronized (lock){
            synchronized (lock){
                System.out.println("在Java中,锁是可重入的!");
            }
        }

        运行上述代码可以观察到结果正确输出,不会因为上了两次锁而产生问题

        synchronized的使用

        1.synchronized后面括号中的锁对象可以是任意类型

        Object lock1 = new Object();
        String lock2 = new String();
        ArrayList<Integer> lock3 = new ArrayList<>();
        synchronized (lock1){
            System.out.println("Object");
        }
        synchronized (lock2){
            System.out.println("String");
        }
        synchronized (lock3){
            System.out.println("ArrayList");
        }

        

        通过运行结果看到,synchronized后代码块内的代码都正确执行

        2.锁竞争等待

        在上文synchronized的特性中写道:synchronized会起到互斥效果,某个线程执⾏到某个对象的synchronized中时,其他线程如果也执⾏到同⼀个对象synchronized就会阻塞等待。

        也就是说当两个synchronized的锁对象一致,并且两个synchronized代码块同时开始工作时,仅有一个synchronized代码块可以正常工作,另一个要等待工作中的synchronized结束并释放锁对象才能继续执行.

Object lock = new Object();
        Thread t1 = new Thread(() ->{
            synchronized (lock){
                for (int i = 0; i < 5; i++) {
                    System.out.println("t1正在工作");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                System.out.println("t1工作完毕! 接下来释放锁对象");
            }
        });
        Thread t2 = new Thread(() ->{
            synchronized (lock){
                for (int i = 0; i < 5; i++) {
                    System.out.println("t2正在工作");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                System.out.println("t2工作完毕! 接下来释放锁对象");
            }
        });
        t1.start();
        t2.start();

        

        通过运行结果可以看到,当t1 t2两个线程同时开始时,t1抢占到了lock这个锁对象并开始执行,t2此时由于锁对象被抢占处于BLOCKED状态,当t1工作结束 释放lock锁对象后,t2紧跟着开始执行并顺利完成t2的工作内容

        3.修饰方法

        synchronized在修饰方法时,会对调用这个方法的对象加锁,如果方法是静态方法,那么就会对这个类的类对象加锁.

 public synchronized void sout(String str)  {
        for (int i = 0; i < 5; i++) {
            System.out.println(str + "正在工作!");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
    public static void main(String[] args) {
        Text2 lock = new Text2();
        Thread t1 = new Thread(() ->{
            lock.sout("t1");
        });
        Thread t2 = new Thread(() ->{
            lock.sout("t2");
        });
        t1.start();
        t2.start();
    }

        

        当t1线程开始后,会执行lock的sout方法,sout方法是被synchronized所修饰的,这时synchronized就会对lock对象加锁,当t2线程开始时,也会尝试执行lock的sout方法,但是此时lock已被加锁synchronized尝试对lock加锁时会失败,t2就会进入锁竞争等待。t1线程结束后,lock对象被释放,此时t2可以正常对lock加锁并执行相关代码

        synchronized解决线程安全问题

        使用synchronized解决线程安全问题主要解决的是 “多个代码同时修改同一变量”以及“代码不是原子的”这两个问题

        使用刚开始引入线程安全问题时的一个案例,两个线程同时进行count++,每个线程5000次,正确结果应该为10000,但是之前的结果不正确且每次都不一样,将代码加上锁,并观察运行结果

static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        Thread t1 = new Thread(() ->{
            for (int i = 0; i < 5_000; i++) {
                synchronized (lock){
                    count++;
                }
            }
        });
        Thread t2 = new Thread(() ->{
            for (int i = 0; i < 5_000; i++) {
                synchronized (lock){
                    count++;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }

               

        看到此时的运行结果是正确的,因为当t1,t2两个线程其中一个开始工作后,synchronized会对lock加锁,并执行代码块中的内容,另一个线程想要对lock加锁时,发现lock已被加锁,会进入锁竞争等待中,等待lock对象被释放

        这样就解决了“多个线程同时修改同一变量”这个问题。

        再来看原子性,synchronized经过编译之后,对应的是class文件中的monitorenter和monitorexit这两个字节码指令。这两个字节码对应的内存模型的操作是lock(上锁)unlock(解锁)。而且这两个操作之间运行的都是原子的,所以使用synchronized也可以保证原子性.

        synchronized是如何解决可重入问题的?

        在可重⼊锁的内部,包含了"线程持有者""计数器"两个信息.

        如果某个线程加锁的时候,发现锁已经被⼈占⽤,但是恰好占⽤的正是⾃⼰,那么仍然可以继续获取到锁,并让计数器⾃增.

        解锁的时候计数器递减到0的时候,才真正释放锁.(才能被别的线程获取到)

synchronized (locker) {//正常上锁 计数器为1
    synchronized (locker) {//判断到为同一线程 计数器+1 此时为2
        count++;
    }离开 synchronized所修饰的代码块 计数器-1 此时为1,不为0 不解锁
}离开 synchronized所修饰的代码块 计数器-1 此时为0,正常解锁

        

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值