java并发编程

线程状态

  • NEW(新建) 编程语言创建线程,还未被操作系统创建
  • RUNNABLE(可运行) 线程已被操作系统创建,但还未被分配CPU运行
  • RUNNING(运行) 线程执行
  • BLOCKED/WATTING/TIMED_WATTING(阻塞) 线程释放CPU执行权
  • DEAD(死亡) 线程结束,不可复生

happens-before

happens-before的概念指的是两个操作之前的执行顺序
假设A happens-before B,那么A的操作对B是可见的,也可以理解为A一定比B先执行,比如说当A修改了一个变量以后,B立马就能看到修改后的值

synchornized关键字

基于操作系统Mutex Lock实现。
synchornized关键字修饰的代码块为同步代码块,多个线程访问只有一个线程能获得锁,其余线程会阻塞等待释放锁后再去获取锁
获取锁释放锁会导致用户态和内核态的切换,这是操作系统级别的切换,非常影响性能

synchornized优化

JDK1.6之后引入偏向锁 轻量级锁 重量级锁 的概念
多个线程访问同步代码块,当一个线程第一个获取到锁,这个时候获得偏向锁,当另一个线程试图获取锁发现已经被别的线程获取,锁升级为轻量级锁,这个线程开始自旋(死循环试图获取锁),自旋重试次数超过之后还未获取到锁,则升级为重量级锁,此时会阻塞。
偏向锁乐观认为没有竞争,偏向于第一个获取到他的锁,并且不会主动释放,当别的线程试图获取锁的时候才会去检查该线程是否存活,如果存活则升级为轻量级锁,否则释放
轻量级锁认为存在竞争,并且通过自旋去试图获取锁,如果次数超过了以后还未获取到锁,锁升级为重量级锁,未获取到锁的线程则阻塞
锁获取流程:java对象头包含mark word和class metadata address,当线程获取到锁,mark word会存储这个线程ID,另一个线程试图获取锁的时候通过CAS操作替换存储的线程ID,如果前一个线程已经退出,则CAS成功,否则失败,重复这个操作
性能优化:

  • -XX:-UseSpinning 关闭自旋
  • -XX:PreBlockSpin 修改自旋次数(JDK1.7以后不支持,由JVM控制)
  • -XX:-UseBiasedLocking 关闭偏向锁
  • -XX:+UseHeavyMonitors 设置重量级锁

volatile关键字

volatile关键字修饰的变量,线程在修改的时候会强制同步到主内存,线程在读的时候会强制从主内存读取,保证线程能获取到最新的值

volatile实现原理

在生成汇编(javap -v xxx.class)代码时会在volatile修饰的共享变量进行写操作的时候会多出Lock前缀的指令,Lock指令有如下的作用:

  • Lock前缀的指令会引起处理器缓存写回内存;
  • 一个处理器的缓存回写到内存会导致其他处理器的缓存失效;
  • 当处理器发现本地缓存失效后,就会从内存中重读该变量数据,即可以获取当前最新值

volatile不能保证原子性

对于一个i++操作,如果两个线程同时执行,这里面大致是这三个步骤:
A: 读取i -> 执行i+1 -> 将结果赋值给i
B: 读取i -> 执行i+1 -> 将结果赋值给i
首先A线程从主内存中读取i的值到工作内存中,然后在工作内存中计算i+1的结果,最后赋值给i并写入主内存,如果在计算i+1但还未来得及将值赋值给i的时候B线程去做i++的操作,这时候B读取到i的值是未修改的值,最终结果i的值任然是i+1而不是预期的i+2。

Lock

ReentrantLock

ReentrantLock为可重入锁,其实现锁的逻辑由内部类Sync实现,Sync类实现了AQS(AbstractQueuedSynchronizer),派生两个类分别是FairSync(公平锁)和NonfairSync(非公平锁),ReentrantLock构造方法默认为非公平锁。
公平锁与非公平锁的区别:若是公平锁,处于等待队列的线程会按顺序一个一个运行,而非公平锁则刚运行完的线程可能会继续运行
加锁流程:
非公平锁

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    /**
     * Performs lock.  Try immediate barge, backing up to normal
     * acquire on failure.
     */
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

公平锁

static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
        acquire(1);
    }

    /**
     * Fair version of tryAcquire.  Don't grant access unless
     * recursive call or no waiters or is first.
     */
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}
  • 非公平锁在加锁时首先尝试CAS替换state,如果cas成功,则调用AbstractOwnableSynchronizer的setExclusiveOwnerThread方法获取锁,该方法将当前线程设置为独占模式(这里就是非公平锁的体现,如果线程CAS成功就获取锁)
  • 若CAS失败,调用AQS的acquire()方法
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
  • acquire()方法中先调用tryAcquire方法,最终调用Sync的nonfairTryAcquire方法
abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = -5179523762034025860L;

    /**
    * Performs {@link Lock#lock}. The main reason for subclassing
    * is to allow fast path for nonfair version.
    */
    abstract void lock();

    /**
    * Performs non-fair tryLock.  tryAcquire is implemented in
    * subclasses, but both need nonfair try for trylock method.
    */
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
    ...
}
  • nonfairTryAcquire第一步判断当前线程state是否为0,若是则CAS替换state,并获取锁;若不是0并且当前线程是exclusiveOwnerThread则将state+1,这里没有加锁是该线程已经获取锁,此时不需要再加锁,这也就是可重入的体现
  • 若获取锁失败,将该线程加入等待队列
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
  • addWaiter方法仅仅是向队列中追加一个节点,若队列为空则新建一个空节点为头节点,再将当前节点放在头节点后面
  • acquireQueued比较重要,线程未获取到锁的挂起操作是在这个方法中实现的
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
* 首先获取当前节点的前驱节点,若前驱节点是头节点,那么直接尝试获取锁,如果获取成功则将当前节点设为头节点,这里说明头节点就是占有锁的节点
* 获取失败执行shouldParkAfterFailedAcquire方法,来看看该方法的逻辑
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
* 这个方法用来判断节点是否应该被挂起,首先获取当前节点的前驱节点,若前驱节点的waitState为Node.SIGNAL(-1),则返回true,该状态表示后继结点正在等待当前节点唤醒,因此返回true后当前节点被挂起等待唤醒。
* 若前驱节点>0,循环查找最近的一个waitState<=0的前驱节点(waitState>0的只有1,表示节点已取消),并把当前节点设置到该节点之后
* 若waitState=0(初始值)或者<-1,则将前驱节点的值更新为Node.SIGNAL,下一次循环进来时就可让当前节点挂起
* 最后方法返回了true就走到parkAndCheckInterrupt方法,该方法调用LockSupport.park方法将线程挂起

我们再来看公平锁

  • 公平锁加锁直接调用acquire方法,这与非公平锁CAS失败后的逻辑一致
  • 公平锁的acquire方法会调用公平锁的tryAcquire方法,这里与非公平锁不同的是,非公平锁在判断了当前state是0后直接CAS操作获取锁,而公平锁在CAS之前会调用hasQueuedPredecessors方法判断等待队列中是否有线程正在排队的情况,如果没有才会获取锁
public final boolean hasQueuedPredecessors() {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}
  • 这样可以保证遵循FIFO的原则,每一个先来的线程都可以最先获取到锁,但是增加了上下文切换与等待线程的状态变换时间。所以效率相较于非公平锁较慢
    之后的逻辑与非公平锁一致

解锁流程:

  • ReentrantLock解锁需要手动调用unlock方法
/**
* Attempts to release this lock.
*
* <p>If the current thread is the holder of this lock then the hold
* count is decremented.  If the hold count is now zero then the lock
* is released.  If the current thread is not the holder of this
* lock then {@link IllegalMonitorStateException} is thrown.
*
* @throws IllegalMonitorStateException if the current thread does not
*         hold this lock
*/
public void unlock() {
    sync.release(1);
}
  • unlock会调用AQS的release方法,我们来看看release方法
/**
    * Releases in exclusive mode.  Implemented by unblocking one or
    * more threads if {@link #tryRelease} returns true.
    * This method can be used to implement method {@link Lock#unlock}.
    *
    * @param arg the release argument.  This value is conveyed to
    *        {@link #tryRelease} but is otherwise uninterpreted and
    *        can represent anything you like.
    * @return the value returned from {@link #tryRelease}
    */
    public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
  • 这里首先执行tryRelease方法,该方法具体内容由Sync类实现
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}
  • 我们可以看到,tryRelease中实现比较简单,只是判断一下当前state-1是否为0,如果为0的话调用setExclusiveOwnerThread(null)释放线程,并设置线程状态为0
  • 如果当前state-1不为0的话返回false,表示解锁失败
  • 当解锁成功之后,调用unparkSuccessor唤醒后面的节点,该方法原理就是遍历后面的节点,唤醒wateState为0或者-1的节点

ReentrantLock状态总结
state:初始为0,获取到锁+1,之后每重入一次都会执行+1的操作;释放锁(调用unlock)会-1,当state为0的时候真正释放资源
waitState:

  • 初始为0,抢锁失败之后前驱节点的waitState会被设置为-1,该状态的意思是后面的节点被挂起需要释放
  • CANCELLED(1): 当前节点已取消
  • SIGNAL(-1):后继节点等待唤醒,后继结点入队时,会将前继结点的状态更新为SIGNAL。
  • CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
  • PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。

ReentrantReadWriteLock

RRWL允许多个线程同时读取共享变量,只允许一个线程写共享变量,当有线程在写变量的时候又不允许线程读取共享变量。
RRWL的state高16位记录了读锁的数量,低16为记录了写锁的数量
RRWL写锁加锁流程:

  1. 首先判断当前state是否为0,如果为0,就判断线程是否应该阻塞,这里走的是公平锁和非公平锁的逻辑,不阻塞的话就获取锁
  2. 若state不为0,说明有线程获取锁,首先获取state的低16位w(即写锁数量),如果w=0(说明此刻有读锁)或者当前线程未获取锁,则加锁失败,否则说明当前线程已经占有锁,cas替换state
    RRWL读锁加锁流程:
  3. 首先获取写锁数量w,若w>0并且获取写锁的线程不是当前线程,则加锁失败,如果当前线程持有写锁,则可以获取读锁(支持锁降级)
  4. 获取读锁的数量r,如果r不被阻塞,则CAS替换state,否则进入阻塞队列等待

ThreadLocal

在多线程环境中常常用到ThreadLocal,ThreadLocal中有个内部类ThreadLocalMap,内部有一个entry数组,其key为ThreadLocal本身,value为任意object

static class ThreadLocalMap {

    /**
     * The entries in this hash map extend WeakReference, using
     * its main ref field as the key (which is always a
     * ThreadLocal object).  Note that null keys (i.e. entry.get()
     * == null) mean that the key is no longer referenced, so the
     * entry can be expunged from table.  Such entries are referred to
     * as "stale entries" in the code that follows.
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
}

Thread存储的数据在Thread.threadLocals上,其类型正好是ThreadLocalMap

public class Thread implements Runnable {
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

也就是说,每个线程都存储了ThreadLocalMap<ThreadLocal, Object>这样的数据,当我们在使用ThrealLocal.get()方法时,都会获取到当前线程的threadLocals,根据threadLocal为key获取值。

  • ThreadLocal与Thread的设计不容易产生内存泄漏,在多线程环境中,ThreadLocal的存活时间一般都比较长,如果threadLocals是放在ThreadLocal中,那么threadLocal就会一直持有thread的引用,导致线程不会被回收
  • 线程池中使用ThreadLocal,容易导致内存泄漏,由于线程池中的线程存活时间特别长,因此threadLocalMap一直不会被回收,虽然ThreadLocalMap对ThreadLocal是弱引用(Entry extends WeakReference<ThreadLocal<?>>),但对于value是强引用,所以value不会被清除,可以手动调用threadLocal.remove()方法清除value

ThreadPoolExecutor

在需要并发操作的场景下,频繁的创建销毁线程不仅占用内存,还占用cpu资源,影响系统性能,利用线程池可以很好的解决这个问题,ThreadPoolExecutor为jdk1.5提供的线程池执行器。

构造方法:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue);
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory);
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue,  RejectedExecutionHandler handler);
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)

参数:

  • corePoolSize 线程池保留线程数
  • maximumPoolSize 最大线程数
  • keepAliveTime 空闲线程等待最长时间,超时销毁
  • unit 等待时间的时间单位
  • workQueue 线程等待队列
  • threadFactory 创建线程的工厂
  • handler 线程池拒绝线程任务时的处理,默认抛异常
  1. 核心线程数corePoolSize的值是多少,那么线程池最少就有多少个线程,即使线程都是空闲的。
  2. 当线程数量超过corePoolSize时,剩下的线程如果没有超过workQueue的容量,则剩下的线程会进入队列中等待核心线程执行完。
  3. 当线程数量超过corePoolSize与workQueue容量的总和,但剩下的部分没有超过maximumPoolSize时,会开启新的线程来执行剩下的线程。
  4. 当线程数量超过前三者之和时,剩余的线程会走RejectedExecutionHandler 的rejectedExecution方法,如果没有配置的话,这些线程会被拒绝执行,并抛出异常。
  5. 如果设置了allowCoreThreadTimeOut为true的话,即便空闲线程数量低于核心线程数也会在超时的时候销毁。

ThreadPoolExecutor执行线程有两种方式,一种是threadPoolExecutor.submit(),一种是threadPoolExecutor.execute()

  • submit
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);

submit执行的是Runnable类型或者Callable类型的任务,并且返回Future对象

  • execute
void execute(Runnable command);

execute方法直接执行Runnable类型的任务

submit与execute的区别

  • submit有返回值,execute没有
  • submit任务执行失败后不会抛异常,但在调用get()方法后会抛出异常,而execute会立即抛出来

关闭线程池
ThreadPoolExecutor提供了两个关闭线程池的方法

/**
 * 启动有序关闭,在该关闭中执行先前提交的任务,但不接受任何新任务。如果已经关闭,则调用不会产生任何其他影响。
 *
 * <p>此方法不等待先前提交的任务完成执行。使用{@link #awaitTermination awaitTermination}可以做到这一点。
 *
 * <p>除了尽力而为后,无法保证停止处理主动执行的任务。此实现通过{@link Thread#interrupt}取消任务,因此任何未能响应中断的任务都可能永远不会终止。
 *
 * @throws SecurityException(如果存在安全管理器并关闭此ExecutorService)可能会操纵不允许调用方修改的线程,
 *         因为该调用方不包含{@link java.lang.RuntimePermission}{@code(“ modifyThread”)},
 *         或安全管理器的{@code checkAccess}方法拒绝访问。
 */
void shutdown();
/**
 * 尝试停止所有正在执行的任务,暂停正在等待的任务的处理,并返回正在等待执行的任务的列表。 
 *
 * <p>此方法不等待主动执行的任务终止。使用{@link #awaitTermination awaitTermination}可以做到这一点。
 *
 * <p>除了尽力而为后,无法保证停止处理主动执行的任务。例如,典型的实现将通过{@link Thread#interrupt}取消,因此任何无法响应中断的任务都可能永远不会终止。 
 *
 * @return 从未开始执行的任务列表
 * @thrwos 如果存在安全管理器并关闭此ExecutorService
 *         则抛出SecurityException
 *         这可能会操纵不允许调用者修改的线程
 *         因为该调用者不持有{@link java.lang.RuntimePermission}
 *         {@code (“ modifyThread”)}
 *         或安全管理器的{@code checkAccess}方法拒绝访问。
 */
List<Runnable> shutdownNow();
  • shutdown方法不接受新的任务,等待之前提交的任务执行完后关闭
  • shutdownNow方法会停止正在执行的任务,并将已提交但未执行的任务返回
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值