Java并发编程:深入了解synchronized关键字

目录

 

1:synchronized简介

2:synchronized使用方式

3:synchronized可重入性

4:synchronized可见性

5:synchronized实现原理

6:synchronized的缺陷

7:synchronized的使用注意


1:synchronized简介

在多线程环境中对同一资源同时操作可能会导致结果的不确定性。java内置了synchronized关键字来解决这种问题。synchronized通常也被称为重量级锁,尽管从JDK1.5新增了Lock,但synchronized凭借java自带的高封装性依旧被广泛的使用。synchronized可以把任何一个非null的对象作为锁。synchronized有两种主要用法:第一种是对象锁,包括方法锁(默认锁对象为this当前的实例对象)和同步代码块锁(自己指定的锁对象)。第二种是类锁,指synchronized修饰静态的方法或指定锁为Class对象。

2:synchronized使用方式

首先,我先写一段代码

public class SynchronizedMethod1 implements Runnable {
    static SynchronizedMethod1 synchronizedMethod = new SynchronizedMethod1();
    static int i = 0;
    @Override
    public void run() {
        for (int j = 0; j < 100000; j++) {
            i++;
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(synchronizedMethod);
        Thread t2 = new Thread(synchronizedMethod);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("最终的结果是:" + i);
    }
}

这段代码大家应该可以看出来是有问题的,没有保证对共享变量i的原子操作,导致的结果就是:无法确定i输出的值。

下面来演示synchronized的几种用法,如何保证数据的准确性。

方式一: 同步代码块

public class SynchronizedMethod2 implements Runnable {
    static SynchronizedMethod2 instance = new SynchronizedMethod2();
    static int i = 0;

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

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("最终的结果是:" + i);
    }

}

this指的是当前的实例对象。

方式二: 同步方法

public class SynchronizedMethod3 implements Runnable {
    static SynchronizedMethod3 instance = new SynchronizedMethod3();
    static int i = 0;

    @Override
    public void run() {
        add();
    }

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

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("最终的结果是:" + i);
    }
}

第二种方式也能够保证数据的准确。

下面我再对上述代码进行改动。实例化两个对象,分别给线程使用。这时会发现,上述的两种方式都尽管都使用了synchronized,但依然无法保证数据的准确。原因是因为我们使用的是synchronized的对象锁,对于不同的实例对象,线程只能够对自己引用的对象进行加锁。

public class SynchronizedMethod3 implements Runnable {
    static SynchronizedMethod3 instance1 = new SynchronizedMethod3();
    static SynchronizedMethod3 instance2 = new SynchronizedMethod3();
    static int i = 0;

    @Override
    public void run() {
        add();
    }

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

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instance1);
        Thread t2 = new Thread(instance2);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("最终的结果是:" + i);
    }
}

演示下面用法的时候先讲一个概念:Java类可能有很多个对象,但只有1个Class对象。这个很重要,要考的!!!比如我new一个Student对象A,一个Student对象B,这是两个对象,但它们只有一个Class对象,这个Class对象由JVM实现。

方式三: 静态方法锁

与第二种方式相比就是在同步方法上多加了static

public class SynchronizedMethod4 implements Runnable{
    static SynchronizedMethod4 instance1 = new SynchronizedMethod4();
    static SynchronizedMethod4 instance2 = new SynchronizedMethod4();
    static int i = 0;

    @Override
    public void run() {
        try {
            add();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static synchronized void add() throws InterruptedException {
        System.out.println(Thread.currentThread().getName());
        for (int j = 0; j < 100000; j++) {
            i++;
        }
        Thread.sleep(2000);
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instance1);
        Thread t2 = new Thread(instance2);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("最终的结果是:" + i);
    }

当作用在静态方法时锁住的便是对象对应的Class实例,因为Class数据存在于永久带,因此静态方法锁相当于该类的一个全局锁。所以就算new了多个对象,但一样能够锁住。

方式四: synchronized锁为Class对象

public class SynchronizedMethod5 implements Runnable {
    static SynchronizedMethod5 instance1 = new SynchronizedMethod5();
    static SynchronizedMethod5 instance2 = new SynchronizedMethod5();
    static int i = 0;

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

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instance1);
        Thread t2 = new Thread(instance2);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("最终的结果是:" + i);
    }

}

在方法四中,我们的锁对象是*.class,无论实例化多少对象,这些对象也只有一个class对象,所以,这种方式也是可以锁住的。

3:synchronized可重入性

synchronized和Lock(ReentrantLock)都是可重入锁,可重入锁又叫做递归锁。在这里只说明synchronized的可重入性,不对二者进行比较。

可重入性定义:同一线程在外层函数获取到锁后,内层函数可以直接再次获取到锁

synchronized的可重入性有两点好处:避免死锁,提升封装性。

这是什么意思呢?我们用如下代码来说明:

public class SynchronizedMethod6 {
    static SynchronizedMethod6 instance = new SynchronizedMethod6();

    public void method1() {
        synchronized (SynchronizedMethod6.class) {
            System.out.println("method1执行成功");
            method2();
        }
    }

    public void method2() {
        synchronized (SynchronizedMethod6.class) {
            System.out.println("method2执行成功");
        }
    }

    public static void main(String[] args) {
        instance.method1();
    }
}

我们假设synchronized不具有重入性,在调用method2时,由于当前持有的锁没有释放,又无法获取到method2中的锁,这时就会导致死锁。

由于synchronized是java内置的锁,自带重入性,所以封装性更强。

原理:加锁次数计数器。

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

 

4:synchronized可见性

既然说到线程之间的可见性,那么必须要先了解java的内存模型JMM(这篇博客中简单简单讲解了一下java的内存模型:https://blog.csdn.net/love1793912554/article/details/88618453)。在这里不再详细讲述JMM。对于synchronized来说,在释放锁的时候,线程会把操作的数据刷新到主内存中去,由此就可以保证线程之间的数据可见性。

5:synchronized实现原理

synchronized是java内置的关键字,由此可见它的重要性。但它无法直接通过源码来分析。下面我用反编译字节码信息来分析一下synchronized的实现原理。

先用javac将方法一中的.java文件编译成.class文件,执行javap -v SynchronizedMethod1.class,javap是jdk自带的工具,想要仔细了解可以参考(https://blog.csdn.net/junsure2012/article/details/7099222)。

执行同步代码块需要先获取对象的监视器monitor。monitorenter对应多个monitorexit,释放锁的情况可能有多种,正常释放锁,异常释放锁,所以monitorexit相比较于monitorenter会多。

6:synchronized的缺陷

锁的释放情况少。synchronized释放锁只有两种情况,一种是正常流程运行结束,另一种是发生了异常。如果说一个线程正在执行IO操作(当然,不建议在锁中进行耗时的IO操作,只是举例),那么另一个线程就只能焦急的等待。

试图获得锁时不能设置超时时间。排队获取锁的线程会一直存在,不会因为暂时的堵塞而撤退。与之对应的tryLock(long time, TimeUnit unit) 在指定的时间内不能获得锁就会主动放弃。

不能中断一个正在试图获得锁的线程。

不够灵活:每个锁仅有单一的条件,加锁和释放锁的时机单一。

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

7:synchronized的使用注意

1:锁的对象不能为空,锁是存在于对象头中的,对象都没有,如何加锁。

2:锁的作用域不宜过大,简单点说就是加锁的代码块不能过多,如果代码块都存在与锁中,代 码的执行就变成了串行执行。这就无法体现多线程的效率了。

3:避免死锁。在一个线程中避免获取不同的锁。

 

记录自己的学习和成长,如果写的有什么不对的地方,还请大家多多指正。

 

发布了21 篇原创文章 · 获赞 18 · 访问量 7578

猜你喜欢

转载自blog.csdn.net/love1793912554/article/details/89430137