Java中多线程同步

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
  • 不变类,StringIntegerLocalDate
  • 没有成员变量的类 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

原文地址:https://www.cnblogs.com/engeng/p/15544466.html