JUC-不得不说的Java“锁”事

发布于:2022-11-05 ⋅ 阅读:(344) ⋅ 点赞:(0)

不得不说的Java“锁”事

前言

Java提供了种类丰富的锁,每种锁因其特性的不同,在适当的场景下能够展现出非常高的效率。本文旨在对锁相关源码(本文中的源码来自JDK 8和Netty 3.10.6)、使用场景进行举例,为读者介绍主流锁的知识点,以及不同的锁的适用场景。

Java中往往是按照是否含有某一特性来定义锁,我们通过特性将锁进行分组归类,再使用对比的方式进行介绍,帮助大家更快捷的理解相关知识。下面给出本文内容的总体分类目录:
在这里插入图片描述

一synchronized

修饰实例方法,作用于当前实例,进入同步代码前需要先获取实例的锁
修饰静态方法,作用于类的Class对象,进入修饰的静态方法前需要先获取类的Class对象的锁
修饰代码块,需要指定加锁对象(记做lockobj),在进入同步代码块前需要先获取lockobj的锁

Java并发编程这个领域中synchronized关键字一直都是元老级的角色,很久之前很多人都会称它为“重量级锁”。但是,在JavaSE 1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后变得在某些情况下并不是那么重了。

1. 变量安全性

如果两个线程同时操作对象中的实例变量,则会出现“非线程安全”,解决办法就是在方法前加上synchronized关键字即可。

2.多个对象多个锁

HasSelfPrivateNum.java

public class HasSelfPrivateNum {

	private int num = 0;

	synchronized public void addI(String username) {
		try {
			if (username.equals("a")) {
				num = 100;
				System.out.println("a set over!");
				//如果去掉hread.sleep(2000),那么运行结果就会显示为同步的效果
				Thread.sleep(2000);
			} else {
				num = 200;
				System.out.println("b set over!");
			}
			System.out.println(username + " num=" + num);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

}

ThreadA.java

public class ThreadA extends Thread {

	private HasSelfPrivateNum numRef;

	public ThreadA(HasSelfPrivateNum numRef) {
		super();
		this.numRef = numRef;
	}

	@Override
	public void run() {
		super.run();
		numRef.addI("a");
	}

}

ThreadB.java

public class ThreadB extends Thread {

	private HasSelfPrivateNum numRef;

	public ThreadB(HasSelfPrivateNum numRef) {
		super();
		this.numRef = numRef;
	}

	@Override
	public void run() {
		super.run();
		numRef.addI("b");
	}

}

Run.java

public class Run {

	public static void main(String[] args) {

		HasSelfPrivateNum numRef1 = new HasSelfPrivateNum();
		HasSelfPrivateNum numRef2 = new HasSelfPrivateNum();

		ThreadA athread = new ThreadA(numRef1);
		athread.start();

		ThreadB bthread = new ThreadB(numRef2);
		bthread.start();

	}

}

运行结果:
a num=100停顿一会才执行

a 	set	over
b	set	over
b num = 200
a num = 100

上面实例中两个线程ThreadA和ThreadB分别访问同一个类的不同实例的相同名称的同步方法,但是效果确实异步执行。
为什么会这样呢?
这是因为synchronized取得的锁都是对象锁,而不是把一段代码或方法当做锁。所以在上面的实例中,哪个线程先执行带synchronized关键字的方法,则哪个线程就持有该方法所属对象的锁Lock,那么其他线程只能呈等待状态,前提是多个线程访问的是同一个对象。本例中很显然是两个对象。

在本例中创建了两个HasSelfPrivateNum类对象,所以就产生了两个锁。当ThreadA的引用执行到addI方法中的runThread.sleep(2000)语句时,ThreadB就会“乘机执行”。

所以才会导致执行如上结果(备注:由于runThread.sleep(2000),“a num=100”停顿了两秒才输出

3.synchronized方法与锁对象

通过上面我们知道synchronized取得的锁都是对象锁,而不是把一段代码或方法当做锁。如果多个线程访问的是同一个对象,哪个线程先执行带synchronized关键字的方法,则哪个线程就持有该方法,那么其他线程只能呈等待状态。如果多个线程访问的是多个对象则不一定,因为多个对象会产生多个锁。

那么我们思考一下当多个线程访问的是同一个对象中的非synchronized类型方法会是什么效果?

答案是:会异步调用非synchronized类型方法,但是会导致脏读的发生。解决办法也很简单在非synchronized类型方法前加上synchronized关键字即可。具体细节如下:脏读

4.脏读

发生脏读的情况实在读取实例变量时,此值已经被其他线程更改过。
PublicVar.java

public class PublicVar {

	public String username = "A";
	public String password = "AA";

	synchronized public void setValue(String username, String password) {
		try {
			this.username = username;
			Thread.sleep(5000);
			this.password = password;

			System.out.println("setValue method thread name="
					+ Thread.currentThread().getName() + " username="
					+ username + " password=" + password);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
     //该方法前加上synchronized关键字就同步了
	 public void getValue() {
		System.out.println("getValue method thread name="
				+ Thread.currentThread().getName() + " username=" + username
				+ " password=" + password);
	}
}

ThreadA.java

public class ThreadA extends Thread {

	private PublicVar publicVar;

	public ThreadA(PublicVar publicVar) {
		super();
		this.publicVar = publicVar;
	}

	@Override
	public void run() {
		super.run();
		publicVar.setValue("B", "BB");
	}
}

Test.java

public class Test {

	public static void main(String[] args) {
		try {
			PublicVar publicVarRef = new PublicVar();
			ThreadA thread = new ThreadA(publicVarRef);
			thread.start();

			Thread.sleep(200);//打印结果受此值大小影响

			publicVarRef.getValue();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}
}

运行结果:

getValue method thread name=main username=B password=AA
setValue method thread name=Thread-0 username=B password=BB	

为什么会有上述执行结果:当执行thread.start();时会调用PublicVar 的setValue方法并将username和password都修改为BB,在setValue里面修改username为B之后线程停了一段时间,此时publicVarRef.getValue();即另一个线程进来拿到的username为B(被线程Thread修改过的值)、password为AA(线程Thread并没有执行到this.password = password;所以没有被修改)此时会输出getValue method thread name=main username=B password=AA,当线程ThreadA被唤醒之后(Thread.sleep(5000);执行完毕)会打印setValue method thread name=Thread-0 username=B password=BB
解决办法:getValue()方法前加上synchronized关键字即可。这样ThreadA线程拿到PublicVar 对象锁没有释放之前,其他线程时拿不到PublicVar 对象的,也就不会出现脏读。

加上synchronized关键字后的运行结果:

getValue method thread name=main username=B password=B
setValue method thread name=Thread-0 username=B password=BB

小提示
线程的2个方法
1)start:用来开启一个新的线程并且执行run方法
2)run:不新起线程,直接在当前线程中执行Thread的run方法

main线程和Thread默认的执行顺序问题
main线程,Thread1,Thread2,他们的执行先后顺序是随机无序的,cpu抢到哪个线程,就执行哪个。

5.synchronized作用于静态方法

当synchronized作用于静态方法时,锁的对象就是当前类的Class对象。如:

public class Demo3 {
    static int num = 0;
    public static synchronized void m1() {
        for (int i = 0; i < 10000; i++) {
            num++;
        }
    }
    public static class T1 extends Thread {
        @Override
        public void run() {
            Demo3.m1();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        T1 t1 = new T1();
        T1 t2 = new T1();
        T1 t3 = new T1();
        t1.start();
        t2.start();
        t3.start();
        //等待3个线程结束打印num
        t1.join();
        t2.join();
        t3.join();
        System.out.println(Demo3.num);
        /**
         * 打印结果:
         * 30000
         */
    }
}

上面代码打印30000,和期望结果一致。m1()方法是静态方法,有synchronized修饰,锁用于与Demo3.class对象,和下面的写法类似:

public static void m1() {
    synchronized (Demo4.class) {
        for (int i = 0; i < 10000; i++) {
            num++;
        }
    }
}

synchronized方法的缺点
使用 synchronized关键字 声明方法有些时候是有很大的弊端的,比如我们有两个线程一个线程A调用同步方法后获得锁,那么另一个线程B就需要等待A执行完,但是如果说A执行的是一个很费时间的任务的话这样就会很耗时。
可以使用synchronized同步块来解决这个问题。但是要注意synchronized同步块的使用方式,如果synchronized同步块使用不好的话并不会带来效率的提升。

6.synchronized同步代码块

除了使用关键字修饰实例方法和静态方法外,还可以使用同步代码块,在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了,同步代码块的使用示例如下:

public class Demo5 implements Runnable {
    static Demo5 instance = new Demo5();
    static int i = 0;
    @Override
    public void run() {
        //省略其他耗时操作....
        //使用同步代码块对变量i进行同步操作,锁对象为instance
        synchronized (instance) {
            for (int j = 0; j < 10000; 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作用于一个给定的实例对象instance,即当前实例对象就是锁对象,每次当线程进入synchronized包裹的代码块时就会要求当前线程持有instance实例对象锁,如果当前有其他线程正持有该对象锁,那么新到的线程就必须等待,这样也就保证了每次只有一个线程执行i++;操作。当然除了instance作为对象外,我们还可以使用this对象(代表当前实例)或者当前类的class对象作为锁,如下代码:

//this,当前实例对象锁
synchronized(this){
    for(int j=0;j<1000000;j++){
        i++;
    }
}
//class对象锁
synchronized(Demo5.class){
    for(int j=0;j<1000000;j++){
        i++;
    }
}

分析代码是否互斥的方法
先找出synchronized作用的对象是谁,如果多个线程操作的方法中synchronized作用的锁对象一样,那么这些线程同时异步执行这些方法就是互斥的。如下代码:

public class Demo6 {
    //作用于当前类的实例对象
    public synchronized void m1() {
    }
    //作用于当前类的实例对象
    public synchronized void m2() {
    }
    //作用于当前类的实例对象
    public void m3() {
        synchronized (this) {
        }
    }
    //作用于当前类Class对象
    public static synchronized void m4() {
    }
    //作用于当前类Class对象
    public static void m5() {
        synchronized (Demo6.class) {
        }
    }
    public static class T extends Thread{
        Demo6 demo6;
        public T(Demo6 demo6) {
            this.demo6 = demo6;
        }
        @Override
        public void run() {
            super.run();
        }
    }
    public static void main(String[] args) {
        Demo6 d1 = new Demo6();
        Thread t1 = new Thread(() -> {
            d1.m1();
        });
        t1.start();
        Thread t2 = new Thread(() -> {
            d1.m2();
        });
        t2.start();
        Thread t3 = new Thread(() -> {
            d1.m2();
        });
        t3.start();
        Demo6 d2 = new Demo6();
        Thread t4 = new Thread(() -> {
            d2.m2();
        });
        t4.start();
        Thread t5 = new Thread(() -> {
            Demo6.m4();
        });
        t5.start();
        Thread t6 = new Thread(() -> {
            Demo6.m5();
        });
        t6.start();
    }
}

分析上面代码:

  • 线程t1、t2、t3中调用的方法都需要获取d1的锁,所以他们是互斥的
  • t1/t2/t3这3个线程和t4不互斥,他们可以同时运行,因为前面三个线程依赖于d1的锁,t4依赖于d2的锁
  • t5、t6都作用于当前类的Class对象锁,所以这两个线程是互斥的,和其他几个线程不互斥
    #4、ReentrantLock

7.synchronized锁重入

可重入锁” 概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。当存在父子类继承关系时,子类是完全可以通过“可重入锁”调用父类的同步方法。另外出现异常时,其锁持有的锁会自动释放。

synchronized特殊案例

在Jvm中具有String常量池缓存的功能

	String s1 = "a";
	String s2="a";
	System.out.println(s1==s2);//true

上面代码输出为true.这是为什么呢?

字符串常量池中的字符串只存在一份! 即执行完第一行代码后,常量池中已存在 “a”,那么s2不会在常量池中申请新的空间,而是直接把已存在的字符串内存地址返回给s2。

因为数据类型String的常量池属性,所以synchronized(string)在使用时某些情况下会出现一些问题,比如两个线程运行

synchronized("abc"){
}
和
synchronized("abc"){
}

修饰的方法时,这两个线程就会持有相同的锁,导致某一时刻只有一个线程能运行。所以尽量不要使用synchronized(string)而使用synchronized(object)

8.synchronized与Lock的区别

  • 首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
  • synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
  • synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁)
  • Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
  • 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
  • synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)、
  • Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。

一 Lock接口

1.1 Lock接口简介

锁是用于通过多个线程控制对共享资源的访问的工具。通常,锁提供对共享资源的独占访问:一次只能有一个线程可以获取锁,并且对共享资源的所有访问都要求首先获取锁。 但是,一些锁可能允许并发访问共享资源,如ReadWriteLock的读写锁。

在Lock接口出现之前,Java程序是靠synchronized关键字实现锁功能的。JDK1.5之后并发包中新增了Lock接口以及相关实现类来实现锁功能。虽然synchronized方法和语句的范围机制使得使用监视器锁更容易编程,并且有助于避免涉及锁的许多常见编程错误,但是有时您需要以更灵活的方式处理锁。

Lock锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象

Lock接口的实现类:

ReentrantLock , ReentrantReadWriteLock.ReadLock , ReentrantReadWriteLock.WriteLock

1.2 Lock代码示例

  Lock lock=new ReentrantLock();
	  lock.lock();
	  try {
	  } finally {
	    lock.unlock();
	  }

因为Lock是接口所以使用时要结合它的实现类,另外在finall语句块中释放锁的目的是保证获取到锁之后,最终能够被释放。
注意: 最好不要把获取锁的过程写在try语句块中,因为如果在获取锁时发生了异常,异常抛出的同时也会导致锁无法被释放。

1.3 Lock接口的特性和常见方法

Lock接口提供的synchronized关键字不具备的主要特性:

特性 描述
尝试非阻塞地获取锁 当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁
能被中断地获取锁 获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放
超时获取锁 在指定的截止时间之前获取锁, 超过截止时间后仍旧无法获取则返回

Lock接口基本的方法:

方法名称 描述
void lock() 获得锁。如果锁不可用,则当前线程将被禁用以进行线程调度,并处于休眠状态,直到获取锁。
void lockInterruptibly() 获取锁,如果可用并立即返回。如果锁不可用,那么当前线程将被禁用以进行线程调度,并且处于休眠状态,和lock()方法不同的是在锁的获取中可以中断当前线程(相应中断)。
Condition newCondition() 获取等待通知组件,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件的wait()方法,而调用后,当前线程将释放锁。
boolean tryLock() 只有在调用时才可以获得锁。如果可用,则获取锁定,并立即返回值为true;如果锁不可用,则此方法将立即返回值为false 。
boolean tryLock(long time, TimeUnit unit) 超时获取锁,当前线程在一下三种情况下会返回: 1. 当前线程在超时时间内获得了锁;2.当前线程在超时时间内被中断;3.超时时间结束,返回false.
void unlock() 释放锁。

二 Lock接口的实现类:ReentrantLock

ReentrantLocksynchronized关键字一样可以用来实现线程之间的同步互斥,但是在功能是比synchronized关键字更强大而且更灵活。

ReentrantLock是Lock的默认实现,在聊ReentranLock之前,我们需要先弄清楚一些概念:

  • 可重入锁:可重入锁是指同一个线程可以多次获得同一把锁;ReentrantLock和关键字Synchronized都是可重入锁
  • 可中断锁:可中断锁时子线程在获取锁的过程中,是否可以相应线程中断操作。synchronized是不可中断的,ReentrantLock是可中断的
  • 公平锁和非公平锁:公平锁是指多个线程尝试获取同一把锁的时候,获取锁的顺序按照线程到达的先后顺序获取,而不是随机插队的方式获取。synchronized是非公平锁,而ReentrantLock是两种都可以实现,不过默认是非公平锁

synchronized的局限性
synchronized是java内置的关键字,它提供了一种独占的加锁方式。synchronized的获取和释放锁由jvm实现,用户不需要显示的释放锁,非常方便,然而synchronized也有一定的局限性,例如:
1.当线程尝试获取锁的时候,如果获取不到锁会一直阻塞,这个阻塞的过程,用户无法控制
2.如果获取锁的线程进入休眠或者阻塞,除非当前线程异常,否则其他线程尝试获取锁必须一直等待

JDK1.5之后发布,加入了Doug Lea实现的java.util.concurrent包。包内提供了Lock类,用来提供更多扩展的加锁功能。Lock弥补了synchronized的局限,提供了更加细粒度的加锁功能。

1.ReentrantLock类常见方法:

构造方法:

方法名称 描述:
ReentrantLock() 创建一个 ReentrantLock的实例。
ReentrantLock(boolean fair) 创建一个特定锁类型(公平锁/非公平锁)的ReentrantLock的实例

ReentrantLock类常见方法(Lock接口已有方法这里没加上):

方法名称 描述
int getHoldCount() 查询当前线程保持此锁定的个数,也就是调用lock()方法的次数。
protected Thread getOwner() 返回当前拥有此锁的线程,如果不拥有,则返回 null
protected Collection getQueuedThreads() 返回包含可能正在等待获取此锁的线程的集合
int getQueueLength() 返回等待获取此锁的线程数的估计。
protected Collection getWaitingThreads(Condition condition) 返回包含可能在与此锁相关联的给定条件下等待的线程的集合。
int getWaitQueueLength(Condition condition) 返回与此锁相关联的给定条件等待的线程数的估计。
boolean hasQueuedThread(Thread thread) 查询给定线程是否等待获取此锁。
boolean hasQueuedThreads() 查询是否有线程正在等待获取此锁。
boolean hasWaiters(Condition condition) 查询任何线程是否等待与此锁相关联的给定条件
boolean isFair() 如果此锁的公平设置为true,则返回 true 。
boolean isHeldByCurrentThread() 查询此锁是否由当前线程持有。
boolean isLocked() 查询此锁是否由任何线程持有。

2.ReentrantLock基本使用

我们使用3个线程来对一个共享变量++操作,先使用synchronized实现,然后使用ReentrantLock实现。

synchronized方式:

public class Demo2 {
    private static int num = 0;
    private static synchronized void add() {
        num++;
    }
    public static class T extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                Demo2.add();
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T();
        T t2 = new T();
        T t3 = new T();
        t1.start();
        t2.start();
        t3.start();
        t1.join();
        t2.join();
        t3.join();
        System.out.println(Demo2.num);
    }
}

ReentrantLock方式:

import java.util.concurrent.locks.ReentrantLock;

public class Demo3 {
    private static int num = 0;
    private static ReentrantLock lock = new ReentrantLock();
    private static void add() {
        lock.lock();
        try {
            num++;
        } finally {
            lock.unlock();
        }
    }
    public static class T extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                Demo3.add();
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T();
        T t2 = new T();
        T t3 = new T();
        t1.start();
        t2.start();
        t3.start();
        t1.join();
        t2.join();
        t3.join();
        System.out.println(Demo3.num);
    }
}

ReentrantLock的使用过程:

  • 创建锁:ReentrantLock lock = new ReentrantLock();
  • 获取锁:lock.lock()
  • 释放锁:lock.unlock();

对比上面的代码,与关键字synchronized相比,ReentrantLock锁有明显的操作过程,开发人员必须手动的指定何时加锁,何时释放锁,正是因为这样手动控制,ReentrantLock对逻辑控制的灵活度要远远胜于关键字synchronized,上面代码需要注意lock.unlock() 一定要放在finally中,否则,若程序出现了异常,锁没有释放,那么其他线程就再也没有机会获取这个锁了。

3.Condition接口简介

我们通过之前的学习知道了:synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。

在使用notify/notifyAll()方法进行通知时,被通知的线程是有JVM选择的,使用ReentrantLock类结合Condition实例可以实现“选择性通知”,这个功能非常重要,而且是Condition接口默认提供的。

而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程

Condition接口的常见方法:

方法名称 描述
void await() 相当于Object类的wait方法
boolean await(long time, TimeUnit unit) 相当于Object类的wait(long timeout)方法
signal() 相当于Object类的notify方法
signalAll() 相当于Object类的notifyAll方法

3.1使用Condition实现等待/通知机制

3.1.1使用单个Condition实例实现等待/通知机制:

UseSingleConditionWaitNotify.java

public class UseSingleConditionWaitNotify {

	public static void main(String[] args) throws InterruptedException {

		MyService service = new MyService();

		ThreadA a = new ThreadA(service);
		a.start();

		Thread.sleep(3000);

		service.signal();

	}

	static public class MyService {

		private Lock lock = new ReentrantLock();
		public Condition condition = lock.newCondition();

		public void await() {
			lock.lock();
			try {
				System.out.println(" await时间为" + System.currentTimeMillis());
				condition.await();
				System.out.println("这是condition.await()方法之后的语句,condition.signal()方法之后我才被执行");
			} catch (InterruptedException e) {
				e.printStackTrace();
			} finally {
				lock.unlock();
			}
		}

		public void signal() throws InterruptedException {
			lock.lock();
			try {				
				System.out.println("signal时间为" + System.currentTimeMillis());
				condition.signal();
				Thread.sleep(3000);
				System.out.println("这是condition.signal()方法之后的语句");
			} finally {
				lock.unlock();
			}
		}
	}

	static public class ThreadA extends Thread {

		private MyService service;

		public ThreadA(MyService service) {
			super();
			this.service = service;
		}

		@Override
		public void run() {
			service.await();
		}
	}
}

运行结果
在这里插入图片描述
在使用wait/notify实现等待通知机制的时候我们知道必须执行完notify()方法所在的synchronized代码块后才释放锁。在这里也差不多,必须执行完signal所在的try语句块之后才释放锁,condition.await()后的语句才能被执行。
注意: 必须在condition.await()方法调用之前调用lock.lock()代码获得同步监视器,不然会报错。

3.1.2. 使用多个Condition实例实现等待/通知机制:

public class UseMoreConditionWaitNotify {
	public static void main(String[] args) throws InterruptedException {

		MyserviceMoreCondition service = new MyserviceMoreCondition();

		ThreadA a = new ThreadA(service);
		a.setName("A");
		a.start();

		ThreadB b = new ThreadB(service);
		b.setName("B");
		b.start();

		Thread.sleep(3000);

		service.signalAll_A();

	}
	static public class ThreadA extends Thread {

		private MyserviceMoreCondition service;

		public ThreadA(MyserviceMoreCondition service) {
			super();
			this.service = service;
		}

		@Override
		public void run() {
			service.awaitA();
		}
	}
	static public class ThreadB extends Thread {

		private MyserviceMoreCondition service;

		public ThreadB(MyserviceMoreCondition service) {
			super();
			this.service = service;
		}

		@Override
		public void run() {
			service.awaitB();
		}
	}
	
}

MyserviceMoreCondition.java

public class MyserviceMoreCondition {

	private Lock lock = new ReentrantLock();
	public Condition conditionA = lock.newCondition();
	public Condition conditionB = lock.newCondition();

	public void awaitA() {
		lock.lock();
		try {
			System.out.println("begin awaitA时间为" + System.currentTimeMillis()
					+ " ThreadName=" + Thread.currentThread().getName());
			conditionA.await();
			System.out.println("  end awaitA时间为" + System.currentTimeMillis()
					+ " ThreadName=" + Thread.currentThread().getName());
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}

	public void awaitB() {
		lock.lock();
		try {			
			System.out.println("begin awaitB时间为" + System.currentTimeMillis()
					+ " ThreadName=" + Thread.currentThread().getName());
			conditionB.await();
			System.out.println("  end awaitB时间为" + System.currentTimeMillis()
					+ " ThreadName=" + Thread.currentThread().getName());
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}

	public void signalAll_A() {
		lock.lock();
		try {			
			System.out.println("  signalAll_A时间为" + System.currentTimeMillis()
					+ " ThreadName=" + Thread.currentThread().getName());
			conditionA.signalAll();
		} finally {
			lock.unlock();
		}
	}

	public void signalAll_B() {
		lock.lock();
		try {		
			System.out.println("  signalAll_B时间为" + System.currentTimeMillis()
					+ " ThreadName=" + Thread.currentThread().getName());
			conditionB.signalAll();
		} finally {
			lock.unlock();
		}
	}
}

运行结果:
在这里插入图片描述
只有A线程被唤醒了。

3.1.3使用Condition实现顺序执行

public class ConditionSeqExec {

	volatile private static int nextPrintWho = 1;
	private static ReentrantLock lock = new ReentrantLock();
	final private static Condition conditionA = lock.newCondition();
	final private static Condition conditionB = lock.newCondition();
	final private static Condition conditionC = lock.newCondition();

	public static void main(String[] args) {

		Thread threadA = new Thread() {
			public void run() {
				try {
					lock.lock();
					while (nextPrintWho != 1) {
						conditionA.await();
					}
					for (int i = 0; i < 3; i++) {
						System.out.println("ThreadA " + (i + 1));
					}
					nextPrintWho = 2;
					//通知conditionB实例的线程运行
					conditionB.signalAll();
				} catch (InterruptedException e) {
					e.printStackTrace();
				} finally {
					lock.unlock();
				}
			}
		};

		Thread threadB = new Thread() {
			public void run() {
				try {
					lock.lock();
					while (nextPrintWho != 2) {
						conditionB.await();
					}
					for (int i = 0; i < 3; i++) {
						System.out.println("ThreadB " + (i + 1));
					}
					nextPrintWho = 3;
					//通知conditionC实例的线程运行
					conditionC.signalAll();
				} catch (InterruptedException e) {
					e.printStackTrace();
				} finally {
					lock.unlock();
				}
			}
		};

		Thread threadC = new Thread() {
			public void run() {
				try {
					lock.lock();
					while (nextPrintWho != 3) {
						conditionC.await();
					}
					for (int i = 0; i < 3; i++) {
						System.out.println("ThreadC " + (i + 1));
					}
					nextPrintWho = 1;
					//通知conditionA实例的线程运行
					conditionA.signalAll();
				} catch (InterruptedException e) {
					e.printStackTrace();
				} finally {
					lock.unlock();
				}
			}
		};
		Thread[] aArray = new Thread[5];
		Thread[] bArray = new Thread[5];
		Thread[] cArray = new Thread[5];

		for (int i = 0; i < 5; i++) {
			aArray[i] = new Thread(threadA);
			bArray[i] = new Thread(threadB);
			cArray[i] = new Thread(threadC);

			aArray[i].start();
			bArray[i].start();
			cArray[i].start();
		}

	}
}

运行结果:
在这里插入图片描述
通过代码很好理解,说简单就是在一个线程运行完之后通过condition.signal()/condition.signalAll()方法通知下一个特定的运行运行,就这样循环往复即可。

注意: 默认情况下ReentranLock类使用的是非公平锁

4.ReentrantLock获取锁的过程是可中断的

对于synchronized关键字,如果一个线程在等待获取锁,最终只有2种结果:

  • 要么获取到锁然后继续后面的操作
  • 要么一直等待,直到其他线程释放锁为止

而ReentrantLock提供了另外一种可能,就是在等待获取锁的过程中(发起获取锁请求到还未获取到锁这段时间内)是可以被中断的,也就是说在等待锁的过程中,程序可以根据需要取消获取锁的请求。有些使用这个操作是非常有必要的。比如:你和好朋友越好一起去打球,如果你等了半小时朋友还没到,突然你接到一个电话,朋友由于突发状况,不能来了,那么你一定达到回府。中断操作正是提供了一套类似的机制,如果一个线程正在等待获取锁,那么它依然可以收到一个通知,被告知无需等待,可以停止工作了。
示例代码:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class Demo6 {
    private static ReentrantLock lock1 = new ReentrantLock(false);
    private static ReentrantLock lock2 = new ReentrantLock(false);
    public static class T extends Thread {
        int lock;
        public T(String name, int lock) {
            super(name);
            this.lock = lock;
        }
        @Override
        public void run() {
            try {
                if (this.lock == 1) {
                    lock1.lockInterruptibly();
                    TimeUnit.SECONDS.sleep(1);
                    lock2.lockInterruptibly();
                } else {
                    lock2.lockInterruptibly();
                    TimeUnit.SECONDS.sleep(1);
                    lock1.lockInterruptibly();
                }
            } catch (InterruptedException e) {
                System.out.println("中断标志:" + this.isInterrupted());
                e.printStackTrace();
            } finally {
                if (lock1.isHeldByCurrentThread()) {
                    lock1.unlock();
                }
                if (lock2.isHeldByCurrentThread()) {
                    lock2.unlock();
                }
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T("t1", 1);
        T t2 = new T("t2", 2);
        t1.start();
        t2.start();
    }
}

先运行一下上面代码,发现程序无法结束,使用jstack查看线程堆栈信息,发现2个线程死锁了。

Found one Java-level deadlock:
=============================
"t2":
  waiting for ownable synchronizer 0x0000000717380c20, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
  which is held by "t1"
"t1":
  waiting for ownable synchronizer 0x0000000717380c50, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
  which is held by "t2

lock1被线程t1占用,lock2被线程t2占用,线程t1在等待获取lock2,线程t2在等待获取lock1,都在相互等待获取对方持有的锁,最终产生了死锁,如果是在synchronized关键字情况下发生了死锁现象,程序是无法结束的。

我们对上面代码改造一下,线程t2一直无法获取到lock1,那么等待5秒之后,我们中断获取锁的操作。主要修改一下main方法,如下:

T t1 = new T("t1", 1);
T t2 = new T("t2", 2);
t1.start();
t2.start();
TimeUnit.SECONDS.sleep(5);
t2.interrupt();

新增了2行代码TimeUnit.SECONDS.sleep(5);t2.interrupt();,程序可以结束了,运行结果:

java.lang.InterruptedException
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
    at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
    at com.itsoku.chat06.Demo6$T.run(Demo6.java:31)
中断标志:false

从上面信息中可以看出,代码的31( lock1.lockInterruptibly();)行触发了异常,中断标志输出:false

t2在31行一直获取不到lock1的锁,主线程中等待了5秒之后,t2线程调用了interrupt()方法,将线程的中断标志置为true,此时31行会触发InterruptedException异常,然后线程t2可以继续向下执行,释放了lock2的锁,然后线程t1可以正常获取锁,程序得以继续进行。线程发送中断信号触发InterruptedException异常之后,中断标志将被清空。

关于获取锁的过程中被中断,注意几点:

  1. ReentrankLock中必须使用实例方法lockInterruptibly()获取锁时,在线程调用interrupt()方法之后,才会引发InterruptedException异常
  2. 线程调用interrupt()之后,线程的中断标志会被置为true
  3. 触发InterruptedException异常之后,线程的中断标志会被清空,即置为false
  4. 所以当线程调用interrupt()引发InterruptedException异常,中断标志的变化是:false->true->false

5.ReentrantLock锁申请等待限时

申请锁等待限时是什么意思?一般情况下,获取锁的时间我们是不知道的,synchronized关键字获取锁的过程中,只能等待其他线程把锁释放之后才能够有机会获取到锁。所以获取锁的时间有长有短。如果获取锁的时间能够设置超时时间,那就非常好了。

ReentrantLock刚好提供了这样功能,给我们提供了获取锁限时等待的方法tryLock(),可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。

1.tryLock无参方法

看一下源码中tryLock方法:

public boolean tryLock()

返回boolean类型的值,此方法会立即返回,结果表示获取锁是否成功,示例:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class Demo8 {
    private static ReentrantLock lock1 = new ReentrantLock(false);
    public static class T extends Thread {
        public T(String name) {
            super(name);
        }
        @Override
        public void run() {
            try {
                System.out.println(System.currentTimeMillis() + ":" + this.getName() + "开始获取锁!");
                //获取锁超时时间设置为3秒,3秒内是否能否获取锁都会返回
                if (lock1.tryLock()) {
                    System.out.println(System.currentTimeMillis() + ":" + this.getName() + "获取到了锁!");
                    //获取到锁之后,休眠5秒
                    TimeUnit.SECONDS.sleep(5);
                } else {
                    System.out.println(System.currentTimeMillis() + ":" + this.getName() + "未能获取到锁!");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (lock1.isHeldByCurrentThread()) {
                    lock1.unlock();
                }
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T("t1");
        T t2 = new T("t2");
        t1.start();
        t2.start();
    }
}

代码中获取锁成功之后,休眠5秒,会导致另外一个线程获取锁失败,运行代码,输出

1563356291081:t2开始获取锁!
1563356291081:t2获取到了锁!
1563356291081:t1开始获取锁!
1563356291081:t1未能获取到锁!

2.tryLock有参方法

可以明确设置获取锁的超时时间,该方法签名:

public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException

该方法在指定的时间内不管是否可以获取锁,都会返回结果,返回true,表示获取锁成功,返回false表示获取失败。此方法有2个参数,第一个参数是时间类型,是一个枚举,可以表示时、分、秒、毫秒等待,使用比较方便,第1个参数表示在时间类型上的时间长短。此方法在执行的过程中,如果调用了线程的中断interrupt()方法,会触发InterruptedException异常。

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class Demo7 {
    private static ReentrantLock lock1 = new ReentrantLock(false);
    public static class T extends Thread {
        public T(String name) {
            super(name);
        }
        @Override
        public void run() {
            try {
                System.out.println(System.currentTimeMillis() + ":" + this.getName() + "开始获取锁!");
                //获取锁超时时间设置为3秒,3秒内是否能否获取锁都会返回
                if (lock1.tryLock(3, TimeUnit.SECONDS)) {
                    System.out.println(System.currentTimeMillis() + ":" + this.getName() + "获取到了锁!");
                    //获取到锁之后,休眠5秒
                    TimeUnit.SECONDS.sleep(5);
                } else {
                    System.out.println(System.currentTimeMillis() + ":" + this.getName() + "未能获取到锁!");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (lock1.isHeldByCurrentThread()) {
                    lock1.unlock();
                }
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T("t1");
        T t2 = new T("t2");
        t1.start();
        t2.start();
    }
}

程序中调用了ReentrantLock的实例方法tryLock(3, TimeUnit.SECONDS),表示获取锁的超时时间是3秒,3秒后不管是否能否获取锁,该方法都会有返回值,获取到锁之后,内部休眠了5秒,会导致另外一个线程获取锁失败。

运行程序,输出:

1563355512901:t2开始获取锁!
1563355512901:t1开始获取锁!
1563355512902:t2获取到了锁!
1563355515904:t1未能获取到锁!

输出结果中分析,t2获取到锁了,然后休眠了5秒,t1获取锁失败,t1打印了2条信息,时间相差3秒左右。

关于tryLock()方法和tryLock(long timeout, TimeUnit unit)方法,说明一下:

  • 都会返回boolean值,结果表示获取锁是否成功
  • tryLock()方法,不管是否获取成功,都会立即返回;而有参的tryLock方法会尝试在指定的时间内去获取锁,中间会阻塞的现象,在指定的时间之后会不管是否能够获取锁都会返回结果
  • tryLock()方法不会响应线程的中断方法;而有参的tryLock方法会响应线程的中断方法,而触发InterruptedException异常,这个从2个方法的声明上可以可以看出来

6.ReentrantLock其他常用的方法

isHeldByCurrentThread:实例方法,判断当前线程是否持有ReentrantLock的锁,上面代码中有使用过。
获取锁的4种方法对比

获取锁的方法 是否立即响应(不会阻塞) 是否响应中断
lock() × ×
lockInterruptibly() ×
tryLock() ×
tryLock(long timeout, TimeUnit unit) ×

三总结

  • ReentrantLock可以实现公平锁和非公平锁
  • ReentrantLock默认实现的是非公平锁
  • ReentrantLock的获取锁和释放锁必须成对出现,锁了几次,也要释放几次
  • 释放锁的操作必须放在finally中执行
  • lockInterruptibly()实例方法可以相应线程的中断方法,调用线程的interrupt()方法时,lockInterruptibly()方法会触发InterruptedException异常
  • 关于InterruptedException异常说一下,看到方法声明上带有 throws InterruptedException,表示该方法可以相应线程中断,调用线程的interrupt()方法时,这些方法会触发InterruptedException异常,触发InterruptedException时,线程的中断中断状态会被清除。所以如果程序由于调用interrupt()方法而触发InterruptedException异常,线程的标志由默认的false变为ture,然后又变为false
  • 实例方法tryLock()会尝试获取锁,会立即返回,返回值表示是否获取成功
  • 实例方法tryLock(long timeout, TimeUnit unit)会在指定的时间内尝试获取锁,指定的时间内是否能够
  • 取锁,都会返回,返回值表示是否获取锁成功,该方法会响应线程的中断

四悲观锁和乐观锁

悲观锁
认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。

synchronized关键字和Lock的实现类都是悲观锁

适合写操作多的场景,先加锁可以保证写操作时数据正确。

显式的锁定之后再操作同步资源

//=============悲观锁的调用方式
public synchronized void m1()
{
    //加锁后的业务逻辑......
}

// 保证多个线程使用的是同一个lock对象的前提下
ReentrantLock lock = new ReentrantLock();
public void m2() {
    lock.lock();
    try {
        // 操作同步资源
    }finally {
        lock.unlock();
    }
}

乐观锁

//=============乐观锁的调用方式
// 保证多个线程使用的是同一个AtomicInteger
private AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.incrementAndGet();

乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。

如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作

乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。

适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

乐观锁则直接去操作同步资源,是一种无锁算法,得之我幸不得我命,再抢

乐观锁一般有两种实现方式:

  • 采用版本号机制
  • CAS(Compare-and-Swap,即比较并替换)算法实现

五自旋锁 VS 适应性自旋锁

在介绍自旋锁前,我们需要介绍一些前提知识来帮助大家明白自旋锁的概念。

阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。

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

而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。
在这里插入图片描述
自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。

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

public final int getAndAddInt(Object varl,Long var2,int var4){
	int var5;
	do{
		var5=this.getIntVolatile(varl,var2);
	}while(!this.compareAndSwapInt(var1,var2,var5,var5:var5 var4));
	return var5;

自旋锁在JDK1.4.2中引入,使用-XX:+UseSpinning来开启。JDK 6中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。

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

在自旋锁中 另有三种常见的锁形式:TicketLock、CLHlock和MCSlock,本文中仅做名词介绍,不做深入讲解,感兴趣的同学可以自行查阅相关资料

六无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁

这四种锁是指锁的状态,专门针对synchronized的。在介绍这四种锁状态之前还需要介绍一些额外的知识。

首先为什么Synchronized能实现线程同步?

在回答这个问题之前我们需要了解两个重要的概念:“Java对象头”、“Monitor”。

Java对象头

synchronized是悲观锁,在操作同步资源之前需要给同步资源先加锁,这把锁就是存在Java对象头里的,而Java对象头又是什么呢?

我们以Hotspot虚拟机为例,Hotspot的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。

Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。

Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

Monitor

Monitor可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁。

Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

现在话题回到synchronized,synchronized通过Monitor来实现线程同步,Monitor是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的线程同步。

如同我们在自旋锁中提到的“阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长”。这种方式就是synchronized最初实现同步的方式,这就是JDK 6之前synchronized效率低的原因。这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”,JDK 6中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。

所以目前锁一共有4种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁。锁状态只能升级不能降级。

通过上面的介绍,我们对synchronized的加锁机制以及相关知识有了一个了解,那么下面我们给出四种锁状态对应的的Mark Word内容,然后再分别讲解四种锁状态的思路以及特点:

锁状态 存储内容 存储内容
无锁 对象的hashCode、对象分代年龄、是否是偏向锁(0) 01
偏向锁 偏向线程ID、偏向时间戮、对像象分代年龄、是否是偏向锁(1) 01
轻量级锁 指向栈中锁记录的指针 00
重量级锁 指向互斥量(重量级锁)的指针 10

无锁

无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。

无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。上面我们介绍的CAS原理及应用即是无锁的实现。无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的。

偏向锁

偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。

在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能。

当一个线程访问同步代码块并获取锁时,会在Mark Word里存储锁偏向的线程ID。在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测Mark Word里是否存储着指向当前线程的偏向锁。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令即可。

偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态。撤销偏向锁后恢复到无锁(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

偏向锁在JDK 6及以后的JVM里是默认启用的。可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。

轻量级锁

是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。

在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,然后拷贝对象头中的Mark Word复制到锁记录中。

拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向对象的Mark Word。

如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,表示此对象处于轻量级锁定状态。

如果轻量级锁的更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁。

若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。

重量级锁

升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。

整体的锁状态升级流程如下:
在这里插入图片描述
综上,偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。

  1. 公平锁 VS 非公平锁

七公平锁和非公平锁

在大多数情况下,锁的申请都是非公平的,也就是说,线程1首先请求锁A,接着线程2也请求了锁A。那么当锁A可用时,是线程1可获得锁还是线程2可获得锁呢?这是不一定的,系统只是会从这个锁的等待队列中随机挑选一个,因此不能保证其公平性。这就好比买票不排队,大家都围在售票窗口前,售票员忙的焦头烂额,也顾及不上谁先谁后,随便找个人出票就完事了,最终导致的结果是,有些人可能一直买不到票。而公平锁,则不是这样,它会按照到达的先后顺序获得资源。公平锁的一大特点是:它不会产生饥饿现象,只要你排队,最终还是可以等到资源的;synchronized关键字默认是有jvm内部实现控制的,是非公平锁。而ReentrantLock运行开发者自己设置锁的公平性。

看一下jdk中ReentrantLock的源码,2个构造方法:

public ReentrantLock() {
    sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

默认构造方法创建的是非公平锁。

第2个构造方法,有个fair参数,当fair为true的时候创建的是公平锁,公平锁看起来很不错,不过要实现公平锁,系统内部肯定需要维护一个有序队列,因此公平锁的实现成本比较高,性能相对于非公平锁来说相对低一些。因此,在默认情况下,锁是非公平的,如果没有特别要求,则不建议使用公平锁。

公平锁和非公平锁在程序调度上是很不一样,来一个公平锁示例看一下:

import java.util.concurrent.locks.ReentrantLock;

public class Demo5 {
    private static int num = 0;
    private static ReentrantLock fairLock = new ReentrantLock(true);
    public static class T extends Thread {
        public T(String name) {
            super(name);
        }
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                fairLock.lock();
                try {
                    System.out.println(this.getName() + "获得锁!");
                } finally {
                    fairLock.unlock();
                }
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T("t1");
        T t2 = new T("t2");
        T t3 = new T("t3");
        t1.start();
        t2.start();
        t3.start();
        t1.join();
        t2.join();
        t3.join();
    }
}

看一下输出的结果,锁是按照先后顺序获得的。
改一下上面代码,改为非公平锁试试,如下:

ReentrantLock fairLock = new ReentrantLock(false);

从ReentrantLock卖票编码演示公平和非公平现象

import java.util.concurrent.locks.ReentrantLock;

class Ticket
{
    private int number = 30;
    ReentrantLock lock = new ReentrantLock();

    public void sale()
    {
        lock.lock();
        try
        {
            if(number > 0)
            {
                System.out.println(Thread.currentThread().getName()+"卖出第:\t"+(number--)+"\t 还剩下:"+number);
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

public class SaleTicketDemo
{
    public static void main(String[] args)
    {
        Ticket ticket = new Ticket();

        new Thread(() -> { for (int i = 0; i <35; i++)  ticket.sale(); },"a").start();
        new Thread(() -> { for (int i = 0; i <35; i++)  ticket.sale(); },"b").start();
        new Thread(() -> { for (int i = 0; i <35; i++)  ticket.sale(); },"c").start();
    }
}

生活中,排队讲求先来后到视为公平。程序中的公平性也是符合请求锁的绝对时间的,其实就是 FIFO,否则视为不公平

1、源码解读

​ 按序排队公平锁,就是判断同步队列是否还有先驱节点的存在(我前面还有人吗?),如果没有先驱节点才能获取锁;先占先得非公平锁,是不管这个事的,只要能抢获到同步状态就可以
在这里插入图片描述

2、为什么默认非公平?

恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU 的时间片,尽量减少 CPU 空闲状态时间。
使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。

3、使⽤公平锁会有什么问题

公平锁保证了排队的公平性,非公平锁霸气的忽视这个规则,所以就有可能导致排队的长时间在排队,也没有机会获取到锁,这就是传说中的 “锁饥饿”

4、什么时候用公平?什么时候用非公平?

如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;否则那就用公平锁,大家公平使用

八可重入锁(又名递归锁)

是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。

如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

1、“可重入锁”这四个字分开来解释:

可:可以。
重:再次。
入:进入。
锁:同步锁。
进入什么:进入同步域(即同步代码块/方法或显式锁锁定的代码)
一句话:一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。
自己可以获取自己的内部锁

2、可重入锁种类

1、隐式锁(即synchronized关键字使用的锁)默认是可重入锁

指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。简单的来说就是:在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的

与可重入锁相反,不可重入锁不可递归调用,递归调用就发生死锁。
同步块

public class ReEntryLockDemo{
    public static void main(String[] args){
        final Object objectLockA = new Object();

        new Thread(() -> {
            synchronized (objectLockA){
                System.out.println("-----外层调用");
                synchronized (objectLockA){
                    System.out.println("-----中层调用");
                    synchronized (objectLockA){
                        System.out.println("-----内层调用");
                    }
                }
            }
        },"a").start();
    }
}

同步方法

public class ReEntryLockDemo{
    public synchronized void m1(){
        System.out.println("-----m1");
        m2();
    }
    public synchronized void m2(){
        System.out.println("-----m2");
        m3();
    }
    public synchronized void m3(){
        System.out.println("-----m3");
    }

    public static void main(String[] args){
        ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo();

        reEntryLockDemo.m1();
    }
}

2、显式锁(即Lock)也有ReentrantLock这样的可重入锁。

public class Demo4 {
    private static int num = 0;
    private static ReentrantLock lock = new ReentrantLock();
    private static void add() {
        lock.lock();
        lock.lock();
        try {
            num++;
        } finally {
            lock.unlock();
            lock.unlock();
        }
    }
    public static class T extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                Demo4.add();
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T();
        T t2 = new T();
        T t3 = new T();
        t1.start();
        t2.start();
        t3.start();
        t1.join();
        t2.join();
        t3.join();
        System.out.println(Demo4.num);
    }
}

上面代码中add()方法中,当一个线程进入的时候,会执行2次获取锁的操作,运行程序可以正常结束,并输出和期望值一样的30000,假如ReentrantLock是不可重入的锁,那么同一个线程第2次获取锁的时候由于前面的锁还未释放而导致死锁,程序是无法正常结束的。ReentrantLock命名也挺好的Re entrant Lock,和其名字一样,可重入锁。

代码中还有几点需要注意:

  • lock()方法和unlock()方法需要成对出现,锁了几次,也要释放几次,否则后面的线程无法获取锁了;可以将add中的unlock删除一个事实,上面代码运行将无法结束
  • unlock()方法放在finally中执行,保证不管程序是否有异常,锁必定会释放
/**
 * @create 2020-05-14 11:59
 * 在一个Synchronized修饰的方法或代码块的内部调用本类的其他Synchronized修饰的方法或代码块时,是永远可以得到锁的
 */
public class ReEntryLockDemo{
    static Lock lock = new ReentrantLock();

    public static void main(String[] args){
        new Thread(() -> {
            lock.lock();
            try
            {
                System.out.println("----外层调用lock");
                lock.lock();
                try
                {
                    System.out.println("----内层调用lock");
                }finally {
                    // 这里故意注释,实现加锁次数和释放次数不一样
                    // 由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。
                    lock.unlock(); // 正常情况,加锁几次就要解锁几次
                }
            }finally {
                lock.unlock();
            }
        },"a").start();

        new Thread(() -> {
            lock.lock();
            try
            {
                System.out.println("b thread----外层调用lock");
            }finally {
                lock.unlock();
            }
        },"b").start();

    }
}

3、Synchronized的重入的实现机理

每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。

​ 当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。

​ 在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。

​ 当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。

九死锁

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。

1、产生死锁主要原因

系统资源不足
进程运行推进的顺序不合适
资源分配不当

public class DeadLockDemo{
    public static void main(String[] args){
        final Object objectLockA = new Object();
        final Object objectLockB = new Object();

        new Thread(() -> {
            synchronized (objectLockA){
                System.out.println(Thread.currentThread().getName()+"\t"+"自己持有A,希望获得B");
                //暂停几秒钟线程
                try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
                synchronized (objectLockB)
                {
                    System.out.println(Thread.currentThread().getName()+"\t"+"A-------已经获得B");
                }
            }
        },"A").start();

        new Thread(() -> {
            synchronized (objectLockB){
                System.out.println(Thread.currentThread().getName()+"\t"+"自己持有B,希望获得A");
                //暂停几秒钟线程
                try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
                synchronized (objectLockA){
                    System.out.println(Thread.currentThread().getName()+"\t"+"B-------已经获得A");
                }
            }
        },"B").start();

    }
}

2、如何排查死锁

纯命令

jps -l
jstack 进程编号

图形化
jconsole

末尾

请添加图片描述

难免有不足之处,奢求大佬给予指正。

本文含有隐藏内容,请 开通VIP 后查看