Java 并发 之 Synchronized关键字

Synchronized简介

官方解释:

Synchronized keyword enable a simple strategy for preventing thread interference and memory consistency errors: if an object is visible to more than one thread, all reads or writes to that object’s variables ard done through synchronized methods.
上述解释的意思是:synchronized关键字可以实现一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象对多个线程是可见的,那么对该对象的所有读或者写都将通过同步的方式来进行。

总结就一句话:
    能够保证在同一时刻最多只有一个线程执行该段代码,以保证程序并发安全性。

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

不使用并发手段是什么后果?

Demo

public class DisappearRequest1 implements Runnable {
    static DisappearRequest1 instance = new DisappearRequest1();
    static int count =0;
    public static void main(String[] args) throws InterruptedException {
    	//2个 线程 同时去操作 一个对象
        Thread t1 =new Thread(instance);
        Thread t2 =new Thread(instance);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
    public void run() {
        for(int j=0;j<100000;j++){
            count++;
        }
    }
}

上面的代码在这里插入图片描述
在这里插入图片描述
每次两个线程去执行 count++ 返回的结果每次都不一样 这样在实际生产环境中毫无疑问 是不可行的。
原因是什么呢:
count ++ ,它看上去只是一个操作,但实际上包含了三个动作:

  1. 读取count
  2. 将count 加一
  3. 将count的值写到内存中
    然而在多线程中 每一步 操作同时可能有其他线程也在执行操作,导致 这三步 没有按照顺序走完。

Synchronized 的两个用法

对象锁

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

类锁

指synchronized修饰静态的方法或指定锁为Class对象

第一个用法:对象锁

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

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

对象锁石栗:

public class SynchronizedObjectCodeBlock implements Runnable {
    static SynchronizedObjectCodeBlock instance = new SynchronizedObjectCodeBlock();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 =new Thread(instance);
        Thread t2 =new Thread(instance);
        t1.start();
        t2.start();
        //在 t1 t2 结束前保持主线程运行
        while(t1.isAlive() || t2.isAlive()){
        }
    }
    public void run() {
        //当前对象作为锁
        synchronized (this){
            System.out.println("对象锁代码块形式,name" + Thread.currentThread().getName());
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "运行结束");
        }

    }
}

程序保持串行运行 执行了第一个线程 后才开始执行第二个线程。
在这里插入图片描述
前面锁对象是当前对象 this 此时如果 用其他对象作为锁,效果是一样的

	//new 一个对象作为锁
    Object ob = new Object();
    //将锁设置为这个对象
    synchronized (ob)
为什么会用到 其他对象作为锁?

由于业务需要可能需要 2把锁。

public class SynchronizedObjectCodeBlock implements Runnable {
    static SynchronizedObjectCodeBlock instance = new SynchronizedObjectCodeBlock();
    Object ob1 = new Object();
    Object ob2 = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 =new Thread(instance);
        Thread t2 =new Thread(instance);
        t1.start();
        t2.start();
        //在 t1 t2 结束前保持主线程运行
        while(t1.isAlive() || t2.isAlive()){
        }
    }
    public void run() {
        //当前对象作为锁
        synchronized (ob1){
            System.out.println("lock1 对象锁代码块形式,name" + Thread.currentThread().getName());
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "lock1 运行结束");
        }

        //当前对象作为锁
        synchronized (ob2){
            System.out.println("lock2 对象锁代码块形式,name" + Thread.currentThread().getName());
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "lock2 运行结束");
        }

    }
}

在这里插入图片描述
2把锁 时 并行执行的代码 都是先执行第一个线程再去执行第二个线程。

IDEA中的多线程调试

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
此时可以看到线程在生命周期中处于哪一种状态。

Synchronized 修饰 普通方法

public class SynchronizedObjectMethod implements Runnable {
    static SynchronizedObjectMethod instance = new SynchronizedObjectMethod();
    Object ob1 = new Object();
    Object ob2 = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 =new Thread(instance);
        Thread t2 =new Thread(instance);
        t1.start();
        t2.start();
        //在 t1 t2 结束前保持主线程运行
        while(t1.isAlive() || t2.isAlive()){
        }
        System.out.println("finished");
    }
    public void run() {
        method();
        //当前对象作为锁

    }
    //修饰普通方法
    public synchronized void method(){
        System.out.println("我的对象锁的党发修饰符形式,我叫" +Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

在这里插入图片描述

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

第二个用法:类锁

  • 概念(重要):Java类可能有多个对象,但只有一个Class对象
    -本质:其实和对象锁一样但对象锁是一个实例 可以有多个实例 但类锁这个对象 只能有一个 同一时刻。总而言之 就是对象锁 可能会有多个 但类锁 一个class 就职于一个锁。
  • 形式1:synchronized加上static方法上
public class SynchronizedStaticMethod implements  Runnable {
    static SynchronizedStaticMethod instatce1 = new SynchronizedStaticMethod();
    static SynchronizedStaticMethod instatce2 = new SynchronizedStaticMethod();

	// 看这里
	// 如果不加satitc name 上面初始化的是2个实例 name结果就是并行 输出
    public  synchronized void method(){
        System.out.println("我是锁的第一种形式:static形式 call me" +Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() +"运行结束");
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 =new Thread(instatce1);
        Thread t2 =new Thread(instatce2);
        t1.start();
        t2.start();
        //在 t1 t2 结束前保持主线程运行
        while(t1.isAlive() || t2.isAlive()){
        }
        System.out.println("finished");
    }

    public void run() {
        method();
    }

synchronized方法 没加static 修饰符 并行执行
在这里插入图片描述

	//加上static修饰符
    public static   synchronized void method()

在这里插入图片描述
结果就是串行运行了,在全局情况下 需要同步这个方法的话就应该使用static 处理。

  • 形式2:synchronized(*.class)代码块
public class SynchronizedClassClas implements Runnable {
    static SynchronizedStaticMethod instatce1 = new SynchronizedStaticMethod();
    static SynchronizedStaticMethod instatce2 = new SynchronizedStaticMethod();

    public   void method(){
        synchronized (SynchronizedClassClas.class){
            System.out.println("我是锁的第二种形式:(.class) call me" +Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() +"运行结束");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 =new Thread(instatce1);
        Thread t2 =new Thread(instatce2);
        t1.start();
        t2.start();
        //在 t1 t2 结束前保持主线程运行
        while(t1.isAlive() || t2.isAlive()){
        }
        System.out.println("finished");
    }

    public void run() {
        method();
    }
}

结果如上不同实例 也会串行运行。

使用synchronized解决Count++

那么讲了那么多 就让我们来解决 刚开始那个cout++ 技术错误的问题吧。在这里插入图片描述
在方法前面加上 Synchronized 就叫做 对象锁 结果计算完全正确是不是很棒啊。
还可以怎么加呢

 public  void run() {
        synchronized (this){
            for(int j=0;j<100000;j++){
                i++;
            }
        }
    }

加类锁

public  void run() {
        synchronized (DisappearRequest1.class){
            for(int j=0;j<100000;j++){
                i++;
            }
        }

    }
因为 run方法是重写方法 不能加static 所以类锁还有一种形式  知识展示 实际上是无法 运行的
public  static synchronized void run() 

以上就是 对象锁 和类锁的几种形式都能解决问题。

多线程访问同步方法的7种情况(面试常考)

  1. 两个线程同时访问一个对象的同步方法
  • 当两个对象访问一个对象的锁时必然会有一个对象需要等待锁被释放
  1. 两个线程访问两个对象的同步方法
  • Synchronized这对象访问方法不会有影响 因为对象锁 是可以有多个的 所以2个对象就会有2把锁 就不会有冲突,会并行执行。
  1. 两个线程访问的是 synchronized的 静态方法
  • 应为静态方法修饰的锁 是类锁 即便是不同的对象 但是还是只能生成一把锁 所以他们会串行 必然会有一个线程需要等待上一个线程释放锁。
  1. 同时访问同步方法与非同步方法
  • 非同步方法 不会受到 同步方法的影响 两者同时运行。
public class SynchronizedYesAndNo6  implements Runnable{
        static SynchronizedYesAndNo6 instatce1 = new SynchronizedYesAndNo6();
        static SynchronizedYesAndNo6 instatce2 = new SynchronizedYesAndNo6();
    public void run() {
        if(Thread.currentThread().getName().equals("Thread-0")){
            method1();
        }else{
            method2();
        }

    }
    public synchronized  void method1(){
        System.out.println("我是加锁了的方法: call me" +Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() +"运行结束");
    }

    public void method2(){
        System.out.println("我是没加锁的方法: call me" +Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() +"运行结束");


    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1 =new Thread(instatce1);
        Thread t2 =new Thread(instatce2);
        t1.start();
        t2.start();
        //在 t1 t2 结束前保持主线程运行
        while(t1.isAlive() || t2.isAlive()){
        }
        System.out.println("finished");
    }
}

在这里插入图片描述

  1. 访问同一个对象的 不同的 普通同步方法
public class SynchronizedDifferentMethod implements Runnable {

    static SynchronizedDifferentMethod instatce1 = new SynchronizedDifferentMethod();

    public void run() {
        if(Thread.currentThread().getName().equals("Thread-0")){
            method1();
        }else{
            method2();
        }

    }
    public synchronized  void method1(){
        System.out.println("我是加锁了的方法1: call me" +Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() +"运行结束");
    }

    public synchronized void method2(){
        System.out.println("我是加锁了的方法2: call me" +Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() +"运行结束");


    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1 =new Thread(instatce1);
        Thread t2 =new Thread(instatce1);
        t1.start();
        t2.start();
        //在 t1 t2 结束前保持主线程运行
        while(t1.isAlive() || t2.isAlive()){
        }
        System.out.println("finished");
    }

}

在这里插入图片描述
串行运行相当于那当当前对象 作为锁。

  1. 同时访问 静态synchronized 和 非静态synchronized方法
public class SynchronizedStaticAndNormal implements Runnable {
    static SynchronizedStaticAndNormal instatce1 = new SynchronizedStaticAndNormal();

    public void run() {
        if(Thread.currentThread().getName().equals("Thread-0")){
            method1();
        }else{
            method2();
        }

    }
    public synchronized static void method1(){
        System.out.println("我是静态加锁了的方法1: call me" +Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() +"运行结束");
    }

    public synchronized void method2(){
        System.out.println("我是加锁了的方法2: call me" +Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() +"运行结束");


    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1 =new Thread(instatce1);
        Thread t2 =new Thread(instatce1);
        t1.start();
        t2.start();
        //在 t1 t2 结束前保持主线程运行
        while(t1.isAlive() || t2.isAlive()){
        }
        System.out.println("finished");
    }
}

在这里插入图片描述
一个对象 没有 static 修饰拿到的是 对象锁 加了 static 修饰拿到的是 类锁 对象锁 和 类锁 是2把锁 所以他们不冲突 所以是并行运行。

  1. 方法抛异常后,会释放锁
public class SynchronizedException implements Runnable{
        static SynchronizedException instatce1 = new SynchronizedException();
        public void run() {
            if(Thread.currentThread().getName().equals("Thread-0")){
                method1();
            }else{
                method2();
            }

        }
        public synchronized static void method1(){
            System.out.println("我是静态加锁了的方法1: call me" +Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //抛出异常后 程序会自动释放锁
            throw  new RuntimeException();
        }

        public synchronized void method2(){
            System.out.println("我是加锁了的方法2: call me" +Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() +"运行结束");


        }
        public static void main(String[] args) throws InterruptedException {
            Thread t1 =new Thread(instatce1);
            Thread t2 =new Thread(instatce1);
            t1.start();
            t2.start();
            //在 t1 t2 结束前保持主线程运行
            while(t1.isAlive() || t2.isAlive()){
            }
            System.out.println("finished");
        }
    }

总结:关于锁3点核心思想

  1. 一把锁只能同时被一个线程获取,没有拿到锁的线程必须等待(如上1-5中情况)
  2. 每个实例都对应有自己的一把锁,不同实例之间互不影响;例外的情况:锁对象是*.class以及Synchronized 修饰的是static方法此时所有对象共用一把锁(如上2,3,4,6的情况)
  3. 无论是正常执行完毕或者方法抛出异常,都会释放锁(如上第七种情况)

Synchronized 的性质

  • 可重入
    • 指的是同一线程的 外层函数获得锁之后,内层函数可以直接再次获取该锁。 同步函数是递归函数时 如果 每次都要重新释放锁获取锁的 话 那就没法玩了。
    • 好处:避免思索、提升封装性
      粒度
    • 情况1:证明同一个方法是可冲入的
public class synchronizedRecursion {
    int a = 0;
    public static void main(String[] args) {
        synchronizedRecursion synchronizedRecursion= new synchronizedRecursion();
        synchronizedRecursion.method1();
        
    }

    private synchronized  void method1() {
        System.out.println("这时method1,a=" + a);
        if(a == 0){
            a++;
            //递归调用自己 此时内部方法 不需要从外再次获取锁
            method1();
        }
    }
}
 - 情况2:证明可重入不要求是同一个方法
public class synchronizedMethod11 {
    public synchronized  void method1(){
        System.out.println("我是method1");
        //调用自己累的同步方法 不需要再次获取锁 是可重入的
        method2();
    }
    private synchronized  void method2(){
        System.out.println("我是method2");
    }

    public static void main(String[] args) {
        synchronizedMethod11 s = new synchronizedMethod11();
        s.method1();
    }

}

 - 情况3:证明可重入不要求是同一个类中
public class synchronizedSuperClass {
    public synchronized void doSomething(){
        System.out.println("我是父类方法");

    }
}
class TestClass extends synchronizedSuperClass{
    public synchronized void doSomething(){
        System.out.println("子类方法");
        //调用其他方法 不需要获取锁 
        super.doSomething();
    }
    public static void main(String[] args) {
        TestClass a =new TestClass();
        a.doSomething();
    }
}

可重入性的粒度 只要当前线程获取到锁进入到方法内 就可以 访问其他同步方法

性质:不可中断

一旦这个锁已经被别人获得了,如果我还想获得,我只能选择等待或者阻塞,直到别的线程释放这个锁。如果别人永远不是放这个锁那我只能永远地等下去。
相比之下,Lock类,可以拥有中断能力,第一点,如果我觉得我等的时间太长了,有权中断现在已经获取到锁的线程的执行;第二点,如果我觉得我等待的时间太长可不想再等待了,也可以退出。

Synchronized 原理

  • 加锁和释放锁的原理:现象、时机、深入JVM看字节码

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

  • 保证可见性的原理:内存模型

加锁和释放锁的原理

  • 现象:每一个类的实例都对应一把锁 当有线程调用带有Synchronized修饰的方法首先必须得先获得该实例的锁才能执行 否则这个线程会阻塞,这个方法一旦执行就独占了这把锁直到它返回或抛出异常才释放锁直到释放后那些被阻塞的线程才能获取到锁并进入到可执行的状态。
  • 获取和释放锁的时机:内置锁
public class synchronizedToLock {
    Lock lock =new ReentrantLock();
    public synchronized  void method1(){
        System.out.println("我是synchronized形式的锁");
    }

    //这个方法 和 synchronized 锁的逻辑是等价的
    public void method2(){
        lock.lock();
        try{
            System.out.println("我是lock形式的锁");
        }finally {
            lock.unlock();

        }
    }

    public static void main(String[] args) {
        synchronizedToLock lo = new synchronizedToLock();
        //
        lo.method1();
        lo.method2();
    }
}

加锁和释放锁的原理:深入JVM看字节码

  • 概况
    Synchronized用的锁 存在java对象头 有一个字段 进入对象退出对象是基于monitorenter对象来实现的 monitorenter对象主要两个指令 monitorenter会插在进入方法的开始 monitorexit 会插在退出
public class Decomplation {
    private Object object;
    public void insert (Thread thread){
        synchronized (object){

        }
    }
}

javac Decomplation.class 编译
javap -verbose 查看编译后的代码

在这里插入图片描述

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

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

可见性原理:Java内存模型

在这里插入图片描述
线程 A 和线程B之间通行 是通过从主线程 创建一个主线程副本 线程一般比主程序速度快 要实现A 和 B通信 线程A将修改的副本 从新写回主内存中 然后B从主内存读取 这个过程有JMM控制,当被Synchronized 修饰的方法 在运行线程退出之前都要将修改的内容回主内存中,保证了每一次从主内存读取的内容都是最新的。

缺陷

  • 效率低:锁的释放情况少、视图获得锁时不能设定超时、不能中断一个正在试图获得锁的线程。
    只有当线程执行完了 才可能释放锁其他的时候要么就是在等待锁的释放。
  • 不够灵活(读写锁更灵活): 加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的。
  • 无法知道是否成功获取到锁

更灵活的锁

   public static void main(String[] args) throws InterruptedException {
        Lock lock = new ReentrantLock();
        //获得这把锁
        lock.lock();
        //打开这把锁
        lock.unlock();
        //尝试获得这把锁
        lock.tryLock();
        //超时就放弃这把锁
        lock.tryLock(1000, TimeUnit.SECONDS);
    }

常见面试问题

  1. 使用注意点:锁对象不能为空、作用域不宜过大、避免死锁
  2. 多线程访问同步方法的各种具体情况

拓展思考

  1. 多个线程同时等待同一个Synchronized 锁的时候,JMV如何选择下一个获取锁的是哪个线程?
    随机不可控制

  2. Synchronized使得同时只有一个线程可以执行,性能较差,有什么办法可以提升性能?
    这个锁的范围竟可能的小,其他类型的锁 如读写锁。

  3. 我想更灵活地控制锁的获取和释放(现在释放锁的时机都被定死了)怎么办
    自己可以根据需求去实现锁的机制
    4.什么是锁的升级、降级?什么是JVM里的偏斜锁、轻量级锁、重量级锁?

总结

一句话介绍 Synchronized

  • JVM会自动通过使用monitor来加锁和解锁,保证同时只有一个线程可以执行指定代码,从而保证了线程安全,同时具有可重入和不可中断的性质。
发布了67 篇原创文章 · 获赞 5 · 访问量 3190

猜你喜欢

转载自blog.csdn.net/weixin_41315492/article/details/103029595