JAVA锁详解

JAVA锁详解

锁是为了避免多个线程并发访问同一个资源时出现异常情况,也就是在并发控制中满足互斥的要求;

而在JAVA中,提供了两种锁机制来限制线程的并发访问,一种是由JVM实现的synchronized关键字,而另一种则是由JDK实现的Lock接口。

几种常见的锁

乐观锁与悲观锁

乐观锁:乐观锁总是会假设最好的情况,每次去获取数据的时候都认为其他线程不会修改数据,所以不会上锁,只是在更新的时候会判断在此期间其他线程有没有更新这个数据;

悲观锁:悲观锁总是会假设最坏的情况,每次去获取数据的时候都认为其他线程会修改数据,所以会上锁,直到这个线程完成操作;

公平锁与非公平锁

公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁;

非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁;

可重入锁

可重入锁,也叫做递归锁,是指在一个线程中可以多次获取同一把锁。

synchronized关键字

synchronized关键字提供了一种简单的互斥锁,被synchronized修饰的方法或者代码块会同步执行,保证同一时刻只有一个线程会执行某个同步方法或同步代码块;

synchronized关键字提供了三种使用方式:

修饰方法:锁的是拥有实例方法的对象,进入同步方法时需要获取当前对象的锁;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class DemoTest extends Thread {
//共享资源
static int count = 0;

/**
* synchronized 修饰实例方法
*/
public synchronized void increase() throws InterruptedException {
sleep(1000);
count++;
System.out.println(Thread.currentThread().getName() + ": " + count);
}
@Override
public void run() {
try {
increase();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
DemoTest test = new DemoTest();
Thread t1 = new Thread(test);
Thread t2 = new Thread(test);
t1.setName("threadOne");
t2.setName("threadTwo");
t1. start();
t2. start();
}
}

TIPS:这里需要注意,如果synchronized修饰的是普通方法,则锁的是对象,如果在非单例模式下,调用不同对象的同步方法,会导致锁失效。

修饰静态方法:锁的时方法对应的class对象,进入同步方法时需要获取class对象的锁;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class DemoTest extends Thread {
//共享资源
static int count = 0;

/**
* synchronized 修饰实例方法
*/
public static synchronized void increase() throws InterruptedException {
sleep(1000);
count++;
System.out.println(Thread.currentThread().getName() + ": " + count);
}
@Override
public void run() {
try {
DemoTest.increase();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
DemoTest test = new DemoTest();
Thread t1 = new Thread(test);
Thread t2 = new Thread(test);
t1.setName("threadOne");
t2.setName("threadTwo");
t1. start();
t2. start();
}
}

修饰代码块:锁的是给定的对象;

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* synchronized 修饰实例方法
*/
static final Object objectLock = new Object(); //创建一个对象锁
public static void increase() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "获取到锁,其他线程在我执行完毕之前,不可进入。" );
synchronized (objectLock) {
sleep(1000);
count++;
System.out.println(Thread.currentThread().getName() + ": " + count);
}
}

Lock接口

相较于synchronized关键字,Lock 接口支持那些语义不同(重入、公平等)的锁规则,可以在非阻塞式结构的上下文(包括 hand-over-hand 和锁重排算法)中使用这些规则;主要的实现是 ReentrantLock

Lock接口的方法:

1
2
3
4
5
6
7
8
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}

方法介绍:

  • lock():获取锁。如果锁不可用,出于线程调度目的,将禁用当前线程,并且在获得锁之前,该线程将一直处于休眠状态;
  • lockInterruptibly():如果当前线程未被中断,则获取锁;
  • tryLock():仅在调用时锁为空闲状态才获取该锁。如果锁可用,则获取锁,并立即返回值 true。如果锁不可用,则此方法将立即返回值 false
  • tryLock(long time, TimeUnit unit):如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁;
  • unlock():释放锁。在等待条件前,锁必须由当前线程保持。调用 Condition.await () 将在等待前以原子方式释放锁,并在等待返回前重新获取锁;
  • newCondition():返回绑定到此 Lock 实例的新 Condition 实例。

使用方式:

1
2
3
4
5
6
7
8
9
Lock lock = new ReentrantLock();
lock.lock();
try {
//TODO
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}

死锁

如果两个线程互相申请对方所持有的资源,而进入等待状态,由于线程被无限期的阻塞,所以程序无法正常终止,这就是死锁:

死锁的四个必要条件:

  • 互斥条件:任意资源在同一时刻只能由一个线程访问;
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放;
  • 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源;
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
作者

ero

发布于

2022-04-08

更新于

2022-06-11

许可协议

评论