Java锁机制详细了解一下


说到Java的锁,肯定会涉及到Sychronized和ReentrantLock这两种锁,接下来分别谈谈这两种锁。

一、Synchronized

synchronized 是 Java 中的关键字,是利用锁的机制来实现同步的,它修饰的对象有以下几种:

  • 代码块中,修饰该类的实例对象(this)
  • 代码块中,修饰任意实例对象(Object)
  • 代码块中,修饰该类的类对象(Class)
  • 修饰一个普通的成员方法
  • 修饰一个静态的成员方法
    图片来源于https://www.jianshu.com/p/d53bf830fa09
    图片来源于让你彻底理解Synchronized。(侵删)

1、代码块中修饰该类的实例对象

修饰一个代码块。被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是 调用这个代码块的 对象


1、一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞。如下面的一个例子:

synchronized的用法:

// 同步代码块,锁住的是该类的实例对象
synchronized(this){
	······
}

【demo1】:锁住该类的实例对象

/**
 * @Author: Jason
 * @Date: 2020/5/18 7:06 下午
 * @Description: synchronized修饰一个代码块
 */
public class SynchronizedDemo1 {
    public static void main(String[] args) {
        SyncThread syncThread = new SyncThread();
        Thread thread1 = new Thread(syncThread, "SyncThread1");
        Thread thread2 = new Thread(syncThread, "SyncThread2");
        thread2.start();  // 这个调用start的先后顺序和线程执行的顺序没有关联,都是由系统分配
        thread1.start();
    }
}

class SyncThread implements Runnable {
    private static int count;

    public SyncThread() {
        count = 0;
    }

    public void run() {
        synchronized(this) {
            for (int i = 0; i < 5; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + ":" + (count++));
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public int getCount() {
        return count;
    }
}

输出结果:

SyncThread1:0
SyncThread1:1
SyncThread1:2
SyncThread1:3
SyncThread1:4
SyncThread2:5
SyncThread2:6
SyncThread2:7
SyncThread2:8
SyncThread2:9

当两个并发线程(thread1和thread2)访问同一个对象(syncThread)中的synchronized代码块时,在同一时刻只能有一个线程得到执行,另一个线程受阻塞,必须等待当前线程执行完这个代码块以后才能执行该代码块。Thread1和thread2是互斥的,因为在执行synchronized代码块时会锁定当前的对象,也既 syncThread,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象。该方法也等价于在非静态成员方法上加锁

我们再把SyncThread的调用稍微改一下:

SyncThread syncThread1 = new SyncThread();
SyncThread syncThread2 = new SyncThread();
Thread thread1 = new Thread(syncThread1, "SyncThread1");
Thread thread2 = new Thread(syncThread2, "SyncThread2");
thread1.start();
thread2.start();

输出结果:

SyncThread2:1
SyncThread1:0
SyncThread2:2
SyncThread1:3
SyncThread1:5
SyncThread2:4
SyncThread2:7
SyncThread1:6
SyncThread2:8
SyncThread1:9

上面的例子中发现,thread1和thread2在同时执行。这是因为synchronized(this)锁的是该类的实例对象,也既 syncThread1syncThread2 这两个实例分别对应一把锁,这两把锁是互不干扰,也不形成互斥的,所以两个线程可以同时执行。


2、当一个线程访问对象的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该对象中的非synchronized(this)同步代码块。

【demo2】:多个线程访问synchronized和非synchronized代码块

/**
 * @Author: Jason
 * @Date: 2020/5/18 7:50 下午
 * @Description: 多个线程访问synchronized和非synchronized代码块
 */
public class SynchronizedDemo2 {
    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread thread1 = new Thread(counter, "A");
        Thread thread2 = new Thread(counter, "B");
        thread1.start();
        thread2.start();
    }
}

class Counter implements Runnable{
    private int count;

    public Counter() {
        count = 0;
    }

    public void countAdd() {
        synchronized(this) {
            for (int i = 0; i < 5; i ++) {
                try {
                    System.out.println(Thread.currentThread().getName() + ":" + (count++));
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    //非synchronized代码块,未对count进行读写操作,所以可以不用synchronized
    public void printCount() {
        for (int i = 0; i < 5; i ++) {
            try {
                System.out.println(Thread.currentThread().getName() + " count:" + count);
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void run() {
        String threadName = Thread.currentThread().getName();
        if (threadName.equals("A")) {
            countAdd();
        } else if (threadName.equals("B")) {
            printCount();
        }
    }
}

输出结果如下:

A:0
B count:1
A:1
B count:1
A:2
B count:3
B count:3
A:3
A:4
B count:5

上面代码中 countAdd是一个synchronized的,printCount是非synchronized的 。从上面的结果中可以看出一个线程访问一个对象的synchronized代码块时,别的线程可以访问该对象的非synchronized代码块而不受阻塞。

2、代码块中修饰任意实例对象

用法:

// 同步代码块,锁住的是配置的实例对象
public void method(Object obj){
	// obj 锁定的对象
	synchronized(obj){
		// todo
	}
}

【demo3】:利用特殊的实例作为锁

/**
 * @Author: Jason
 * @Date: 2020/5/18 9:17 下午
 * @Description: 指定给某个对象加锁
 */
public class SynchronizedDemo3 {
    public static void main(String[] args) {
        Account account = new Account("zhang san", 10000.0f);
        AccountOperator accountOperator = new AccountOperator(account);
        final int THREAD_NUM = 5;
        Thread threads[] = new Thread[THREAD_NUM];
        for (int i = 0; i < THREAD_NUM; i++) {
            threads[i] = new Thread(accountOperator, "Thread" + i);
            threads[i].start();
        }
    }
}

/**
 * 银行账户类
 */
class Account {
    String name;
    float amount;

    public Account(String name, float amount) {
        this.name = name;
        this.amount = amount;
    }
    //存钱
    public void deposit(float amt) {
        amount += amt;
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    //取钱
    public void withdraw(float amt) {
        amount -= amt;
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public float getBalance() {
        return amount;
    }
}

/**
 * 账户操作类
 */
class AccountOperator implements Runnable{
    private Account account;
    public AccountOperator(Account account) {
        this.account = account;
    }

    public void run() {
        synchronized (account) {
            account.deposit(500);
            account.withdraw(500);
            System.out.println(Thread.currentThread().getName() + ":" + account.getBalance());
        }
    }
}

结果如下:

Thread0:10000.0
Thread4:10000.0
Thread3:10000.0
Thread2:10000.0
Thread1:10000.0

在AccountOperator 类中的run方法里,我们用synchronized 给account对象加了锁。这时,当一个线程访问account对象时,其他试图访问account对象的线程将会阻塞,直到该线程访问account对象结束。也就是说谁拿到那个锁谁就可以运行它所控制的那段代码。从结果中可以看出,5个线程分别对account的操作都是互斥进行的,所以它们每个线程最后所剩存款都是10000.0,而不会产生其他的数字。

当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的对象来充当锁:

class Test implements Runnable
{
   private byte[] lock = new byte[0];  // 特殊的instance变量
   public void method()
   {
      synchronized(lock) {
         // todo 同步代码块
      }
   }

   public void run() {

   }
}

说明:零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。

3、代码块中修饰该类的类对象

Synchronized还可以在代码块中修饰一个类对象,其用法如下:

注意:

加了类锁,只要有多个线程同时访问了静态同步代码块,就一定是同步互斥进行的。
比如:

  • 线程A访问了该类 实例1 的静态同步代码块,线程B访问了该类 实例1 的静态同步代码块
  • 线程A访问了该类 实例1 的静态同步代码块,线程B访问了该类 实例2 的静态同步代码块

以上的情况都会同步进行。


而类锁和对象锁之间是不会冲突的。
比如:

  • 线程A访问了该类 实例1 的静态同步代码块,线程B访问了该类 实例1非静态 同步代码块,线程A和B不会同步互斥。
  • 线程A访问了该类 实例1 的静态同步代码块,线程B访问了该类 实例2非静态 同步代码块,线程A和B也不会同步互斥。
// 同步代码块,锁住的是该类的类对象
class ClassName {
   public void method() {
      synchronized(ClassName.class) {
         // todo
      }
   }
}

【demo4】:

/**
 * @Author: Jason
 * @Date: 2020/5/18 9:41 下午
 * @Description: 代码块中修饰该类的类对象
 */
public class SynchronizedDemo4 {
    public static void main(String[] args) {
        SyncThread syncThread1 = new SyncThread();
        SyncThread syncThread2 = new SyncThread();
        Thread thread1 = new Thread(syncThread1, "SyncThread1");
        Thread thread2 = new Thread(syncThread2, "SyncThread2");
        thread1.start();
        thread2.start();
    }
}

class SyncThread implements Runnable {
    private static int count;

    public SyncThread() {
        count = 0;
    }

    public static void method() {
        synchronized(SyncThread.class) {
            for (int i = 0; i < 5; i ++) {
                try {
                    System.out.println(Thread.currentThread().getName() + ":" + (count++));
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

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

输出结果如下:

SyncThread1:0
SyncThread1:1
SyncThread1:2
SyncThread1:3
SyncThread1:4
SyncThread2:5
SyncThread2:6
SyncThread2:7
SyncThread2:8
SyncThread2:9

从输出结果中可以看出,在代码块中给SyncThread.class加上synchronized之后,SyncThread类的所有实例对象都共用同一把锁,所以syncThread1和syncThread2才会依次互斥地执行。也就是说sychronized在代码块中作用于一个类上的时候,锁住的是整个类对象,也既该类的模板,也就是该类的所有实例对象共用同一把锁,锁住的范围扩大了。该锁等价于sychronized修饰一个静态成员方法。

4、修饰一个普通的成员方法

用法:

// 修饰成员方法,锁住的是该类的实例对象
public synchronized void method(){
	// todo
}

Synchronized修饰一个方法很简单,就是在方法的前面加synchronized,public synchronized void method(){//todo}; synchronized修饰方法和修饰一个代码块类似,只是作用范围不一样,修饰代码块是大括号括起来的范围,而修饰方法范围是整个函数。下面的写法和上面的用法是等价的,都是锁定了该类的实例对象

public void method(){
	synchronized(this){
		// todo
	}
}

【demo5】:修饰一个普通的成员方法,在SyncThread类中的run方法前加synchronized

/**
 * @Author: Jason
 * @Date: 2020/5/18 7:06 下午
 * @Description: synchronized修饰一个普通的成员方法
 */
public class SynchronizedDemo1 {
    public static void main(String[] args) {
        SyncThread syncThread = new SyncThread();
        Thread thread1 = new Thread(syncThread, "SyncThread1");
        Thread thread2 = new Thread(syncThread, "SyncThread2");
        thread2.start();  // 这个调用start的先后顺序和线程执行的顺序没有关联,都是由系统分配
        thread1.start();
    }
}

class SyncThread implements Runnable {
    private static int count;

    public SyncThread() {
        count = 0;
    }

    public synchronized void run() {
        for (int i = 0; i < 5; i++) {
            try {
                System.out.println(Thread.currentThread().getName() + ":" + (count++));
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public int getCount() {
        return count;
    }
}

上面代码的输出结果如下:

SyncThread1:0
SyncThread1:1
SyncThread1:2
SyncThread1:3
SyncThread1:4
SyncThread2:5
SyncThread2:6
SyncThread2:7
SyncThread2:8
SyncThread2:9

其效果等价于,代码块中修饰该类的实例对象(synchronized(this){…})。

在用synchronized修饰方法时要注意以下几点:

1、synchronized关键字不能继承。

虽然可以使用synchronized来定义方法,但synchronized并不属于方法定义的一部分,因此,synchronized关键字不能被继承。如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。这两种方式的例子代码如下:

在子类方法中加上synchronized关键字

class Parent {
   public synchronized void method() { }
}
class Child extends Parent {
   public synchronized void method() { }  // 显式的加上synchronized才能同步。
}

在子类方法中调用父类的同步方法

class Parent {
   public synchronized void method() {   }
}
class Child extends Parent {
   public void method() { super.method();   }  // 此时子类的方法也相当于同步了。
} 

2、在定义接口方法时不能使用synchronized关键字。

3、构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步。

5、修饰一个静态的成员方法

synchronized也可以用来修饰一个静态方法,用法如下:

// 静态方法,锁住的是类对象。
public synchronized static void method() {
   // todo
}

【demo6】:修饰一个静态的方法

/**
 * @Author: Jason
 * @Date: 2020/5/18 9:41 下午
 * @Description: 代码块中修饰该类的类对象
 */
public class SynchronizedDemo4 {
    public static void main(String[] args) {
        SyncThread syncThread1 = new SyncThread();
        SyncThread syncThread2 = new SyncThread();
        Thread thread1 = new Thread(syncThread1, "SyncThread1");
        Thread thread2 = new Thread(syncThread2, "SyncThread2");
        thread1.start();
        thread2.start();
    }
}

class SyncThread implements Runnable {
    private static int count;

    public SyncThread() {
        count = 0;
    }

    public static synchronized void method() {
        for (int i = 0; i < 5; i++) {
            try {
                System.out.println(Thread.currentThread().getName() + ":" + (count++));
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

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

输出结果:

SyncThread1:0
SyncThread1:1
SyncThread1:2
SyncThread1:3
SyncThread1:4
SyncThread2:5
SyncThread2:6
SyncThread2:7
SyncThread2:8
SyncThread2:9

我们知道静态方法是属于类的而不属于对象的。同样的,synchronized修饰的静态方法锁定的是这个类的所有对象。syncThread1和syncThread2是SyncThread的两个对象,但在thread1和thread2并发执行时却保持了线程同步。这是因为run中调用了静态方法method,而静态方法是属于类的,所以syncThread1和syncThread2相当于用了同一把锁。该用法和在代码块中修饰一个类是等价的,锁住的都是整个类模板,该类的所有实例对象共用同一把锁。

二、ReentrantLock

JDK5起,JUC包下就提供了一种全新的互斥同步手段,重入锁(ReentrantLock),它是java.util.concurrent.locks.Lock接口的最常见的一种实现,synchronized与它一样也是可重入的。

  • 可重入性:是指一条线程能够反复进入被它自己持有锁的同步块的特性,既锁关联的计数器,如果持有锁的线程再次获得它,则将计数器的值加一,每次释放锁时计数器的值减一,当计数器的值为零时,才能真正释放锁。

1、为何要引入Lock接口中的ReentrantLock?

因为在JDK5以前,多线程环境下synchronized的吞吐量随着线程数的增加而急剧下降,而ReentrantLock却能基本保持在同一相对稳定的水平上,所以在JDK5就引入了ReentrantLock。但随着后面JDK6对synchronized的大量优化,发现优化后的synchronized和ReentrantLock性能相差不大,所以性能已经不再是选择synchronized和ReentrantLock的决定因素了。

2、ReentrantLock的使用

先看看生产-消费模式下的synchronized的使用,代码如下:

class AirCondition{

    private static int temperature = 0;  // 共享的资源

    public synchronized void increment() throws Exception{
        // 1、判断
        while(temperature != 0){
            this.wait();
        }
        // 2、干活
        temperature++;
        System.out.println(Thread.currentThread().getName()+"\t"+temperature);
        // 3、通知
        this.notifyAll();
    }

    public synchronized void decrement() throws Exception{
        // 1、判断
        while(temperature == 0){
            this.wait();
        }
        // 2、干活
        temperature--;
        System.out.println(Thread.currentThread().getName()+"\t"+temperature);
        // 3、通知
        this.notifyAll();
    }
}

public class ProdConsumerDemo1 {
    public static void main(String[] args) {
        AirCondition airCondition = new AirCondition();

        // 生产的线程,采用lambda表达式编写
        new Thread(() -> {
            for(int i=0;i<30;i++) {
                try {
                    airCondition.increment();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

        // 消费的线程
        new Thread(() -> {
            for(int i=0;i<30;i++){
                try {
                    airCondition.decrement();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        },"B").start();
    }
}

与synchronized不同的是,ReentrantLock是通过Lock接口的子类实例实现的,需要借助Condition类来实现等待和唤醒,代码如下:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class NewAirCondition{

    private static int temperature = 0;
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void increment() throws Exception{

        lock.lock();
        try{
            // 1、判断
            while(temperature != 0){
                // this.wait();
                condition.await();
            }
            // 2、干活
            temperature++;
            System.out.println(Thread.currentThread().getName()+"\t"+temperature);
            // 3、通知
            // this.notifyAll();
            condition.signalAll();
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void decrement() throws Exception{

        lock.lock();
        try{
            // 1、判断
            while(temperature == 0){
                // this.wait();
                condition.await();
            }
            // 2、干活
            temperature--;
            System.out.println(Thread.currentThread().getName()+"\t"+temperature);
            // 3、通知
            // this.notifyAll();
            condition.signalAll();
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}


public class ProdConsumerDemo2 {

    public static void main(String[] args) {
        NewAirCondition newAirCondition = new NewAirCondition();

        // 生产的线程
        new Thread(() -> {
            for(int i=0;i<30;i++) {
                try {
                    newAirCondition.increment();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

        // 消费的线程
        new Thread(() -> {
            for(int i=0;i<30;i++){
                try {
                    newAirCondition.decrement();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        },"B").start();

    }
}

3、与synchronized相比增加的高级功能

ReentrantLock与synchronized相比增加了一些高级功能,主要有以下三项:

(1)等待可中断

等待可中断是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。可中断特性对处理执行时间非常长的同步块很有帮助。而被 synchronized修饰 的同步块在持有锁锁的线程执行完毕并释放锁之前,会无条件地阻塞后面其他线程的进入。

(2)可实现公平锁

  • 公平锁
    公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。
    优点:是等待锁的线程不会饿死。
    缺点:是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。
    实现:公平锁可以通过带布尔值的ReentrantLock构造函数来实现。

  • 非公平锁
    非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。
    优点:是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。
    缺点:是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。
    实现:synchronized是非公平锁、ReentrantLock默认为非公平锁。

  • ReentrantLock源码分析公平/非公平锁
    在这里插入图片描述
    根据代码可知,ReentrantLock里面有一个内部类Sync,Sync继承AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在Sync中实现的。它有公平锁FairSync和非公平锁NonfairSync两个子类。ReentrantLock默认使用非公平锁,也可以通过构造器来显示的指定使用公平锁。
    而公平锁和非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()
    再进入hasQueuedPredecessors(),可以看到该方法主要做一件事情:主要是判断当前线程是否位于同步队列中的第一个。如果是则返回true,否则返回false。
    在这里插入图片描述
    综上,公平锁就是通过同步队列来实现多个线程按照申请锁的顺序来获取锁,从而实现公平的特性。非公平锁加锁时不考虑排队等待问题,直接尝试获取锁,所以存在后申请却先获得锁的情况。

(3)锁绑定多个条件

锁绑定多个条件是指一个ReentrantLock对象可以同时绑定多个Condition对象。在synchronized中,锁对象的wait()跟它的notify()或者notifyAll()方法配合可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外添加一把锁;而ReentrantLock多次调用newCondition()方法即可实现绑定多个。

【demo7】:利用ReentrantLock写一个进程间通信的实例,要求按顺序通知

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class MyDemo{
    // 标记位 number
    private int number = 1; // A : 1; B : 2; C : 3;
    private Lock lock = new ReentrantLock();
    // 每个线程一把钥匙
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();

    public void print5(){
        lock.lock();
        try{
            while(number != 1){
                condition1.await();
            }
            for(int i = 0; i < 5; i++){
                System.out.println(Thread.currentThread().getName()+" "+i);
            }
            // 通知
            number = 2;
            condition2.signal();
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void print10(){
        lock.lock();
        try{
            while(number != 2){
                condition2.await();
            }
            for(int i = 0; i < 10; i++){
                System.out.println(Thread.currentThread().getName()+" "+i);
            }
            // 通知
            number = 3;
            condition3.signal();
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void print15(){
        lock.lock();
        try{
            while(number != 3){
                condition3.await();
            }
            for(int i = 0; i < 15; i++){
                System.out.println(Thread.currentThread().getName()+" "+i);
            }
            // 通知
            number = 1;
            condition1.signal();
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

public class ProdConsumerDemo {
    public static void main(String[] args) {
        MyDemo myDemo = new MyDemo();

        new Thread(() -> {
            for(int i = 0; i < 10; i++){
                myDemo.print5();
            }
        },"A").start();

        new Thread(() -> {
            for(int i = 0; i < 10; i++){
                myDemo.print10();
            }
        },"B").start();

        new Thread(() -> {
            for(int i = 0; i < 10; i++){
                myDemo.print15();
            }
        },"C").start();
    }
}

输出结果为:A线程打印0-4,紧接着B线程打印0-9,接着C线程打印0-14,依次顺序打印10个循环。

4、synchronized与ReentrantLock的区别

  • synchronized以块结构来实现同步,ReentrantLock以非块结构来实现互斥同步。
  • ReentrantLock需要在finally里释放锁,如果被保护的代码块发生异常,则有可能永远不会释放锁,synchronized会自动释放锁。
  • ReentrantLock可以让等待的线程放弃等待,而synchronized会阻塞后面其他进入的线程。
  • ReentrantLock可以实现公平锁,synchronized是非公平锁。
  • ReentrantLock可以绑定多个Condition对象,synchronized一把锁就一个条件。
  • JVM可以在线程和对象的元数据中记录synchronized中锁的相关信息,而Lock却很难得知具体哪些锁对象是由特定线程锁持有的。

当jdk6中加入了大量对synchronized锁的优化措施时,ReentrantLock和synchronized的性能基本上能够持平。

三、锁优化

1、为何优化synchronized锁

Java的线程是映射到操作系统的原生内核之上的,使用synchronized加锁,会导致其他的线程进入阻塞,也同样需要操作系统对阻塞的线程进行唤醒,这就不可避免地陷入用户态到核心态的转换中。而进行这种转换需要耗费很多处理器时间(尤其是对于代码特别简单的同步块,状态转换消耗的时间甚至会比用户代码本身执行的时间更长),因此才说 synchronized是java语言的重量级锁。synchronized操作也称为 阻塞同步

简言之,使用synchronized锁导致线程的阻塞从而导致 操作系统进行 状态转换 所花费的时间多,使得线程在 高并发 中的 效率降低。所以需要对synchronized锁进行优化。

而synchronized整个优化的流程为:无锁 => 偏向锁 => 轻量级锁 => 重量级锁。

2、CAS操作

在讲如何进行优化之前,有个关键的操作需要了解下——CAS(Compare-and-Swap),因为后续的诸多优化方法都会涉及到CAS操作,当然,CAS本身也是一种 非阻塞式的同步 优化策略。

2.1 什么是CAS?

使用锁时,线程获取锁是一种 悲观锁策略,即假设每一次执行临界区代码都会产生冲突,所以当前线程获取到锁的时候同时也会阻塞其他线程获取该锁。而CAS操作(又称为无锁操作)是一种 乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。因此,线程就不会出现阻塞停顿的状态。那么,如果出现冲突了怎么办?无锁操作是使用CAS(compare and swap)又叫做比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。也因此这种同步被称为 非阻塞同步,使用这种措施的代码也称为 无锁编程

  • 乐观锁:乐观锁适合 操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
  • 悲观锁:悲观锁适合 操作多的场景,先加锁可以保证写操作时数据正确。

2.2 CAS的操作过程

CAS算法涉及到三个操作数:

  • 需要读写的内存值V
  • 进行比较的值A
  • 要写入的新值B

当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。一般情况下,“更新”是一个不断重试的操作。

2.3 CAS的具体实现

那CAS是如何通过不加锁的方式来实现操作的原子性呢?我们可以通过CAS具体实现的源码中看出。顺带一提,java.util.concurrent包中的原子类就是通过CAS来实现乐观锁的。

【调用方法】

private AtomicInteger atomicInteger = new AtomicInteger();  // 需要保证多个线程使用的是同一个
AtomicInteger atomicInteger.incrementAndGet(); //执行自增1

【demo8】:Atomic的原子自增运算

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicTest {
    public static AtomicInteger race = new AtomicInteger(0);
//    public static int race = 0;
    public static void increase(){
        race.incrementAndGet();
//        race++;
    }
    private static final int THREADS_COUNT = 20;

    public static void main(String[] args) throws Exception {
        Thread[] threads = new Thread[THREADS_COUNT];
        for(int i = 0; i < THREADS_COUNT; i++){
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int i = 0; i < 10000; i++){
                        increase();
                    }
                }
            });
            threads[i].start();
        }
        // 等待所有累加线程都结束
        while(Thread.activeCount()>1){
            Thread.yield();
        }
        System.out.println(race);
    }
}

这个demo创建了20个线程,每个线程都对race进行自增10000次,正是因为 AtomicInteger.incrementAndGet方法的原子性,才得以保证输出结果为200000。如果替换成普通的int并对race++,那就不同步了,即使使用volatile关键字也无法保证同步。

进入AtomicInteger的源码查看下:
【AtomicInteger的源码】
在这里插入图片描述
接下来,我们查看AtomicInteger的自增函数incrementAndGet()的源码时,发现自增函数底层调用的是unsafe.getAndAddInt()。但是由于JDK本身只有Unsafe.class,只通过class文件中的参数名,并不能很好的了解方法的作用,所以我们通过OpenJDK 8 来查看Unsafe的源码:

// ------------------------- JDK 8 -------------------------
// AtomicInteger 自增方法
public final int incrementAndGet() {
  return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

// Unsafe.class
public final int getAndAddInt(Object var1, long var2, int var4) {
  int var5;
  do {
      var5 = this.getIntVolatile(var1, var2);
  } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
  return var5;
}

// ------------------------- OpenJDK 8 -------------------------
// Unsafe.java
public final int getAndAddInt(Object o, long offset, int delta) {
   int v;
   do {
       v = getIntVolatile(o, offset);
   } while (!compareAndSwapInt(o, offset, v, v + delta));
   return v;
}

根据OpenJDK 8的源码我们可以看出,getAndAddInt()循环获取给定对象o中的偏移量处的值v,然后判断内存值是否等于v。如果相等则将内存值设置为 v + delta,否则返回false,继续循环进行重试,直到设置成功才能退出循环,并且将旧值返回。整个“比较+更新”操作封装在compareAndSwapInt()中,在JNI里是借助于一个CPU指令完成的,属于原子操作,可以保证多个线程都能够看到同一个变量的修改值。

后续JDK通过CPU的cmpxchg指令,去比较寄存器中的 A 和 内存中的值 V。如果相等,就把要写入的新值 B 存入内存中。如果不相等,就将内存值 V 赋值给寄存器中的值 A。然后通过Java代码中的while循环再次调用cmpxchg指令进行重试,直到设置成功为止。

2.4 CAS的问题

  • ① ABA问题:CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。
  • ② 自旋时间过长:CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。
  • ③ 只能保证一个共享变量的原子操作:对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。

3、自旋与自适应自旋

3.1 自旋锁

在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。

而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是 自旋锁
在这里插入图片描述
图片来源:不可不说的Java“锁”事(侵删)

3.2 自旋锁的实现

自旋锁的实现原理同样也是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。
在这里插入图片描述

3.3 自旋锁相关参数

  • -XX:PreBlockSpin:自旋限定次数,默认为10次。
  • -XX:+UseSpinning:开启自旋,JDK1.4.2中引入,JDK 6中变为默认开启,并引入自适应自旋锁。

3.4 自适应自旋锁

自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

4、轻量级锁

参考:
《深入理解Java虚拟机》
让你彻底理解synchronized
不可不说的Java“锁”事

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值