万字长文带你彻底理解synchronized关键字(上)

简介: Synchronized关键字一直是工作和面试中的重点。这篇文章准备彻彻底底的从基础使用到原理缺陷等各个方面来一个分析,这篇文章由于篇幅比较长,但是如果你有时间和耐心,相信会有一个比较大的收获,所以,学习请慢慢来。这篇文章主要从以下几个方面进行分析讲解.1、Synchronized关键字的简介,主要是为什么要使用Synchronized关键字,极其作用地位。2、Synchronized关键字的使用,主要是从对象锁和类锁两个角度。3、Synchronized关键字的使用注意事项。分析了6种常见的使用情况。4、Synchronized关键字的两个性质,主要是可重入性和不可中断性。

一、简介


Synchronized一句话来解释其作用就是:能够保证同一时刻最多只有一个线程执行该段代码,以达到并发安全的效果。也就是说Synchronized就好比是一把锁,某个线程把资源锁住了之后,别人就不能使用了,只有当这个线程用完了别人才能用。


对于Synchronized关键字来说,它是并发编程中一个元老级角色,也就是说你只要学习并发编程,就必须要学习Synchronized关键字。由此可见其地位。


说了这么多,好像我们还没体验过它的威力。我们就直接举个例子,来分析一下。

public class SynTest01 implements Runnable{
    static int a=0;
    public static void main(String[] args) 
                throws InterruptedException {
        SynTest01 syn= new SynTest01();
        Thread thread1 = new Thread(syn);
        Thread thread2 = new Thread(syn);
        thread1.start();thread1.join();
        thread2.start();thread2.join();
        System.out.println(a);
    }
    @Override
    public void run() {
        for(int i=0;i<1000;i++) {
            a++;
        }
    }
}

上面代码要完成的功能就是,thread1对a进行增加,一直到1000,thread2再对a进行增加,一直到2000。不过如果我们运行过之后我们就会发现,最后的输出值总是小于2000,这是为什么呢?


这是因为我们在执行a++的时候其实包含了以下三个操作:


(1)线程1读取a

(2)线程1将a加1

(3)将a的值写入内存


出错原因的关键就在于第二操作和第三个操作之间,此时线程1还没来得及把a的值写入内存,线程2就把旧值读走了,这也就造成了a加了两次,但是内存中的a的值只增加了1。这也就是不同步现象。


但是如果说我们使用了Synchronized关键字之后呢?

public class SynTest01 implements Runnable{
    static int a=0;
    Object object = new Object();
    public static void main(String[] args) throws InterruptedException {
        SynTest01 syn= new SynTest01();
        Thread thread1 = new Thread(syn);
        Thread thread2 = new Thread(syn);
        thread1.start();thread1.join();
        thread2.start();thread2.join();
        System.out.println(a);
    }
    @Override
    public void run() {
        synchronized (object) {
            for(int i=0;i<1000;i++) {
                a++;
            }
        }//结束
    }
}

现在我们使用synchronized关键字把这一块代码锁住,不管你怎么输出都是2000了,锁住之后,同一时刻只有一个线程进入。也就不会发生上面a写操作不同步的现象了。

现在相信你开始觉得synchronized关键字的确很实用,可以解决多线程中的很多问题。上面这个小例子只是带我们去简单的认识一下,下面我们就来看看其详细的使用。


二、使用


对于synchronized关键字来说,一共可以分为两类:对象锁和类锁。

v2-831a62474f5a066f2df94e4b6e3cbab2_1440w.jpg

我们一个一个来看如何使用。


1、对象锁


对于对象锁来说,又可以分为两个,一个是方法锁,一个是同步代码块锁。


(1)同步代码块锁


同步代码块锁主要是对代码块进行加锁,其实已经演示过了,就是上面的那个案例。不过为了保持一致我们再举一个例子。

public class SynTest01 implements Runnable {
    Object object = new Object();
    public static void main(String[] args) throws InterruptedException {
        SynTest01 syn = new SynTest01();
        Thread thread1 = new Thread(syn);
        Thread thread2 = new Thread(syn);
        thread1.start();
        thread2.start();
        //线程1和线程2只要有一个还存活就一直执行
        while (thread1.isAlive() || thread2.isAlive()) {}
        System.out.println("main程序运行结束");
    }
    @Override
    public void run() {
        synchronized (object) {
            try {
                System.out.println(Thread.currentThread().getName() 
                        + "线程执行了run方法");
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName() 
                        + "执行2秒钟之后完毕");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

在这个例子中,我们使用了synchronized锁住了run方法中的代码块。表示同一时刻只有一个线程能够进入代码块。就好比是去医院挂号,前面一个人办完了业务,下一个人才开始。


在这里面我们看到,线程1和线程2使用的是同一个锁,也就是我们new的Object。如果我们让线程1和线程2每一个人拥有一个锁对象呢?

public class SynTest01 implements Runnable {
    Object object1 = new Object();
    Object object2 = new Object();
    public static void main(String[] args) throws InterruptedException {
        SynTest01 syn = new SynTest01();
        Thread thread1 = new Thread(syn);
        Thread thread2 = new Thread(syn);
        thread1.start();
        thread2.start();
        //线程1和线程2只要有一个还存活就一直执行
        while (thread1.isAlive() || thread2.isAlive()) {}
        System.out.println("main程序运行结束");
    }
    @Override
    public void run() {
        synchronized (object1) {
            try {
                System.out.println(Thread.currentThread().getName() 
                        + "线程执行了object1");
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName() 
                        + "执行object1完毕");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        synchronized (object2) {
            try {
                System.out.println(Thread.currentThread().getName() 
                        + "线程执行object2");
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName() 
                        + "执行object2完毕");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

现在线程1和线程2每个人拥有一把锁,去访问不同的方法资源。这时候会出现什么情况呢?

v2-9f4814d763d8eeea6c3ee9bf6e123afc_1440w.jpg

我们同样用一张图看一下其原理。

v2-1ab8f44a5492a95d73f22b261b0666a8_1440w.jpg

也就是说,相当于两个业务有俩窗口都可以办理,但是两个任务都需要排队办理。


同步代码块锁总结:


同步代码块锁主要是对代码块进行加锁,此时同一时刻只能有一个线程获取到该资源,要注意每一把锁只负责当前的代码块,其他的代码块不管。

以上就是同步代码快的使用方法。下面我们看对象锁的另外一种形式,那就是方法锁。这里的方法锁指代的是普通方法。


(2)方法锁


方法锁相比较同步代码块锁就简单很多了,就是在普通方法上添加synchronized关键字修饰即可。

public class SynTest2 implements Runnable {
    public static void main(String[] args) throws InterruptedException {
        SynTest2 syn = new SynTest2();
        Thread thread1 = new Thread(syn);
        Thread thread2 = new Thread(syn);
        thread1.start();
        thread2.start();
        // 线程1和线程2只要有一个还存活就一直执行
        while (thread1.isAlive() || thread2.isAlive()) {
        }
        System.out.println("main程序运行结束");
    }
    @Override
    public void run() {
        method();
    }
    public synchronized void method() {
        try {
            System.out.println(Thread.currentThread().getName() + "进入到了方法");
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + "执行完毕");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这个例子中我们使用两个线程对同一个普通方法进行访问,结果可想而知,也就是同一时刻只能有一个线程进入到此方法。我们运行一下,看一下结果。

v2-48f14f84654fd70b2ad209a093b42d75_1440w.jpg

跟我们预想的一样,很简单。不过我们想过一个问题没有,此时我们synchronized关键字加了一把锁,这个锁指代是谁呢?像同步代码块锁synchronized (object),这里面都有object,但是方法锁是谁呢?


答案就是this对象,也就是说我们在方法锁里面synchronized其实锁的就是当前this对象。我们如何去验证this锁的存在呢?不如我们再举一个例子:

public class SynTest3 implements Runnable {
    public static void main(String[] args) throws InterruptedException {
        SynTest3 syn = new SynTest3();
        Thread thread1 = new Thread(syn);
        Thread thread2 = new Thread(syn);
        thread1.start();
        thread2.start();
        // 线程1和线程2只要有一个还存活就一直执行
        while (thread1.isAlive() || thread2.isAlive()) {}
        System.out.println("main程序运行结束");
    }
    @Override
    public void run() {
        method1();
        method2();
    }
    public synchronized void method1() {
        try {
            System.out.println(Thread.currentThread().getName() + "进入到了方法1");   
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + "离开方法1,并释放锁");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public synchronized void method2() {
        try {
            System.out.println(Thread.currentThread().getName() + "进入到了方法2");
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + "离开方法2,并释放锁");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

上面这个例子中,我们定义了两个synchronized关键字修饰的方法method1和method2,然后让两个线程同时运行,我们测试一下看看会出现什么结果:v2-dd723ea9d9448f843952f7587b3f8739_1440w (1).jpg

从结果来看,会发现不管是method1还是method2,同一个时刻两个方法只能有一个线程在运行。这也就是this锁导致的。我们再给一张图描述一下其原理。

v2-12c08429702bd089c0836cd55869e038_1440w.jpg

现在应该明白了吧,这也就验证了方法锁的存在。也验证了方法锁的原理。下面我们继续。讨论一下类锁。


2、类锁


上面的锁都是对象锁,下面我们看看类锁。类锁其实也有两种形式,一种是static方法锁,一种是class锁。


(1)static方法锁


在java中,java的类对象可能有无数个,但是类却只有一个。首先我们看第一种形式。

public class SynTest4 implements Runnable {
    public static void main(String[] args) throws InterruptedException {
        SynTest4 instance1 = new SynTest4();
        SynTest4 instance2 = new SynTest4();
        Thread thread1 = new Thread(instance1);
        Thread thread2 = new Thread(instance2);
        thread1.start();
        thread2.start();
        System.out.println("main程序运行结束");
    }
    @Override
    public void run() {
        method1();
    }
    public static synchronized void method1() {
        try {
            System.out.println(Thread.currentThread().getName() + "进入到了静态方法");
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + "离开静态方法,并释放锁");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这个例子中我们定义了两个不同的对象instance1和instance2。分别去执行了method1。会出现什么结果呢?

v2-ef68a9cf2a55f501503c254f2d405405_1440w.png

如果我们把static关键字去掉,很明显现在就是普通方法了,如果我们再去运行,由于instance1和instance2是两个不同的对象,那么也就是两个不同的this锁,这时候就能随便进入了。我们去掉static关键字之后运行一下:

v2-4bf23217d78970ceb2a4ee36ac6fc878_1440w.jpg

现在看到了,由于是两个不同的this锁,所以都能进入,就好比是一个门有两把钥匙,每一把都能打开门。


(2)class锁


这种用法我们直接看例子再来分析一下:

public class SynTest5 implements Runnable {
    public static void main(String[] args) throws InterruptedException {
        SynTest5 instance1 = new SynTest5();
        SynTest5 instance2 = new SynTest5();
        Thread thread1 = new Thread(instance1);
        Thread thread2 = new Thread(instance2);
        thread1.start();
        thread2.start();
        // 线程1和线程2只要有一个还存活就一直执行
        while (thread1.isAlive() || thread2.isAlive()) {}
        System.out.println("main程序运行结束");
    }
    @Override
    public void run() {
        method1();
    }
    public void method1() {
        synchronized (SynTest5.class) {
            try {
                System.out.println(Thread.currentThread().getName() + "进入到了方法");
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName() + "离开方法");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

在这个例子中我们使用了同步代码块,不过synchronized关键字包装的可不是object了,而是SynTest5.class。我们还定义了两个不同的对象实例instance1和instance2。运行一下我们会发现,线程1和线程2依然会依次执行。


以上就是synchronized关键字的几种常见的用法,到这里我们来一个总结:


对于同步不同步,关键点在于锁,两个线程执行的是同一把锁,那么就依次排队等候,两个线程执行的不是同一把锁,那就各干各的事。


基本的使用我们也讲完了,下面我们进入下一个专题,那就是我们需要注意的事项。这是面试常考的一个问题,不管是机试还是面试。



相关文章
|
6月前
|
缓存 安全 Java
《volatile使用与学习总结:》多层面分析学习java关键字--volatile
《volatile使用与学习总结:》多层面分析学习java关键字--volatile
35 0
|
2月前
|
缓存 Java 编译器
【多线程-从零开始-伍】volatile关键字和内存可见性问题
【多线程-从零开始-伍】volatile关键字和内存可见性问题
37 0
|
5月前
|
存储 Java 程序员
Java内存模式以及volatile关键字的使用
Java内存模式以及volatile关键字的使用
45 0
|
6月前
|
缓存 Java 编译器
必知的技术知识:Java并发编程:volatile关键字解析
必知的技术知识:Java并发编程:volatile关键字解析
27 0
|
Java
关于关键字volatile的一二
关于关键字volatile的一二
81 0
|
消息中间件 缓存 Java
volatile 关键字与计算机底层的一些杂谈
volatile 是 Java 并发编程中一个非常重要,也是面试常问的一个技术点,用起来很简单直接修饰在变量前面即可,但是我们真的懂这个关键字吗?它在 JVM 底层,甚至在 CPU 层面到底是如何发挥作用的?
|
缓存 安全 Java
《JUC并发编程 - 高级篇》05 -共享模型之无锁 (CAS | 原子整数 | 原子引用 | 原子数组 | 字段更新器 | 原子累加器 | Unsafe类 )
《JUC并发编程 - 高级篇》05 -共享模型之无锁 (CAS | 原子整数 | 原子引用 | 原子数组 | 字段更新器 | 原子累加器 | Unsafe类 )
《JUC并发编程 - 高级篇》05 -共享模型之无锁 (CAS | 原子整数 | 原子引用 | 原子数组 | 字段更新器 | 原子累加器 | Unsafe类 )
|
编译器 C语言
C语言程序设计——volatile关键字、函数重入
C语言程序设计——volatile关键字、函数重入
139 0
C语言程序设计——volatile关键字、函数重入
|
编译器
C零散知识点汇总之volatile关键字
C零散知识点汇总之volatile关键字
|
SQL 缓存 安全
Java并发编程学习系列七:深入了解volatile关键字
Java并发编程学习系列七:深入了解volatile关键字
118 0
Java并发编程学习系列七:深入了解volatile关键字