线程的同步
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) { var t1 = new Counter1(); new Thread(()-> { t1.add(-4); }).start(); try { Thread.sleep(100); } catch (InterruptedException e) { 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) { 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) { e.printStackTrace(); } synchronized(t1.lockA) { this.another -= m; System.out.println("get dec lockA"); } } }
|
线程t1执行add(),线程t2执行dec(),那么:
- 线程t1:进入add(),获得lockA;
- 线程t2:进入dec(),获得lockB。
接着:
- 线程t1:准备获取lockB,失败,等待中;
- 线程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) { 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 (queue.isEmpty()) { this.wait(); } return queue.remove(); }
|
notify()方法(或notifyAll()方法):唤醒等待锁的线程,这样wait()方法才能返回
1 2 3 4
| public synchronized void addTask(String s) { this.queue.add(s); this.notify(); }
|