Java多线程详解

基本概念

多线程:

指的是这个程序(一个进程)运行时产生了不止一个线程

并行:

多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。

并发:

通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力。

线程安全:

指在并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响任何结果。这个时候使用多线程,我们只需要关注系统的内存,cpu是不是够用即可。反过来,线程不安全就意味着线程的调度顺序会影响最终结果。

同步:

通过人为的控制和调度,保证共享资源的多线程访问成为线程安全,来保证结果的准确。

线程的状态

NEW(新建状态)

当一个线程创建以后,就处于新建状态。那什么时候这个状态会改变呢?只要它调用的start()方法,线程就进入了锁池状态。

BLOCKED(锁池)

进入锁池以后就会参与锁的竞争,当它获得锁以后还不能马上运行,因为一个单核CPU在某一时刻,只能执行一个线程,所以他需要操作系统分配给它时间片,才能执行。

RUNNABLE(运行状态)

当一个持有对象锁的线程获得CPU时间片以后,开始执行这个线程,此时叫做运行状态。

TIMED_WAITING(定时等待)、WAITING(等待)

处于运行状态的线程还可能调用wait()方法、或者带时间参数的wait(long milli)方法。这时候线程就会将对象锁释放,进入等待队列里面(如果是调用wait()方法则进入等待状态,如果是调用带时间参数的则进入定时等待状态)

TERMINATED(终止、结束)

当一个线程正常执行完,那么就进入终止(死亡)状态。系统就会回收这个线程占用的资源。


线程状态.png

注意:1.当线程调用sleep()方法或当前线程中有其他线程调用了带时间参数的join()方法的时候进入了定时等待状态(TIMED_WAITING)
2.当其他线程调用了不带时间参数的join()(join内部调用的是sleep,所以可看成sleep的一种)方法时进入等待状态(WAITING)。
3.当线程遇到I/O的时候还是运行状态(RUNNABLE)
4.当一个线程调用了suspend()方法挂起的时候它还是运行状态(RUNNABLE)。

synchronized、Lock

他们是应用于同步问题的人工线程调度工具,wait/notify必须存在于synchronized块中。并且,这三个关键字针对的是同一个监视器(某对象的监视器)。这意味着wait之后,其他线程可以进入同步块执行。

同步原理

JVM规范规定JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现,而方法同步是使用另外一种方式实现的,细节在JVM规范里并没有详细说明,但是方法的同步同样可以使用这两个指令来实现。monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处, JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个 monitor 与之关联,当且一个monitor 被持有后,它将处于锁定状态。线程执行到 monitorenter 指令时,将会尝试获取对象所对应的 monitor 的所有权,即尝试获得对象的锁。

synchronized的使用

锁的本质是对象实例,对于非静态方法来说,Synchronized 有两种呈现形式,Synchronized方法体和Synchronized语句块。两种呈现形式本质上的锁都是对象实例。

1.锁定实例
public class SynchronizeDemo {
 public void doSth1() {
        /**
         * 锁对象实例 synchronizeDemo
         */
        synchronized (synchronizeDemo){
            try {
                System.out.println("正在执行方法");
                Thread.sleep(10000);
                System.out.println("正在退出方法");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void doSth2() {
        /**
         * 锁对象实例 this 等同于 synchronizeDemo
         */
        synchronized (this){
            try {
                System.out.println("正在执行方法");
                Thread.sleep(10000);
                System.out.println("正在退出方法");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }


}

synchronized块中的方法获取了lock实例的monitor,如果实例相同,那么只有一个线程能执行该块内容

2.直接用于方法
    public synchronized void doSth3() {
        /**
         *  表面呈现是锁方法体,实际上是synchronized (this) ,等价于上面
         */
        try {
            System.out.println("正在执行方法");
            Thread.sleep(10000);
            System.out.println("正在退出方法");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

相当于上面代码中用lock来锁定的效果,实际获取的是Thread1类的monitor。更进一步,如果修饰的是static方法,则锁定Synchronized 的Class对象

Lock的使用

lock: 在java.util.concurrent包内。共有三个实现:
1.ReentrantLock
2.ReentrantReadWriteLock.ReadLock
3.ReentrantReadWriteLock.WriteLock
lock的主要目的是和synchronized一样,但是lock更灵活
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。Sychronized的锁是非公平的,ReentrantLock默认情况下也是非公平的,但可以通过带boolean值的构造函数要求使用公平锁;
锁绑定多个条件是指一个ReentrantLock对象可以同时绑定多个Condition对象,而在Synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件,如果要多于一个条件关联的时候,就不得不额外添加一个锁,而ReentrantLock无需这样做,只需要多次调用newCondition()方法即可。

ReentrantLock
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 有界阻塞队列<br/>
 * 当队列为空,队列将会阻塞删除并获取操作的线程,直到队列中有新元素;<br/>
 * 当队列已满,队列将会阻塞添加操作的线程,直到队列有空位置;
 * <p>
 * Created by LeonWong on 16/4/29.
 */
public class BoundedQueue<T> {
    private Object[] items;

    // 添加的下标,删除的下标和数组当前数量
    private int addIndex, removeIndex, count;
    private Lock lock = new ReentrantLock();
    private Condition notEmpty = lock.newCondition();
    private Condition notFull = lock.newCondition();

    public BoundedQueue() {
        items = new Object[5];
    }

    public BoundedQueue(int size) {
        items = new Object[size];
    }

    /**
     * 添加一个元素,数组满则添加线程进入等待状态
     *
     * @param t
     * @throws InterruptedException
     */
    public void add(T t) throws InterruptedException {
        lock.lock();
        try {
            while (items.length == count) {
                System.out.println("添加队列--陷入等待");
                notFull.await();
            }
            items[addIndex] = t;
            addIndex = ++addIndex == items.length ? 0 : addIndex;
            count++;
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    /**
     * 删除并获取一个元素,数组空则进入等待
     *
     * @return
     * @throws InterruptedException
     */
    public T remove() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                System.out.println("删除队列--陷入等待");
                notEmpty.await();
            }
            Object tmp = items[removeIndex];
            items[removeIndex] = null;// 这一步可以有可无
            removeIndex = ++removeIndex == items.length ? 0 : removeIndex;
            count--;
            notFull.signal();
            return (T) tmp;
        } finally {
            lock.unlock();
        }
    }

    public Object[] getItems() {
        return items;
    }

    public void setItems(Object[] items) {
        this.items = items;
    }

    public int getAddIndex() {
        return addIndex;
    }

    public void setAddIndex(int addIndex) {
        this.addIndex = addIndex;
    }

    public int getRemoveIndex() {
        return removeIndex;
    }

    public void setRemoveIndex(int removeIndex) {
        this.removeIndex = removeIndex;
    }

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }

}
ReentrantReadWriteLock

允许多个读线程同时访问,但不允许写线程和读线程、写线程和写线程同时访问。相对于排他锁,提高了并发性。在实际应用中,大部分情况下对共享数据(如缓存)的访问都是读操作远多于写操作,这时ReentrantReadWriteLock能够提供比排他锁更好的并发性和吞吐量。

进程切换导致的系统开销

Java的线程是直接映射到操作系统线程之上的,线程的挂起、阻塞、唤醒等都需要操作系统的参与,因此在线程切换的过程中是有一定的系统开销的。在多线程环境下调用Synchronized方法,有可能需要多次线程状态切换,因此可以说Synchronized是在Java语言中一个重量级操作。
虽然如此,JDK1.6版本后还是对Synchronized关键字做了相关优化,加入锁自旋特性减少系统线程切换导致的开销,几乎与ReentrantLock的性能不相上下,因此建议在能满足业务需求的前提下,优先使用Sychronized。

volatile

多线程的内存模型:main memory(主存)、working memory(线程栈),在处理数据时,线程会把值从主存load到本地栈,完成操作后再save回去(volatile关键词的作用:每次针对该变量的操作都激发一次load and save)。

使用volatile关键字仅能实现对原始变量(如boolen、 short 、int 、long等)操作的原子性,但需要特别注意, volatile不能保证复合操作的原子性,即使只是i++,实际上也是由多个原子操作组成:read i; inc; write i,假如多个线程同时执行i++,volatile只能保证他们操作的i是同一块内存,但依然可能出现写入脏数据的情况。

volatile和synchronized的区别

1.volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
2.volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
3.volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
4.volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
5.volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

常用方法

Thread.yield()

当前线程可转让cpu控制权,让别的就绪状态线程运行(切换)

Thread.sleep()

暂停一段时间

join()

在一个线程中调用other.join(),将等待other执行完后才继续本线程。

interrupte()

中断线程
中断是一个状态!interrupt()方法只是将这个状态置为true而已。所以说正常运行的程序不去检测状态,就不会终止,而wait等阻塞方法会去检查并抛出异常。如果在正常运行的程序中添加while(!Thread.interrupted()) ,则同样可以在中断后离开代码体

future模式

使用步骤

(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。

(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。

(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。

(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

多线程控制类

1.ThreadLocal类

https://www.jianshu.com/p/e465ec03f326

2.原子类(AtomicInteger、AtomicBoolean……)

3.容器类

BlockingQueue

阻塞队列。该类是java.util.concurrent包下的重要类,通过对Queue的学习可以得知,这个queue是单向队列,可以在队列头添加元素和在队尾删除或取出元素
除了传统的queue功能(表格左边的两列)之外,还提供了阻塞接口put和take,带超时功能的阻塞接口offer和poll。put会在队列满的时候阻塞,直到有空间时被唤醒;take在队 列空的时候阻塞,直到有东西拿的时候才被唤醒。用于生产者-消费者模型尤其好用

常见的阻塞队列有:

ArrayListBlockingQueue
LinkedListBlockingQueue
DelayQueue
SynchronousQueue
ConcurrentHashMap

高效的线程安全哈希map

4.线程池

https://www.jianshu.com/p/bf2368937918

5. 信号量(Semaphore)

信号量是一个非负整数(车位数),所有通过它的线程(车辆)都会将该整数减一(通过它当然是为了使用资源),当该整数值为零时,所有试图通过它的线程都将处于等待状态。在信号量上我们定义两种操作: Wait(等待) 和 Release(释放)。 当一个线程调用Wait(等待)操作时,它要么通过然后将信号量减一,要么一直等下去,直到信号量大于一或超时。Release(释放)实际上是在信号量上执行加操作,对应于车辆离开停车场,该操作之所以叫做“释放”是因为加操作实际上是释放了由信号量守护的资源。

推荐阅读更多精彩内容