• Synchronized锁的是什么?


    Synchronized锁的是什么?

    临界区与锁

    并发编程中不可避免的会出现多个线程共享同一个资源的情况,为了防止出现数据不一致情况的发生,人们引入了临界区的概念。临界区是一个用来访问共享资源的代码块,同一时间内只运行一个线程进入。

    那么如何实现这个临界区呢?这就用到我们的锁了,当进程想要访问一个临界区时,它先会去看看是否已经有其他线程进入了,也就是看是否能获得锁。如果没有其他线程进入,那么它就进入临界区,其他线程就无法进入,相当于加锁。反之,则会被挂起,处于等待状态,直到其他线程离开临界区,且本线程被JVM选中才可进入(因为可能有其他线程也在等待)。

    利用Synchronized解决并发问题

    Synchronize是一个重量级锁,它会降低程序性能,因此如果对数据一致性没有要求,就不要使用它。如果方法被Synchronize关键字声明,那么该方法的代码块被视为临界区。当某个线程调用该对象的synchronized方法或者访问synchronized代码块时,这个线程便获得了该对象的锁,其他线程暂时无法访问这个方法,只有等待这个方法执行完毕或者代码块执行完毕,这个线程才会释放该对象的锁,其他线程才能执行这个方法或者代码块。

    下面我们将创建两个线程A,B来同时访问一个对象:A从账户里取钱,B从账户里存钱。首先是不使用Synchronized关键字。

    创建账户类

    它拥有一个私有变量balance表示金额,addAmount和subtractAmount分别对金额执行加减操作。

    public class Account {
        private double balance;
    
        public double getBalance() {
            return balance;
        }
    
        public void setBalance(double balance) {
            this.balance = balance;
        }
    
        public void addAmount(double amount){
            System.out.println("addAmount start");
            double temp=balance;
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            temp+=amount;
            balance=temp;
            System.out.println("addAmount end");
        }
    
        public void subtractAmount(double amount){
            System.out.println("subtractAmount start");
            double temp=balance;
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            temp-=amount;
            balance=temp;
            System.out.println("subtractAmount end");
        }
    }
    

    创建A,B俩线程,分别对账户存钱和取钱。

    public class A implements Runnable {
        private Account account;
        public A(Account account){
            this.account=account;
        }
        @Override
        public void run() {
            for(int i=0;i<10;i++){
                account.addAmount(1000);
            }
        }
    }
    
    public class B implements Runnable  {
        private Account account;
        public B(Account account){
            this.account=account;
        }
        @Override
        public void run() {
            for(int i=0;i<10;i++){
                account.subtractAmount(1000);
            }
        }
    }
    

    最后在main里面测试

    public class Main {
        public static void main(String[] args) {
            Account account=new Account();
            account.setBalance(1000);
            A a=new A(account);
            Thread ThreadA=new Thread(a);
            B b=new B(account);
            Thread ThreadB=new Thread(b);
            System.out.println("Account Balance:"+account.getBalance());
            ThreadA.start();
            ThreadB.start();
            try {
                ThreadA.join();
                ThreadB.join();
                System.out.println("Account Balance:"+account.getBalance());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    

    ThreadA往账户中执行了10次存入操作,每次存入1000元,ThreadB则是以同样的金额执行了10次取出操作。那么按照我们的推测,最后账户的金额应该维持不变,但程序的结果却不是我们想要的数字。这是为什么呢?因为我们在对数据进行操作的时候,另外一个线程可能也在进行操作,逻辑上应该先后执行的方法变成了同时执行,所以出现了错误。

    现在我们给addAmount和subtractAmount加上synchronized关键字,保证数据一致性,这样程序就不会出问题了。

    如果是使用synchronize保护代码块,则需要将对象引用作为参数传入。一般来说传入this关键字作为引用执行方法的对象就可以了。

    锁的到底是什么?

    或许在上面的例子你因为粗心只为其中一个方法加了关键字,那么你会看到这样的现象:

    保护代码块要将对象传入,那应该锁的是对象呀。你可能会想:我执行subtractAmout,按道理应该等我执行完addAmount才能执行,它都没有account这个对象的锁,不应该在中间插这么一段呀。但是,只有加了锁的方法,线程执行该方法时才会去尝试获得锁,看看是否有线程进入临界区。访问非同步方法无需获得锁,你把synchronized去掉跟你只加一个的情况是一样的,同步方法与非同步遵循的是不同的规则。也就是说你可以在调用该对象的加了synchronized方法的同时,调用其他的非同步方法。

    两个线程怎么同时访问了同一个对象的两个synchronized方法?

    你可能在捣鼓这个关键字的时候,惊讶的发现静态方法的与众不同。如果一个对象中的静态方法用synchronized修饰,那么其他线程可以在该静态方法被访问的同时,访问该对象中的非静态方法(当然,该静态方法同一时间只能被一个线程访问)。换句话说,两个线程可以同时访问一个对象中的两个synchronized方法。

    等等,不是说锁对象吗?到底锁的是什么?锁的确实是对象,但对于静态方法我们说的是T.class(T 为类名),非静态方法锁的是this ,也就是类的实例对象,两者是不同的。

    class T {
      // 修饰非静态方法
      public synchronized void a() {
        // 临界区
      }
      // 修饰静态方法
      public synchronized static void b() {
        // 临界区
      }
    }  
    

    上面那段代码相当于:

    class T {
      // 修饰非静态方法
      public synchronized(this) void a() {
        // 临界区
      }
      // 修饰静态方法
      public synchronized(T.class) static void b() {
        // 临界区
      }
    }  
    

    实际上加锁本质就是在锁对象的对象头中写入当前线程id。我们可以通过下面的代码验证,每次都传入new Object()。

    class Account {
        private double balance;
        public synchronized void addAmount(double amount){
            synchronized (new Object()){
                System.out.println("addAmount start");
                double temp=balance;
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                temp+=amount;
                balance=temp;
                System.out.println("addAmount end");
            }
        }
        public void subtractAmount(double amount){
        	synchronized (new Object()){
                System.out.println("subtractAmount start");
                double temp=balance;
                try {
                    Thread.sleep(100); 
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                temp-=amount;
                balance=temp;
                System.out.println("subtractAmount end");
            }
        }
    }
    

    因为线程每次调用方法锁的都是新new的对象,所以加锁无效。甚至编译器可能会将synchronized给优化掉,因为这相当于多把锁保护同一个资源,编译器一看,每个人都弄把锁就进来了,那我还不如不加,反正都一个样。

    另外需要注意的是,synchronized是可重入锁。也就是说当线程访问对象的同步方法时,在调用其他同步方法时无需再去获取其访问权。因为我们实际上锁的是对象,对象头里面纪录的都是当前线程的ID。

    总结

    • 修饰函数,锁的是当前类的实例化对象
    • 修饰静态方法,锁的是当前类的Class对象
    • 修饰同步代码块,锁的是括号里的对象

    加锁实际上就是在锁对象的对象头中写入当前线程id,每个线程要想调用这个同步方法,都会先去锁对象的对象头看看当前线程id是不是自己的。

    参考

    synchronized锁定的到底是什么?-知乎

  • 相关阅读:
    创建商品APP
    商品模块表结构分析
    sprintf 和 fprintf
    linux中sys目录
    linux中proc目录
    ioctl()函数
    ffmpeg下载安装
    【转】写给小白的实时音视频技术入门提纲
    linux常见目录解释
    linux nfs客户端开启失败解决办法
  • 原文地址:https://www.cnblogs.com/AD-milk/p/13253972.html
Copyright © 2020-2023  润新知