Java使用多线程编程带来的问题就是,多线程同时读写共享变量,会出现数据不一致的问题。
对于语句:
n = n + 1;
对变量的赋值操作,实际上对应三条指令:
ILOAD // 从内存中取出变量值 IADD // 对其加1操作 ISTORE //放入变量对应的内存地址
由于多线程的并发执行,线程2从内存中取出的值,很可能并不是线程1放入后的值。因此需要一种机制,保证线程执行这三条指令的时候,不会有其他线程干扰。待操作完成后,再将“特权”交给其他线程。
1、使用 synchronized 关键字
synchronized(Counter.lock) { // 获取锁 ... } // 释放锁
synchronized 使用一个对象作为锁,多个线程在执行 synchronized 下的代码时,只有获得锁之后,才能继续运行。以此来保证对共享变量的有序访问。示例如下:
public class ThreadLock { public static void main(String[] args) throws InterruptedException { var ts = new Thread[] { new AddStudentThread(), new DesStudentThread(), new AddTeacherThread(), new DesTeacherThread() }; for (Thread t : ts) { t.start(); } for (Thread t : ts) { t.join(); } System.out.println(Counter.teacherCount); System.out.println(Counter.studentCount); } } class Counter { public static final Object lockStudent = new Object(); public static final Object lockTeacher = new Object(); public static int studentCount = 0; public static int teacherCount = 0; } class AddStudentThread extends Thread { @Override public void run() { for (int i = 0; i <= 1000; i++) { synchronized (Counter.lockStudent) { Counter.studentCount++; } } } } class DesStudentThread extends Thread { @Override public void run() { for (int i = 0; i <= 1000; i++) { synchronized (Counter.lockStudent) { Counter.studentCount--; } } } } class AddTeacherThread extends Thread { @Override public void run() { for (int i = 0; i <= 1000; i++) { synchronized (Counter.lockTeacher) { Counter.teacherCount++; } } } } class DesTeacherThread extends Thread { @Override public void run() { for (int i = 0; i <= 1000; i++) { synchronized (Counter.lockTeacher) { Counter.teacherCount--; } } } }
对共享变量 studentCount、teacherCount 进行操作时候,使用 synchronized 进行加锁操作,保证当前线程执行完成前,不会对共享变量访问操作。同时对于两个变量,使用了两个对象作为锁,保证执行效率。
2、原子操作
原子操作指的是不会被操作系统线程调度机制打断的操作。原子操作是不需要synchronized,JVM规范定义了几种原子操作:
- 基本类型变量赋值;(long、double 未定义,x64平台是作为原子操作的实现)
- 引用类型赋值;
注意,多行赋值语句非原子操作,多线程同步需要加锁。
3、同步方法
使用synchronized,需要指定一个锁对象,同时加锁的逻辑与业务逻辑代码会混在一起,造成逻辑混乱。更好的做法是封装加锁的逻辑,外层代码只负责调用,而无需考虑线程安全。
使用synchronized修饰方法,表示该方法是同步方法,使用this实例进行加锁。设计一个线程安全的类:
class MyCounter { private int count = 0; public synchronized void add(int n) { this.count += n; } public synchronized void dec(int n) { this.count -= n; } }
由于方法使用 synchronized 修饰,表示方法执行需要先获取锁(this)。
4、线程安全
一个类被设计为允许多线程正确访问,这个类就是“线程安全”的(thread-safe)。线程安全的类有:
- StringBuffer
- 不变类,
String
,Integer
,LocalDate
- 没有成员变量的类 Math
一个类默认是非线程安全的。
5、可重入锁
可重入锁值得是一个线程重复获取同一个锁。java支持可重入锁。
class MyCounter { private int count = 0; public synchronized void add(int n) { if(n < 0) { this.dec(n); } this.count += n; } public synchronized void dec(int n) { this.count -= n; } }
在add方法中,调用dec方法,由于两个方法均使用synchronized修饰,因此在add方法内部执行dec方法,需要再次获得锁。
6、死锁
死锁指的是两个线程各持有对方锁,且双方均等待对方手中的锁,造成无限期等待下去。死锁发生后只能强制结束JVM进程。
一个很好理解的例子:假设有两扇门,门上两把锁,钥匙A和B。甲使用钥匙A、B可进门,乙也可以使用钥匙A和B进门。但两人同时开门,会因缺少对方手里的钥匙而相互僵持。这就是死锁。
class Door { private Object lockA = new Object(); private Object lockB = new Object(); public void enter() { synchronized(lockA) { synchronized(lockB) { // } } } public void goin() { synchronized(lockB) { synchronized(lockA) { // } } } }
当 enter方法和goin方法同时执行,在各自获得锁之后,又会各自等待对方手中的锁,造成无限等待。解决的方法与上述开门解锁的道理相同,要么让甲先进门,要么让乙先进。因此设置锁的顺序很重要。
当获取A、B锁的顺序一致时,任何一方使用A锁时,另一方必须等待。不存在各自持有A、B锁的情况。
参考链接:
https://www.liaoxuefeng.com/wiki/1252599548343744/1306580888846370