史上最牛逼的synchronized教程来了,建议收藏

为什么需要锁:

因为存在临界资源,所谓临界资源,就是统一时间只能有一个在操作的资源,比如打印机,如果同时执行多个打印任务就会错乱,临界资源在程序中就是同一时间只有一个进程或者线程访问的资源,那么怎么怎么保证统一时间只有一个线程访问了,就是加锁。

史上最牛逼的synchronized教程来了,建议收藏

如以下这段代码,size变量就是临界资源,正常情况下,程序执行结果,size的值应该是10000,但是实际的结果会是一个小于10000的值,这就是没有加锁造成的线程安全问题。

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;

public class ThreadUnSafeDemo {

    static int size;

    public static void main(String[] args) throws InterruptedException {
        
        // CountDownLatch的作用是尽量让线程同时开始执行
        CountDownLatch countDownLatch = new CountDownLatch(1);
        final List<Thread> list = new ArrayList<>(10);
        for (int i = 0; i < 10; i++) {
            Thread thread =
                    new Thread(
                            () -> {
                                try {
                                    countDownLatch.await();
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                                for (int j = 0; j < 1000; j++) {
                                    size++;
                                }
                            });
            thread.start();
            list.add(thread);
        }
        countDownLatch.countDown();
        
        // 这里等所有的线程执行完成再让主线程执行
        for (Thread thread : list) {
            thread.join();
        }
        
        // 预期结果应该10000
        System.out.println(size);
    }
}

使用方法:

共有三种使用方法,在静态方法上、在非静态方法上、自定义的代码块。
其中,静态方法上使用当前类的class对象当作锁对象来处理的,非静态方法上是调用该方法的对象,this,来当作锁对象处理,自定的代码块就是自己指定的对象。

在使用静态方法和普通方法上使用sychronized关键子的时候,要特别注意,锁对象是一个的话,会有很大的性能问题。

性能瓶颈的案例:
在test2执行的时候,test1是不能执行的,因为test2方法获取到了user对象的锁,test1方法要等到锁被释放。

public class Test {

    public static void main(String[] args) throws InterruptedException {
        User user = new User();
        new Thread(
                        () -> {
                            for (; ; ) {
                                user.test1();
                            }
                        })
                .start();

        new Thread(
                        () -> {
                            try {
                                user.test2();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        })
                .start();
    }
}

class User {

    public synchronized void test1() {
        System.out.println("开始");
    }

    public synchronized void test2() throws InterruptedException {
        Thread.sleep(5000);
    }
}

锁的重入:

就是同一个锁对象,不论是synchronized还是ReentrantLock,同一个线程可以多次持有这个锁,就是获取到了这个锁对象之后,如果再一次遇到了这个锁对象同步的资源,依然可以进入。

那么为什么要这么设计呢,因为我们在日常开发的过程中,方法里面大多数都是方法调用方法的,不能说我这个方法获取到了锁,到下一个方法同一个锁我还要等着锁被释放,这个锁本来就是我持有,那就死锁了。所以synchronized、ReentrantLock都是“重入锁”。

public class ReentrantDemo {

    final Object lock = new Object();

    private void method1() {
        synchronized (lock) {
            // do something
            method2();
        }
    }

    private void method2() {
        
        // 如果锁不是重入的,在这就会永远获取不到锁,就会发生死锁。
        synchronized (lock) {
            // do something
        }
    }
}

Synchronized优化:

锁升级(不可逆):
在jdk1.6之前,sychronized的性能是没有lock锁好的,因为sychronized锁,之前上来就是重量级锁,是依赖协程的,要和os进行交互,效率不是很高,jdk1.6之后,首先上的锁是偏向锁,根据后面对锁资源的竞争程度,依次升级为轻量级锁和重量级锁。其中偏向锁,基本上没有锁的竞争。轻量级锁是锁竞争很小,释放锁的时间也很短,不用释放cpu的执行权,而重量级锁是处理锁的竞争很激烈的情况的。

在jdk1.6之前sychronized的性能要低于ReentrantLock的,在jdk1.6之后做了锁升级的优化之后性能就和ReentrantLock差不多,但是ReentrantLock的功能要多一些。

具体现在的锁是什么级别,是存储在对象头中的。

锁粗化:
加锁和解锁也是要消耗资源的,如果存在一连串的加锁和解锁操作,可能会造成不必要的性能损耗,这时候会将这些锁拓展成一个更大的锁,避免频繁的加锁和解锁操作。

// 优化前
class User {

    private final Object lock = new Object();

    public void test() {

        synchronized (lock) {
            System.out.println("print 1");
        }

        synchronized (lock) {
            System.out.println("print 2");
        }

        synchronized (lock) {
            System.out.println("print 3");
        }
    }
}

// 优化后
class User {

    private final Object lock = new Object();

    public void test() {

        synchronized (lock) {
            System.out.println("print 1");
            System.out.println("print 2");
            System.out.println("print 3");
        }

    }

}

锁消除:
Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,经过逃逸分析,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间。

class User {
    
    public void test() {

        final Object lock = new Object();
        
        // 这个锁是没有意义的,就会被消除掉
        synchronized (lock) {
            System.out.println("print 1");
            System.out.println("print 2");
            System.out.println("print 3");
        }
    }
}

可见性:
sychronized是可以保证线程的可见性的,因为获取锁的时候,线程会把临界资源拷贝到自己的工作内存中去,等到释放锁的时候再把资源刷新回主内存中,这个操作过程中,其他线程一直在阻塞着,所以保证了共享变量内存可见性。

上一篇:多线程中 synchronized 锁升级的原理


下一篇:Java 多线程梳理(三、线程同步机制)