线程同步-锁

锁概述

锁是解决线程安全问题的最基本的解决方案。

加锁的代码块,只允许持有锁的线程通过,其他线程会进入同步队列阻塞。持有锁的线程执行完代码块后释放锁。才会唤醒同步队列的线程抢夺锁。通过加锁,同一时间只允许一个线程(持有锁的线程)执行加锁的代码。使得会发生线程安全问题的代码单线程串行。从而解决线程安全问题。

image

锁的分类

公平锁和非公平锁

公平锁是按照锁申请的顺序来分配锁资源。

非公平锁是允许插队的,可能后申请的线程比先申请的线程优先获取锁。

1
2
3
java中锁一般默认都是非公平锁。因为公平锁为了保障公平往往会增加线程的唤醒和暂停。例如一个
运行中的线程要获取锁必须先检查有没有其他排队的线程,有就需要一次暂停。而非公平锁,在一个运行中的线程申请锁时有可能直接获取锁。所以公平锁的开销比
公平锁大。公平锁适用于锁持有时间相对长或者线程申请锁平均间隔时间相对大的情况。

独占锁和共享锁

独占锁,顾名思义,独占锁只允许一个线程持有。

共享锁,共享锁允许多个线程持有。读写锁ReadWriteLock中的读锁就是共享锁

注意:当有线程持有写锁时,不允许其他线程获取读锁。

内部锁 synchronized

内部锁是一种排他锁,可以保证原子性,可见性和有序性。

通过synchronized关键字使用。有三种用法:
  1. 同步代码快
1
2
3
4
5
6
7
8
9
10
   private int i=0;
private final Object o=new Object();
public void test(){
//可以用一任意对象作为锁。用同个对象锁的synchronized代码块,只有获取到该对象锁的才能进去。
synchronized (o) {
while(i<100){
i++;
}
}
}

2.同步方法

1
2
3
4
5
6
7
8
   private int i=0;
//同步方法的锁对象用的是this
public synchronized void test1(){

while(i<100){
i++;
}
}

3.同步静态方法

1
2
3
4
5
6
7
8
   private static int i=0;
//静态同步方法的锁对象是该类字节码对象,XX.class
public synchronized static void test2(){

while(i<100){
i++;
}
}

显示锁 Lock

显示锁是1.5加入jdk的。作用与内部锁相同,用于作为线程同步机制。它提供了内部锁没有的特效,但并不能替代内部锁。

Lock使用

一个Lock接口的实例就是一个显示锁。通过lock方法加锁,和unlock方法释放锁。类java.util.concurrent.locks.ReentrantLock是Lock的实现。Lock既支持公平锁,也支持非公平锁。默认使用非公平锁。可以通过构造函数设置 new ReentrantLock(boolean isFair)

Lock API
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface Lock {

//获取锁
void lock();

//如果线程未被中断,则获取锁
void lockInterruptibly() throws InterruptedException;

//锁没被占用才能获取锁
boolean tryLock();

//在给定时间内获取空闲锁
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

//释放锁
void unlock();

//获取监视器
Condition newCondition();
}
Lock使用

注意:锁的释放一定要在finally里释放,防止锁泄露。

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
31
32
33
34
   //新建一个锁对象 
Lock lock =new ReentrantLock();
private static int i=0;
@Test
public void test01(){

Runnable runnable = new Runnable(){

public void run() {
//获取锁
lock.lock();
try{

while(i<100){
System.out.println(i++);
}
}finally{
//释放锁
lock.unlock();
}
};

};
Thread thread = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread.start();
thread2.start();
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

读写锁

读写锁是一种改进型的排他锁。将读和写的锁分离。可以多个线程进行读操作,但是读操作时不允许写操作。一个线程进行写操作时,其他线程不能进行读和写操作。

读写锁分为读锁和写锁

  1. 读锁:一个线程持有读锁不会妨碍其他线程获取读锁。
  2. 写锁:一个线程持有写锁,其他线程无法获取读锁和写锁。
读写锁使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private final ReadWriteLock readWriteLock= new ReentrantReadWriteLock();
private final Lock readLock = readWriteLock.readLock();
private final Lock writeLock = readWriteLock.writeLock();
public void read(){
//获取读锁
readLock.lock();
try{
//读取共享变量
}finally{
readLock.unlock();
}
}

public void write(){
//获取写锁
writeLock.lock();
try{
//写共享变量
}finally{
writeLock.unlock();
}
}

读写锁适用于读操作比写操作频繁且读操作持有锁时间比较长的情况。

内部锁和显示锁的区别

  1. 内部锁基于代码块的锁,没有灵活性可言。显示锁可以灵活使用,但也意味容易出错。例如显示锁容易导致锁泄露(即锁没有释放),内部锁不存在锁泄露。
  2. 内部锁获取锁只能阻塞(相当于显示锁的lock)。显示锁可以有tryLock方法。有闲置的锁就可以获取锁放回true,否则返回false,不会造成阻塞。
  3. 内部锁的线程通信 notify/notifyAll/wait。显示锁可以通过一个或多个Condition来处理等待唤醒,更加灵活。

锁适用情况

多线程共享一组数据,一个线程有以下操作时

  1. check-then-act,读取共享数据判断下个操作是什么。
  2. read-modify-write,读取共享数据,修改再写回。如:i++
  3. 多个线程对多个共享数据更新,如果多个共享数据是有关联的。如:服务器ip,端口。就需要加锁保持原子性。