1. synchronized关键字的类锁和对象锁

synchronized关键字的作用位置可能包含以下几种情况:

  1. 非静态方法上(在执行该方法时,对这个方法所属的对象进行加锁);
  2. 静态方法上(在执行该方法时,对该方法对应的整个类进行加锁);
  3. 直接在代码块中对某个对象加锁(在执行该代码块时,对该对象进行加锁);
  4. 直接在代码块中对某个类进行加锁(在执行该代码块时,对这个类进行加锁);

这里我们先写一个线程不安全的实例:

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 TestThread {

public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
Thread thread = new Thread(new Thread1());
service.submit(thread);
service.submit(thread);
service.shutdown();
}

}

class Thread1 implements Runnable {

@Override
public void run() {
System.out.println("线程开始...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程结束...");
}
}

1.1. 对非静态方法进行同步

上面这个例子不能让一个线程连续的打印出线程开始和线程结束语句,这里我们只需要在run方法上面加上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
public class TestThread {

public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
Thread thread = new Thread(new Thread1());
service.submit(thread);
service.submit(thread);
service.shutdown();
}

}

class Thread1 implements Runnable {

@Override
public synchronized void run() {
System.out.println("线程开始...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程结束...");
}
}

但是需要注意的是,千万不要认为给run方法加上了synchronized关键字就万事ok了,在上面的这个例子中确实是这样,但是这是因为我们两个线程使用的是同一个Thread对象(还记得我前面说的非静态方法上的synchronized关键字是对该方法所属的对象进行加锁),所以这个run方法对应的对象会被加锁而导致了同步。但是如果这个run方法是属于两个不同的对象,那么即使你对run方法加上了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
public class TestThread {

public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
// 创建的是两个不同的对象,synchronized只会对自己方法所对应的对象加锁
service.submit(new Thread1());
service.submit(new Thread1());
service.shutdown();
}

}

class Thread1 implements Runnable {

@Override
public synchronized void run() {
System.out.println("线程开始...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程结束...");
}
}

了解了非静态方法的synchronized关键字的含义和用法之后,我们再来了解一下静态方法的synchronized关键字的含义。

1.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
26
27
28
29
30
31
32
public class TestThread {

public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
service.submit(new Thread1());
service.submit(new Thread1());
service.shutdown();
}

}

class Thread1 implements Runnable {

@Override
public void run() {
Test.sayHello();
}
}

class Test {

static void sayHello() {
System.out.println("线程开始...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程结束...");
}

}

两个线程都会试图执行Test类的sayHello()静态方法,如果不进行同步操作,将会打印出如下语句:

线程开始...
线程开始...
线程结束...
线程结束...

但是当我们在sayHello()方法上加上了synchronized关键字之后,就能够保证这两个线程在执行sayHello()静态方法的时候不会产生竞争条件了,修改后的代码如下:

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
public class TestThread {

public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
service.submit(new Thread1());
service.submit(new Thread1());
service.shutdown();
}

}

class Thread1 implements Runnable {

@Override
public void run() {
Test.sayHello();
}
}

class Test {

synchronized static void sayHello() {
System.out.println("线程开始...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程结束...");
}

}

讲完了synchronized关键字对方法的修饰方式之后,我们来了解一下synchronized关键字在代码块中的操作,首先我们了解一下synchronized关键字在代码块中对对象进行同步的操作。

1.3. 对代码快进行同步:锁住对象

synchronized关键字在代码块中锁对象的最简单的操作就是锁住 this 对象,这种情况的操作其实和把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
public class TestThread {

public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
Thread thread = new Thread(new Thread1());
service.submit(thread);
service.submit(thread);
service.shutdown();
}

}

class Thread1 implements Runnable {

@Override
public void run() {
synchronized (this) {
System.out.println("线程开始...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程结束...");
}
}
}

这里的效果和直接在方法 run 上加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
public class TestThread {

public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
Test test = new Test();
service.submit(new Thread1(test, 1)); // 线程 1
service.submit(new Thread1(test, 2)); // 线程 2
service.shutdown();
}

}

class Thread1 implements Runnable {

private Test test;
private int name;

Thread1(Test test, int name) {
this.test = test;
this.name = name;
}

@Override
public void run() {
if (name == 1) {
test.i = 100;
// 线程 1 休眠一段时间,让线程 2 有机会改变 test 的对象的值
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1:" + test.i);
} else {
test.i = 200;
System.out.println("线程2:" + test.i);
}
}
}

class Test {

int i;

}

我们发现,线程1打印的值也变成了200,这不是我们想要的,下面我们通过锁操作来避免这种情况的产生:

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
public class TestThread {

public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
Test test = new Test();
service.submit(new Thread1(test, 1)); // 线程 1
service.submit(new Thread1(test, 2)); // 线程 2
service.shutdown();
}

}

class Thread1 implements Runnable {

private Test test;
private int name;

Thread1(Test test, int name) {
this.test = test;
this.name = name;
}

@Override
public void run() {
synchronized (test) {
if (name == 1) {
test.i = 100;
// 线程 1 休眠一段时间,让线程 2 有机会改变 test 的对象的值
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1:" + test.i);
} else {
test.i = 200;
System.out.println("线程2:" + test.i);
}
}
}
}

class Test {

int i;

}

如上所示,我们对 test 对象进行了加锁操作,保证在指定的代码块执行的时候,test对象身上是有锁的,避免了多个线程能够同时操作test对象,保证了不会发生竞争条件。

1.4. 对代码块进行同步:锁住类

同样我们先提供一个产生了竞争条件的程序,它使得两个线程可以同时的改变一个类的静态属性的值:

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
public class TestThread {

public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
service.submit(new Thread1(1)); // 线程 1
service.submit(new Thread1(2)); // 线程 2
service.shutdown();
}

}

class Thread1 implements Runnable {

private int name;

Thread1(int name) {
this.name = name;
}

@Override
public void run() {
if (name == 1) {
Test.i = 100;
// 线程 1 休眠一段时间,让线程 2 有机会改变 Test 类的静态属性值
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1:" + Test.i);
} else {
Test.i = 200;
System.out.println("线程2:" + Test.i);
}
}
}

class Test {

static int i;

}

以上程序的执行结果为:

线程2:200
线程1:200

显然这种结果是不符合要求的,下面我们通过对类进行加锁来避免这种情况的发生,修改后的代码如下:

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 TestThread {

public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
service.submit(new Thread1(1)); // 线程 1
service.submit(new Thread1(2)); // 线程 2
service.shutdown();
}

}

class Thread1 implements Runnable {

private int name;

Thread1(int name) {
this.name = name;
}

@Override
public void run() {
synchronized (Test.class) {
if (name == 1) {
Test.i = 100;
// 线程 1 休眠一段时间,让线程 2 有机会改变 Test 类的静态属性值
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1:" + Test.i);
} else {
Test.i = 200;
System.out.println("线程2:" + Test.i);
}
}
}
}

class Test {

static int i;

}

我们通过给Test类上锁,来避免两个线程同时操作类的属性,避免了竞争条件的产生。

1.5. 小结

上面一共有四种情况,不外乎就是方法、代码块、类、对象的四种组合。如果你稍微了解JVM,那么应该知道JVM中只有堆内存区和方法区是线程共享的,而诸如程序计数器、栈(包含Java栈和本地方法栈)这样的内存空间是各个线程各自独有的,所以我们的关注点自然就放到了堆和方法区中了。此外我们还知道,对象是存储在堆中、类属性是保存在方法区中的,所以我们只需要了解这些基础知识就可以很容易的了解内存中数据的操作情况了。

2. 线程间的协作(wait和notify关键字)

我们知道,线程的状态一般可以分为就绪、阻塞、运行这三种状态,而所谓的wait和notify方法只是分别让线程进入了阻塞和就绪的状态而已。

当一个线程上的同步对象调用wait方法时(只能是同步对象才能调用,否则会产生异常,notify方法也是同理),会使得当前线程进入阻塞;而notifyAll方法则是让所有在该对象上的阻塞线程进入就绪态,其中notify方法只是从所有的阻塞线程中随机的选择一个线程进入就绪态,一般不推荐使用。

了解了wait和notifyAll方法的运行原理,下面看一个实际使用了这两个方法的例子,这个例子的目的是使得两个线程绝对的交替执行。

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 TestThread {

public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
Test test = new Test();
service.submit(new Thread1(test, 1)); // 线程 1
service.submit(new Thread1(test, 2)); // 线程 2
service.shutdown();
}

}

class Thread1 implements Runnable {

private Test test;
private int name;

Thread1(Test test, int name) {
this.test = test;
this.name = name;
}

@Override
public void run() {
synchronized (test) {
while (true) {
test.notifyAll(); // 唤醒所有在 test 对象上阻塞的线程,使他们进入就绪态
System.out.println(name);
try {
test.wait(); // 当前线程进入阻塞态
Thread.sleep(1000); // 降低打印速度,方便看清输出
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}

class Test {}

上面这个程序的执行中,两个线程的状态我们可以如下这张表来表示:

时间片 线程1状态 线程2状态
1 运行 就绪
2 阻塞 就绪
3 阻塞 运行
4 就绪 运行
5 就绪 阻塞
6 运行 阻塞
7 运行 就绪
8

上面这个过程将会一直的执行下去,如此往复,保证了控制台能够交替稳定的打印出1和2。

3. Java 实现Future模式

所谓的future设计模式,简单来说,就是把客户端的请求改为异步,当客户端进行某种请求时,可以立即返回,可以方便客户端去做接下来的其他事情。接下来,当服务端的请求完成时,客户端再去执行相应的操作,从而提高了效率。

Future模式本质上其实是创建了一个新的线程,利用这个新的线程来去进行真实的请求和响应接受操作,下面就是一个简单的例子(这个例子参考了这里,如有侵权请告知删除)。

例子中我们一共会包含以下几个文件:

├── root
│   ├── Data.java
│   ├── RealData.java
│   ├── FutureData.java
│   ├── Client.java
│   ├── Test.java
  • Data.java 是一个接口,定义了 String getResult() 方法;
  • RealData.java 实现了Data接口,是 getResult() 方法的业务实现的类(方法真正需要实现的操作就放在这里面);
  • FutureData.java同样实现了Data接口,它是Future设计模式的核心,它将会在内部获取一个 RealData 的对象,并执行该对象的 getResult() 方法;
  • Client.java是一个类,他封装了Future设计模式中的一些核心操作,来提供给调用者使用,抽象了Future模式,降低了使用者的操作成本(我会在最后再提供一个不使用Client类来实现Future模式的Demo,它们的原理都是一样的);
  • Test.java是测试类,包含了我们的 main 方法;

Data.java

1
2
3
interface Data {
String getResult() throws InterruptedException;
}

RealData.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class RealData implements Data {

private String data;

RealData(String data) {
// 利用sleep方法来表示RealData构造过程是非常缓慢的
// 表示我们正在调用一个执行很慢的函数,它要很久才能返回
try {
System.out.println("RealData生成中...");
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.data = data;
}

@Override
public String getResult() {
return "RealData: " + data;
}

}

FutureData.java

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
class FutureData implements Data {

private RealData realData;

// 是否已经设置了realData
private boolean isReady = false;

synchronized void setRealData(RealData realData) {
if (isReady) // 如果已经设置了RealData,那么直接返回就可以了
return;
this.realData = realData;
isReady = true;
System.out.println("FutureData中已经设置好了RealData数据了");
notifyAll();
}

@Override
public synchronized String getResult() throws InterruptedException {
if (!isReady) {
System.out.println("FutureData中的RealData还没有设置好,等待ing");
wait(); // 一直等到RealData注入到FutureData中
System.out.println("RealData已经被设置到FutureData中了");
}
return realData.getResult();
}

}

Client.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Client {

// 创建一个 Data
static Data request(final String string) {
System.out.println("试图构建一个RealData...");
final FutureData futureData = new FutureData();

new Thread(() -> {
// RealData的构建很慢,所以放在单独的线程中运行
RealData realData = new RealData(string);
futureData.setRealData(realData);
}).start();

System.out.println("立即返回futureData");
return futureData;
}

}

Test.java

1
2
3
4
5
6
7
public class Test {
public static void main(String[] args) throws InterruptedException {
Data data = Client.request("真实的结果");
System.out.println("后台已经开始异步处理数据了,这个时候干点什么好呢,不如果傻傻的等2s钟吧ʅ(´◔౪◔)ʃ");
System.out.println("查看真实数据返回(如果还没有返回堵塞等待)= " + data.getResult());
}
}

上面的注释已经讲得比较清楚了,要理解Future的核心就在于创建了一个新的线程来帮助我们去执行一些比较耗时的操作,此线程在执行的操作完成之后,此时可以分为两种情况:

  1. 主线程阻塞在了该方法的调用上,那么就主动提醒主线程操作已经执行完成,此时主线程可以继续向下执行;
  2. 主线程在执行其它的操作,当主线程想要查看这个操作的执行结果的时候,如果该操作还没结束,那么主线程就会阻塞,变成情况1;如果此时异步线程的操作已经结束了,那么主线程就直接返回结果,操作结束,主线程继续向下执行;

示例代码我在GitHub上面也放了一份,可能查阅起来更清晰一点,地址在这里