简明易懂多线程(三) JAVA

多线程(一)
多线程(二)
多线程(三)

线程同步

由于线程的调度由操作系统决定,程序本身无法决定。因此,任何一个线程都有可能在任何一个操作步骤被暂停,然后在某个时间在被操作系统执行。
这也就带来了一个单线程不会存在的问题:如果多个线程同时读写共享变量,会出现数据不一致的问题。
下面来看一个例子

public class Main {
    public static void main(String[] args) throws Exception {
        var add = new AddThread();
        var dec = new DecThread();
        add.start();
        dec.start();
        add.join();
        dec.join();
        System.out.println(Counter.count);
    }
}

class Counter {
    public static int count = 0;
}

class AddThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) { Counter.count += 1; }
    }
}

class DecThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) { Counter.count -= 1; }
    }
}

上述代码看起最后得到的结构应该是0,但实际上这个结果是不确定的
想要保证结果的正确,必须保证在对变量进行读写操作时,使用原子操作
原子操作: 不能被中断的一个或者一系列操作。
例如对语句n=n+1看上去是一行语句,实际上对应了三条指令

ILOAD
IADD
ISTORE

这三条指令在任何一个地方都有可能会被操作系统暂停。
我们假设n=100,如果两个线程同时执行n=n+1,最后得到的结果很可能不是102而是101,原因在于:
如果线程1在执行ILOAD后被操作系统中断,此刻如果线程2被调度执行,它执行ILOAD后获取的值仍然是100,最终结果被两个线程的ISTORE写入后变成了101,而不是期待的102。
综上所述,如果要保证线程的同步,对共享变量读写时必须保证一组指令以原子方式执行:即某个线程执行时,另外的线程等待。

通过加锁和解锁的操作,就能保证指令总是在一个线程执行期间,不会有其他线程会进入此指令区间。即使在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。只有执行线程将锁释放后,其他线程才有机会获得锁并执行。这种加锁和解锁之间的代码块我们称之为临界区(Critical Section)任何时候一个临界区最多只有一个线程能执行
通过加锁与解锁可以保证一段代码的原子性。

java中实现锁的方法有

  • synchronized关键字
  • ReentrantLock (可重入锁)
  • ReadWriteLock
  • 原子类

本文暂时只介绍synchronized关键字

synchronized

我们把上面的代码用synchronized改写如下:

public class Main {
    public static void main(String[] args) throws Exception {
        var add = new AddThread();
        var dec = new DecThread();
        add.start();
        dec.start();
        add.join();
        dec.join();
        System.out.println(Counter.count);
    }
}

class Counter {
    public static final Object lock = new Object();
    public static int count = 0;
}

class AddThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) {
            synchronized(Counter.lock) {
                Counter.count += 1;
            }
        }
    }
}

class DecThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) {
            synchronized(Counter.lock) {
                Counter.count -= 1;
            }
        }
    }
}

它表示用Counter.lock实例作为锁,两个线程在执行各自的synchronized(Counter.lock) { ... }代码块时,必须先获得锁,才能进入代码块进行。执行结束后,在synchronized语句块结束会自动释放锁。这样一来,对Counter.count变量进行读写就不可能同时进行。上述代码无论运行多少次,最终结果都是0。

synchronized常见使用方式

  • 同步方法
public synchronized void synchronizedMethod() {
    // 同步方法体
}

当一个线程调用一个对象的被 synchronized 修饰的方法时,该对象的锁被获取。其他试图调用该对象的 synchronized 方法的线程将被阻塞,直到拥有锁的线程执行完毕并释放锁。这确保了同一时刻只有一个线程可以执行该方法的代码块。

  • 同步代码块
public void someMethod() {
    synchronized (lockObject) {
        // 同步代码块
    }
}

在同步代码块中,lockObject 是一个对象引用,这个对象可以是任何对象。当一个线程进入 synchronized 代码块时,它会尝试获取 lockObject 的锁。如果该锁被其他线程持有,那么线程将被阻塞,直到获取到锁为止。

  • 对象锁和类锁
    • 对象锁: 当使用 synchronized 关键字修饰一个实例方法或实例代码块时,锁定的是对象实例。只有同一个对象实例的方法被调用时,才会发生互斥。
    • 类锁: 当使用 synchronized 关键字修饰一个静态方法或静态代码块时,锁定的是该类的 Class 对象。这意味着同一时刻只能有一个线程执行该类的所有 synchronized 静态方法或静态代码块。

synchronized相关特性

  • 内部锁(Intrinsic Lock)
    在Java中,每个对象都有一个内部锁(也称为监视器锁或互斥锁),当使用 synchronized 修饰方法或代码块时,实际上是在获取对象的内部锁。只有拥有锁的线程才能进入同步代码块,其他线程将被阻塞直到锁被释放。
  • 重入性(Reentrancy)
    Java 的锁是可重入的,这意味着同一个线程可以多次获取同一个锁,而不会发生死锁。当线程已经持有锁时,它可以继续执行其他 synchronized 方法或代码块,而不会被自己持有的锁阻塞。
  • 释放锁
    当同步方法或同步代码块执行完成时,锁会自动释放。如果方法抛出异常,锁也会被释放,确保其他线程可以继续执行。无需担心异常导致死锁

synchronized解决了多线程同步访问共享变量的正确性问题。但是,它的缺点是带来了性能下降。因为synchronized代码块无法并发执行。此外,加锁和解锁需要消耗一定的时间,所以,synchronized会降低程序的执行效率。

不需要synchronized的操作

JVM规范定义了几种原子操作:

  • 基本类型(long和double除外)赋值,例如:int n = m;

  • 引用类型赋值,例如:List list = anotherList。

  • long和double是64位数据,JVM没有明确规定64位赋值操作是不是一个原子操作,不过在x64平台的JVM是把long和double的赋值作为原子操作实现的。

  • 单条原子操作的语句不需要同步
    一条赋值语句不需要同步
    但是如果多条赋值语句,就必须保证同步操作

  • 不可变对象无需同步
    如果多线程读写的是一个不可变对象,那么无需同步,因为不会修改对象的状态

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值