并发编程之多线程

线程安全

概念

1
2
3
1、当多个线程访问访问某一个类(对象或方法)时,这个类或对象或方法始终能表现出正确的行为或我们想要的结果,那么这个类(对象或方法)
就是线程安全的。
2、synchronized:可以在任意的对象及方法上加锁,而加锁的这段代码称之为互斥区或者临界区。

线程和锁

多个线程多个锁:多个线程,每个线程都可以拿到自己指定的锁,分别获得锁之后,执行synchronized方法体的内容。

代码示例

两个线程t1,t2分别依次start,访问两个对象的synchronized修饰的printNum方法,Code如下:

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
/**
* 关键字synchronized取得的锁都是对象锁,而不是把一段代码(方法)当做锁,
* 所以代码中哪个线程先执行synchronized关键字的方法,哪个线程就持有该方法所属对象的锁(Lock),
*
* 在静态方法上加synchronized关键字,表示锁定.class类,类一级别的锁(独占.class类)。
* @author xujin
*
*/
public class MultiThread {
private int num = 0;
public synchronized void printNum(String tag) {
try {
if (tag.equals("a")) {
num = 100;
System.out.println("tag a, set num over!");
Thread.sleep(1000);
} else {
num = 200;
System.out.println("tag b, set num over!");
}
System.out.println("tag " + tag + ", num = " + num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 注意观察run方法输出顺序
public static void main(String[] args) {
// 两个不同的对象
final MultiThread m1 = new MultiThread();
final MultiThread m2 = new MultiThread();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
m1.printNum("a");
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
m2.printNum("b");
}
});
t1.start();
t2.start();
}
}

执行结果如下:

1
2
3
4
tag a, set num over!
tag b, set num over!
tag b, num = 200
tag a, num = 100

关键字synchronized取得的锁都是对象锁,而不是把一段代码(方法)当做锁,
所以代码中哪个线程先执行synchronized关键字的方法,哪个线程就持有该方法所属对象的锁(Lock)

如果,在静态方法上printNum()加一个synchronized关键字修饰的话,那这个线程调用printNum()获得锁,就是这个类级别的锁。这是时候无论你实例化出多少个对象m1,m2都是没有任何关系的。
这样测试的结果是

1
2
3
4
tag a, set num over!
tag a, num = 100
tag b, set num over!
tag b, num = 200

小结:
关键字synchronized取得的锁都是对象锁,而不是把一段代码或方法当做锁,所以示例中代码中的哪个线程先执行synchronized关键字的方法,哪个线程就持有该方法对象的锁,也就是Lock,两个对象,线程获得的就是两个不同的锁,他们互不影响。
有一种情况则是相同的锁,即在静态方法上加synchronized关键字,表示锁定.class类,类一级别的锁独占.class类。

脏读

业务整体需要使用完整的synchronized,保持业务的原子性。

在我们对对象中的一个方法加锁的时候,需要考虑业务的或程序的整体性,也就是为程序中的set和get方法同时加锁synchronized同步关键字,保证业务的(service层)的原子性,不然会出现数据错误,脏读。

synchronized的重入

什么是synchronized的重入锁

synchronized,它拥有强制原子性的内置锁机制,是一个重入锁,所以在使用synchronized时,当一个线程请求得到一个对象锁后再次请求此对象锁,可以再次得到该对象锁,就是说在一个synchronized方法/块的内部调用本类的其他synchronized方法/块时,是永远可以拿到锁。
当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞

简单的说:关键字synchronized具有锁重入的功能,也就是在使用synchronized时,当一个线程得到一个对象锁的锁后,再次请求此对象时可以再次得到该对象对应的锁。

这里的对象锁只有一个,就是child对象的锁,当执行child.doSomething时,该线程获得child对象的锁,在doSomething方法内执行doAnotherThing时再次请求child对象的锁,因为synchronized是重入锁,所以可以得到该锁,继续在doAnotherThing里执行父类的doSomething方法时第三次请求child对象的锁,同理可得到,如果不是重入锁的话,那这后面这两次请求锁将会被一直阻塞,从而导致死锁。
所以在Java内部,同一线程在调用自己类中其他synchronized方法/块或调用父类的synchronized方法/块都不会阻碍该线程的执行,就是说同一线程对同一个对象锁是可重入的,而且同一个线程可以获取同一把锁多次,也就是可以多次重入。因为java线程是基于“每线程(per-thread)”,而不是基于“每调用(per-invocation)”的(java中线程获得对象锁的操作是以每线程为粒度的,per-invocation互斥体获得对象锁的操作是以每调用作为粒度的)

其实现方法是为每个锁关联一个线程持有者和计数器,当计数器为0时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为0,则释放该锁。

注意就是不要使用String的常量加锁,会出现死循环问题。

锁对象的改变问题:

当使用一个对象进行加锁的时候,要注意对象本身发生变化的时候,那么持有的锁就不同。如果对象本身不发生改变,那么依然是同步的,即使是对象的属性发生了变化。

同一对象属性的修改不会影响锁的情况

如:

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 ModifyLock {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public synchronized void changeAttributte(String name, int age) {
try {
System.out.println("当前线程 : " + Thread.currentThread().getName() + " 开始");
this.setName(name);
this.setAge(age);
System.out.println("当前线程 : " + Thread.currentThread().getName() + " 修改对象内容为: " + this.getName() + ", "
+ this.getAge());
Thread.sleep(2000);
System.out.println("当前线程 : " + Thread.currentThread().getName() + " 结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
final ModifyLock modifyLock = new ModifyLock();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
modifyLock.changeAttributte("许进", 25);
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
modifyLock.changeAttributte("李四X", 21);
}
}, "t2");
t1.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}

运行结果:

1
2
3
4
5
6
当前线程 : t1 开始
当前线程 : t1 修改对象内容为: 许进, 25
当前线程 : t1 结束
当前线程 : t2 开始
当前线程 : t2 修改对象内容为: 李四X, 21
当前线程 : t2 结束