Java多线程之Synchornized定义,底层实现原理,以及锁对象

synchornized关键字

首先我们先了解一下什么是synchornized,及synchornized的作用

我们先看一个示例 ,假如两个线程同时都对n进行自加操作

package Synchornized;

public class TestSynchornized0 {
    int n = 0;
    public void func(){
        n++;
    }
    public static void main(String[] args) throws InterruptedException {
        TestSynchornized0 testSynchornized0 = new TestSynchornized0();
        Thread t1 = new Thread(){
            @Override
            public void run() {
                for (int i = 0; i<5000; i++) {
                    testSynchornized0.func();     //线程1让n自加5000次
                }
            }
        };
        Thread t2 = new Thread(){
            @Override
            public void run() {
                for (int i = 0; i<5000; i++) {
                    testSynchornized0.func();   //线程2也让n自加50000次
                }
            }
        };
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(testSynchornized1.n); //第一次输出结果是9865
    }                                            //第二次输出结果是8520
}

我们的预期结果n的值应该是10000。但是结果确比10000少,且结果不确定,第一次执行是9865,第二次结果是8520。其实问题就出在两个线程同时修改一个变量,会造成线程不安全(比如n=0,两次线程同时从内存获取到n,分别进行自加后,两个线程的n就都是1,然后把n写入到内存,照我们的预想应该是2,但是结果是1,从结果来看就是少加了一次)。synchornized其实就是为了解决并发编程而诞生的产物,它可以让一个对象的一段代码,或者一个方法,同一时间只能有一个线程执行,另一个线程如果也要执行的话,就会阻塞在那,需要等正在执行的线程执行完了再执行。我们在下边的synchornized使用中会解决上边这个例子存在的问题

synchronized解决了可见性是因为synchronized每次加锁释放锁都会刷新工作内存,将更新完的数据写回到主内存中,然后从高内存中重新读取最新的数据

synchronized是Java提供的一个并发控制的关键字,作用于对象上。主要有两种用法,分别是同步方法(访问对象和clss对象)和同步代码块(需要加入对象),保证了代码的原子性和可见性以及有序性,但是不会处理重排序以及代码优化的过程,但是在一个线程中执行肯定是有序的,因此是有序的。

synchornized又被成为同步锁或者监视器锁

synchornized实现的底层原理

synchronized的底层是使用操作系统的mutex lock实现的。
当线程释放锁时,JMM会把该线程对应的工作内存中的共享变量刷新到主内存中
当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量,这样保证了内存的可见性
synchronized用的锁是存在Java对象里面的
引自比特科技
synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入,这样保证了操作的原子性

synchornized锁对象

其实synchornized同步锁/监视器锁中重点在于要搞清楚锁的对象,接下来我们分别看一下分别锁不同的对象的区别

synchornized修饰方法(对象锁)

  • synchornized修饰普通方法

synchornized如果用来修饰方法,就会让整个方法变成同步代码块。它锁的对象就是调用当前方法的对象,即两个线程调同一个对象的这个方法,就会造成线程阻塞。如果两个线程分别同时调用两个对象的方法,就不会造成阻塞,因为synchornized锁的是调用方法的对象。

package Synchornized;

import java.util.TreeMap;

public class TestSynchornized2 {
    public synchronized void func() {
        for (int i = 0; i < 10; i++) {
            System.out.print(i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        TestSynchornized2 testSynchornized2 = new TestSynchornized2();
        Thread t1 = new Thread(){
            @Override
            public void run() { testSynchornized2.func(); }
        };
        Thread t2 = new Thread(){
            @Override
            public void run() { testSynchornized2.func(); }
        };
        t1.start(); t2.start();
        t1.join();  t2.join();
    }
}

调用一个对象的func方法程序执行结果
在这里插入图片描述
当我们把main函数改一下,改成两个线程调用两个对象的func方法

public static void main(String[] args) throws InterruptedException {
    TestSynchornized2 ts1 = new TestSynchornized2();
    TestSynchornized2 ts2 = new TestSynchornized2();
    Thread t1 = new Thread(){
        @Override
        public void run() { ts1.func(); }
    };
    Thread t2 = new Thread(){
        @Override
        public void run() { ts2.func(); }
    };
    t1.start(); t2.start();
    t1.join(); t2.join();
}

调用两个对象的func方法程序执行结果
在这里插入图片描述

从结果对比我们可以看出来,第一次成功让线程阻塞,第二次确还是同步进行,为什么第二次没有阻塞呢?因为synchornized锁的是两个对象。执行的不是同一个方法,是两个对象的两个方法。由此可见,synchornized只能锁当前调用方法的对象。

  • synchornized修饰静态方法
public class TestSynchornized5 {
    public static synchronized void func() {
        for (int i = 0; i < 10; i++) {
            System.out.print(i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        TestSynchornized5 testSynchornized2 = new TestSynchornized5();
        Thread t1 = new Thread(){
            @Override
            public void run() { testSynchornized2.func(); }
        };
        Thread t2 = new Thread(){
            @Override
            public void run() { testSynchornized2.func(); }
        };
        t1.start(); t2.start();
        t1.join();  t2.join();
    }
}

我们用两个对象去测试的时候

 public static void main(String[] args) throws InterruptedException {
        TestSynchornized5 testSynchornized1 = new TestSynchornized5();
        TestSynchornized5 testSynchornized2 = new TestSynchornized5();
        Thread t1 = new Thread(){
            @Override
            public void run() { testSynchornized1.func(); }
        };
        Thread t2 = new Thread(){
            @Override
            public void run() { testSynchornized2.func(); }
        };
        t1.start(); t2.start();
        t1.join();  t2.join();
    }
}

在这里插入图片描述
不管我们写一个对象还是两个对象,线程执行的收都出现了阻塞,其实当我们用synchornized修饰静态方法的时候,因为类的静态属性只有一个,所以就相当于我们锁了所有的实例对象,这和我们后边说的锁类对象是一样的。

synchornized修饰代码块(对象锁)

synchornized锁代码块相比较于锁方法更加灵活方 便,因为代码块的范围可以自己控制,而锁方法是锁的整个方法,没法控制范围

锁(this)对象

其实synchornized锁this对象和锁方法是一样的,因为this就是当前对象。

package Synchornized;

public class TestSynchornized3 {
    public  void func() {
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                System.out.print(i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        TestSynchornized3 testSynchornized3 = new TestSynchornized3();
        Thread t1 = new Thread(){
            @Override
            public void run() { testSynchornized3.func(); }
        };
        Thread t2 = new Thread(){
            @Override
            public void run() { testSynchornized3.func(); }
        };
        t1.start(); t2.start();
        t1.join();  t2.join();
    }
}

在这里插入图片描述
我们可以看到结果是两个线程执行的时候出现了阻塞,和synchornized修饰方法的时候一样。
当我们把main函数改一下,改成两个线程调用两个对象的func方法
在这里插入图片描述
在这里插入图片描述
我们可以看我们两次运行的结果和修饰方法的时候一样,这也印证了我们之前的结论,synchornized修饰方法和修饰代码块this对象的效果是一样的。

不管是修饰一个普通方法还是修饰代码块的this对象,就好比工厂机器削苹果,假如给机器1和机器2都分配了一箱苹果,我们把方法上锁,就好比给每箱苹果都加了锁,一箱苹果同一时间只能由一个机器来削他,假如工人忘了给机器2放苹果,机器2想削机器1的苹果就得等机器1削完累了,然后再削。但是如果机器2分配了苹果,那两个机器就可以同时运转。因为两个机器削的是两箱苹果,互不干扰。

锁具体对象

当我们不想把被锁对象写的范围太大,比如this啊,我们就可以去锁一个具体对象

public class TestSynchornized4 {

    public  void func(TestSynchornized4 test) {    //传入要锁的对象
        synchronized (test) {
            for (int i = 0; i < 10; i++) {
                System.out.print(i);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        TestSynchornized4 test1 = new TestSynchornized4();
        TestSynchornized4 test2 = new TestSynchornized4();
        Thread t1 = new Thread(){
            @Override
            public void run() { test2.func(test1); } //第一个线程我们锁test1
        };
        t1.start();
        Thread t2 = new Thread(){
            @Override
            public void run() { test2.func(test2); } //第一个线程我们锁test2
        };
        t2.start();
        t1.join(); t2.join();
    }
}

在这里插入图片描述
我们可以看到运行结果是两个进程同步进行,同一个对象调用同一个方法,为什么没有阻塞串行执行呢?就是因为我们两个传的参数不一样,锁的对象也不一样,所以就不会造成线程阻塞。依然是并行执行。当我们确定了被锁对象,我们就可以这么写

class Test 
{
   public void method( Object lock)   
   {
      synchronized(lock) {
         // 同步代码块
      }
   }

还有一种情况就是我们不确定锁的对象,但是想让一段代码同步,那我们就可以这么写

class Test 
{
   private Object lock = new Object; 
   public void method()
   {
      synchronized(lock) {
         // 同步代码块
      }
   }

我们还是用削苹果来举例子,修饰具体对象的时候就相当于给某一箱特定的苹果加上锁,机器削其他箱苹果都可以几个机器同时削,但是当削到这一箱特殊的苹果的时候,就不行了,一个时间段只能只能有一个机器来削这箱苹果。

锁类对象

当我们锁类对象的时候,是这么写的

public class TestSynchornized6 {
    public  void func() {
        synchronized (TestSynchornized6.class) {
            for (int i = 0; i < 10; i++) {
                System.out.print(i);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

我们用和锁this对象一样的测试用例,结果不管是一个对象,还是两个对象,测试结果都是线程阻塞
在这里插入图片描述
其实我们用synchornized修饰静态方法,和修饰代码块(“类名.class”)是一样的效果,因为类对象和静态方法一样都是只有一个,所以不管哪个实例对象调用,调用的都是同一个方法,所以会造成线程阻塞。

这就相当于我们给工厂的存放苹果的仓库加了一个锁,不管有几台机器,只要这个机器削苹果,那就得排着队一个来。

总结
在这里插入图片描述
另外,还有一个需要注意的点就是,我们用synchornized修饰一个方法的时候,一个线程正在访问被修饰方法的时候,另一个线程依然可以调用这个对象没有被synchornized修饰的方法

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值