java中的多线程

关键字:内置锁,可重入锁,可中断锁,公平锁,自旋锁,cas, 锁升级,偏向锁,轻量级锁,重量级锁

线程是程序中的一个执行序列,多个线程就意味着程序可以有多个执行序列。使用多线程可以提高CPU的使用效率。比如在生产者消费者模型中,生产者和消费者分别属于两个不同的线程,两个线程可以同时进行。只要缓冲区还没满,生产者就可以一直生产数据;只要缓冲区有数据,消费者也可以一直消费,两个过程是可以同时进行的

存在的挑战

  • 并发访问共享变量,会造成共享变量值的不准确,需用通过线程同步的手段来保证结果的正确性
  • 线程上下文切换,需要付出额外的性能
  • 线程死锁会导致程序异常

线程间的通信方式

线程间的通信指的是一个线程通知并把数据共享给另外一个线程, 在java中,线程间的通信是同过共享变量的方式实现的,这个通信过程是隐式

int count = 0;
    void calc(){
        Thread a = new Thread(() ->{
                int m = count + 1;  //  A
                count = m;
            }
        });

        Thread b = new Thread(() ->{
                int n = count + 1;  //  B
                count = n;
            }
        });
        a.start();
        b.start();
    }
复制代码

如上面的代码,两个线程各为count变量加上1,线程a和线程b是通过共享变量count来实现通信。这种通信方式的优点就是实现简单,不需要编写额外的代码来实现通信。但是简单背后却会引入一些复杂的问题,比如共享变量的并发访问问题,线程间的同步问题

共享变量并发访问

由于多线程间是同时执行的,有可能会导致读取了一些脏数据,并错误的修改共享变量。如上面的例子,原本count的值为0,如果线程a执行了A处得到了m的值为1,这时切换到线程b执行B处得到了n的值为1,m和n的值都为1导致count更新过后的值为1,与期望的值不一致

多线程间的同步

所谓的线程同步就是让多个线程在执行某段代码块时能够按照一定的顺序来执行,或者一次只能有一个线程执行代码块。线程的同步是通过java的内置锁sychronized或者是Lock接口及其实现类来实现的

sychronized blocks

  • sychronized 是java中的内置锁,可以限制线程对代码块或者方法的访问
  • sychronized可以修饰类方法,实例方法,代码块
  • 在执行sychronized方法或代码块时,线程需要先获取被修饰对象的锁。一次只能有一个线程可以获取到一个对象的锁,同一个线程可以多次获取同一个对象的锁(可重入锁)
  • sychronized 不能响应中断,当一个线程在等待锁的时候,调用该线程的interrupt是不起作用的
  • 锁的获取和释放是隐式的,进入同步sychronized blocks后会获取锁,离开sychronized blocks后会释放锁

Obejct类的wait/notify方法

  • wait/notify是用于线程同步的方法
  • wait方法会使得当前线程放弃调用对象的监控,并使当前线程进入等待。直到调用了该对象的notify方法或者notifyAll方法(语法上是这样设计,但存在例外)
  • 可以多次调用对象的wait方法,notify方法只会随机释放一个wait方法等待,与调用顺序无关。如果要释放所有的wait调用可以调用notifyAll方法
  • 调用wait的线程有可能会存在interrupt,虚假唤醒的情况,导致wait方法返回,但实际并没有调用对象的notify方法。在使用时通常会搭配一个lock flag和loop使用
boolean isLocked = false;

  public synchronized void lock()
  throws InterruptedException{
    while(isLocked){
      wait();
    }
    isLocked = true;
  }

public synchronized void unlock()
  throws InterruptedException{
    notify();
    isLocked = false;
  }
复制代码

对象头

  • 对象头可以分为Mark Word,Class Metadata Address,Array length三个部分。
  • 对象头存储着对象的hashcode,锁信息,gc信息,对象类型指针,数组长度(数组类型对象)等数据。
  • sychronized用的锁是保存在对象头里的

锁的升级

  • Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状 态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高 获得锁和释放锁的效率。在锁的获取和升级中使用了大量的cas操作
偏向锁

大多数情况下,锁都是由同一个线程获取。当线程获取对象的偏向锁时,会在对象头的Mark Word和线程栈帧的锁记录中存储偏向的线程ID;当同一个线程在次获取锁时只需要判断Mark Word指向的线程ID是否等于当前线程ID,相等则说明当前线程已经获取了锁

轻量级锁

获取轻量级锁时,先把对象头的Mark Word复制到当前线程栈帧中的锁记录中,然后使用cas操作将对象头的Mark Word替换为指向锁记录的指针;如果成功则获取轻量级锁成功,否则通过自旋锁来将Mark Work替换为指向锁记录的指针;自旋获取轻量级锁失败后,则锁膨胀成重量级锁

重量级锁

当膨胀成重量级锁后,竞争锁的线程会被阻塞。当持有锁的线程释放了锁后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争


优点

缺点

适用场景

偏向锁

加锁和解锁不需要额外消耗,和执行非同步方法相比仅存在毫秒级别的差距

如果线程存在锁竞争会代来额外的锁消耗

适用于只有一个线程访问同步块场景

轻量级锁

竞争线程不会阻塞,提高了程序的响应速度

如果始终得不到锁竞争的线程,使用自旋会消耗CPU

追求响应时间,同步快执行非常快

重量级锁

线程竞争不会使用自旋,不会消耗CPU

线程阻塞,响应时间慢

追求吞吐量,同步快执行时间较长

Lock/ReadWriteLock/Condition接口

  • Lock接口和ReadWriteLock接口位于JUC下,是JDK1.5中提供的并发编程包,用于解决并发编程中遇到的同步问题。并在后续的JDK版本中不断完善
  • Lock接口中的lock/lockInterruptibly可用于获取锁;tryLock用于尝试获取锁,超过指定时间未获取则返回; unlocky用于释放锁。同样属于可重入锁。可响应中断
  • ReadWriteLock接口有readLock/writeLock两个方法用于获取读锁和写锁,当一个线程获取了读锁后,其他线程可以再次获取读锁而不需要等待;而写锁则需要没有其他线程获取读锁和写锁时,才能获取。
  • Condition接口包含await/signal/signalAll方法,与Obejct的wait/notify/notifyAll类似,也是用于线程同步的方法
  • Obejct的wait/notify/notifyAll的调用需要先获取调用对象的监控,而Condition的await/signal/signalAll方法的调用也需要获取对应Lock对象的锁,Conditon 由Lock.newConditon方法创建,调用同步方法前需要调用Lock.lock方法获取锁
  • Condition可以实现比Object.wait/notify更精细的线程同步
  • Lock接口的实现类ReentrantLock类默认是使用非公平锁的,获取锁的线程顺序与请求锁的线程顺序无关,由jvm决定。通过构造方法传入一个boolean类型可以设置成公平锁。非公平锁可能会造成线程饿死,但公平锁则会导致性能下降
  • Lock接口和ReadWriteLock接口对于锁的获取和释放是显式的,需要显式的调用lock和unlock方法,锁的释放处理不当会造成死锁
void doWork(){
    lock.lock();
    try{
        //doSomething
    }catch(InterruptedException e){
     //doSomething
    }finally{
        lock.unlock();
    }
}
复制代码

volatile

由于java的内存模型的原因,线程在修改了共享变量后并不会立即把修改同步到内存中,而是会保存到线程的本地缓存中。volatile关键字修饰的变量在线程修改后会立刻同步到主内存中,使该修改对其他线程可见

自旋锁与cas

  • cas(compare and set)指的是将一个值与指定值比较,如果相等则赋予新值。该操作是原子操作。JUC下的AtomicInteger/AtomicBoolean/AtomicLong都包含compareAndSet方法。当赋值成功后返回true,否则返回false
  • cas操作是非阻塞的
  • cas操作会导致ABA问题
  • 自旋锁是一种非阻塞的锁,通过loop来获取锁,如果获取失败则在次获取,而不会引起当前线程的休眠。这是属于一种乐观锁,当认定程序很少会出现资源竞争的情况,且当出现资源竞争时,竞争的时间很短暂,则可以使用cas搭配自旋锁的方式来同步代码
public class Spinlock {
    volatile AtomicBoolean locked = new AtomicBoolean(false);

    void lock(){
        while (!locked.compareAndSet(false, true)){}
    }

    void unlock(){
        while (!locked.compareAndSet(true, false)){}
    }

    public static void main(String[] args) throws Exception{
        Spinlock spinlock = new Spinlock();
        new Thread(() -> {
            spinlock.lock();
            try {
                Thread.sleep(2000);
                System.out.println("done something");
            }catch (InterruptedException e){
            }finally {
                spinlock.unlock();
            }
        }).start();

        Thread.sleep(100);

        new Thread(() -> {
            spinlock.lock();
            System.out.println("next step");
            spinlock.unlock();

        }).start();
    }
}
输出:
done something
next step
复制代码

参考文献