洋蔥

贪婪,找不到比这更好的词了,是件好事。

合理的使用Java多线程可以更好地利用服务器资源。一般来讲,线程内部有自己私有的线程上下文,互不干扰。但是当需要多个线程间相互协作时,就需要掌握线程的通信方式。

阅读全文 »

Java中锁的分类有:

  • 可中断锁
  • 可重入锁(递归锁)
  • 公平锁/非公平锁
  • 独占锁(互斥锁)/共享锁
  • 乐观锁/悲观锁
  • 自旋锁
  • 偏向锁/轻量级锁/重量级锁(锁的状态)
  • 分段锁(锁的设计)

上面是很多锁的名词,这些分类并不是全是指锁的状态,有的指锁的特性,有的指锁的设计,下面总结的内容是对每个锁的名词进行一定的解释。

synchronized与Lock对比

synchronized Lock
别名 隐式锁 显式锁
实现方式 Java中的关键字,语言内置实现 是一个接口
锁的释放方式 同步代码执行结束或抛出异常时自动释放 finally 块中手动释放
多个锁嵌套的释放顺序 依次获取(释放)锁,
最先获取的最后释放
自由获取(释放)锁
发生异常时 自动释放线程占有的锁,不会导致死锁 需在 finally 块中手动释放锁,否则会导致死锁
定时阻塞获取锁 不支持 支持
判断是否已成功获取锁 无法办到 tryLock() 返回值是 boolean
性能 竞争激烈时(大量线程同时获取锁),性能更优
可中断锁
(获取锁时能否响应线程中断指令)

tryLock(long time, TimeUnit unit)
lockInterruptibly()
可重入锁
公平锁 非公平锁,无法判断锁是否公平 默认非公平锁。
实现类的构造方法可指定锁的公平性。isFair() 判断锁是否公平。
独占锁(互斥锁)/共享锁 独占锁(互斥锁) 独占锁:synchronized、ReentrantLock、ReentrantReadWriteLock.WriteLock
共享锁:ReentrantReadWriteLock.ReadLock
通过 AQS(AbstractQueuedSynchronizer)实现独占和共享。
读写锁 ReentrantReadWriteLock.ReadLock
ReentrantReadWriteLock.WriteLock

可中断锁

中断锁指的是可中断获取锁的等待状态,也就是在获取锁时可以响应线程中断指令,从而中断获取锁。

如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。

在 Java 中,synchronized 是不可中断锁,Lock 是可中断锁

可重入锁(递归锁)(特性)

可重入互斥锁 - 维基百科

reentrant :可重入; 可重入的; 重入; 可再入的; 重进入;

可重入锁又名递归锁。特点:

  • 可重复进入持有相同锁的同步作用域(同步代码块、同步方法、Lock锁同步代码)。
  • 在一个线程中可以多次获取同一把锁,但也需要释放同样加锁次数的锁,即重入了多少次,就要释放多少次,不然也会导致锁不被释放。

如果不设计成可重入锁,反复给自己加锁就会产生死锁。

锁类型 锁对象
可重入锁 synchronized
可重入锁 ReentrantLock(Re entrant Lock 重新进入锁)
可重入锁 ReentrantReadWriteLock.ReadLock
可重入锁 ReentrantReadWriteLock.WriteLock
不可重入锁 没有内置对象,需自定义

可重入锁演示

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class Test {
public static void main(String[] args) {
new Test().同步代码块();
new Test().同步方法();
Test.静态同步方法();
Test.Lock锁();
}

// 可选择锁对象
public void 同步代码块() {
synchronized (this) {
System.out.println("同步代码块,第一次获取锁");
synchronized (this) {
System.out.println("同步代码块,第二次获取锁");
}
}
}

// 对象锁,锁对象是this
public synchronized void 同步方法() {
System.out.println("同步方法,第一次获取锁");
同步方法2();
}

public synchronized void 同步方法2() {
System.out.println("同步方法,第二次获取锁");
}


// 类锁,锁对象是当前类的Class对象
public static synchronized void 静态同步方法() {
System.out.println("静态同步方法,第一次获取锁");
静态同步方法2();
}

public static synchronized void 静态同步方法2() {
System.out.println("静态同步方法,第二次获取锁");
}

// Lock锁
public static void Lock锁() {
Lock lock = new ReentrantLock();
lock.lock();
System.out.println("ReentrantLock,第一次获取锁");
lock.lock();
System.out.println("ReentrantLock,第二次获取锁");
try {
// ... 同步代码
} finally {
lock.unlock();
lock.unlock();
}
}
}

不可重入锁演示

实际开发中几乎不会用到。

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public class Test {
public static void main(String[] args) {
Lock lock = new UnReentrantLock();
lock.lock();
System.out.println("UnReentrantLock,第一次获取锁");
lock.lock();
System.out.println("UnReentrantLock,第二次获取锁");
try {
// ... 同步代码
} finally {
lock.unlock();
lock.unlock();
}
}
}

/**
* 自定义一个不可重入锁
*/
class UnReentrantLock implements Lock {
// 当前绑定的线程,记录以获取锁的线程
private Thread bindThread;

@Override
public void lock() {
synchronized (this) {
// 当已有线程拿到锁时
while (bindThread != null) {
try {
// 使当前线程等待
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 绑定当前线程
this.bindThread = Thread.currentThread();
}
}

@Override
public void unlock() {
synchronized (this) {
// 当绑定的线程不是当前线程时
if (bindThread != Thread.currentThread()) {
return;
}
// 解绑线程
bindThread = null;
// 唤醒所有线程,唤醒的线程可以继续绑定了
notifyAll();
}
}

@Override
public void lockInterruptibly() throws InterruptedException {

}

@Override
public boolean tryLock() {
return false;
}

@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}

@Override
public Condition newCondition() {
return null;
}
}

公平锁/非公平锁

是否公平:指的是每个线程获取锁的机会是否平等。

公平锁:多个线程按照申请锁的顺序来获取锁。比如同时有多个线程在等待同一个锁,当这个锁被释放时,等待时间最久的线程(最先申请的线程)会获得该锁。

非公平锁:多个线程争抢同一个锁时,有可能后申请的线程比先申请的线程优先获取锁。可能造成优先级反转或者饥饿现象(导致某些线程永远获取不到锁)。优点是比公平锁吞吐量大。

对于 synchronized 而言,也是一种非公平锁。由于其并不像 ReentrantLock 是通过 AQS(AbstractQueuedSynchronizer) 的来实现线程调度,所以并没有任何办法使其变成公平锁。

对于 ReentrantLockReentrantReadWriteLock 实现而言,默认是非公平锁,构造方法可指定锁的公平性

在 ReentrantLock 和 ReentrantReadWriteLock 中都定义了两个静态内部类,分别用来实现非公平锁和公平锁。可以在创建对象时,通过构造方法指定锁的公平性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
// 同步锁的结构和 ReentrantLock 完全相同
}

public class ReentrantLock implements Lock, java.io.Serializable {
// 此锁的同步控制基础。下面分为公平和非公平版本。使用 AQS 状态来表示锁上的持有数量。
abstract static class Sync extends AbstractQueuedSynchronizer {
}
// 公平锁的同步实现
static final class FairSync extends Sync {
}
// 非公平锁的同步实现
static final class NonfairSync extends Sync {
}
}

非公平锁演示

下面示例中,每次都是 Thread-0 拿到锁,验证了 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
public class Test {
public static void main(String[] args) {
Task task = new Task();
Thread thread0 = new Thread(task);
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread0.start();
thread1.start();
thread2.start();
}
}

class Task implements Runnable {
@Override
public void run() {
while (true) {
// 同步锁为this的同步代码块
synchronized (this) {
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}

修改上面示例 Task 类如下,验证 Lock 的非公平锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Task implements Runnable {
/**
* 默认非公平锁
*/
private Lock lock = new ReentrantLock();

@Override
public void run() {
while (true) {
lock.lock();
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}

公平锁演示

修改上面示例 private Lock lock = new ReentrantLock(true); 即可。

判断锁是否公平

1
2
3
public final boolean isFair() {
return sync instanceof FairSync;
}

该方法只能判断 ReentrantLock、ReentrantReadWriteLock:

1
2
3
4
5
6
public class Test {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
System.out.println(lock.isFair()); // false
}
}

独占锁(互斥锁)/共享锁

排他锁 - 维基百科互斥锁 - 维基百科

共享锁 - 百度百科

读写锁 - 维基百科

独占锁(互斥):该锁一次只能被一个线程所持有。例如:synchronized、ReentrantLock、读写锁中的写锁 ReentrantReadWriteLock.WriteLock

共享锁(非互斥):该锁可被多个线程所持有。例如:读写锁中的读锁 ReentrantReadWriteLock.ReadLock,但读写、写读 、写写的过程是互斥的。

Lock 锁的 独占共享 是通过 AQS(AbstractQueuedSynchronizer)来实现的。

共享锁演示

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
35
36
public class Test {
public static void main(String[] args) {
Task task = new Task();
Thread thread0 = new Thread(task);
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread0.start();
thread1.start();
thread2.start();
}
}

class Task implements Runnable {
/**
* 读写锁
*/
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

/**
* 读锁
*/
private Lock readLock = this.readWriteLock.readLock();

@Override
public void run() {
this.readLock.lock();
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
}
}

独占锁演示

修改上述代码:

1
2
3
4
5
6
class Task implements Runnable {
/**
* 写锁
*/
private Lock writeLock = this.readWriteLock.writeLock();
}

乐观锁/悲观锁

乐观锁与悲观锁指的不是锁类型,而是指 并发策略 是乐观的还是悲观的。

乐观并发控制 - 维基百科

悲观并发控制 - 维基百科

悲观锁:只要编码中 利用到锁,并发策略就是悲观的。

乐观锁:无锁编程,通常采用 CAS 等原子操作来实现。典型的例子就是原子类,通过 CAS 自旋实现原子操作。

自旋锁

自旋锁 - 维基百科

自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,避免线程上下文切换带来的开销。但由于获取锁的线程一直处在运行状态,所以不适合单核单线程的CPU。

通常采用 CAS 等原子操作来实现,可以参考 自旋锁的实现

偏向锁/轻量级锁/重量级锁(锁的状态)

这三种锁是指锁的状态,并且是针对 synchronized。在 Java 5 通过引入锁升级的机制来实现高效 synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。

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

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

重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

分段锁(锁的设计)

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

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

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

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

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

推荐链接:

Java 实例 - 死锁及解决方法 - 菜鸟

概述

死锁:两个(或两个以上)线程在执行的过程中,因争夺资源产生的一种互相等待现象。

产生条件:

  • 两个(或两个以上)线程
  • 两个(或两个以上)锁
  • 两个(或两个以上)线程持有不同锁
  • 争夺对方的锁

编写死锁代码

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class Test {
public static void main(String[] args) {
Thread threadA = new Thread(new LockA());
Thread threadB = new Thread(new LockB());
threadA.start();
threadB.start();
}
}

class LockA implements Runnable {
@Override
public void run() {
printA();
}

public static synchronized void printA() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("A");
// 持有类锁(LockA.class),争夺对方的锁(LockB.class)
LockB.printB();
}
}

class LockB implements Runnable {
@Override
public void run() {
printB();
}

public static synchronized void printB() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("B");
// 持有类锁(LockB.class),争夺对方的锁(LockA.class)
LockA.printA();
}
}

Lock未主动释放锁

Semaphore信号嵌套

卖票

取钱

synchronized

继承Thread类

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public class Test {
public static void main(String[] args) {
Account account = new Account("260731000", 100L);

Draw yourself = new Draw(account, 50L, "Yourself");
Draw girlFriend = new Draw(account, 31L, "GirlFriend");

yourself.start();
girlFriend.start();
}
}

@Data
class Account {
private String name;
private long money;

public Account(String name, long money) {
this.name = name;
this.money = money;
}
}

class Draw extends Thread {
private Account account;

private long drawMoney; // 取款金额

public Draw(Account account, long drawMoney, String threadName) {
super(threadName);
this.account = account;
this.drawMoney = drawMoney;
}

@Override
public void run() {
// synchronized(被锁对象):锁的对象就是数据修改的对象
synchronized (account) {
if (account.getMoney() - drawMoney < 0) {
System.out.println(Thread.currentThread().getName() + "余额不足");
return;
}

// sleep模拟取钱过程
try {
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.printf("%s 账户余额: %d \t 取款金额: %d \t 剩余金额: %d \t 取款人: %s \n",
account.getName(),
account.getMoney(),
drawMoney,
account.getMoney() - drawMoney,
this.getName());

account.setMoney(account.getMoney() - drawMoney);
}
}
}

实现Runnable接口

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public class Test {
public static void main(String[] args) {
Account account = new Account("260731000", 100L);

Draw yourselfDraw = new Draw(account, 50L);
Thread yourself = new Thread(yourselfDraw, "Yourself");

Draw girlFriendDraw = new Draw(account, 31L);
Thread girlFriend = new Thread(girlFriendDraw, "GirlFriend");

yourself.start();
girlFriend.start();
}
}

@Data
class Account {
private String name;
private long money;

public Account(String name, long money) {
this.name = name;
this.money = money;
}
}

class Draw implements Runnable {
private Account account;

private long drawMoney; // 取款金额

public Draw(Account account, long drawMoney) {
this.account = account;
this.drawMoney = drawMoney;
}

@Override
public void run() {
// synchronized(被锁对象):锁的对象就是数据修改的对象
synchronized (account) {
if (account.getMoney() - drawMoney < 0) {
System.out.println(Thread.currentThread().getName() + "余额不足");
return;
}

// sleep模拟取钱过程
try {
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.printf("%s 账户余额: %d \t 取款金额: %d \t 剩余金额: %d \t 取款人: %s \n",
account.getName(),
account.getMoney(),
drawMoney,
account.getMoney() - drawMoney,
Thread.currentThread().getName());

account.setMoney(account.getMoney() - drawMoney);
}
}
}

ReentrantLock

有时候继承 Thread 类会更方便。

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
public class Test {
public static void main(String[] args) {
Account account = new Account("260731000", 100L);

Draw yourselfDraw = new Draw(account, 50L);
Thread yourself = new Thread(yourselfDraw, "Yourself");

Draw girlFriendDraw = new Draw(account, 31L);
Thread girlFriend = new Thread(girlFriendDraw, "GirlFriend");

yourself.start();
girlFriend.start();
}
}

@Data
class Account {
private String name;
private long money;
private final ReentrantLock lock = new ReentrantLock(true);

public Account(String name, long money) {
this.name = name;
this.money = money;
}
}

class Draw implements Runnable {
private Account account;

private long drawMoney; // 取款金额

public Draw(Account account, long drawMoney) {
this.account = account;
this.drawMoney = drawMoney;
}

@Override
public void run() {
// synchronized(被锁对象):锁的对象就是数据修改的对象
try {
account.getLock().lockInterruptibly();
if (account.getMoney() - drawMoney < 0) {
System.out.println(Thread.currentThread().getName() + "余额不足");
return;
}

// sleep模拟取钱过程
try {
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.printf("%s 账户余额: %d \t 取款金额: %d \t 剩余金额: %d \t 取款人: %s \n",
account.getName(),
account.getMoney(),
drawMoney,
account.getMoney() - drawMoney,
Thread.currentThread().getName());

account.setMoney(account.getMoney() - drawMoney);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
account.getLock().unlock();
account.getLock().unlock(); // 抛出: IllegalMonitorStateException – 如果当前线程没有持有这个锁
}
}
}

Java中与线程安全有关的类:安全的集合类、java.util.concurrent JUC包下类 等。

集合类

线程安全(Thread-safe)的集合对象:

  • Vector:单个操作是原子性的。但是如果两个原子操作复合而来,这个组合的方法是非线程安全的,需要使用锁来保证线程安全。
  • HashTable
  • CopyOnWriteArrayList:JUC包

如何把一个线程不安全的集合类变成一个线程安全的集合类?

1
2
3
4
5
6
7
public class ThreadDemo {
public static void main(String[] args) {
// Vector是线程安全的时候才去考虑使用的,但即使要安全,也不用
List<String> list1 = new ArrayList<String>();// 线程不安全
List<String> list2 = Collections.synchronizedList(list1); // 线程安全
}
}

字符串类

StringBuffer

原子类

CAS

包含三个操作数:当前内存值、预期原值、新值,如果内存值和预期原值匹配,就将内存值更改为新值;否则什么也不做。

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
35
36
37
38
public class Test {
public static volatile int i = 0;

public static void main(String[] args) {
Thread thread = new Thread() {
@Override
public void run() {
int expectedValue = i;
System.out.println("期望值: " + expectedValue);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// CAS操作
boolean result = compareAndSwap(i, expectedValue, i + 1);
System.out.println("设置是否成功: " + result);
System.out.println("新值: " + i);
}
};
thread.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
i = 5;
}

public static boolean compareAndSwap(int memoryValue, int expectedValue, int newValue) {
// 当预期原值等于内存值时
if (expectedValue == memoryValue) {
i = memoryValue;
return true;
}
return false;
}
}

ABA问题

假如一个值原来是A,现在变成了B,后来又变成了A,那么CAS检查时会发现它的值没有发生变化,但是实际上却变化了。

AtomicMarkableReference 是一个带修改标记的引用类型原子类。

AtomicStampedReference 是一个带版本号的引用类型原子类。

Lock接口

JUC.locks包

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface Lock {
// 这四个方法都可以获取锁
void lock();
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void lockInterruptibly() throws InterruptedException;

// 释放锁
void unlock();

// 用于等待通知(线程间通信)
Condition newCondition();
}

ReentrantLock

java.util.concurrent.locks.ReentrantLock

这个是 JDK @since 1.5 添加的一种颗粒度更小的锁,它完全可以替代 synchronized 关键字来实现它的所有功能,而且 ReentrantLock 锁的灵活度要远远大于 synchronized 关键字,它更清晰的表达了如何加锁释放锁

image-20210714185046829

lock() 阻塞

获取锁,平常使用最多的一个方法。

如果其他线程没有持有锁,则获取该锁并立即返回,将锁持有计数设置为 1。

如果当前线程已经持有锁,那么持有计数加一并且该方法立即返回。

如果该锁被另一个线程持有,那么当前线程将因线程调度目的而被禁用并处于休眠状态(等待获取锁),直到获得该锁为止,此时锁持有计数设置为 1。

注意:

  • 必须主动去释放锁。
  • 发生异常时,不会自动释放锁。所以使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。
1
2
3
4
5
6
7
8
9
10
11
12
private final Lock lock = new XxxLock();

public void doSomething() {
lock.lock(); // 获取锁,阻塞
try {
// ... 同步代码
} catch (Exception e) {

} finally {
lock.unlock(); // 释放锁
}
}

示例1:

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
public class Test {
public static void main(String[] args) {
Task task = new Task();
Thread thread0 = new Thread(task);
Thread thread1 = new Thread(task);
thread0.start();
thread1.start();
}
}

class Task implements Runnable {
private Lock lock = new ReentrantLock();

@Override
public void run() {
lock.lock(); // 获取锁,阻塞
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 释放锁
}
}
}

示例2:

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
class SellTicket implements Runnable {
// 定义票
private int tickets = 1;
// 定义锁对象
private Lock lock = new ReentrantLock();

@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
lock.lock(); // 获取锁,阻塞
try {
if (tickets <= 100) {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets++) + "张票");
} else {
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();// 释放锁
}
}
}
}

tryLock() 非阻塞

tryLock() 有返回值,它表示用来尝试获取锁。

如果其他线程没有持有锁,则获取该锁并立即返回true值,将锁持有计数设置为 1。即使此锁已设置为使用公平排序策略,调用tryLock()将立即获取可用的锁,无论其他线程当前是否正在等待该锁。这种“闯入”行为在某些情况下很有用,即使它破坏了公平。 如果你想尊重这个锁的公平性设置,那么使用 tryLock(0, TimeUnit.SECONDS) 这几乎是等效的(它也检测中断)。

如果当前线程已持有此锁,则持有计数将增加 1 并且该方法返回 true。

如果锁被另一个线程持有,那么这个方法将立即返回 false 值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private final Lock lock = new XxxLock();

public void doSomething() {
if (lock.tryLock()) { // 获取锁,非阻塞
try {
// ... 同步代码
} catch (Exception e) {

} finally {
lock.unlock(); // 释放锁
}
} else {
System.out.println("锁被占用,执行其他任务。");
}
}

tryLock(time,unit) 定时阻塞,可中断

如果在给定的等待时间内成功获取锁(没有被另一个线程持有),且当前线程没有被中断,则返回true,将锁持有计数设置为 1。

由于该方法的声明中抛出了异常,所以它必须放在try块中或者在调用的方法上抛出 InterruptedException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private final Lock lock = new XxxLock();

public void doSomething() throws InterruptedException {
try {
if (lock.tryLock(1000, TimeUnit.SECONDS)) { // 定时获取锁,定时阻塞,可中断
try {
// ... 同步代码
} catch (Exception e) {

} finally {
lock.unlock(); // 释放锁
}
} else {
System.out.println("锁被占用,执行其他任务。");
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 获取锁时被中断");
}
}

示例:

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
35
36
37
38
39
40
41
public class Test {
public static void main(String[] args) {
Task task = new Task();
Thread thread0 = new Thread(task);
Thread thread1 = new Thread(task);
thread0.start();
thread1.start();

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 中断线程
thread1.interrupt();
}
}

class Task implements Runnable {
private Lock lock = new ReentrantLock();

@Override
public void run() {
try {
if (lock.tryLock(1000, TimeUnit.SECONDS)) { // 定时获取锁,定时阻塞,可中断
try {
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName() + " 执行完成");
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " sleep时被中断");
} finally {
lock.unlock(); // 释放锁
}
} else {
System.out.println("锁被占用,执行其他任务。");
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 获取锁时被中断");
}
}
}

lockInterruptibly() 阻塞,可中断

这个方法比较特殊,当通过该方法获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时使用 lock.lockInterruptibly() 获取某个锁时,假若线程A获取到了锁,线程B在等待,那么对线程B调用 threadB.interrupt() 方法能够中断线程B的等待过程。

由于该方法的声明中抛出了异常,所以它必须放在try块中或者在调用的方法上抛出 InterruptedException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private final Lock lock = new XxxLock();

public void doSomething() {
try {
lock.lockInterruptibly(); // 获取锁,阻塞,可中断
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 获取锁时被中断");
return;
}

try {
// ... 同步代码
} catch (Exception e) {

} finally {
lock.unlock(); // 释放锁
}
}

示例:

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
35
36
37
38
39
40
public class Test {
public static void main(String[] args) {
Task task = new Task();
Thread thread0 = new Thread(task);
Thread thread1 = new Thread(task);
thread0.start();
thread1.start();

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 中断线程
thread1.interrupt();
}
}

class Task implements Runnable {
private Lock lock = new ReentrantLock();

@Override
public void run() {
try {
lock.lockInterruptibly(); // 获取锁,阻塞,可中断
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 获取锁时被中断");
return;
}

try {
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName() + " 执行完成");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 释放锁
}
}
}

其他特有方法

1
2
3
4
public final boolean isFair() // 判断锁是否是公平锁
public boolean isLocked() // 查询此锁是否被任何线程持有。此方法设计用于监视系统状态,而不是用于同步控制。
public boolean isHeldByCurrentThread() // 查询当前线程是否持有此锁。
public final boolean hasQueuedThreads() // 查询是否有线程正在等待获取此锁。请注意,取消可能随时发生,如果可能有其他线程等待获取锁,则返回true。该方法主要设计用于监视系统状态。

ReentrantReadWriteLock

image-20210714204748943

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}

public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
/** Inner class providing readlock */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;

public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }

public static class ReadLock implements Lock, java.io.Serializable {
// ...
}
public static class WriteLock implements Lock, java.io.Serializable {
// ...
}
}

ReentrantReadWriteLock 实现了 ReadWriteLock 接口(非 Lock 接口),调用该接口的 readLock()writeLock() 方法可分别获取读和写的 Lock 锁。

ReentrantReadWriteLock 还提供了丰富的用于监视系统状态的方法。

为什么要分两把锁?

因为读操作的锁,要支持被多个线程获取(多线程同时读),这样可以提升读操作的效率。而写操作的锁,只能被一个线程获取。

读锁被占用时,无法申请写锁,但能申请读锁。

写锁被占用时,无法申请写锁,无法申请读锁。

读读的过程是共享的,读写、写读 、写写 的过程是互斥的。

ReentrantReadWriteLock读写锁详解

读写锁实战高并发容器

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
public class Test {
public static void main(String[] args) {
// 创建高并发容器
ReadWriteDictionary dictionary = new ReadWriteDictionary();
// 创建读、写任务
WriteTask writeTask = new WriteTask(dictionary);
ReadTask readTask = new ReadTask(dictionary);
// 创建读、写线程
Thread writeThread0 = new Thread(writeTask);
Thread writeThread1 = new Thread(writeTask);
Thread writeThread2 = new Thread(writeTask);
Thread readThread3 = new Thread(readTask);
Thread readThread4 = new Thread(readTask);
Thread readThread5 = new Thread(readTask);
// 启动线程
writeThread0.start();
writeThread1.start();
writeThread2.start();
readThread3.start();
readThread4.start();
readThread5.start();
}
}

/**
* 从容器读取数据的任务
*/
class ReadTask implements Runnable {
private ReadWriteDictionary dictionary;

public ReadTask(ReadWriteDictionary dictionary) {
this.dictionary = dictionary;
}

@Override
public void run() {
// 无限读取
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 获取所有key
String[] keys = dictionary.allKeys();
// 遍历所有的key
for (String key : keys) {
Object value = dictionary.get(key);
String threadName = Thread.currentThread().getName();
System.out.println(threadName + "读取 >>> " + key + ": " + value);
}
}
}
}

/**
* 向容器写入数据的任务
*/
class WriteTask implements Runnable {
private ReadWriteDictionary dictionary;

public WriteTask(ReadWriteDictionary dictionary) {
this.dictionary = dictionary;
}

@Override
public void run() {
// 计数器
int i = 0;
// 无限写入
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object value = Thread.currentThread().getName() + "写入" + i++;
// 存储数据
dictionary.put("固定key", value);
}
}
}

/**
* 高并发容器
*/
class ReadWriteDictionary {
/**
* key-value容器
*/
private final Map<String, Object> map = new HashMap<>();

/**
* 读写锁
*/
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);

/**
* 读锁
*/
private final Lock readLock = readWriteLock.readLock();

/**
* 写锁
*/
private final Lock writeLock = readWriteLock.writeLock();

public Object get(String key) {
readLock.lock();
try {
return map.get(key);
} finally {
readLock.unlock();
}
}

/**
* 返回所有键
*/
public String[] allKeys() {
readLock.lock();
try {
return map.keySet().toArray(new String[0]);
} finally {
readLock.unlock();
}
}

public Object put(String key, Object value) {
writeLock.lock();
try {
return map.put(key, value);
} finally {
writeLock.unlock();
}
}

public void clear() {
writeLock.lock();
try {
map.clear();
} finally {
writeLock.unlock();
}
}
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Thread-3读取 >>> 固定key: Thread-0写入1
Thread-5读取 >>> 固定key: Thread-0写入1
Thread-4读取 >>> 固定key: Thread-0写入1
Thread-3读取 >>> 固定key: Thread-1写入2
Thread-5读取 >>> 固定key: Thread-1写入2
Thread-4读取 >>> 固定key: Thread-1写入2
Thread-3读取 >>> 固定key: Thread-2写入3
Thread-4读取 >>> 固定key: Thread-2写入3
Thread-5读取 >>> 固定key: Thread-2写入3
Thread-3读取 >>> 固定key: Thread-1写入4
Thread-4读取 >>> 固定key: Thread-1写入4
Thread-5读取 >>> 固定key: Thread-1写入4
Thread-3读取 >>> 固定key: Thread-1写入5
Thread-4读取 >>> 固定key: Thread-1写入5
Thread-5读取 >>> 固定key: Thread-1写入5
Thread-3读取 >>> 固定key: Thread-1写入6
Thread-4读取 >>> 固定key: Thread-1写入6
Thread-5读取 >>> 固定key: Thread-1写入6
Thread-3读取 >>> 固定key: Thread-0写入7
Thread-5读取 >>> 固定key: Thread-2写入7
Thread-4读取 >>> 固定key: Thread-2写入7

可重入性

读锁 写锁
读锁 可重入 不可重入
写锁 不可重入 不可重入

和 StampedLock 相同。验证读写不可重入:

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
public class Test {
public static final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public static final Lock readLock = readWriteLock.readLock();
public static final Lock writeLock = readWriteLock.writeLock();

public static void main(String[] args) {
printA();
}

private static void printA() {
readLock.lock();
try {
System.out.println("printA");
printB();
} finally {
readLock.unlock();
}
}

private static void printB() {
writeLock.lock();
try {
System.out.println("printB");
} finally {
writeLock.unlock();
}
}
}

LockSupport工具类

实际开发中用的不多。可以用 LockSupport实现互斥锁(等待唤醒机制)

方法介绍:

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
public class LockSupport {
private LockSupport() {} // 无法实例化

/*********** 无限等待 ************/
// 使当前线程等待,直到唤醒或中断
public static void park();
// 使当前线程等待,并指定同步锁,直到唤醒或中断
public static void park(Object blocker);

/*********** 定时等待***********/
// 使当前线程等待,并指定等待时间,直到唤醒或中断或超时
public static void parkNanos(long nanos);
// 使当前线程等待,并指定同步锁和等待时间,直到唤醒或中断或超时
public static void parkNanos(Object blocker, long nanos);
// 使当前线程等待,并指定截止日期,直到唤醒或超时
public static void parkUntil(long deadline);
// 使当前线程等待,并指定同步锁和截止日期,直到唤醒或中断或超时
public static void parkUntil(Object blocker, long deadline);

// 唤醒指定线程
public static void unpark(Thread thread);

// 获取指定线程的同步锁
public static Object getBlocker(Thread t);
}

示例

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
public class Test {
public static void main(String[] args) {
Task task = new Task();
Thread thread = new Thread(task);
thread.start();

try {
thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
LockSupport.unpark(thread);
}
}


/**
* 从容器读取数据的任务
*/
class Task implements Runnable {
@Override
public void run() {
System.out.println("等待前");
LockSupport.park();
System.out.println("等待后");
}
}

StampedLock邮戳锁

线程饥饿

读写锁存在线程饥饿的问题。饥饿指的是某一线程或多个线程因为某些原因一直获取不到资源,导致程序一直无法执行。场景举例:

1、某一线程因优先级太低导致一直分配不到资源。

2、某一线程一直占着某种资源不放,导致其他线程长期无法执行。如:读(写)操作长时间占用读(写)锁,导致写(读)操作一直等待。

如何解决线程饥饿问题:

1、与死锁相比,饥饿现象有可能在一段时间后恢复执行。可以设置合适的线程优先级,来尽量避免饥饿的产生。

2、使用读写锁的升级版 StampedLock 来解决线程饥饿。

和ReadWriteLock相比

ReadWriteLock可以解决多线程同时读,但同时只能有一个线程写的问题。潜在的问题:如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写,这是一种悲观的读锁。

要进一步提升并发执行效率,Java 8引入了新的读写锁:StampedLock

StampedLockReadWriteLock相比,改进之处在于:读的过程中也允许获取写锁后写入!这样一来,我们读的数据就可能不一致,所以,需要一点额外的代码来判断读的过程中是否有写入,这种读锁是一种乐观锁。

乐观锁的意思就是乐观地估计读的过程中大概率不会有写入,因此被称为乐观锁。反过来,悲观锁则是读的过程中拒绝有写入,也就是写入必须等待。显然乐观锁的并发效率更高,但一旦有小概率的写入导致读取的数据不一致,需要能检测出来,再读一遍就行。

常用方法

方法 描述
public StampedLock() 只有一个无参构造
public long readLock() 获取读锁,返回锁标识
public long writeLock() 获取写锁,返回锁标识
public void unlock(long stamp) 释放读写锁,参数stamp为锁标识
public void unlockRead(long stamp) 释放读锁,参数stamp为锁标识
public void unlockWrite(long stamp) 释放写锁,参数stamp为锁标识
public Lock asReadLock() 转换为读锁
public Lock asWriteLock() 转换为写锁
public ReadWriteLock asReadWriteLock() 转换为读写锁

示例

饥饿问题复现

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public class Test {
public static void main(String[] args) {
Data data = new Data();
ReadTask readTask = new ReadTask(data);
WriteTask writeTask = new WriteTask(data);
Thread thread0 = new Thread(readTask);
Thread thread1 = new Thread(writeTask);
thread0.start();
thread1.start();
}
}

class ReadTask implements Runnable {
private Data data;

public ReadTask(Data data) {
this.data = data;
}

@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
System.out.println(data.getMessage());
}
}
}

class WriteTask implements Runnable {
private Data data;

public WriteTask(Data data) {
this.data = data;
}

@Override
public void run() {
// 计数器
int i = 0;
while (!Thread.currentThread().isInterrupted()) {
data.setMessage("当前值: " + i++);
}
}
}

class Data {
private String message;
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final Lock readLock = readWriteLock.readLock();
private final Lock writeLock = readWriteLock.writeLock();

public String getMessage() {
readLock.lock();
try {
Thread.sleep(1000);
return this.message;
} catch (InterruptedException e) {
e.printStackTrace();
return null;
} finally {
readLock.unlock();
}
}

public void setMessage(String message) {
writeLock.lock();
try {
Thread.sleep(1000);
this.message = message;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}
}

输出结果用于最后的对比

1
2
3
4
5
6
7
8
9
10
11
null
当前值: 26
当前值: 30
当前值: 45
当前值: 63
当前值: 64
当前值: 84
当前值: 92
当前值: 128
当前值: 131
当前值: 134

解决饥饿问题

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
class Data {
private String message;
private final StampedLock stampedLock = new StampedLock();

public String getMessage() {
// 获取读锁标识
long readStamp = stampedLock.readLock();
try {
Thread.sleep(1000);
return this.message;
} catch (InterruptedException e) {
e.printStackTrace();
return null;
} finally {
// 释放读锁
stampedLock.unlockRead(readStamp);
}
}

public void setMessage(String message) {
// 获取写锁标识
long writeStamp = stampedLock.writeLock();
try {
Thread.sleep(1000);
this.message = message;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放写锁
stampedLock.unlockWrite(writeStamp);
}
}
}

输出结果

1
2
3
4
5
6
7
8
9
10
11
null
当前值: 51
当前值: 72
当前值: 136
当前值: 152
当前值: 168
当前值: 182
当前值: 205
当前值: 227
当前值: 230
当前值: 260

对比输出结果

输出10行,发现 StampedLock 输出到260,而 ReentrantReadWriteLock 输出到 134,是否能证明饥饿问题的出现和解决呢???

可重入性

和 ReentrantReadWriteLock 相同。验证读写不可重入:

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
public class Test {
public static StampedLock stampedLock = new StampedLock();

public static void main(String[] args) {
printA();
}

private static void printA() {
long stamp = stampedLock.readLock();
try {
System.out.println("printA");
printB();
} finally {
stampedLock.unlockRead(stamp);
}
}

private static void printB() {
long stamp = stampedLock.writeLock();
try {
System.out.println("printB");
} finally {
stampedLock.unlockWrite(stamp);
}
}
}

ThreadLocal

remove()

即便不调用 remove() 也不会导致内存溢出,只不过垃圾回收器压力大些。设置 JVM 参数 -Xms30m -Xmx30m ,用 VisualVM 观察垃圾回收次数,VisualVM也会占用 JVM 内存,所以不能设置太小。

验证代码如下,观察 VisualVM 中的垃圾回收次数 collections:

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
/**
* JVM参数:-Xms30m -Xmx30m
*/
public class Test {
public static ThreadLocal<Object> threadLocal = new ThreadLocal<>();
// 执行次数
public static AtomicInteger cnt = new AtomicInteger(0);

public static void main(String[] args) throws InterruptedException {
// 这里休眠10s,待visualVM连接后继续
TimeUnit.SECONDS.sleep(10);

ExecutorService executorService =
new ThreadPoolExecutor(1, 1, 0, TimeUnit.MILLISECONDS, new SynchronousQueue<>(), new DiscardPolicy());
for (int i = 1000; i > 0; i--) {
executorService.submit(new Task());
TimeUnit.MILLISECONDS.sleep(10);
}

executorService.shutdown();
}
}

class Task implements Runnable {
private int _1MB = 1024 * 1024 * 1;

@Override
public void run() {
Byte[] byt = new Byte[_1MB];
Test.threadLocal.set(byt);
System.out.println(Test.cnt.incrementAndGet());
// Test.threadLocal.remove();
}
}

InheritableThreadLocal

InheritableThreadLocal 可继承的线程本地变量。如线程B被线程A创建,B就可以获取A中的 InheritableThreadLocal。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Test {
public static final ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();

public static void main(String[] args) {
threadLocal.set("Hello World!");
new ThreadB().start();
}
}

class ThreadB extends Thread {
@Override
public void run() {
System.out.println(Test.threadLocal.get());
}
}

TransmittableThreadLocal

https://juejin.cn/post/7214901105977671717

继承自 InheritableThreadLocal。该类由阿里提供。

java中与线程安全有关的关键字:volatile、synchronized

volatile

Java中Volatile关键字详解

可见性

volatile修饰的变量不允许线程内部缓存(保证可见性),即直接修改内存,所以对其他线程是可见的。

变量经过 volatile 修饰后,对此变量进行写操作时,汇编指令中会有一个LOCK前缀指令,这个不需要过多了解,但是加了这个指令后,会引发两件事情:

  1. 将当前处理器缓存行的数据写回到系统内存。
  2. 这个写回内存的操作会使其他处理器缓存数据的内存地址无效。

  1. 对于写操作:对变量更改完之后,要立刻写回到主存中。
  2. 对于读操作:对变量读取的时候,要从主存中读,而不是缓存。

问题来了,既然它可以保证修改的值立即能更新到主存,其他线程也会捕捉到被修改后的值,那么为什么不能保证原子性呢?

比如 volatile int a = 0; 之后有一个操作 a++; 这个变量a具有可见性,但是 a++ 依然是一个非原子操作,也就是这个操作同样存在线程安全问题。

Java中只有对基本类型变量的赋值和读取是原子操作,如 i = 1; 的赋值操作,但是像 j = i; 或者 i++; 这样的操作都不是原子操作,存在线程安全问题。比如先读取 i 的值,再将 i 的值赋值给 j ,两个原子操作加起来就不是原子操作了。

所以,一个变量被volatile修饰,保证了每次读取的变量值是最新的。像变量自增这样的非原子操作,这个变量就不具有原子性了。

有序性

volatile 规定 禁止指令重排序,为了保证数据的一致性。

可见性示例

示例1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test {
private static boolean flag = true;

public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.println("ThreadA开始执行...");
while (flag) { // 等同于 while (true)
// Thread.yield(); // 其实这里sleep或yield一下就不需要用volatile修饰了
if (!flag) {
System.out.println("跳出循环");
break;
}
}
System.out.println("ThreadA结束执行。。。");
}, "ThreadA").start();

TimeUnit.SECONDS.sleep(1);
flag = false;
System.out.println("标识已经变更");
}
}

输出:

1
2
ThreadA开始执行...
标识已经变更

示例2

示例1变形,改变 while 循环体内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test {
private static boolean flag = true;

public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.println("ThreadA开始执行...");
while (flag) {
System.out.println("跳出循环");
}
System.out.println("ThreadA结束执行。。。");
}, "ThreadA").start();

TimeUnit.SECONDS.sleep(1);
flag = false;
System.out.println("标识已经变更");
}
}

输出:

1
2
3
4
5
6
ThreadA开始执行...
跳出循环
...
跳出循环
标识已经变更
ThreadA结束执行。。。

示例3

继续改变 while 循环体内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test {
private static boolean flag = true;

public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.println("ThreadA开始执行...");
int i = 0;
while (flag) {
i++;
}
System.out.println("ThreadA结束执行。。。");
}, "ThreadA").start();

TimeUnit.SECONDS.sleep(1);
flag = false;
System.out.println("标识已经变更");
}
}

输出:

1
2
ThreadA开始执行...
标识已经变更

总结

有以上 3 个示例可得出,flag 的可见性会根据 while 循环体内容儿变化,不知道为什么,请指教。

所以为了保证 flag 可见,用 volatile 修饰。

非原子性示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Test {
// volatile不保证原子性
// 原子性:保证数据一致性、完整性
private static volatile int num = 0;

public static void main(String[] args) throws InterruptedException {
for (int j = 0; j < 2; j++) {
new Thread(() -> {
for (int i = 0; i < 10000; i++) {
Test.num++;
}
}, String.valueOf(j)).start();
}

// 后台默认两个线程:一个是main线程,一个是gc线程
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "final num result = " + Test.num);
Thread.sleep(5000); // 由于不了解Thread.activeCount(),担心还有线程未执行完,所以休眠后再输出一次,事实证明Thread.activeCount()可靠
// 如果volatile保证原子性的话,最终的结果应该是20000。但是每次程序执行结果都不等于20000。
System.out.println(Thread.currentThread().getName() + "final num result = " + Test.num);
}
}

输出:

1
2
3
main final num result = 17114
main final num result = 20000
main final num result = 19317

三次执行,都是不同的结果。

为什么会出现这种呢?因为 num++ 被拆分成3个指令:

  • 执行 getfield 拿到主内存中的原始值 num。
  • 执行 iadd 进行加1操作。
  • 执行 putfield 把工作内存中的值写回主内存中。

当多个线程并发执行 putfield 指令的时候,会出现写回主内存覆盖问题,所以才会导致最终结果不为 20000,所以 volatile 不能保证原子性。

总结

  • volatile可用于内存访问的可见性,保证每次读的都是最新写的数据
  • volatile不具备原子性,原子功能需要使用Atomic类
  • 原子操作和原子操作合并之后,不是原子操作。

synchronized

什么是同步锁

sychronized 采取的同步策略互斥同步

在每次获取资源之前,都需要检查是否有线程占用该资源。已经进入的线程尚未执行完,将会阻塞后面其他线程。

同步锁的本质是对象实例。多线程安全需保证每个线程使用的是同一个锁对象

同步锁保证多线程下的每个操作都是原子性的。

同步锁、同步监听对象、同步监视器、互斥锁 它们都是同步锁,说的是一个意思。

弊端:当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。

1、对象是什么?

我们可以随便创建一个对象试试。

2、需要同步的代码是哪些?

多条语句操作共享数据的代码部分给包起来

注意:

  • 同步可以解决安全问题的根本原因就在那个对象上。该对象如同锁的功能。

  • 这里的锁对象可以是任意对象。多个线程必须是同一把锁

使用方式

同步代码块(对象锁、类锁)

格式:

1
2
3
4
5
6
7
8
9
10
11
synchronized(对象) { // 对象锁
// 需要被同步的代码;
}

synchronized(this) { // 对象锁
// 需要被同步的代码;
}

synchronized(类.class) { // 类锁
// 需要被同步的代码;
}

优化 SellTicket :

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class SellTicketDemo {
public static void main(String[] args) {
// 创建资源对象
SellTicket st = new SellTicket();

// 创建三个线程对象
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");

// 启动线程
t1.start();
t2.start();
t3.start();
}
}

class SellTicket extends Stock implements Runnable {

/**
* 创建锁对象
*/
private Object objLock = new Object();

@Override
public void run() {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 已售票数小于库存,就进行售票
while (sellTickets < originalStock) {
// synchronized (new Object()) 不同锁对象
// synchronized (this) 对象锁
// synchronized (Object.class) 类锁
synchronized (objLock) {
if (sellTickets < originalStock) {
try {
// 模拟售票员正在售票
Thread.sleep(100);
// 注意这里通过 Thread.currentThread().getName() 获取线程名
System.out.println(Thread.currentThread().getName() + "\t\t正在出售第" + (++sellTickets) + "张票\t\t" +
"原始库存" + originalStock + "张票\t\t");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
stopWatch.stop();
System.out.println(Thread.currentThread().getName() + "总耗时: " + stopWatch.getTotalTimeNanos());
}
}

同步方法(对象锁)

把同步加在方法上。

这里的锁对象是this

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public class SellTicketDemo {
public static void main(String[] args) {
// 创建资源对象
SellTicket st = new SellTicket();

// 创建三个线程对象
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");

// 启动线程
t1.start();
t2.start();
t3.start();
}
}

class SellTicket extends Stock implements Runnable {

@Override
public void run() {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 已售票数小于库存,就进行售票
while (sellTickets < originalStock) {
sell();
}
stopWatch.stop();
System.out.println(Thread.currentThread().getName() + "总耗时: " + stopWatch.getTotalTimeNanos());
}

// 下面两个sell方法的锁对象相同
private synchronized void sell() {
if (sellTickets < originalStock) {
try {
// 模拟售票员正在售票
Thread.sleep(100);
// 注意这里通过 Thread.currentThread().getName() 获取线程名
System.out.println(Thread.currentThread().getName() + "\t\t正在出售第" + (++sellTickets) + "张票\t\t" +
"原始库存" + originalStock + "张票\t\t");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void sell() {
synchronized (this) {
if (sellTickets < originalStock) {
try {
// 模拟售票员正在售票
Thread.sleep(100);
// 注意这里通过 Thread.currentThread().getName() 获取线程名
System.out.println(Thread.currentThread().getName() + "\t\t正在出售第" + (++sellTickets) + "张票\t" +
"\t" +
"原始库存" + originalStock + "张票\t\t");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}

静态同步方法(类锁)

把同步加在方法上。只需要在同步方法上加一个 static

这里的锁对象是当前类的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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public class SellTicketDemo {
public static void main(String[] args) {
// 创建资源对象
SellTicket st1 = new SellTicket();
SellTicket st2 = new SellTicket();
SellTicket st3 = new SellTicket();

// 创建三个线程对象
Thread t1 = new Thread(st1, "窗口1");
Thread t2 = new Thread(st2, "窗口2");
Thread t3 = new Thread(st3, "窗口3");

// 启动线程
t1.start();
t2.start();
t3.start();
}
}

class SellTicket extends Stock implements Runnable {

@Override
public void run() {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 已售票数小于库存,就进行售票
while (sellTickets < originalStock) {
sell();
}
stopWatch.stop();
System.out.println(Thread.currentThread().getName() + "总耗时: " + stopWatch.getTotalTimeNanos());
}

// 类锁
private static synchronized void sell() {
if (sellTickets < originalStock) {
try {
// 模拟售票员正在售票
Thread.sleep(100);
// 注意这里通过 Thread.currentThread().getName() 获取线程名
System.out.println(Thread.currentThread().getName() + "\t\t正在出售第" + (++sellTickets) + "张票\t\t" +
"原始库存" + originalStock + "张票\t\t");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 类锁
private void sell() {
synchronized (this.getClass()) {
if (sellTickets < originalStock) {
try {
// 模拟售票员正在售票
Thread.sleep(100);
// 注意这里通过 Thread.currentThread().getName() 获取线程名
System.out.println(Thread.currentThread().getName() + "\t\t正在出售第" + (++sellTickets) + "张票\t" +
"\t" +
"原始库存" + originalStock + "张票\t\t");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}

Double-Check单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton(); // 1
}
}
}
return singleton; // 2
}
}
// Double-Check的目的是延迟初始化单例的一种方法
// 使用volatile关键字的主要目的是防止指令重排序。如果volatile不加入,jvm可以对指令进行重排,与类锁无关,过程1可能被拿到过程2进行延迟初始化,这样就违背了Double-Check目的与原则。
// 第一层if判断是为了减少多次synchronized,减少每次都同步,提升性能
// 第二层if为了防止同时锁后,进行多个实例创建

原子性

原子是世界上的最小单位,具有不可分割性。

比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作时原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。

非原子操作都会存在线程安全问题,需要我们使用同步技术(synchronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。

如果把一个事务看作是一个程序,要么全部执行,要么全不执行。这种特性就叫原子性。

在 Java 中 synchronized 和在 lock、unlock 中的操作保证了原子性。

concurrent 包下提供了一些原子类,我们可以通过阅读API来了解这些原子类的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。

扩展链接:

什么是原子性,什么是原子性操作?

[Redis的单个操作是原子性的,多个操作支持事务](https://www.runoob.com/redis/redis-intro.html#:~:text=原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。)

可见性

可见性与Java的内存模型有关,模型采用缓存与主存的方式对变量进行操作,也就是说,每个线程都有自己的缓存空间,对变量的操作都是在缓存中进行的,之后再将修改后的值返回到主存中,这就带来了问题,有可能一个线程修改共享变量后,还没有来的及将缓存中的变量刷新到主存,另外一个线程就对共享变量进行修改,那么这个线程拿到的值是主存中未被修改的值,这就是可见性的问题。

可见性,指线程间内存的可见性一个线程的修改,对另一个线程立马可见

在 Java 中 volatile、synchronized 实现可见性。

可见性问题示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test {
public static boolean stopped = false;
//public static volatile boolean stopped = false; // volatile关键字保证可见性

public static void main(String[] args) {
new Thread(() -> {
// 当stopped为true时停止循环
while (!stopped) {
//Thread.sleep(0); // 也可以获取到stopped状态的改变
}
}).start();

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 停止循环
stopped = true;
}
}

有序性

Java 语言提供了 volatilesynchronized 两个关键字来保证线程之间操作的有序性。

volatile 是因为其本身包含 禁止指令重排序 的语义。

synchronized 是由 一个变量在同一个时刻只允许一条线程对其进行 lock 操作 这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行

什么是线程安全

所谓线程安全指的是多个线程对同一资源进行访问时,有可能产生数据不一致问题,导致线程访问的资源并不是安全的。

判断线程安全

判断一个程序是否存在线程安全问题的依据

  • 是否有多线程环境
  • 是否有共享数据
  • 是否有多条语句操作共享数据
阅读全文 »
0%