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获得到锁执行代码。在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/gongsenlin341/article/details/111526661