synchronized你用对了吗?

1. 概述

本篇博客记录synchronized的使用,注意的事项。

2. synchronized关键字锁的是什么?

synchronized关键字锁定的是对象不是代码块,Demo1中锁的是Object对象的实例。

锁定的对象有两种:1.类的实例 2.类对象(类锁)。

加synchronized关键字之后不一定能实现线程安全,具体还要看锁定的对象是否唯一。

下面举几个例子来说明

@Slf4j(topic = "s")
public class Demo1 {

    private int count = 10;
    private Object object = new Object();
    public void test(){
        synchronized (object){
            //临界区
            count--;
            log.debug("count = " + count);
        }
    }
}
@Slf4j(topic = "s")
public class Demo2 {

    private int count = 10;

    public void test(){
        /**
         * synchronized(this)锁定的是当前类的实例,这里锁定的是Demo2类的实例
         */
        synchronized (this){
            count--;
            log.debug("count = " + count);
        }
    }
}

demo1和demo2 锁的都是实例对象,区别在于demo1 锁的是调用test方法的对象内的一个属性object对象。

而demo2锁的是调用test方法的对象本身注意只有非静态方法才可以用demo2的写法。

@Slf4j(topic = "s")
public class Demo3 {
    private int count = 10;

    //直接加在方法声明上,相当于是synchronized(this)
    public synchronized void test(){
        count--;
        log.debug("count = " + count);
    }
}

Demo3 直接将 synchronized作为方法的修饰符,效果相当于demo2。

但是区别是直接加在方法上,该方法可以是静态方法,若是静态方法的话,那么它锁的其实就是类对象,也就是类锁。例如下面的Demo4

@Slf4j(topic = "s")
public class Demo4 {

    private static int count = 10;

    //synchronized 关键字修饰静态方法锁定的是类的对象
    public synchronized static void test(){
        count--;
        log.debug("count =" + count);
    }

    //与上面的效果相同
    public static void test2(){
        synchronized (Demo4.class){//这里不能替换成this
            count--;
        }
    }
}

3.锁对象的属性改变和锁对象改变对于锁的使用的影响

锁定某对象o,如果o的属性发生改变,不影响锁的使用。但是如果o变成另外一个对象,则锁定的对象发生改变,此时就会影响锁的使用。应该避免将锁定对象的引用变成另外一个对象。

下面举个例子

@Slf4j(topic = "s")
public class Demo1 {


    O o = new O();

    public void test(){
        synchronized (o) {
            //这里无限执行
            while (true) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("xxxxxxx");
            }
        }
    }

    public static void main(String[] args) {
        Demo1 demo = new Demo1();

        new Thread(demo :: test, "t1").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Thread t2 = new Thread(demo :: test, "t2");

        //锁对象改变
//        demo.o = new O();
        //锁对象的属性改变
        demo.o.num = 1;
        //t2能否执行?
        t2.start();
    }

    public static class O{
        int num = 0;
    }

}

程序比较简单,线程t1 先启动,获得到了锁执行打印,线程t2 启动后,无法获取锁,尽管此时修改了锁对象的属性num,对于锁的使用也是没有影响的。控制台输出结果如下:

在这里插入图片描述

只有线程t1 在打印东西。

若此时将代码改成如下

@Slf4j(topic = "s")
public class Demo1 {


    O o = new O();

    public void test(){
        synchronized (o) {
            //这里无限执行
            while (true) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("xxxxxxx");
            }
        }
    }

    public static void main(String[] args) {
        Demo1 demo = new Demo1();

        new Thread(demo :: test, "t1").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Thread t2 = new Thread(demo :: test, "t2");

        //锁对象改变
        demo.o = new O();
        //锁对象的属性改变
//        demo.o.num = 1;
        //t2能否执行?
        t2.start();
    }

    public static class O{
        int num = 0;
    }

}

此时t1线程和t2线程都在执行打印,因为他们现在锁的对象不是同一个,所以不存在竞争,都可以执行。

在这里插入图片描述

4.同步方法和非同步方法是否可以同时调用?

答:可以

例如下面的例子,同步方法获取到锁,并不会影响其他非同步方法的使用。

@Slf4j(topic = "s")
public class Demo {

    public synchronized void test1(){
        log.debug(" test1 start...");
        try {
            //睡眠5s 由于还要t2要执行 cpu回去执行t2
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug(" test1 end...");
    }

    public void test2(){
        log.debug(" test2 start...");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug(" test2 end...");
    }

    public static void main(String[] args) {
        Demo demo = new Demo();
        //正在执行一个同步方法  没有释放锁
        new Thread(demo :: test1,"t1").start();
        //不影响其他线程执行非同步方法(就算他是一个同步方法,如果锁的不是同一个对象也不影响)
        new Thread(demo :: test2,"t2").start();
    }

}

控制台输出结果如下:

在这里插入图片描述

5.读方法需不需要加synchronized?

在set方法的时候,涉及到了共享资源的修改,所以需要加上synchronized。那读方法需要吗?

读方法是否需要加synchronized 首先取决于,这个方法内 存不存在线程安全的问题。若有线程安全的问题那么就要加锁。

然后再看具体的业务是否允许脏读,若不允许脏读,那么就需要加锁。

例如下面的代码

@Slf4j(topic = "s")
public class Demo {

    //卡的持有人  senlin
    String name;

    //卡上的余额  0
    double balance;

    public synchronized void set(String name,double balance){
        this.name = name;
        try {
            log.debug("set");
            //模拟存钱耗时  银行系统处理
            Thread.sleep(2000);
            log.debug("set end");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.balance = balance;
    }

    public double getBalance(String name){
        return this.balance;
    }

//    public synchronized double getBalance(String name){
//        return this.balance;
//    }

    @SneakyThrows
    public static void main(String[] args) {
        Demo demo = new Demo();
        //没有启动
        Thread zl = new Thread(() -> {
            log.debug("余额-{}", demo.getBalance("senlin"));
            try {
                TimeUnit.SECONDS.sleep(2);
            }catch (Exception e){
                e.printStackTrace();
            }

            log.debug("----余额-{}", demo.getBalance("senlin"));
        }, "senlin");

        //2s
        new Thread(() -> demo.set("senlin", 100.0), "yiyi").start();
        TimeUnit.SECONDS.sleep(1);
        zl.start();
    }

}

代码很简单,yiyi向senlin转了100块钱。与此同时senlin查账户,查了两次,出现了脏读。控制台结果如下:

在这里插入图片描述

若当前业务不允许脏读的出现。那么对读方法也需要加锁。

代码改成如下:

@Slf4j(topic = "s")
public class Demo {

    //卡的持有人  senlin
    String name;

    //卡上的余额  0
    double balance;

    public synchronized void set(String name,double balance){
        this.name = name;
        try {
            log.debug("set");
            //模拟存钱耗时  银行系统处理
            Thread.sleep(2000);
            log.debug("set end");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        /**
         *
         */
        this.balance = balance;
    }

//    public double getBalance(String name){
//        return this.balance;
//    }

    public synchronized double getBalance(String name){
        return this.balance;
    }

    @SneakyThrows
    public static void main(String[] args) {
        Demo demo = new Demo();
        //没有启动
        Thread zl = new Thread(() -> {
            log.debug("余额-{}", demo.getBalance("senlin"));
            try {
                TimeUnit.SECONDS.sleep(2);
            }catch (Exception e){
                e.printStackTrace();
            }

            log.debug("----余额-{}", demo.getBalance("senlin"));
        }, "senlin");

        //2s
        new Thread(() -> demo.set("senlin", 100.0), "yiyi").start();
        TimeUnit.SECONDS.sleep(1);
        zl.start();
    }

}

此时的就解决了脏读的问题,控制台输出结果如下:

在这里插入图片描述

6. synchronized是否支持可重入

答:支持可重入。

在一个同步方法里面调用另一个同步方法,可以正常的执行。

@Slf4j(topic = "s")
public class Demo {

    synchronized void test1() throws InterruptedException {
        log.debug("test1 start.........");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        test2();

        log.debug("test1 end.........");
    }


    synchronized void test2() throws InterruptedException {
        log.debug("test2 start.......");
        TimeUnit.SECONDS.sleep(1);
        log.debug("test2 end.......");
    }


    public static void main(String[] args) throws InterruptedException {
        Demo demo= new Demo();
        demo.test1();
    }
}

首先调用test1方法获取到了锁,test1方法内部调用了同步方法test2,此时可以正常执行,说明synchronized支持可重入。

控制台的结果如下:

在这里插入图片描述

7.synchronized可重入的另一种情况,继承

子类调用同步方法,同步方法内部调用父类的同步方法,可正常执行。

代码如下:

@Slf4j(topic = "s")
public class Demo {

    synchronized void test(){
        log.debug("demo test start........");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("demo test end........");
    }

    public static void main(String[] args) {
            new Demo2().test();
    }

}

@Slf4j(topic = "s")
class Demo2 extends Demo {

    @Override
    synchronized void test(){
        log.debug("demo2 test start........");
        super.test();
        log.debug("demo2 test end........");
    }

}

Demo2是子类,调用了同步方法test,此时锁定的对象就是Demo2实例,内部调用父类的同步方法test(),可正常执行,这是可重入的另一种形式。

控制台输出结果如下:在这里插入图片描述

8.synchronized同步方法内发生异常,是否会释放锁?

这要分两种情况,

  1. 若对异常进行了处理,则不会释放锁
  2. 若不处理异常,则会释放锁。

代码如下:

@Slf4j(topic = "s")
public class Demo {
    Object o = new Object();

    int count = 0;

     void test(){
         synchronized(o) {
             //t1进入并且启动
             log.debug("start......");
             //t1 会死循环 t1 讲道理不会释放锁
             while (true) {
                 count++;
                 log.debug(" count = {}", count);
                 try {
                     TimeUnit.SECONDS.sleep(1);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
               		//加5次之后 发生异常
                 if (count == 5) {
                     try {
                         int i = 1 / 0;
                     }catch (Exception e){
                        e.printStackTrace();
                     }

                 }
             }
         }
    }

    public static void main(String[] args) throws InterruptedException {
        Demo demo = new Demo();
      
        new Thread(()->{
            demo.test();
        },"t1").start();

        TimeUnit.MILLISECONDS.sleep(1);
        new Thread(()->{
            demo.test();
        }, "t2").start();
    }
}

上面的代码对异常进行了处理,此时不会释放锁,t2线程拿不到锁。控制台输出结果如下:

在这里插入图片描述

只有t1线程在执行。

若将代码改成如下,不处理异常

@Slf4j(topic = "s")
public class Demo {
    Object o = new Object();

    int count = 0;

    void test() {
        //
        synchronized (o) {
            //t1进入并且启动
            log.debug("start......");
            //t1 会死循环 t1 讲道理不会释放锁
            while (true) {
                count++;
                log.debug(" count = {}", count);
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //加5次之后 发生异常
                if (count == 5) {
                    int i = 1 / 0;
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Demo demo = new Demo();

        new Thread(() -> {
            demo.test();
        }, "t1").start();


        TimeUnit.MILLISECONDS.sleep(1);

        new Thread(() -> {
            demo.test();
        }, "t2").start();
    }
}

此时异常不处理,锁会释放掉,t1线程结束,t2获得到锁执行代码。在这里插入图片描述

<think>嗯,用户问的是Javasynchronized关键字如何保证原子性、有序性和可见性,需要了解实现原理。首先,我得回忆一下synchronized的基本机制。根据引用里的内容,synchronized通过monitorenter和monitorexit这两个字节码指令来实现锁的获取和释放。那原子性方面,引用4提到synchronized块之间的操作具备原子性,因为JVM的lock和unlock操作确保了代码块整体作为一个原子执行。 接下来是有序性。用户可能知道指令重排序会影响多线程的执行结果,而synchronized如何防止这个呢?我记得锁的互斥性会让线程串行执行,这样在同步块内的代码即使被重排序,也不会影响到其他线程看到的顺序,所以synchronized通过互斥来保证有序性,引用1也提到这点,把并发执行变成序列化。 然后是可见性。JMM中,当线程释放锁时,会把变量刷回主内存,获取锁时则会从主内存重新取,确保共享变量的可见性。引用4提到基础数据类型的原子性由JVM保证,而synchronized通过lock/unlock提供更大范围的原子性,这也涉及到可见性。 可能用户还关心底层实现,比如汇编层的lock指令,引用1里提到lock cmpxchg,这个指令会锁定总线或缓存行,确保操作的原子性,同时内存屏障防止指令重排。这部分需要结合JVM和硬件层面来解释。 另外,用户提供的引用中有同步方法和同步代码块的例子,说明不同使用方式锁的对象不同,比如类对象或实例对象,这可能影响到锁的范围,但核心原理应该是一样的。需要明确说明不同用法但原理相同。 还要注意,用户的问题可能涉及到对这三个性质的具体实现机制的区别,比如原子性是通过互斥,可见性是通过内存同步,有序性是通过禁止重排序。需要分点详细说明,确保回答结构清晰。 最后,生成的相关问题要围绕synchronized的实现细节、锁升级过程、与volatile的区别以及实际应用中的注意事项,这样能帮助用户深入理解。</think>### Javasynchronized关键字如何保证原子性、有序性和可见性? #### 1. **原子性** **定义**:原子性指一个操作是不可中断的,要么全部执行成功,要么完全不执行。 **实现原理**: - synchronized通过互斥锁(`monitorenter`和`monitorexit`字节码指令)保证同步代码块内的操作作为一个整体执行[^4]。 - 线程进入同步代码块前必须获取锁,其他线程无法同时执行该代码块,从而避免了竞态条件。例如: ```java public synchronized void increment() { count++; // 即使包含多个底层操作(-改-写),也能保证原子性 } ``` #### 2. **可见性** **定义**:一个线程修改共享变量后,其他线程能立即看到修改后的值。 **实现原理**: - 在释放锁时,JVM强制将工作内存中的变量刷新到主内存; - 在获取锁时,JVM会清空本地内存,从主内存重新载变量[^4]。 - 通过内存屏障(Memory Barrier)禁止指令重排序,确保共享变量的修改对所有线程可见[^1]。 #### 3. **有序性** **定义**:程序执行的顺序符合代码的先后顺序。 **实现原理**: - synchronized通过互斥锁将并发执行变为串行执行,相当于隐式禁止了同步代码块内的指令重排序[^1]。 - 例如以下代码中,`doSth1()`和`doSth2()`内部的代码不会被重排序到同步块外: ```java public void doSth1() { synchronized (this) { // 代码块内的操作不会被重排序到锁外 } } ``` --- ### 底层实现细节 1. **锁的升级过程**: synchronized在JVM中经历了从偏向锁→轻量级锁→重量级锁的升级过程,以适应不同竞争场景[^1][^3]。 2. **汇编层实现**: 最终通过`lock cmpxchg`(CAS指令)实现原子操作,该指令会锁定总线或缓存行,确保原子性和可见性[^1][^4]。 --- ### 示例代码分析 ```java public class SynchronizedTest { // 同步代码块(锁类对象) public void doSth1() { synchronized (SynchronizedTest.class) { // monitorenter System.out.println("HelloWorld"); } // monitorexit } // 同步方法(锁当前实例) public synchronized void doSth2() { // 方法标记ACC_SYNCHRONIZED System.out.println("HelloWorld"); } } ``` - 同步代码块:通过`monitorenter`和`monitorexit`显式控制锁的获取和释放[^2][^4]。 - 同步方法:通过方法访问标志`ACC_SYNCHRONIZED`隐式实现锁机制[^3]。 ---
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值