跳至主要內容

Java - 多线程与反射2

codejava约 1738 字大约 6 分钟

多线程2

线程锁和线程同步

多线程下java内存管理

20250226223324
20250226223324

线程之间的共享变量(比如之前悬念中的value变量)存储在主内存(main memory)中,每个线程都有一个私有的工作内存(本地内存),工作内存中存储了该线程以读/写共享变量的副本。

类似于我们在计算机组成原理中学习的多核心处理器高速缓存机制:

高速缓存通过保存内存中数据的副本来提供更加快速的数据访问,但是如果多个处理器的运算任务都涉及同一块内存区域,就可能导致各自的高速缓存数据不一致,在写回主内存时就会发生冲突,这就是引入高速缓存引发的新问题,称之为:缓存一致性

实际上,Java的内存模型也是这样类似设计的,当我们同时去操作一个共享变量时,如果仅仅是读取还好,但是如果同时写入内容,就会出现问题
好比说一个银行,如果我和我的朋友同时在银行取我账户里面的钱,难道取1000还可能吐2000出来吗?我们需要一种更加安全的机制来维持秩序,保证数据的安全性!

比如我们可以来看看下面这个问题:

private static int value = 0;

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 10000; i++) value++;
        System.out.println("线程1完成");
    });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 10000; i++) value++;
        System.out.println("线程2完成");
    });
    t1.start();
    t2.start();
    Thread.sleep(1000);  //主线程停止1秒,保证两个线程执行完成
    System.out.println(value);
}

实际上,当两个线程同时读取value的时候,可能会同时拿到同样的值,而进行自增操作之后,也是同样的值,再写回主内存后,本来应该进行2次自增操作,实际上只执行了一次!

线程锁 synchronized

为了避免上面的这个现象,引入 线程锁 的概念

synchronized(对象 |){
  代码块
}

synchronized需要在括号中填入一个内容,必须是一个对象或是一个类,因此有对象锁和类锁区别(传入的如果是对象,就是对象锁,不同的对象代表不同的对象锁,如果是类,就是类锁,类锁只有一个,实际上类锁也是对象锁,是Class类实例,但是Class类实例同样的类无论怎么获取都是同一个),但是注意两个线程必须使用同一把锁

当一个线程进入到同步代码块时,会获取到当前的锁,而这时如果其他使用同样的锁的同步代码块也想执行内容,就必须等待当前同步代码块的内容执行完毕,在执行完毕后会自动释放这把锁,而其他的线程才能拿到这把锁并开始执行同步代码块里面的内容(实际上synchronized是一种悲观锁,随时都认为有其他线程在对数据进行修改,后面在JUC中我们还会讲到乐观锁,如CAS算法)

private static int value = 0;

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 10000; i++) {
            synchronized (Main.class){  
                //使用synchronized关键字创建同步代码块
                value++;
            }
        }
        System.out.println("线程1完成");
    });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 10000; i++) {
            synchronized (Main.class){
                value++;
            }
        }
        System.out.println("线程2完成");
    });
    t1.start();
    t2.start();
    Thread.sleep(1000);  //主线程停止1秒,保证两个线程执行完成
    System.out.println(value);
}

当对象不同时,获取到的是不同的锁,因此并不能保证自增操作的原子性,最后也得不到我们想要的结果。

synchronized关键字也可以作用于方法上,调用此方法时也会获取锁:

private static int value = 0;

private static synchronized void add(){
    value++;
}

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 10000; i++) add();
        System.out.println("线程1完成");
    });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 10000; i++) add();
        System.out.println("线程2完成");
    });
    t1.start();
    t2.start();
    Thread.sleep(1000);  //主线程停止1秒,保证两个线程执行完成
    System.out.println(value);
}

我们发现实际上效果是相同的,只不过这个锁不用你去给,
如果是静态方法,就是使用的类锁

public class Main {
    private static int i = 0;

    private static synchronized void add(){
        i++;
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            for(int j = 0;j < 100000;j++)
                add();
        }).start();

        new Thread(()->{
            for(int j = 0;j < 100000;j++)
                add();
        }).start();

        new Thread(()->{
            for(int j = 0;j < 100000;j++)
                synchronized (Main.class){
                    // Main.class 就保证三个线程的锁是一致的 类锁
                    i++;
                }
        }).start();

        Thread.sleep(1000);
        System.out.println(i);
    }
}

而如果是普通成员方法,就是使用的对象锁

public class Main {
    private static int i = 0;

    private synchronized void add(){
        i++;
    }

    public static void main(String[] args) throws InterruptedException {
        Main main = new Main();

        new Thread(()->{
            for(int j = 0;j < 100000;j++)
                main.add();
        }).start();

        new Thread(()->{
            for(int j = 0;j < 100000;j++)
                main.add();
        }).start();

        new Thread(()->{
            for(int j = 0;j < 100000;j++)
                synchronized (main){
                    // 使用 main 才正确
                    i++;
                }
        }).start();

        Thread.sleep(1000);
        System.out.println(i);
    }
}

通过灵活的使用synchronized就能很好地解决我们之前提到的问题了。

死锁

死锁是指两个线程相互持有对方需要的锁,但是又迟迟不释放,导致程序卡住:

20250226231025
20250226231025

我们发现,线程A和线程B都需要对方的锁,但是又被对方牢牢把握,由于线程被无限期地阻塞,因此程序不可能正常终止。我们来看看以下这段代码会得到什么结果:

public static void main(String[] args) throws InterruptedException {
    Object o1 = new Object();
    Object o2 = new Object();
    Thread t1 = new Thread(() -> {
        synchronized (o1){
            try {
                Thread.sleep(1000);
                synchronized (o2){
                    System.out.println("线程1");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    Thread t2 = new Thread(() -> {
        synchronized (o2){
            try {
                Thread.sleep(1000);
                synchronized (o1){
                    System.out.println("线程2");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    t1.start();
    t2.start();
}

所以,我们在编写程序时,一定要注意,不要出现这种死锁的情况。那么我们如何去检测死锁呢?
我们可以利用jstack命令来检测死锁,首先利用jps找到我们的java进程号,然后 jstack pid 即可

jstack自动帮助我们找到了一个死锁,并打印出了相关线程的栈追踪信息,同样的,使用jconsole也可以进行监测。

因此,前面说不推荐使用 suspend() 去挂起线程的原因,是因为suspend()在使线程暂停的同时,并不会去释放任何锁资源。其他线程都无法访问被它占用的锁。直到对应的线程执行resume()方法后,被挂起的线程才能继续,从而其它被阻塞在这个锁的线程才可以继续执行。但是,如果resume()操作出现在suspend()之前执行,那么线程将一直处于挂起状态,同时一直占用锁,这就产生了死锁。

上次编辑于: