Java并发编程系列(一)-Synchronized和ReentrantLock爱恨纠葛

Java提供了两种锁机制来控制多个线程对共享资源的互斥访问

  • Synchronized:同步关键字,属于Jvm内置关键字,由虚拟机控制
  • ReentrantLock:可重入锁,JDK实现,由开发人员控制

Synchronized关键字

当多个线程对同一资源进行访问时,可以通过Synchronized关键字去进行加锁,以防止线程安全问题。JVM将加锁的技术包装成关键字,降低门槛,非常容易使用。

Synchronized的作用对象:

  1. 作用于代码块
  2. 作用于方法

作用于代码块

作用于当前对象

使用this表示当前对象

public void Demo{
    public void func(){
        synchronized(this){
            // ...
        }
    }
}

举个例子,使用synchronized锁住当前对象

public void Demo{
    public void func(){
        synchronized(this){
            for(int i=0;i<10;i++){
                System.out.print(i + " ");
            }
        }
    }
}

同一个对象,开启两个线程执行

public static void main(String[] args){
    ExecutorService es = Executors.newCachedThreadPool();
    Demo demo = new Demo();
    es.execute(()->demo.func());
    es.execute(()->demo.func());
    executorService.shutdown();
    // 输出 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
}

不同对象,各自开启线程执行

public static void main(String[] args){
    ExecutorService es = Executors.newCachedThreadPool();
    Demo demo1 = new Demo();
    Demo demo2 = new Demo();
    es.execute(()->demo1.func());
    es.execute(()->demo2.func());
    executorService.shutdown();
    // 输出 0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
}

因此,如果需要将代码块的锁上升到整个类的话,可以使用下面的方式

作用于整个类

指定Demo.class,表示作用于整个类
public void Demo{
    public void func(){
        synchronized(Demo.class){
            // ...
        }
    }
}

举个例子,使用synchronized锁住当前对象

public void Demo{
    public void func(){
        synchronized(Demo.class){
            for(int i=0;i<10;i++){
                System.out.print(i + " ");
            }
        }
    }
}

不同对象,各自开启线程执行

public static void main(String[] args){
    ExecutorService es = Executors.newCachedThreadPool();
    Demo demo1 = new Demo();
    Demo demo2 = new Demo();
    es.execute(()->demo1.func());
    es.execute(()->demo2.func());
    executorService.shutdown();
    // 输出 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
}

作用于方法

同代码块加锁的作用域一致,synchronized作用于方法时,也可以对当前对象,或者当前类进行加锁。

作用于当前对象

public void Demo{
    public synchronized void func(){
        // ...
    }
}

作用于整个类

public void Demo{
    public static synchronized void func(){
        // ...
    }
}

是的,你没有看错,只要将方法变成静态的,就可以将作用域上升到类。

ReentrantLock可重入锁

talk is cheap, show you my code ~我们用一段代码来直观地看下可重入锁的基本使用

ReentrantLock lock = new ReentrantLock();
lock.lock(); // 加锁。当线程拥有锁权限时,可以往下执行;否则在此阻塞
lock.unlock(); // 解锁。

再上一段实战代码,了解一下真实场景的使用方式

public class Demo{
    private static ReentrantLock lock = new ReentrantLock();
    public void func(){
        try {
            // 加锁
            lock.lock();
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        } finally {
            // 释放锁
            lock.unlock();
        }
    }
}
public static void main(String[] args){
    ExecutorService es = Executors.newCachedThreadPool();
    Demo demo1 = new Demo();
    Demo demo2 = new Demo();
    es.execute(()->demo1.func());
    es.execute(()->demo2.func());
    executorService.shutdown();
    // 输出 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
}

ReentrantLock一般要与try-finally结合使用,主要释放锁(避免造成死锁)

Synchronized VS ReentrantLock

对比指标 Synchronized ReentrantLock 理由 使用场景
风险/使用难度 Synchronized由jvm控制,开发人员只需要进行加锁即可,锁的释放由jvm管理,不用担心死锁情况;而ReentrantLock需要开发人员自行加锁,自行解锁,从风险角度考虑,Synchronized更胜一筹 简单场景
顺序 Synchronized是非公平锁,无法保证线程执行的顺序;而ReentrantLock可以通过构造器ReentrantLock(boolean fair)去初始化一个公平锁 对线程池队列顺序有要求时使用
灵活性 Synchronized由jvm控制,因此开发人员无法自行解锁;ReentrantLock可以自信定义加锁解锁,更加灵活 复杂场景,在程序中需要手动解锁
性能 参考网上说吧,这个还没验证过。但是性能不是凭证,这个指标不作为参考依据

此外,jvm对Synchronized还做了非常多的锁优化策略,比如偏向锁,自旋锁,轻量级锁,重量级锁,这个以后再专门来讲。

小结

学习完两种加锁方式后,我们应当清楚业务场景是啥,再决定使用哪种锁去实现。简单锁使用Synchronized,复杂的使用ReentrantLock。

你可能感兴趣的:(java,java,jvm,开发语言)