Java锁的类型和功能

Java锁的种类

乐观锁/悲观锁
自旋锁/适应性自旋锁
无锁/偏向锁/轻量级锁/重量级锁
公平锁/非公平锁
可重入锁/非可重入锁
独享锁/共享锁(互斥锁/读写锁)
分段锁

1. 悲观锁/乐观锁

乐观锁与悲观锁并不是特指某两种类型的锁,是人们定义出来的概念或思想,主要是指看待并发同步的角度。

乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS(Compare and Swap 比较并交换)实现的。

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。比如Java里面的同步原语synchronized关键字和Lock的实现类reentrantlock就是悲观锁。
悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。

1.1 乐观锁实现机制

(1) 数据版本机制
  实现数据版本一般有两种,第一种是使用版本号,第二种是使用时间戳。以版本号方式为例。

版本号方式:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
核心SQL代码:
update table set xxx=#{xxx}, version=version+1 where id=#{id} and version=#{version};

(2) CAS操作
  CAS(Compare and Swap 比较并交换),当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

CAS操作中包含三个操作数——需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B,否则处理器不做任何操作。

1.2 悲观锁实现机制

悲观锁认为对于同一个数据的并发操作,一定会发生修改的,哪怕没有修改,也会认为修改。因此对于同一份数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁并发操作一定会出问题。

在对任意记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)。

如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。具体响应方式由开发者根据实际需要决定。

如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。

期间如果有其他对该记录做修改或加排他锁的操作,都会等待我们解锁或直接抛出异常。

在这里插入图片描述
根据从上面的概念描述我们可以发现:

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

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

1.3 乐观锁基于Atomic类的CAS实现:

package javatest;

import java.util.concurrent.atomic.AtomicInteger;

//乐观锁 CAS(compare and set)


//多线程争用的数据类
class Counter {
    //int count = 0; //使用基本数据类型而不用AtomicInteger进行并发操作是,会有并发问题
    
    //使用AtomicInteger代替基本数据类型
	原子性引用,使用原子性引用的方法进行数据更新时,插入方式自带原子性和CAS检测机制,可以避免并发性问题
    AtomicInteger count = new AtomicInteger(0);

    public int getCount() {
        //return count;
        return count.get();
    }


    public void add() {
        //count += 1;
        count.addAndGet(1);
    }

    public void dec() {
        //count -= 1;
        count.decrementAndGet();
    }
}

//争用数据做加操作的线程
class AddDataThread extends Thread {

    Counter counter;

    public AddDataThread(Counter counter) {
        this.counter = counter;
    }

    @Override
    public void run() {
        for (int i = 0; i < JavaAtomLock.LOOP; ++i) {
            counter.add();
        }
    }
}

//争用数据做减法操作的线程
class DecDataThread extends Thread {

    Counter counter;

    public DecDataThread(Counter counter) {
        this.counter = counter;
    }

    @Override
    public void run() {
        for (int j = 0; j < JavaAtomLock.LOOP; j++) {
            counter.dec();
        }
    }
}

public class JavaAtomLock {
    final static int LOOP = 10000;

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread addThread = new AddDataThread(counter);
        Thread decThread = new DecDataThread(counter);
        addThread.start();
        decThread.start();
        addThread.join();
        decThread.join();
        System.out.println(counter.getCount());
    }

}


package javatest;

import java.util.concurrent.atomic.AtomicReference;

public class JavaAtomLock2 {

	//普通引用, 在没有加synchronized或者其他锁时,运行多线程会有并发问题
	private static myName myName = new myName("Kevin", 40);
	//原子性引用,使用原子性引用的方法进行数据更新时,插入方式自带原子性和CAS检测机制,可以避免并发性问题
	private static AtomicReference<myName> aRmyName = new AtomicReference<myName>(myName);

	static class Task1 implements Runnable {
		Object obj = new Object();

		public void run() {
			while (true) {
				//synchronized (obj) {
					//在不加synchronized悲观锁的情况下, 下面代码会出现并发问题
//					myName.setAge(20);
//					try {
//						Thread.currentThread().sleep(100);
//					} catch (InterruptedException e) {
//						// TODO Auto-generated catch block
//						e.printStackTrace();
//					}
//					myName.setName("Tom1");
//					System.out.println("Thread1 Values " + myName.toString());
				
					//在不加synchronized悲观锁的情况下, 下面代码表面是一条命令(上面是2条),但其实不具备原子性,
					//除了x=1的赋值是原子性, 下面这种不是, 所以一样有并发问题
					//myName = new myName("Tom1", 10);
					//System.out.println("Thread1 Values " + myName.toString());
					
					//Atomic类通过CAS(无锁算法)实现乐观锁
					if (aRmyName.compareAndSet(aRmyName.get(), new myName("Tom1", 10))) {
						System.out.println("Thread1 Values " + aRmyName.get().toString());
	                }
					
					
					
				
					
				//}
			}
		}
	}

	static class Task2 implements Runnable {
		Object obj = new Object();

		public void run() {
			while(true) {
			//synchronized (obj) {
				//在不加synchronized悲观锁的情况下, 下面代码会出现并发问题
//				myName.setAge(40);
//				try {
//					Thread.currentThread().sleep(100);
//				} catch (InterruptedException e) {
//					// TODO Auto-generated catch block
//					e.printStackTrace();
//				}
//				myName.setName("Tom2");
				
//				System.out.println("Thread2 Values " + myName.toString());
				
				//在不加synchronized悲观锁的情况下, 下面代码表面是一条命令(上面是2条),但其实不具备原子性,
				//除了x=1的赋值是原子性, 下面这种不是, 所以一样有并发问题
				//myName = new myName("Tom2", 20);
				//System.out.println("Thread2 Values " + myName.toString());
				
				//Atomic类通过CAS(无锁算法)实现乐观锁,在不加悲观锁的情况下compareAndSet自带原子性操作,避免并发问题
				if (aRmyName.compareAndSet(aRmyName.get(), new myName("Tom2", 20))) {
					System.out.println("Thread2 Values " + aRmyName.get().toString());
                }
				
				
			
				
			//}
		}
		}
	}
	
	static class Task3 implements Runnable {
		Object obj = new Object();

		public void run() {
			while (true) {
				//synchronized (obj) {
					//在不加synchronized悲观锁的情况下, 下面代码会出现并发问题
//					myName.setAge(20);
//					try {
//						Thread.currentThread().sleep(100);
//					} catch (InterruptedException e) {
//						// TODO Auto-generated catch block
//						e.printStackTrace();
//					}
//					myName.setName("Tom1");
//					System.out.println("Thread1 Values " + myName.toString());
					
					//在不加synchronized悲观锁的情况下, 下面代码表面是一条命令(上面是2条),但其实不具备原子性,
					//除了x=1的赋值是原子性, 下面这种不是, 所以一样有并发问题
					//myName = new myName("Tom3", 30);
					//System.out.println("Thread3 Values " + myName.toString());
				
					//Atomic类通过CAS(无锁算法)实现乐观锁
					if (aRmyName.compareAndSet(aRmyName.get(), new myName("Tom3", 30))) {
						System.out.println("Thread3 Values " + aRmyName.get().toString());
	                }
										
				//}
			}
		}
	}

	public static void main(String[] args) throws InterruptedException {
		// TODO Auto-generated method stub
		
		//System.out.println("Init name is " + myName.toString());
		System.out.println("Atomic Init name is " + aRmyName.get().toString());
		
		Thread Thread1 = new Thread(new Task1());
		Thread Thread2 = new Thread(new Task2());
		Thread Thread3 = new Thread(new Task3());

		Thread1.start();
		Thread2.start();
		Thread3.start();
		// Thread3.start();
		// Thread4.start();
		// Thread5.start();
		//
		Thread1.join();
		Thread2.join();
		Thread3.join();
		// Thread3.join();
		// Thread4.join();
		// Thread5.join();

		//System.out.println("Now name is " + myName.toString());
		System.out.println("Atomic Now name is " + aRmyName.get().toString());

	}

}

class myName{
	
	private String myname;
	private int age;
		
	public myName(String name, int age){
		this.myname = name;
		this.setAge(age);
	}
	
	public void setName(String name){
		this.myname= name;
	}
		
	public String getName(){
		return myname;
	}
	
	public String toString() {
        return "[name: " + this.myname + " age: " + this.age + "]";
    }

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}
	
}

1.4 悲观锁基于Syschronized实现:

package javatest;

//悲观锁-synchronized
class ticket implements Runnable {
	int ticket = 100;
	Object obj = new Object();

	public void run() {
		while (true) {
			synchronized (obj) {
				if (ticket > 0) {
					try {
						Thread.currentThread().sleep(100);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName() + ": " + ticket--);
				} else
					break;
			}
		}
	}
}

public class testsysn {

	public static void main(String[] args) {
		// TODO Auto-generated method stub

		ticket p = new ticket();
		Thread t1 = new Thread(p);
		t1.start();
		Thread t2 = new Thread(p);
		t2.start();

	}

}

1.5 悲观锁基于Reentrantlock实现:

package javatest;

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

//悲观锁-reentrantlock
class ticketNumber implements Runnable {
	int ticket = 100;
	Lock thisLock = new ReentrantLock();

	public void run() {
		while (true) {
			thisLock.lock();
			try {
				if (ticket > 0) {

					Thread.currentThread().sleep(100);
					System.out.println(Thread.currentThread().getName() + ": " + ticket--);
				} else
					break;
				
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				} finally {
					
					thisLock.unlock();
				}

		}
	}
}

public class testReentrantlLock {

	public static void main(String[] args) {
		// TODO Auto-generated method stub

		ticketNumber p = new ticketNumber();
		Thread t1 = new Thread(p);
		t1.start();
		Thread t2 = new Thread(p);
		t2.start();

	}

}

1.6 Synchronized和ReentrantLock的区别

相似点: 这两个同步方式有很多相似之处,他们都是加锁方式同步,而且都是阻塞式同步,也就是说当一个线程获取对象锁之后,进入同步块,其他访问该同步块的线程都必须阻塞在该同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的(操作系统需要在用户态和内核态之间来回切换,代价很高,不过可以通过对锁优化进行改善)。

功能区别: 这两种方式最大的区别就是对于synchronized来说,它是Java语言关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock他是jdk1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句来完成
Synchronized:
等待不可中断,可能会产生死锁
非公平锁, 获得锁的线程和等待时间无关,随机
要么随机唤醒一个线程要么唤醒全部线程。
ReentrantLock:
等待可中断, 避免死锁(lock.lockInterruptibly())
默认非公平锁, 可通过参数设为公平锁(ReentrantLock reentrantLock = new ReentrantLock(true)😉
一个ReentrantLock对象可以同时绑定对个对象。ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们
ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁

便利性: 很明显Synchronized的使用方便简洁,并且由编译器去保证锁的加锁和释放锁,而ReentrantLock则需要手动声明加锁和释放锁的方法,为了避免忘记手动释放锁,最好是在finally中声明释放锁。

锁的细粒度和灵活度: 很明显ReenTrantLock优于Synchronized

性能区别: 在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。

2. 自旋锁 VS 适应性自旋锁

在这里插入图片描述
如果当前资源已经被其他线程占用(被加锁), 现有线程有2个选择:

  1. 阻塞, 放弃CPU时间片,等待CPU通知资源已被释放
    好处: 不会长时间占住CPU时间片
    坏处: 如果等待时间短, 阻塞和唤醒的消耗会大于等待的时间或者代码执行时间

  2. 自旋等待, 不放弃CPU时间片, 等待自旋结束后看资源是不是被释放
    好处: 如果确实等待时间很短资源就被释放了, 节约了阻塞和唤醒的时间消耗
    坏处; 如果等待时间不确定或者太长, 大量占用CPU时间片而没有进展

自旋锁的实现原理也是CAS算法

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

3. 无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁

这四种锁是指锁的状态,专门针对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字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

无锁

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

无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。上面我们介绍的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个线程占用时, 为提高长时间占用的效率, 资源状态变为偏向锁, 该线程会自动获得锁
轻量级锁: 资源有1个线程占用时来了另外一个线程竞争, 资源状态变为轻量级锁,告诉竞争线程通过自旋等待, 不用阻塞
重量级锁: 当竞争线程>1时或者第1个竞争者自旋结束还没有获得锁,资源状态变为重量级锁, 告诉所有没有得到锁的线程阻塞等待

4. 公平锁/非公平锁

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

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

5. 可重入锁 VS 非可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。下面用示例代码来进行分析:
在这里插入图片描述
在上面的代码中,类中的两个方法都是被内置锁synchronized修饰的,doSomething()方法中调用doOthers()方法。因为内置锁是可重入的,所以同一个线程在调用doOthers()时可以直接获得当前对象的锁,进入doOthers()进行操作。

如果是一个不可重入锁,那么当前线程在调用doOthers()之前需要将执行doSomething()时获取当前对象的锁释放掉,实际上该对象锁已被当前线程所持有,且无法释放。所以此时会出现死锁。

public class MyLockTest implements Runnable {
    public synchronized void get() {
        System.out.println("2 enter thread name-->" + Thread.currentThread().getName());
        //reentrantLock.lock();
        System.out.println("3 get thread name-->" + Thread.currentThread().getName());
        set();
        //reentrantLock.unlock();
        System.out.println("5 leave run thread name-->" + Thread.currentThread().getName());
    }

    public synchronized void set() {
        //reentrantLock.lock();
        System.out.println("4 set thread name-->" + Thread.currentThread().getName());
        //reentrantLock.unlock();
    }

    @Override
    public void run() {
        System.out.println("1 run thread name-->" + Thread.currentThread().getName());
        get();
    }

    public static void main(String[] args) {
        MyLockTest test = new MyLockTest();
        for (int i = 0; i < 10; i++) {
            new Thread(test, "thread-" + i).start();
        }
    }

}

6. 独享锁/共享锁(互斥锁/读写锁)

独享锁: 该锁一次只能被一个线程所持有。
共享锁: 该锁可被多个线程所持有。

Synchronized:独享锁
ReentrantLock:独享锁。
ReentrantReadWriteLock.ReadLock: 读锁是共享锁,
ReentrantReadWriteLock.WriteLock 写锁是独享锁。
读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。

独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。

package javatest;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class testReentrantReadWriteLock {

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Cache.put("key", new String(Thread.currentThread().getName() + " joke"));
                }
            }, "threadW-" + i).start();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Cache.get("key"));
                }
            }, "threadR-" + i).start();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Cache.clear();
                }
            }, "threadC-" + i).start();
        }
    }
}

class Cache {
    static Map<String, Object> map = new HashMap<String, Object>();
    static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    static Lock r = rwl.readLock();
    static Lock w = rwl.writeLock();

    // 获取一个key对应的value
    public static final Object get(String key) {
        r.lock();
        try {
            System.out.println("get " + Thread.currentThread().getName());
            return map.get(key);
        } finally {
            r.unlock();
        }
    }

    // 设置key对应的value,并返回旧有的value
    public static final Object put(String key, Object value) {
        w.lock();
        try {
            System.out.println("put " + Thread.currentThread().getName());
            return map.put(key, value);
        } finally {
            w.unlock();
        }
    }

    // 清空所有的内容
    public static final void clear() {
        w.lock();
        try {
            System.out.println("clear " + Thread.currentThread().getName());
            map.clear();
        } finally {
            w.unlock();
        }
    }
}

7. 分段锁

分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。

我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7和JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。

当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在哪一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。

但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。

分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

AQS(AbstractQueuedSynchronized)

AbstractQueuedSynchronized 抽象队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch
在这里插入图片描述
AQS维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。

state的访问方式有三种:
1 getState()
2 setState()
3 compareAndSetState()

AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。

不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:

1 isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
2 tryAquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
3 tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
4 tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
5 tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其他线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多少次,这样才能保证state是能回到零态的。

再以CountDownLatch为例,任务分为N个子线程去执行,state为初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会await()函数返回,继续后余动作。

一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。

CAS(Compare and Swap)

CAS(Compare and Swap 比较并交换)是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其他线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

CAS操作中包含三个操作数——需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B,否则处理器不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值(在CAS的一些特殊情况下将仅返回CAS是否成功,而不提取当前值)。CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可”。这其实和乐观锁的冲突检查+数据更新的原理是一样的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值