多线程之synchronized基础

Synchronized简介

synchronized 关键字,代表这个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B(或者C 、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。它包括两种用法:synchronized 方法和 synchronized 块。

以上来自百度结果,好啰嗦,接下来简单阐述。

作用:能够保证在 同一时刻 最多只有 一个线程 执行该段代码,已达到保证并发安全的效果。

地位:是 Java 的关键字,被 Java 语言原生支持;是最基本的互斥同步手段;是并发编程中的元老级角色,是并发编程的必学内容。

不使用并发手段的后果:
经典例子:两个线程同时进行对 a 进行 a++,在给定一定范围循环,理论来说 a 的结果是确定的(循环次数相同,a++ 次数相同,一直累加即可)。 但实际上 a 的值会比预计的小很多,因为可能两个线程同时拿到锁,即同时拿到 a 对其进行操作,导致结果出错。
使用 synchronized 可以解决。

Synchronized 用法

1.对象锁
包括 方法锁 (默认锁对象为 this 当前实例对象)和 同步代码块锁(自己指定锁对象)

代码块形式:手动指定锁对象

    public void test1()  {   
         synchronized(this)  {   
              int i = 5;   
              while( i-- > 0)  {   
                   System.out.println(Thread.currentThread().getName() + " : " + i);   
                   try  {   
                        Thread.sleep(500);   
                   }  
                   catch (InterruptedException ie) {   
                   }   
              }   
         }   
    }   

方法锁形式:synchronized 修饰普通方法,锁对象默认为 this

 public synchronized void test2()  {   
         int i = 5;   
         while( i-- > 0)  {   
              System.out.println(Thread.currentThread().getName() + " : " + i);   
              try  {   
                   Thread.sleep(500);   
              }  
              catch (InterruptedException ie)  {   
              }   
         }   
    } 

main方法

public static void main(String[] args)  
    {   
         final TestSynchronized myt2 = new TestSynchronized();   
         Thread test1 = new Thread(  new Runnable() {  public void run() {  myt2.test1();  }  }, "test1"  );   
         Thread test2 = new Thread(  new Runnable() {  public void run() { myt2.test2();   }  }, "test2"  );   
         test1.start();;   
         test2.start();   
    }

2.类锁
指 Synchronized 修饰 静态 的方法或指定锁为 Class对象

O_o Java类可能有很多个对象,但只有一个 Class对象
本质:所谓的类锁,是 Class对象的锁

形式1:synchronized 加在 static 方法上

    public static synchronized void test1()  {   
         int i = 5;   
         while( i-- > 0)  {   
              System.out.println(Thread.currentThread().getName() + " : " + i);   
              try  {   
                   Thread.sleep(500);   
              }  
              catch (InterruptedException ie)  {   
              }   
         }   
    }
O^O注意不能给 run() 方法加 static,应该在 run() 中再调用另外写的 static 方法

形式2:synchronized(.class) 代码块**

public void test2()  {   
         synchronized(TestSynchronized.class)  {   
              int i = 5;   
              while( i-- > 0)  {   
                   System.out.println(Thread.currentThread().getName() + " : " + i);   
                   try  {   
                        Thread.sleep(500);   
                   }  
                   catch (InterruptedException ie)  {   
                   }   
              }   
         }   
    }

main方法

 public static void main(String[] args)  {   
         final TestSynchronized myt2 = new TestSynchronized();   
         Thread test1 = new Thread(  new Runnable() {  public void run() {  myt2.test1();  }  }, "test1"  );   
         Thread test2 = new Thread(  new Runnable() {  public void run() { TestSynchronized.test2();   }  }, "test2"  );   
         test1.start();   
         test2.start();   
    }  

多线程访问同步方法的 7 种情况

1.两个线程同时访问一个对象的同步方法

竞争争抢锁,相互等待,不能同时持有

2.两个线程访问的是两个对象的同步方法

并行执行,互不影响拥有的锁对象不是同一个

3.两个线程访问的是 synchronized 的静态方法

静态方法对应的锁对象同一个 类锁,所以需要相互等待

4.同时访问同步方法与非同步方法

同时开始,同时结束。synchronized 关键字只影响被修饰的方法,而不会去影响 非同步方法。(两个线程访问不同方法)

5.访问同一个对象不同的普通同步方法

持有的是同一把锁,所以需要相互等待

6.同时访问静态synchronized 和 非静态synchronized 方法(复杂)

持有锁对象不同。一个是类锁,一个是对象锁。所以互不影响,各自运行

7.方法抛出异常后,会释放锁

问题:如果调用了一个 synchronized 方法,里面又调用了一个非 synchronized 方法,安全吗?

答:不安全,因为该非同步方法可能会被不同线程调用,当某一线程在调用时可能另一线程恰巧也在调用,可能造成操作失败数据混乱。

Synchronized性质

可重入性质

指同一线程的外层函数获得锁之后,内层函数可以直接再次获取该锁

好处:避免死锁,提升封装性
粒度:线程而非调用

情况1:同一个方法是可重入的

//可重入粒度测试:递归调用本方法
    static class SynchronizedRecurision{
        int a = 0;
        public static void main(String[] args) {
            SynchronizedRecurision s = new SynchronizedRecurision();
            s.method1();
        }
        
        private synchronized void method1(){
            System.out.println("这是method1,a="+a);
            if(a==0){
                a++;
                method1();
            }
        }

情况2:可重入不要求是同一个方法

//可重入粒度测试:调用不同方法
        static class SynchronizedOtherMethod{
            public static void main(String[] args) {
                SynchronizedOtherMethod s = new SynchronizedOtherMethod();
                s.method1();
            }
            public synchronized void method1(){
                System.out.println("我是 method1");
                method2();
            }
            public synchronized void method2(){
                System.out.println("我是 method2");
            }
        }

情况3:可重入不要求是在同一个类中

//可重入粒度测试:调用其他类的方法
        static class SynchronizedSuperClass{
            public synchronized void doSomething(){
                System.out.println("我是父类方法");
            }
            static class TestClass extends SynchronizedSuperClass{
                @Override
                public synchronized void doSomething(){
                    System.out.println("我是子类方法");
                    super.doSomething();
                }
            }

            public static void main(String[] args) {
                TestClass t = new TestClass();
                t.doSomething();
            }
        }

总结:只要调用的方法他所需要的锁是当前拥有的锁,就可以直接使用。

不可中断性质

一旦这个锁已经被别人获得,如果我还想获得,那我只能选择等待或者阻塞,直到其他线程释放这个锁。如果其他线程永远不释放锁,那么我只能永远等待。
(相比之下 Lock 类更灵活,拥有中断的能力。如果觉得等待时间过长,有权中断现在已经获取到锁的线程的执行;或者当等待时间太长,不想等待可以主动选择退出。)

加锁、释放锁原理

以下两个方法等价

在这里插入图片描述

可重入原理:加锁次数计数器

JVM负责跟踪对象被加锁的次数
线程第一次给对象加锁的时候,计数变为1。每当这个相同的线程在此对象上再次获得锁时,计数会递增;每当任务离开时,计数递减,当计数减为0时,锁被完全释放。

保持可见性原理:内存模型

在这里插入图片描述

Synchronized的缺陷

效率低:锁的释放情况少、试图获得锁时不能设定超时、不能中断应该正在试图获得锁的线程。

不够灵活(读写锁更灵活):加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的。

无法知道是否成功获取到锁。

相比之下,Lock 类功能更加强大:
lock(), unlock(), tryLock/tryLock(10,TimeUnit.SECONDS)设置超时时间,等等方法

Synchronized 与 ReentrantLoc的区别

ReentrantLock(可重入锁)

  • synchronized是独占锁,加锁和解锁的过程自动进行,易于操作,但不够灵活。ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活

  • synchronized可重入,因为加锁和解锁自动进行,不必担心最后是否释放锁;
    ReentrantLock也可重入,但加锁和解锁需要手动进行,且次数需一样,否则其他线程无法获得锁。

  • synchronized不可响应中断,一个线程获取不到锁就一直等着;
    ReentrantLock可以响应中断

  • ReentrantLock还可以实现公平锁机制。
    就是在锁上等待时间最长的线程将获得锁的使用权。通俗的理解就是谁排队时间最长谁先执行获取锁。

常见问题

1.使用注意点:锁对象不能为空、作用域不宜过大,避免死锁;

2.如何选择 Lock 和 Synchronized 关键字?
尽量避免使用,如果有现成的工具包就使用工具包;
若没有,就使用 synchronized,减少代码编写,减少出错;
实在需要 Lock,再用 Lock(加解锁机制)。

3.多线程访问同步方法的各种具体情况
前面提到的 7 种情况。

4.多个线程等待同一个 synchronized锁时,JVM如何选择下一个获取锁的是哪个线程
内部锁调度机制,一个持有锁的线程释放锁之后,除了事先等待的线程,还有恰好刚申请锁的线程(还没进入 Block状态),JVM 的选择是随机、不可控制的。

5.Synchronized 使得同时只有一个线程可以执行,性能较差,有什么办法可以提升性能
优化使用范围(synchronized 的修饰范围);
使用 Lock。

6.想更灵活地控制锁的获取和释放(现在释放锁的时机都被规定死了)应该如何做
自己实现一个 Lock 接口,编写对应的功能实现方法。

Synchronized 总结

JVM 会自动通过使用 monitor 来加锁和解锁,保证了同一时间只有一个线程可以执行指定代码,从而保证了线程安全,同时具有可重入和不可中断的性质。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值