Java多线程间的同步(一)

2018-03-27

日常开发中往往会遇到多个线程循环操作某个对象中的某个方法或者间接处理某个对象,如果当前某个对象被多个线程同时操作的话,势必会造成同步的问题,即线程A操作了对象object从而改变了该object的值,此刻线程A获取到的object的值还是之前的值。那么类似这种情况下,我们通常就要用如下几种方式去解决该问题。

1、synchronized 关键字

2、Lock

3、ReadWriteLock

4、其他方式

(一)synchronized

是java中表示同步代码快的关键字。可以放在方法修饰符前,比如private synchronized void test(){},也可以放在方法内部,修饰某一段特定的代码。synchronized有一个地方需要注意,就是在给普通方法加锁与给静态方法加锁机制是不一样的。synchronized在静态方法上表示调用前要获得类的锁,而在非静态方法上表示调用此方法前要获得对象的锁,synchronized可作用于instance变量、object reference(对象引用)、static函数和class literals(类名称字面常量)身上。。

如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class SynTest {   

private static String a="a";

//等同于方法test2
public synchronized void test1(String b){ //调用前要取得SynTest 实例化后对象的锁
System.out.println(a+b);
}
public void test2(String b){
synchronized (this) {//取得SynTest 实例化后对象的锁
System.out.println(a+b);
}
}
//等同于方法test4
public synchronized static void test3(String b){//调用前要取得SynTest .class类的锁
System.out.println(b+a);
}
public static void test4(String b){
synchronized (SynTest .class) { //取得SynTest .class类的锁
System.out.println(a+b);
}

注意:

1、test1和test2中synchronized 锁定的是SynTest的对象即该类的实例化对象,也可认为是object reference(对象引用), test3和test4中synchronized 锁定的是SynTest对象所属的类(Class,而不再是由这个Class产生的某个具体对象了),SynTest.class和synTest.getClass作用于同步锁是不一样的,不能用synTest.getClass()来达到锁这个Class的目的。synTest指的是由SynTest类产生的对象。

2、对synchronized(this)的一些理解
一、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。

二、然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。

三、尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。

四、当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。

3、synchronized 方法的缺陷:若将一个大的方法声明为synchronized 将会大大影响效率,典型地,若将线程类的方法 run() 声明为synchronized ,由于在线程的整个生命期内它一直在运行,因此将导致它对本类任何 synchronized 方法的调用都永远不会成功。当然我们可以通过将访问类成员变量的代码放到专门的方法中,将其声明为synchronized ,并在主方法中调用来解决这一问题,但是 Java 为我们提供了更好的解决办法,那就是 synchronized 块。

4、synchronized 块:通过 synchronized关键字来声明synchronized 块。语法如下:

1
2
3
synchronized(syncObject) {  
//允许访问控制的代码
}

synchronized 块是这样一个代码块,其中的代码必须获得对象 syncObject (如前所述,可以是类实例或类)的锁方能执行,具体机制同前所述。由于可以针对任意代码块,且可任意指定上锁的对象,故灵活性较高。

5、如果只是想单纯的锁某段代码块,当有明确的对象作为锁是可以的,如果没有明确的对象作为锁,则可以创建一个特殊的变量。如下:

1
2
3
4
5
6
7
8
9
class Foo implements Runnable
{
private byte[] lock = new byte[0]; // 特殊的instance变量
Public void methodA()
{
synchronized(lock) { //… }
}
//…..
}

注:零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。

(二)Lock

是java.util.concurrent.locks包下的接口,Lock实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作,因为Lock可以锁定任意一段代码

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
public class LockTest {  
public static void main(String[] args) {
final Outputter output = new Outputter();
new Thread() {
public void run() {
output.output("Alex-Jerry");
};
}.start();
new Thread() {
public void run() {
output.output("AI-Curry");
};
}.start();
}
}
class Outputter {
private Lock lock = new ReentrantLock();// 锁对象
public void output(String name) {
// TODO 线程输出方法
lock.lock();// 得到锁
try {
for(int i = 0; i < name.length(); i++) {
System.out.print(name.charAt(i));
}
} finally {
lock.unlock();// 释放锁
}
}
}

这样就实现了和sychronized一样的同步效果,需要注意的是,用sychronized修饰的方法或者语句块在代码执行完之后锁自动释放,而用Lock需要我们手动释放锁,所以为了保证锁最终被释放(发生异常情况),要把互斥区放在try内,释放锁放在finally内。

(三)ReadWriteLock

前两种方式虽然解决了读和写、写和写之间的互斥,但是读和读之间同样也是互斥的(不同得到线程可以同步执行读取),严重影响了效率,这个时候就该ReadWriteLock出场了。

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
class RWLock{      
private int data;// 共享数据
private ReadWriteLock rwl = new ReentrantReadWriteLock();
public void set(int data) {
rwl.writeLock().lock();// 取到写锁
try {
System.out.println(Thread.currentThread().getName() + "准备写入数据");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.data = data;
System.out.println(Thread.currentThread().getName() + "写入" + this.data);
} finally {
rwl.writeLock().unlock();// 释放写锁
}
}
public void get() {
rwl.readLock().lock();// 取到读锁
try {
System.out.println(Thread.currentThread().getName() + "准备读取数据");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "读取" + this.data);
} finally {
rwl.readLock().unlock();// 释放读锁
}
}
}

测试:
public class ReadWriteLockTest {
public static void main(String[] args) {
final RWLock data = new RWLock();
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
public void run() {
for (int j = 0; j < 5; j++) {
data.set(new Random().nextInt(30));
}
}
}).start();
}
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
public void run() {
for (int j = 0; j < 5; j++) {
data.get();
}
}
}).start();
}
}
}

部分测试输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
Thread-4准备读取数据  
Thread-3准备读取数据
Thread-5准备读取数据
Thread-5读取18
Thread-4读取18
Thread-3读取18
Thread-2准备写入数据
Thread-2写入6
Thread-2准备写入数据
Thread-2写入10
Thread-1准备写入数据
Thread-1写入22

(四)其他方式

线程同步的问题,最上面说的只是一种问题,往往导致同步问题的原因有多种。如线程A和线程B,线程B要等到线程A执行完毕之后才能进行操作,或者子线程执行结束才能让主线程去操作等等,这种情况下,可解决的方案很多,如:使用FutureTask+Callable代替Runnable去执行异步任务(FutureTask 的 get 方法,能够阻塞主线程,直到子线程初始化完成,才继续执行主线程逻辑),使用handler机制,操作线程的wait、notify、join、yield等等。基于这些情况,下篇文章将会逐个去分析和解决。