小小白祈祷中...

前言

上一章,我们通过几个例子介绍了线程安全问题,而说到线程安全就不得不提到线程同步,它是解决线程安全的一种重要方法。本章就来简单地介绍一下线程同步。

从上一章的学习我们知道,当多个线程操作一个资源的时候,有可能由于线程的不确定切换出现数据不一致的安全问题,因此,我们要解决这个问题,就得想办法使得资源在某个时间戳只能被一个线程访问。基于这样的思想,我们提出了“队列与锁”的策略:

通俗理解,就是将所有线程排成一个队列,给要共享的资源上把锁,只有线程获得该资源的锁之后,才能访问该资源

这也是线程同步的基本思想。所谓线程同步,是指同一进程中的多个线程相互协调工作从而达到一致性。使用线程同步,在解决线程安全问题的同时还能提高程序的性能。

基于“队列与锁”的思想,JDK中封装了一些用于实现线程同步的类和关键字。我们主要介绍synchronizedlock 两种。

synchronized

java中,每个对象都有一把唯一的锁,这也是synchronized实现线程同步的基础。总的来说,synchronized实现线程同步主要有三种形式:

形式 特点
实例同步方法 锁的是当前实例对象,执行同步代码前必须获得当前实例的锁
静态同步方法 锁的是当前类的class对象,执行同步代码前必须获得当前类对象的锁
同步代码块 锁的是括号里的对象,对给定对象加锁,执行同步代码块必须获得给定对象的锁

当两个线程同时对一个对象的某个方法进行调用时,只有一个线程能够抢到该对象的锁,因为一个对象只有一把锁。抢到该对象的锁之后,其他线程就无法访问该对象的所有synchronized方法,但仍可以访问该对象中的非synchronized方法

下面,我们用代码来演示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
/**
* @author sixibiheye
* @date 2021/8/31
* @apiNote synchronized用法举例
*/

public class Synchronized implements Runnable{
//公共资源
static int count = 0;
public void increase(){
count++;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
increase();
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(new Synchronized(),"A").start();
new Thread(new Synchronized(),"B").start();
Thread.sleep(2000);
System.out.println(count);
}
}

如果小伙伴们学习了上一章的内容,应该很容易看出这个程序是存在线程安全问题的,它的输出结果如下:

img

上一篇博文中说到,对于“count++”,JVM是这样处理的:

  1. 某线程从内存中读取count值到寄存器
  2. 某线程在寄存器中修改count的值
  3. 某线程将修改后的count值写入内存

简单解释一下,我们开启了两个线程去执行increase()方法,如果没有任何保护机制,假设,当count的值累加到1000时,A线程从主内存中读取到寄存器的count值为1000,执行完“count++”操作后,寄存器中的count值为1001,刚想写入内存,B线程正好抢到了CPU的使用权,开始执行run()方法,由于未写入内存,B线程从内存中读取到的count值为仍为1000,执行完“count++”操作后,B线程成功地将1001写入内存,接着A线程将自己寄存器中的count值1001写入内存(覆盖了B线程的1001),由此导致,虽然执行了两个线程,但count的值只累加了一次,这样的情况多发生几次,计算结果自然就低于20000了。

为了避免以上情况发生,我们给increase()方法加上修饰符synchronized,使得两个线程无法同时调用increase()方法,以保证上面的三步中的任何一步都不会被另外一个线程打断。

这样,“count++”操作就永远不会因线程切换而出错。代码如下:

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
/**
* @author sixibiheye
* @date 2021/8/31
* @apiNote synchronized用法举例
*/

public class Synchronized implements Runnable{
//公共资源
static int count = 0;
//synchronized实现线程同步
public synchronized void increase(){
count++;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
increase();
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(new Synchronized(),"A").start();
new Thread(new Synchronized(),"B").start();
Thread.sleep(2000);
System.out.println(count);
}
}

现在来看运行结果:

img没有任何问题。

此外,使用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
/**
* @author sixibiheye
* @date 2021/8/31
* @apiNote synchronized用法举例
*/

public class Synchronized implements Runnable{
public synchronized void running() throws InterruptedException {
System.out.println("1");
Thread.sleep(1000);
System.out.println("2");
}
@Override
public void run() {
try {
running();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
//用同一个类创建两个对象
Synchronized sync1 = new Synchronized();
new Thread(sync1).start();
Synchronized sync2 = new Synchronized();
new Thread(sync2).start();
}
}

如果我们使用不同的对象访问,那么结果有可能是不同步的:

img因为synchronized修饰实例方法时锁的对象是this对象,而使用两个对象去访问,不是同一把锁。如果我们用同一对象访问:

1
2
3
4
//只创建一个对象
Synchronized sync = new Synchronized();
new Thread(sync).start();
new Thread(sync).start();

那结果是同步的:

img

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
/**
* @author sixibiheye
* @date 2021/8/31
* @apiNote synchronized用法举例
*/

public class Synchronized implements Runnable{
public static synchronized void running() throws InterruptedException {
System.out.println("1");
Thread.sleep(1000);
System.out.println("2");
}
@Override
public void run() {
try {
running();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
//用同一个类创建两个对象
Synchronized sync1 = new Synchronized();
new Thread(sync1).start();
Thread.sleep(2000);
Synchronized sync2 = new Synchronized();
new Thread(sync2).start();
}
}

虽然有sync1sync2两个对象,但是它们是同一个类对象(Synchronized.class)产生的,而synchronized修饰静态方法时,锁的是 Synchronized.class,因此两个线程仍然是同步的:

img

synchronzied修饰同步代码块

它可以锁任何指定的对象,语法示例如下:

1
2
3
4
5
synchronized (this){
System.out.println("1");
Thread.sleep(1000);
System.out.println("2");
}

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
/**
* @author sixibiheye
* @date 2021/8/31
* @apiNote synchronized用法举例
*/

public class Synchronized implements Runnable{
public void running() throws InterruptedException {
for (int i = 0; i < 5; i++) {
System.out.println("这是耗时操作。");
}
//需要同步的代码块写下面
synchronized (this){
System.out.println("1");
Thread.sleep(1000);
System.out.println("2");
}
}
@Override
public void run() {
try {
running();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
Synchronized sync = new Synchronized();
new Thread(sync).start();
new Thread(sync).start();
}
}

运行结果如下:

img再运行一次:

img结果表明,需要同步的代码块确实实现了同步。

lock

前面使用的synchronized关键字可以实现多线程间的同步互斥,其实,在JDK1.5后新增的ReentrantLock 类同样可以实现这个功能,而且在使用上比 synchronized 更为灵活。

翻阅源码,可以看到 ReentrantLock 类实现了Lock接口

img

下面我们用ReentrantLock类来实现简单的线程同步:

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
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
* @author sixibiheye
* @date 2021/8/31
* @apiNote ReentrantLock实现线程同步
*/
public class LockDemo implements Runnable{
private Lock lock = new ReentrantLock();
/**
*
* 按照规范,
* lock()后面应紧跟try代码块,
* 并将unlock()放到finally块的第一行
*
*/
@Override
public void run() {
//上锁
lock.lock();
try {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "线程中的i=" + i);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放锁
lock.unlock();
}
}
public static void main(String[] args)
new Thread(new LockDemo(),"A").start();
new Thread(new LockDemo(),"B").start();
}
}

其实关于LockReentrantLock,还有许多其他的用法,限于篇幅,就不一一介绍了,有兴趣的小伙伴们可以查阅相关资料。

线程同步示例

了解了synchronizedReentrantLock,对于上一章提出的三个线程安全问题,便可以轻松地解决了。下面提供使用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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/**
* @author sixibiheye
* @date 2021/8/28
* @apiNote 线程安全问题一-------取钱问题
*/
public class UnsafeBank {

public static void main(String[] args) {
//账户
Account account = new Account(100,"买房基金");
//你和你的妻子都要取钱
Drawing you = new Drawing(account,50,"你");
Drawing girlFriend = new Drawing(account,100,"妻子");
you.start();
girlFriend.start();
}
}

//账户
class Account{
int money; //余额
String name; //卡名
public Account(int money,String name){
this.money = money;
this.name = name;
}
}

//银行
class Drawing extends Thread{
Account account; //账户
int drawingMoney; //取的钱
int nowMoney; //现手上有的钱
public Drawing(Account account,int drawingMoney,String name){
super(name);
this.account = account;
this.drawingMoney = drawingMoney;
}
@Override
public void run() {
//注意锁的是account对象,而不是this
synchronized (account){
if(account.money - drawingMoney < 0){
System.out.println(Thread.currentThread().getName() + "钱不够了,取不了!");
return;
}
try {
sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//卡内余额
account.money = account.money - drawingMoney;
//手里的钱
nowMoney = nowMoney + drawingMoney;
//打印
System.out.println(account.name + "余额为:" + account.money);
System.out.println(Thread.currentThread().getName() + "手里的钱:" + nowMoney);
}
}
}
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
/**
* @author sixibiheye
* @date 2021/8/27
* @apiNote 线程安全问题一-------买票问题
*/
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
new Thread(buyTicket,"小红").start();
new Thread(buyTicket,"小白").start();
new Thread(buyTicket,"小黑").start();
}
}

class BuyTicket implements Runnable{
//票数
private int tickets = 10;
//线程停止的标志位
private boolean flag = true;
//直接同步实例方法即可
private synchronized void buy() throws InterruptedException {
//判断是否有票
if(tickets <= 0){
flag = false;
return;
}
//模拟延时,保证多人都能买到票
Thread.sleep(20);
//买票
System.out.println(Thread.currentThread().getName() + "拿到了第" + tickets-- +"张票");

}
@Override
public void run() {
while (flag){
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
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
import java.util.ArrayList;
import java.util.List;

/**
* @author sixibiheye
* @date 2021/8/28
* @apiNote 线程安全问题一-------列表问题
*/

public class UnsafeList {
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<String>();
for (int i = 0; i < 10000; i++) {
new Thread( () -> {
//同步需要修改的list对象
synchronized (list){
list.add(Thread.currentThread().getName());
}
}).start();
}
//sleep保证上述for循环跑完再输出
Thread.sleep(3000);
//输出列表大小
System.out.println(list.size());
}
}

小伙伴们可以思考一下如果使用ReentrantLock应该如何解决。

下一章,我们将着重介绍Thread类中的一些常用方法,好啦~本章的内容到这就结束了。

本文作者:LuoYing @ 小小白的笔记屋

本文链接:https://luoying.netlify.app/2021/08/31/8ajv7uz8/

本文标题:Java多线程详解(线程同步)

本文版权:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!