一、JAVA锁的分类
JAVA锁可以分为乐观锁、悲观锁、自旋锁。
1.1 乐观锁
乐观锁是认为共享对象读多写少,遇到并发写的可能性很低的乐观心态,每次去拿对象的时候,都认为别人不会修改,所以不会上锁,但是在更新的时候,会先获取共享对象的版本号,比较跟上一次的版本号是否一致,如果不一致,则需要重复“读取对象-比较-写“的操作,如果一致,就直接更新。
java中乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值和传入值是否一样,一样则成功,不一样则失败。
1.2 悲观锁
悲观锁是任务共享对象是写操作比较多,并发更新对象的肯能性比较高的悲观心态。每次拿数据的时候,都认为别人会修改数据,所以每次读写数据的时候,都会上锁,这样别人想要读写数据时,会block,一直等到拿到锁。java中悲观锁,比如Synchronized和RetreenLock。
1.3 自旋锁
乐观锁和悲观锁都需要挂起线程进入阻塞,再唤醒的操作,比较消耗内核
自旋锁,是认为持有共享对象锁的线程会很快释放锁,那些等待锁的线程就不需要做内核态和用户态的切换进入阻塞挂起状态,直接等一等(自旋)即可重新申请获取锁。
自旋锁有利有弊,虽然不需要阻塞-唤醒状态的切换,但是需要一直占用CPU。如果其他线程处理时间比较长,会有大量并发线程自旋而占用CPU,影响系统性能,所以jdk1.6引入适应性自旋锁,通过设置自旋锁时间阈值,即自旋多少次后,立即释放CPU,来解决这个问题。
二、Java常用所介绍
2.1 Synchronized同步锁
synchronized可以把任一一个非NULL对象当做锁。它属于独占的悲观锁,属于可重入锁。
synchronized三种加锁方式,以及作用范围:
2.1.1 synchronized作用于非静态方法
作用范围是该类的实例(this)。
举个反面的例子,如下代码,synchronized作用于Demo的非静态方法,锁住的是类的独有实例,main方法中,线程都是独自拥有不同的实例,所以并没有达到锁的目的。由于num++非原子操作,不是线程安全的操作,所以在没有正确锁住计算时,输出结果非30000。
public class Demo {
static int num = 0;
public synchronized void m1(){
for (int i = 0;i<10000;i++){
num++;
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
Demo demo1 = new Demo();
demo1.m1();
});
Thread t2 = new Thread(()->{
Demo demo1 = new Demo();
demo1.m1();
});
Thread t3 = new Thread(()->{
Demo demo1 = new Demo();
demo1.m1();
});
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println(num);
}
正确的做法应该让main方法中三个线程使用同一个实例,如下代码
public class Demo {
static int num = 0;
public synchronized void m1(){
for (int i = 0;i<10000;i++){
num++;
}
}
public static void main(String[] args) throws InterruptedException {
Demo demo1 = new Demo();
Thread t1 = new Thread(demo1::m1);
Thread t2 = new Thread(demo1::m1);
Thread t3 = new Thread(demo1::m1);
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println(num);
}
}
synchronized作用于非静态的方法,相当于作用于类的实例,等同于以下代码
public class Demo {
static int num = 0;
public void m1(){
synchronized (this){
for (int i = 0;i<10000;i++){
num++;
}
}
}
....
}
2.1.2 synchronized作用于类的静态方法
由于静态方法和类的一对一性,synchronized作用于类的静态方法时,相当于锁住了class的Class对象,由于Class对象属于持久代(JDK1.8后修改为元数据区),唯一性,所以相当于是锁住了所有调用的线程。
如下代码可正常锁住调用的线程,正常输出30000
public class Demo1 {
static int num = 0;
public synchronized static void m1(){
for (int i = 0;i<10000;i++){
num++;
}
}
public static class T1 extends Thread{
@Override
public void run() {
Demo1.m1();
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new T1();
Thread t2 = new T1();
Thread t3 = new T1();
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println(num);
}
}
静态方法加synchronized,相当于锁住类,同以下代码,下面代码可正常锁住调用线程,输出30000
public class Demo {
static int num = 0;
public void m1(){
synchronized (Demo.class){
for (int i = 0;i<10000;i++){
num++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
Demo demo1 = new Demo();
demo1.m1();
});
Thread t2 = new Thread(()->{
Demo demo1 = new Demo();
demo1.m1();
});
Thread t3 = new Thread(()->{
Demo demo1 = new Demo();
demo1.m1();
});
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println(num);
}
}
2.1.3 synchronized作用于一个对象实例
synchronized作用于一个对象,锁住的是所有以该对象为锁的代码块。
2.2 ReentrantLock
ReentrantLock继承接口Lock,并实现接口中定义的方法,能够实现synchronized所有功能,还提供了可响应中断锁、可轮询锁请求、定时锁等避免死锁的的方法。
首先比较下ReentrantLock和synchronized。
(1)synchronized和ReentrantLock都属于独占锁,但是synchronized加锁和解锁是自动进行,易于操作,但不够灵活,ReentrantLock的加锁和解锁需要手动进行,不易操作,但是很灵活。
(2)synchronized和ReentrantLock都属于可重入锁,synchronized加锁和解锁是自动进行,不必担心最后锁是否释放;ReentrantLock是手动加锁和解锁,如果代码中不手动调用释放锁方法,其他线程将无法获取到锁。
(3)synchronized不可响应中断,即一个线程申请锁时,如果申请不到,将一直在等待队列里;ReentrantLock可以响应中断,这样也可以防止死锁的问题。
ReentrantLock相较于synchronized,优势在于响应中断、公平锁、限时响应。
样例展示
2.2.1 ReentrantLock基本使用
public class ReentrantLockTest {
private static final Lock lock = new ReentrantLock();
public static void test(){
try {
lock.lock();
System.out.println(Thread.currentThread().getName()+"获取到锁");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName()+"释放锁");
lock.unlock();
}
}
public static void main(String[] args){
new Thread(ReentrantLockTest::test,"线程A").start();
new Thread(ReentrantLockTest::test,"线程B").start();
}
}
2.2.2 ReentrantLock实现公平锁
公平锁,即等待时间最长的线程,优先获取到锁。
公平锁因为要维护等待队列,所以会牺牲部分性能,据统计,非公平锁(竞争锁),性能是公平锁的5~10倍。
示例代码如下:
package com.mylean.syn;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @description:
* @author: cc
* @create: 2021-07-18 22:05
**/
public class ReentrantLockTest1 {
//参数true代表开启公平锁
private static final Lock lock = new ReentrantLock(true);
public static void Test(){
for (int i=0;i<2;i++){
try {
lock.lock();
System.out.println(Thread.currentThread().getName()+"获取了锁");
TimeUnit.SECONDS.sleep(2);
}catch (InterruptedException e){
e.printStackTrace();
} finally {
//System.out.println(Thread.currentThread().getName()+"释放锁");
lock.unlock();
}
}
}
public static void main(String[] args){
new Thread(ReentrantLockTest1::Test,"线程A").start();
new Thread(ReentrantLockTest1::Test,"线程B").start();
new Thread(ReentrantLockTest1::Test,"线程C").start();
new Thread(ReentrantLockTest1::Test,"线程D").start();
new Thread(ReentrantLockTest1::Test,"线程E").start();
}
}
以上代码输出结果如下,可以看出第一次获取锁的顺序为A-B-D-C-E和第二期获取锁的打印顺序一样,说明是按等待时间长优先分配锁,实现公平。
线程A获取了锁
线程B获取了锁
线程D获取了锁
线程C获取了锁
线程E获取了锁
线程A获取了锁
线程B获取了锁
线程D获取了锁
线程C获取了锁
线程E获取了锁
2.2.3 响应中断功能
响应中断功能,是当有线程请求ReentrantLock锁资源时,获取不到锁,不会一直傻傻的等待,ReentrantLock会给一个中断回应
如下,线程A中断后,锁lock1自动释放,线程B可获取到锁,正常执行完成。
public class ReentrantLockTest3 {
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
public static void main(String[] args){
Thread thread = new Thread(new ThreadDemo(lock1,lock2),"线程A");
Thread thread1 = new Thread(new ThreadDemo(lock2,lock1),"线程B");
thread.start();
thread1.start();
thread.interrupt();
}
static class ThreadDemo implements Runnable{
Lock firstLock;
Lock secondLock;
public ThreadDemo (Lock firstLock,Lock secondLock){
this.firstLock = firstLock;
this.secondLock = secondLock;
}
@Override
public void run() {
try {
firstLock.lockInterruptibly();
TimeUnit.MILLISECONDS.sleep(50);
secondLock.lockInterruptibly();
} catch (InterruptedException e){
e.printStackTrace();
} finally {
firstLock.unlock();
secondLock.unlock();
System.out.println(Thread.currentThread().getName()+"获取到了资源正常结束!");
}
}
}
}
2.2.4 限时等待
ReentrantLock可设置等待时间,通过tryLock()方法设置,可传入参数,表示等待时长,不传入参数,表示立即返回。
2.3 ReadWriteLock读写锁
独占锁,在对代码加锁的时候,不仅仅限制了更新操作,也显示了读操作,有时并不需要,只需要在写数据的时候,读数据的线程不能读取,没有写数据的时候,读数据的线程可以并发读取。
ReadWriteLock实现了该功能,当有一条线程获取读锁时,其他线程也能同时获取读锁,当有一条线程获取写锁,正在写时,其他线程不能写也不能读。同时写锁的级别高于读锁,也就是有线程获取写锁,持有读锁的线程挂起。具体的实现是ReentrantReadWriteLock。
读 | 写 | |
读 | 允许 | 不允许 |
写 | 不允许 | 不允许 |
代码实例,Thread2需要等Thread3写完,才会获取读锁,打印日志。
public class ReentrantReadWriteLockTest {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(counter.get());
}
},"线程1");
Thread thread3 = new Thread(new Runnable() {
@Override
public void run() {
counter.inc(2);
}
},"线程3");
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(counter.get());
}
},"线程2");
thread1.start();
thread3.start();
thread3.join();
thread2.start();
}
static class Counter{
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock rLock = rwLock.readLock();
private final Lock wLock = rwLock.writeLock();
private int[] counters = new int[10];
public void inc(int index){
wLock.lock();
try {
counters[index] +=1;
System.out.println(Thread.currentThread().getName()+"占用锁");
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
wLock.unlock();
}
}
public int[] get(){
rLock.lock();
try {
return Arrays.copyOf(counters,counters.length);
}finally {
rLock.unlock();
}
}
}
}