Java 多线程

一、简介

1.1、多线程的优势

1、发挥多处理器的强大能力
2、建模的简单性
3、异步事件的简化处理
4、响应更灵敏的用户界面

1.2、多线程带来的风险

1、安全性问题
2、活跃性问题
3、性能问题

二、并发编程

2.1、两大核心

1、JMM内存模型
2、happens-before规则
happens-before原则定义如下:

  1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
  2. 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。
2.2、三大特性

1、原子性
在JMM抽象模型中定义了8中原子操作。

  • lock(锁定):作用于主内存中的变量,将某个变量标识为某个线程的独占状态。
  • unlock(解锁):作用于主内存中的变量,将某个变量从某个线程的独占状态释放出来,可以被其他线程锁定。
  • read(读取):将主存中的变量从主存中读取到线程的工作内存中,供load操作使用。
  • load(载入):作用于线程工作内存,将read从主存读取的变量,保存到工作内存的变量副本。
  • use(使用):作用于工作内存中的变量,当虚拟机执行到需要变量的字节码时,就会需要该动作。
  • assign(赋值):作用于工作内存中的变量,当虚拟机执行变量的赋值字节码时,将执行该操作,将值赋值给工作内存中的变量。
  • store(存储):作用与工作内存中的变量,将工作内存的变量传递给主存。
  • write(写入):作用于主存的变量,将store步骤中传递过来的变量,写入到主存中。
    以上8种操作都是原子性的,JMM内存模型只保证了操作执行的顺序性,但是不保证操作的连续性。

2、有序性

  • 被synchronized修饰的代码只能被被当前线程占用,避免由于其他线程的执行导致的无序行。
  • volatile关键字包含了禁止指令重排序的语义,使其具有有序性。

3、可见性

  • synchronized 关键值,开始时会从内存中读取,结束时,会将变化刷新到内存中,所以是可见的。
  • volatile关键值,通过添加lock指令,也是可见的。

三、多线程同步

要编写正确的并发程序,关键点在于:在访问共享的可变状态时需要进行正确的管理。我们不仅希望防止某个程序正在使用对象状态而另一个程序在同时修改对象状态;而且希望当一个程序修改了对象状态后,其他程序能立即看到发生的对象状态变化。如果没有同步这种情况就无法实现。

3.1、线程封闭

当访问共享的可变数据时,通常需要使用同步,一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步。这种技术被称为线程封闭(Thread Confinement),它是实现线程安全性的最简单的方式之一。当每个对象封闭在一个线程中时,这种用法将自动实现线程安全性,即使被封闭的对象本身不是线程安全的。

1、Ad-hoc线程封闭

Ad-hoc线程封闭是指维护线程封闭性的职责完全由程序实现来承担。Ad-hoc线程封闭是非常脆弱的,因为没有任何一种语言特性,能将对象封闭到目标线程上。事实上,对线程封闭对象的引用通常保存在共有变量中。

2、栈封闭

栈封闭是线程封闭的一种特例,在栈封闭中,只能通过局部变量才能访问对象。
栈封闭比Ad-hoc线程封闭更容易维护。

3、ThreadLocal

维持线程封闭性的一种更规范方法是使用ThreadLocal,这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都保存有一份独立的副本,因此get总是返回由当前执行线程在set时设置的最新值。

3.2、只读共享

只读共享是指多线程操作共享对象时只能对共享对象进行只读操作不能进行写操作。这里分为不可变对象和事实不可变对象的共享。

1、不可变对象(Immutable Object)

如果某个对象在被创建后其状态就不能被修改,那么这个对象就被称为不可变对象。线程安全性是可变对象的固有属性之一,他们的不可变性条件是由构造函数创建的,只要他们的状态不改变,那么这些不可变性条件就能得意维持。

不可变对象一定是线程安全的。

当满足一下条件时,对象是不可变的:

  • 对象创建以后状态就不能更改。
  • 对象的所有域都是final类型。
  • 对象是正确创建的(在对象的创建期间,this引用没有逸出)。
2、事实不可变对象

如果对象从技术上来看是可变的,但其状态在发布后不会再改变,那么这种对象称为“事实不可变对象(Effectively Immutable Object)”。在这些对象发布后,程序只需将他们视为不可变对象即可。通常使用事实不可变对象,不仅可以简化开发过程,而且还能由于减少了同步而提高性能。

例如Date本身是可变的,但如果将它作为不可变对象使用,那么在多个线程之间共享对象时,就可以省去对锁的使用。

3.3、可变对象的安全共享

1、volatile
volatile能保证有序性、可见性,不保证原子性。volatile使用机器指令实现对象锁定。

2、Java内置锁
Java内置锁通过synchronized关键字使用,使用其修饰方法或者代码块,就能保证方法或者代码块以同步方式执行。
内置锁使用起来非常方便,不需要显式的获取和释放,任何一个对象都能作为一把内置锁。使用内置锁能够解决大部分的同步场景。“任何一个对象都能作为一把内置锁”也意味着出现synchronized关键字的地方,都有一个对象与之关联,具体说来:

  • 当synchronized作用于普通方法是,锁对象是this;
  • 当synchronized作用于静态方法是,锁对象是当前类的Class对象;
  • 当synchronized作用于代码块时,锁对象是synchronized(obj)中的这个obj。

3、显式锁Lock
内置锁这么好用,为什么还需多出一个显式锁呢?因为有些事情内置锁是做不了的,比如:
我们想给锁加个等待时间超时时间,超时还未获得锁就放弃,不至于无限等下去;
我们想以可中断的方式获取锁,这样外部线程给我们发一个中断信号就能唤起等待锁的线程;
我们想为锁维持多个等待队列,比如一个生产者队列,一个消费者队列,一边提高锁的效率。
显式锁(ReentrantLock)正式为了解决这些灵活需求而生。ReentrantLock的字面意思是可重入锁,可重入的意思是线程可以同时多次请求同一把锁,而不会自己导致自己死锁。下面是内置锁和显式锁的区别:

可定时:RenentrantLock.tryLock(long timeout, TimeUnit unit)提供了一种以定时结束等待的方式,如果线程在指定的时间内没有获得锁,该方法就会返回false并结束线程等待。

可中断:你一定见过InterruptedException,很多跟多线程相关的方法会抛出该异常,这个异常并不是一个缺陷导致的负担,而是一种必须,或者说是一件好事。可中断性给我们提供了一种让线程提前结束的方式(而不是非得等到线程执行结束),这对于要取消耗时的任务非常有用。对于内置锁,线程拿不到内置锁就会一直等待,除了获取锁没有其他办法能够让其结束等待。RenentrantLock.lockInterruptibly()给我们提供了一种以中断结束等待的方式。

条件队列(condition queue):线程在获取锁之后,可能会由于等待某个条件发生而进入等待状态(内置锁通过Object.wait()方法,显式锁通过Condition.await()方法),进入等待状态的线程会挂起并自动释放锁,这些线程会被放入到条件队列当中。synchronized对应的只有一个条件队列,而ReentrantLock可以有多个条件队列,多个队列有什么好处呢?请往下看。

条件谓词:线程在获取锁之后,有时候还需要等待某个条件满足才能做事情,比如生产者需要等到“缓存不满”才能往队列里放入消息,而消费者需要等到“缓存非空”才能从队列里取出消息。这些条件被称作条件谓词,线程需要先获取锁,然后判断条件谓词是否满足,如果不满足就不往下执行,相应的线程就会放弃执行权并自动释放锁。使用同一把锁的不同的线程可能有不同的条件谓词,如果只有一个条件队列,当某个条件谓词满足时就无法判断该唤醒条件队列里的哪一个线程;但是如果每个条件谓词都有一个单独的条件队列,当某个条件满足时我们就知道应该唤醒对应队列上的线程(内置锁通过Object.notify()或者Object.notifyAll()方法唤醒,显式锁通过Condition.signal()或者Condition.signalAll()方法唤醒)。这就是多个条件队列的好处。

使用内置锁时,对象本身既是一把锁又是一个条件队列;使用显式锁时,RenentrantLock的对象是锁,条件队列通过RenentrantLock.newCondition()方法获取,多次调用该方法可以得到多个条件队列。

一个使用显式锁的典型示例如下:

// 显式锁的使用示例
ReentrantLock lock = new ReentrantLock();

// 获取锁,这是跟synchronized关键字对应的用法。
lock.lock();
try{
    // your code
}finally{
    lock.unlock();
}

// 可定时,超过指定时间为得到锁就放弃
try {
    lock.tryLock(10, TimeUnit.SECONDS);
    try {
        // your code
    }finally {
        lock.unlock();
    }
} catch (InterruptedException e1) {
    // exception handling
}

// 可中断,等待获取锁的过程中线程线程可被中断
try {
    lock.lockInterruptibly();
    try {
        // your code
    }finally {
        lock.unlock();
    }
} catch (InterruptedException e) {
    // exception handling
}

// 多个等待队列,具体参考[ArrayBlockingQueue](https://github.com/CarpenterLee/JCRecipes/blob/master/markdown/ArrayBlockingQueue.md)
/** Condition for waiting takes */
private final Condition notEmpty = lock.newCondition();
/** Condition for waiting puts */
private final Condition notFull = lock.newCondition();

注意,上述代码将unlock()放在finally块里,这么做是必需的。显式锁不像内置锁那样会自动释放,使用显式锁一定要在finally块中手动释放,如果获取锁后由于异常的原因没有释放锁,那么这把锁将永远得不到释放!将unlock()放在finally块中,保证无论发生什么都能够正常释放。

你可能感兴趣的:(Java 多线程)