多线程的学习2

线程的同步

java线程是依靠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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class Main1 {
public static void main(String[] args) throws Exception {
var c1 = new Counter();
var c2 = new Counter();
new Thread(()-> {
c1.add(1);
}).start();
new Thread(()-> {
c1.dec(3);
}).start();
Thread.sleep(100);
System.out.println(c1.get());

new Thread(()-> {
c2.add(6);
}).start();
new Thread(()-> {
c2.dec(3);
}).start();
Thread.sleep(100);
System.out.println(c2.get());
}
}
class Counter{
private int count = 0;
public void add(int n) {
synchronized(this) {
count += n;
}
}
public void dec(int n) {
synchronized(this) {
count -= n;
}
}
public int get() {
return count;
}
}


这样一来,无论实例化几个counter,都不会存在逻辑问题,因为锁住的对象是this

如果一个类被设计为允许多线程正确访问,我们就称该类是线程安全的,上面的counter类是线程安全的,java标准库java.lang.StringBuffer也是线程安全的。

不变类:String,Integer,LocalDate,它们所有成员变量都是final,只能读不能写,所以是线程安全的。

最后。类似math这些只提供静态方法,没有成员变量的类,也是线程安全的。

可以用synchronized修饰方法,表示整个方法都必须用this实例加锁。对static方法加synchronized是锁住JVM为其自动创建的class实例。

针对上述代码中的get方法,由于只是读一个变量,是不需要同步的,但是如果是返回多个变量,则必须同步。

死锁

可重入锁

java的线程锁是可重入的锁,可重入的锁就是指在获取到一个线程的锁后,依然可以继续获取这个锁(官方定义:JVM允许同一个线程重复获取同一个锁,这个能被同一线程反复获取的锁,就叫做可重入锁)。例子如下:

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
public class Main2 {
public static void main(String[] args) {
// TODO Auto-generated method stub
var t1 = new Counter1();
//var t2 = new Counter1();
new Thread(()-> {
t1.add(-4);
}).start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(t1.get());
}
}
class Counter1{
private int count = 0;

public synchronized void add(int n) {
if(n<0) {
dec(-n);
}
else {
count += n;
}
}
public synchronized void dec(int n) {
count -= n;
}
public int get() {
return count;
}
}

如果进入add方法内,表明已经获取到this的锁,add方法内调用dec()方法,则又需要获取一次this的锁。

获取可重入锁的同时,需要记录这是第几次获取,每获取一次记录+1,释放后-1,减到0时真正释放锁。

死锁

多个线程之间获取锁的顺序存在一定问题,造成线程均无法正常结束,比如:

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
private int value = 0, another = 0;
public static Object lockA = new Object();
public static Object lockB = new Object();
public void add(int m) {
synchronized(t1.lockA) {
this.value += m;
System.out.println("get add lockA");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized(t1.lockB) {
this.another += m;
System.out.println("get add lockB");
}
}
}
public void dec(int m) {
synchronized(t1.lockB) {
this.value -= m;
System.out.println("get dec lockB");
try {
Thread.sleep(100); //一定要注意,这里是为了避免将锁快速释放
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized(t1.lockA) {
this.another -= m;
System.out.println("get dec lockA");
}
}
}

线程t1执行add(),线程t2执行dec(),那么:

  1. 线程t1:进入add(),获得lockA;
  2. 线程t2:进入dec(),获得lockB。

接着:

  1. 线程t1:准备获取lockB,失败,等待中;
  2. 线程t2:准备获取lockA,失败,等待中。

之后就是无限等待,JVM无法正常关闭,只能强制关闭。

解决办法:可以适当调整锁的获取顺序,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void dec(int m) {
synchronized(t1.lockA) {
this.value -= m;
System.out.println("get dec lockA");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized(t1.lockB) {
this.another -= m;
System.out.println("get dec lockB");
}
}
}

wait和notify

主要用来解决多进程之间的协调问题,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
class TaskQueue {
Queue<String> queue = new LinkedList<>();

public synchronized void addTask(String s) {
this.queue.add(s);
}

public synchronized String getTask() {
while (queue.isEmpty()) {
}
return queue.remove();
}
}

期望的结果是:线程1调用addTask()不断往队列添加任务,线程2可以调用getTask()获取任务,如果队列为空,等待,直到队列中有任务再返回。

实际上:while循环无法退出,因为进入getTask()时,已经获取了this的锁,不退出该方法,则其他线程无法再获取this的锁,也就无法调用addTask()。

wait()方法:可以使调用的线程释放锁,返回时再次尝试获取锁

1
2
3
4
5
6
7
8
9
public synchronized String getTask() {
//使用while而不是if,是因为如果三个线程被唤醒,且都获得锁,肯定有一个能获得任务,另外两个可能依旧没法获得任务
while (queue.isEmpty()) {
// 释放this锁:
this.wait();
// 重新获取this锁
}
return queue.remove();
}

notify()方法(或notifyAll()方法):唤醒等待锁的线程,这样wait()方法才能返回

1
2
3
4
public synchronized void addTask(String s) {
this.queue.add(s);
this.notify(); // 唤醒在this锁等待的线程
}