线程安全:一个程序在多线程和单线程情况下执行结果一致则,线程安全 线程的上下文切换(Thread Context Switch)
活跃性
两个线程各自获取了一个锁,但是还想要获取对方的锁
死锁
定位
- jconsole 工具
- 使用 jps 定位进程 id,再用 jstack 定位死锁
properties
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x0000019f3fb098c8 (object 0x000000066b2fab80, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x0000019f3fb0d968 (object 0x000000066b2fab90, a java.lang.Object),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
at cn.lopr.day01.DeadLock.lambda$main$1(DeadLock.java:35)
- waiting to lock <0x000000066b2fab80> (a java.lang.Object)
- locked <0x000000066b2fab90> (a java.lang.Object)
at cn.lopr.day01.DeadLock$$Lambda$2/787387795.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"Thread-0":
at cn.lopr.day01.DeadLock.lambda$main$0(DeadLock.java:23)
- waiting to lock <0x000000066b2fab90> (a java.lang.Object)
- locked <0x000000066b2fab80> (a java.lang.Object)
at cn.lopr.day01.DeadLock$$Lambda$1/1879034789.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.
活锁
两个线程对结束标记互相修改,导致结束标记一直变更,最终导致活锁 解决
- 交错指令,给两个线程增加随机睡眠时间
饥饿
很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,饥饿的情况不易演示,讲读写锁时会涉及饥饿问题
线程主要方法
方法 | static | 说明 |
---|---|---|
start() | 启动一个线程 | |
run() | 执行内容 | |
getState() | 获取线程状态 | |
isAlive() | 是否处于活动状态 | |
setPriority() / setPriority() | 更改线程的优先级 / 获取线程优先级 | |
setDaemon | 将该线程标志为守护线程/用户线程 | |
interrupt() | 中断线程 | |
isInterrupt() | 判断线程是不是会被打断(不会清除中断标记) | |
interrupted() | static | 判断当前线程是否被打断(会清除中断标记) |
join() / join(long n) | 等待线程结束运行 / 等待线程结束运行,最多等待 n 毫秒 | |
sleep() | static | sleep,放弃 CPU 的使用权,让出时间片 |
yield() | static | 提示线程调度器让出当前线程对 CPU 的使用 |
getId() | 获取线程 id | |
setName() / getName() | 设置 / 获取线程名 | |
stop() | 过时 | |
suspend() | 过时 挂起暂停线程 | |
resume() | 过时 恢复线程运行 |
start
- 启动线程,会执行 run 方法里面的内容,run 方法如果直接调用和普通方法无异
- start 方法只是让线程进入就绪状态,等待系统分配 CPU 时间片之后才可以执行
- 只能调用一次,会抛出 IllegalThreadStateException
run
- 如果在构造函数中转入了 Runnable 参数,则启动后则会调用 Runnable 的 run 方法。否则默认不执行任何操作
- 也可以重写 run 方法
isAlive
判断线程是否处于活动状态,线程启动,处于正在运行或者开始运行的状态
sleep
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞;阻塞状态不会有 CPU 时间片)
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
- 睡眠结束后的线程未必会立刻得到执行(等待 CPU 时间片)
- 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
- 不要让 while(ture) 空转浪费 CPU,这时候可以使用 sleep 或者 yield 来让出 CPU 时间片
- 使用 wait 或者 条件变量也可以达到类似效果,但是需要加锁,并且需要相应的唤醒操作,一般适用于同步场景
- sleep 适用于无需锁同步场景
yield
- 调用 yield 会让当前线程从 Rumming 进入 Runnable 就绪状态(可以再次获取到 CPU 时间片),然后调度执行其它同优先级的线程。
- 具体的实现依赖于操作系统的任务调度器
join
- 在很多情况下,主线程创建并启动了线程,如果子线程中进行大量耗时运算,主线程往往将早于子线程结束之前结束。这时,如果主线程想等待子线程执行完成之后再结束,比如子线程处理一个数据,主线程要取得这个数据中的值,就要用到 join() 方法了
- join() 的作用是等待线程对象销毁
- join(long n):如果线程需要运行 3 秒,通过此方法等待 4 秒,则实际等待还是 3 秒
- 同步效果,线程会阻塞等待线程完成
java
public static void main(string[] args) {
int r1, r2;
Thread t1 = new Thread(()-> {
sleep(1); // 阻塞 1 秒
r1 = 10;
});
Thread t1 = new Thread(()-> {
sleep(2); // 阻塞 2 秒
r2 = 10;
});
t1.start();
t2.start();
t1.join(); // 此时阻塞 1 秒,等待线程 t1 销毁
t2.join(); // 此时阻塞 1 秒(因为 t1 已经阻塞了 1 秒),等待线程 t1 销毁
}
setPriority
- 线程优先级会提示(hint)调度器优先调用该线程,但它仅仅只作为一个提示,调度器基本可以忽略他
- 如果 CPU 比较忙,那么优先级高的线程会获取更多的时间片,但 CPU 空闲,优先级几乎没有作用
- 可以通过 yield 让出线程时间片,以达到提升其他线程的 CPU 时间片范围
interrupt
- 可以打断处于阻塞状态(如:sleep、wait、join)的线程
- 打断阻塞状态的线程,不会将中断标记置为 true
- 会抛出异常 InterruptedException
- 无法中断阻塞 I/O 和 synchronize 锁
- 可以中断 ReentrantLock
java
public static void main(String[] args) {
Thread thread = new Thread(() -> {
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
TimeUtil.computeStartOfNextSecond(1); // sleep 1
thread.interrupt();
// false
System.out.println(thread.isInterrupted()); // 打印中断标记
}
- 调用 interrupt 方法,只是中断了线程,线程实际还是在运行的,可以通过在方法内调用 isInterrupted 来获取线程中断状态,然后退出线程。
java
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (true) {
boolean interrupted = Thread.currentThread().isInterrupted();
System.out.println("thread is interrupt: " + interrupted);
if (interrupted) {
System.out.println("thread interrupted break.");
break;
}
}
});
thread.start();
Thread.sleep(1000);
thread.interrupt(); // 线程只是被中断了,实际还在执行
System.out.println(thread.isInterrupted());
}
两阶段终止模式
- 在一个线程 T1 中如何“优雅”终止线程 T2?这里的【优雅】指的是给 T2 一个料理后事的机会。
使用 stop 方法杀死线程:如果这个时候线程锁住了共享资源,那么当他被杀死后就再也没有机会释放这些资源的锁,其他线程也就无法获取到这个资源的锁使用 System.exit(int) 方法停止线程:目的是仅停止一个线程
- 通过 interrupt
java
public class Demo01 {
public static void main(String[] args) throws InterruptedException {
Monitor monitor = new Monitor();
monitor.start();
Thread.sleep(1000);
monitor.stop();
}
}
class Monitor {
private Thread monitor;
public void start() {
monitor = new Thread(() -> {
while (true) {
Thread thread = Thread.currentThread();
if (thread.isInterrupted()) {
System.out.println("exit.");
break;
}
try {
Thread.sleep(1000);
System.out.println("running...");
} catch (InterruptedException e) {
e.printStackTrace();
// 如果在 sleep 的时候被打断,不会置为 true
thread.interrupt();
}
}
});
monitor.start();
}
public void stop() {
monitor.interrupt();
}
}
LockSupport.lock()
- 可以阻塞线程
- 可以被打断,打断以后标记为 true
- 如果线程的中断标记为 true,则不会阻塞线程,如果还是想中断线程可以在调用 **Thread.interrupted() **将中断标记置为 false
java
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
System.out.println("running...");
LockSupport.park();
System.out.println("unpack...");
});
thread.start();
sleep(1000);
thread.interrupt();
}
setDaemon
- 定义:守护线程--也称“服务线程”,他是后台线程,它有一个特性,即为用户线程提供公共服务, 在没有用户线程可服务时会自动离开
- 优先级:守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。
- 设置:通过 setDaemon(true) 来设置线程为“守护线程”;将一个用户线程设置为守护线程的方式是 在线程对象创建之前用线程对象的 setDaemon 方法。
- 在 Daemon 线程中产生的新线程也是 Daemon 的。
- 线程则是 JVM 级别的,以 Tomcat 为例,如果你在 Web 应用中启动一个线程,这个线程的生命周期并不会和 Web 应用程序保持同步。也就是说,即使你停止了 Web 应用,这个线程依旧是活跃
- example: 垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的 Thread, 程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是 JVM 上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
- 生命周期:守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依赖于系统,与系统“同生共死”。当 JVM 中所有的线程都是守护线程的时候,JVM 就可以退出了;如果还有一个或以上的非守护线程则 JVM 不会退出。
线程生命周期
五种
从操作系统层面理解
- 新建:就是刚使用 new 方法,new 出来的线程
- 就绪:就是调用的线程的 start()方法后,这时候线程处于等待 CPU 分配资源阶段,谁先抢的 CPU 资源,谁开始执行
- 运行:当就绪的线程被调度并获得 CPU 资源时,便进入运行状态,run 方法定义了线程的操作和功能
- 阻塞:在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如 sleep()、wait() 之后线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用 notify 或者 notifyAll() 方法。唤醒的线程不会立刻执行 run 方法,它们要再次等待 CPU 分配资源进入运行状态
- 阻塞 API:BIO、NIO、AIO
- 销毁:如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源
六种
从 Java API 层面理解 根据类 Thread.State 枚举类
java
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
- NEW:线程刚被创建,但是还么有调用 start() 方法
- RUNNABLE:当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
- BLOCKED:被其他线程阻塞,如:竞争同一个锁
- WATING:无时限的等待,如:等待另个一线程执行完成
- TIME_WATING:有时限的等待,如:sleep(long n)
- TERMINATED:终止状态
创建线程方式
Thread
- Thread 类是实现了 Runable 接口
- 这种方式创建的线程没有返回值
- 一个线程的 start 方法只能被调用一次,执行多次会抛出 **IllegalThreadStateException() **异常
java
public class Thread implements Runnable
java
/**
* If this thread was constructed using a separate
* <code>Runnable</code> run object, then that
* <code>Runnable</code> object's <code>run</code> method is called;
* otherwise, this method does nothing and returns.
* <p>
* Subclasses of <code>Thread</code> should override this method.
*
* @see #start()
* @see #stop()
* @see #Thread(ThreadGroup, Runnable, String)
*/
@Override
public void run() {
if (target != null) {
target.run();
}
}
java
class TestRun2 extends Thread {
@Override
public void run() {
super.run();
}
}
Runnable
- 实现 Runnable 接口,覆写 run 方法
- 调用 Runnable 接口,需要实例化一个 Thread 对象,通过构造参数传入 Runnable 实现类对象
- run 方法只是一个普通的方法,不能作为线程
- 运用到了代理模式:一个接口,两个子类,一个辅助操作,一个实现真正的业务
- 如果同时继承 Thread 并重写 run 方法和实现了 Runnable,则是 Thread 重写的方法
- 好处:
- 用 Runnable 更容易与线程池等高级 API 配合
- 用 Runnable 让任务类脱离了 Thread 继承体系,更加灵活
java
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
java
class TestRun implements Runnable {
@Override
public void run() {
}
}
public static void main(String[] args) {
new Thread(new TestRun()).start();
// 使用 lambda 精简代码
Runnable task = () -> System.out.print("hello");
new Thread(task).start();
}
java
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
Callable
- 1.5 后加入,java.util.concurrent 包
- 实现 Callable 接口后,线程结束是有返回值的
- 类似同步线程,会阻塞等待结果返回
java
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
- 与 Runnable 接口类似,只是用来实现业务逻辑,如果想要真的运行,还需要 **Future **接口
java
class TestRun3 implements Callable<String> {
@Override
public String call() throws Exception {
return this.getClass() + " Callable";
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> stringFutureTask = new FutureTask<>(new TestRun3());
Thread thread = new Thread(stringFutureTask);
thread.start();
System.out.println(stringFutureTask.get());
}
- RunnableFuture 接口继承了 Runnable 和 Future 接口
java
public interface RunnableFuture<V> extends Runnable, Future<V> {
/**
* Sets this Future to the result of its computation
* unless it has been cancelled.
*/
void run();
}
java
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
- FutureTask 实现了 RunnableFuture 接口
java
public class FutureTask<V> implements RunnableFuture<V>
Timer
- 定时器启动线程
java
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println(LocalDateTime.now());
}
}, 0, 1000);
}
线程池
ThreadPoolExecutor
- 构造方法
java
/**
* @param corePoolSize the number of threads to keep in the pool, even
* if they are idle, unless {@code allowCoreThreadTimeOut} is set
* @param maximumPoolSize the maximum number of threads to allow in the
* pool
* @param keepAliveTime when the number of threads is greater than
* the core, this is the maximum time that excess idle threads
* will wait for new tasks before terminating.
* @param unit the time unit for the {@code keepAliveTime} argument
* @param workQueue the queue to use for holding tasks before they are
* executed. This queue will hold only the {@code Runnable}
* tasks submitted by the {@code execute} method.
* @param threadFactory the factory to use when the executor
* creates a new thread
* @param handler the handler to use when execution is blocked
* because the thread bounds and queue capacities are reached
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
- corePoolSize:核心线程数目 (最多保留的线程数),线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会 被销毁,除非设置了 allowCoreThreadTimeOut。这里的最小线程数量即是 corePoolSize
- maximumPoolSize:最大线程数目,一个任务被提交到线程池以后,首先会找有没有空闲存活线程,如果有则直接执行,如果没有则会缓存到工作队列中,如果工作队列满了,才会创建一个新线程(这就是救急线程),然后从工作队列的头部取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列尾部。线程池不会无限制的去创建救急线程,它会有一个最大线程数量的限制,这个数量即由 maximunPoolSize 的数量减去 corePoolSize 的数量来确定,最多能达到 maximunPoolSize 即最大线程池线程数量
- **keepAliveTime:**生存时间,针对急救线程
- **unit:**时间单位,针对急救线程
- **workQueue:**新任务被提交后,如果没有空闲的核心线程就会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk 中常见的任务队列:
- ArrayBlockingQueue:基于数组的有界阻塞队列,按 FIFO 排序。有界的数组可以防止资源耗尽问题。当线程池中线程数量达到 corePoolSize 后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到 maximunPoolSize ,则会执行拒绝策略
- LinkedBlockingQuene:基于链表的无界阻塞队列(其实最大容量为 Interger.MAX ),按照 FIFO 排序。由于该队列的近似无界性,当线程池中线程数量达到 corePoolSize 后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到 maximunPoolSize,因此使用该工作队列时,参数 maximunPoolSize 其实是不起作用的
- SynchronousQuene:一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到 maxPoolSize ,则执行拒绝策略
- PriorityBlockingQueue:具有优先级的无界阻塞队列,优先级通过参数 Comparator 实现
- **threadFactory:**线程工厂 - 可以为线程创建时起个好名字
- **handler:**线程池的拒绝策略,是指当任务添加到线程池中被拒绝,而采取的处理措施。一般因为,线程池异常关闭。任务数量超过线程池的最大限制
- **AbortPolicy:**当任务添加到线程池中被拒绝时,它将抛出 RejectedExecutionException 异常
- **CallerRunsPolicy:**当任务添加到线程池中被拒绝时,会在线程池当前正在运行的 Thread 线程池中处理被拒绝的任务
- **DiscardPolicy:**当任务添加到线程池中被拒绝时,线程池会放弃等待队列中最旧的未处理任务,然后将被拒绝的任务添加到等待队列中
- **DiscardOldestPolicy:**当任务添加到线程池中被拒绝时,线程池将丢弃被拒绝的任务
- 继承 RejectedExecutionHandler:
- 实现
java
ThreadPoolExecutor executorService = new ThreadPoolExecutor(corePoolSize,
maximumPoolSize,
600L,
TimeUnit.SECONDS,
workQueueSize == 0 ? new SynchronousQueue() : new LinkedBlockingQueue(workQueueSize),
new IMServer.ServerThreadFactory(), new IMServer.MyCallerRunsPolicy());
java
private static class ServerThreadFactory implements ThreadFactory {
private static int id = 1;
private ServerThreadFactory() {
}
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "im-server-thread-" + getId());
}
private static synchronized int getId() {
return id++;
}
}
java
private static class MyCallerRunsPolicy implements RejectedExecutionHandler {
public MyCallerRunsPolicy() {
}
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new cn.lopr.im.common.exception.NetException("Task " + r.toString() + " rejected from " + e.toString());
}
}
- 提交任务
java
executor.submit(Callback<T> task);
executor.execute(Runable task);
newCachedThreadPool
java
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
newFixedThreadPool
java
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
newScheduledThreadPool
java
public static ScheduledExecutorService newScheduledThreadPool(
int corePoolSize, ThreadFactory threadFactory) {
return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
/**
* Returns an object that delegates all defined {@link
* ExecutorService} methods to the given executor, but not any
* other methods that might otherwise be accessible using
* casts. This provides a way to safely "freeze" configuration and
* disallow tuning of a given concrete implementation.
* @param executor the underlying implementation
* @return an {@code ExecutorService} instance
* @throws NullPointerException if executor null
*/
public static ExecutorService unconfigurableExecutorService(ExecutorService executor) {
if (executor == null)
throw new NullPointerException();
return new DelegatedExecutorService(executor);
}
newSingleThreadExecutor
java
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
内存泄露
对于应用程序来说,当对象已经不再被使用,但是 Java 的垃圾回收器不能回收它们的时候,就产生了内存泄露。
防止
- 使用 List、Map 等集合时,在使用完成后赋值为 null
- 使用大对象时,在用完后赋值为 null
- 目前已知的 jdk1.6 的 substring()方法会导致内存泄露
- 避免一些死循环等重复创建或对集合添加元素,撑爆内存
- 简洁数据结构、少用静态集合等
- 及时的关闭打开的文件,socket 句柄等
- 多关注事件监听(listeners)和回调(callbacks),比如注册了一个 listener,当它不再被使用的时候,忘了注销该 listener,可能就会产生内存泄露
synchronize
- 同步锁
- 当多个线程同时访问同一个数据时,很容易出现问题。为了避免这种情况出现,我们要保证线程同步互斥,就是指并发执行的多个线程,在同一时间内只允许一个线程访问共享数据。 Java 中可以使用 synchronized 关键字来取得一个对象的同步锁。
- 死锁
- 何为死锁,就是多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。
- 优先级
- 偏向锁
- 轻量锁
- 重量锁 自旋锁
- **Synchronized **作用范围
- 作用于方法时,锁住的是对象的实例(this);
- 当作用于静态方法时,锁住的是 Class 实例,又因为 Class 的相关数据存储在永久带 PermGen (jdk1.8 则是 metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;
- synchronized 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。它有多个队列, 当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。
- **Synchronized **核心组件
- Wait Set:哪些调用 wait 方法被阻塞的线程被放置在这里;
- Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
- Entry List:Contention List 中那些有资格成为候选资源的线程被移动到 Entry List 中;
- OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为 OnDeck;
- Owner:当前已经获取到所资源的线程被称为 Owner;
- !Owner:当前释放锁的线程。
- **Synchronized **实现
- JVM 每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList 会被大量的并发线程进行 CAS 访问,为了降低对尾部元素的竞争,JVM 会将一部分线程移动到 EntryList 中作为候选竞争线程。
- Owner 线程会在 unlock 时,将 ContentionList 中的部分线程迁移到 EntryList 中,并指定 EntryList 中的某个线程为 OnDeck 线程(一般是最先进去的那个线程)。
- Owner 线程并不直接把锁传递给 OnDeck 线程,而是把锁竞争的权利交给 OnDeck,OnDeck 需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在 JVM 中,也把这种选择行为称之为“竞争切换”。
- OnDeck 线程获取到锁资源后会变为 Owner 线程,而没有得到锁资源的仍然停留在 EntryList 中。 如果 Owner 线程被 wait 方法阻塞,则转移到 WaitSet 队列中,直到某个时刻通过 notify 或者 notifyAll 唤醒,会重新进去 EntryList 中。
- 处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统来完成 的(Linux 内核下采用 pthread_mutex_lock 内核函数实现的)。
- Synchronized 是非公平锁。 Synchronized 在线程进入 ContentionList 时,等待的线程会先尝试 自旋获取锁,如果获取不到就进入 ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁资源。
- 每个对象都有个 monitor 对象,加锁就是在竞争 monitor 对象,代码块加锁是在前后分别加上 monitorenter 和 monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的
- synchronized 是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线程加 锁消耗的时间比有用操作消耗的时间更多。
- Java1.6,synchronized 进行了很多的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向锁 等,效率有了本质上的提高。在之后推出的 Java1.7 与 1.8 中,均对该关键字的实现机理做了优 化。引入了偏向锁和轻量级锁。都是在对象头中有标记位,不需要经过操作系统加锁。
- 锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫做锁膨胀;
- JDK 1.6 中默认是开启偏向锁和轻量级锁,可以通过-XX:-UseBiasedLocking 来禁用偏向锁。
monitor
- 加了 synchronize 的对象,则会关联 对象监视器(C++实现)
- 其中的状态会在 Java 对象头的 Mark word 进行展示
- 刚开始 Monitor 中 Owner 为 null (与 mark word)
- 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor 中只能有一个 Owner
- 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进 EntryList BLOCKED
- Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的
- 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲 wait-notify 时会分析
java
private static Object lock = new Demo();
private static int count = 0;
public static void main(String[] args) {
synchronized (lock) {
count++;
}
}
字节码
java
public class cn.lopr.day01.Demo04 {
public cn.lopr.day01.Demo04();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field lock:Ljava/lang/Object;
3: dup
4: astore_1 // lock引用 -> slot 1
5: monitorenter // 将 lock 对象 MarkWord 置为 Monitor 指针
6: getstatic #3 // Field count:I
9: iconst_1 // 准备常数 1
10: iadd // 自增
11: putstatic #3 // Field count:I
14: aload_1 // <- lock引用
15: monitorexit // 将 lock 对象 MarkWord 重置,唤醒 EntryList
16: goto 24
19: astore_2 // e -> slot 2
20: aload_1 // <- lock 引用
21: monitorexit // 将 lock 对象 MarkWord 重置,唤醒 EntryList
22: aload_2 // <- slot 2 (e)
23: athrow // throw e
24: return
Exception table: // 监听异常
from to target type // 如果6到16行出现了异常,则会跳转到19行
6 16 19 any
19 22 19 any
static {};
Code:
0: new #4 // class cn/lopr/day01/Demo
3: dup
4: invokespecial #5 // Method cn/lopr/day01/Demo."<init>":()V
7: putstatic #2 // Field lock:Ljava/lang/Object;
10: iconst_0
11: putstatic #3 // Field count:I
14: return
}
轻量级锁
轻量级锁的使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
使用 synchronize 加锁
- JVM 会优先是轻量级锁
- 创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word
- 让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录
- 如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁
- 如果 cas 失败,有两种情况
- 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
- 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数
- 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
- 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头
- 成功,则解锁成功
- 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
java
private static Object lock = new Demo();
public static void main(String[] args) {
synchronized (lock){
method2();
}
}
public static void method2(){
synchronized (lock){ // 同一个线程 第二次获取同一个锁 可重入锁
//
}
}
可重入锁
- 同一个线程可以对一个对象多次加锁
偏向锁
由重入锁引入
- 轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
- Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现 这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
- 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的 thread、epoch、age 都为 0
- 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 - XX:BiasedLockingStartupDelay=0 来禁用延迟
- 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、 age 都为 0,第一次用到 hashcode 时才会赋值
- 如果某个线程获取了锁,则在临界代码块结束以后也不会释放锁(Mark word),除非有别的线程竞争锁,偏向锁
- 查看 Mark word 信息可以通过 openjdk.jol
停止偏向锁
- 运行参数:-XX:-UseBiasedLocking
- 调用对象的 hashCode 方法:object.hashCode(),
- 偏向锁记录存在 Mark Word 中,调用 hashCode 会导致没有空间存放锁
- 轻量级锁会在锁记录中记录 hashCode
- 重量级锁会在 Monitor 中记录 hashCode
- 被其它线程竞争,也会放弃偏向锁
- 批量重偏向 8. 如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID 9. 当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程
- 批量撤销
- 当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的
重量级锁
- 多个线程对同一个对象加锁时,会导致锁膨胀
- 即为 lock 对象申请 Monitor 锁,让 lock 的 Mark word 指向重量级锁
- 然后没有获取到锁的对象则会进入 Monitor 的 EntryList,被阻塞
- 当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程
自旋锁
- 重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
- 自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
- 线程自旋是需要消耗 cup 的,说白了就是让 cup 在做无用功,如果一直获取不到锁,那线程也不能一直占用 cup 自旋做无用功,所以需要设定一个自旋等待的最大时间。
- 如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。
- 自旋锁的优缺点
- 自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!
- 但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用 cpu 做无用功,占着 XX 不 XX,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要 cup 的线程又不能获取到 cpu,造成 cpu 的浪费。所以这种情况下我们要关闭自旋锁;
- 自旋锁时间阈值(1.6 引入了适应性自旋锁)
- 自旋锁的目的是为了占着 CPU 的资源不释放,等到获取到锁立即进行处理。但是如何去选择自旋的执行间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用 CPU 资源,进而会影响整体系统的性能。因此自旋的周期选的额外重要!
- JVM 对于自旋周期的选择,jdk1.5 这个限度是一定的写死的,在 1.6 引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时 JVM 还针对当前 CPU 的负荷情况做了较多的优化,如果平均负载小于 CPUs 则一直自旋,如果有超过(CPUs/2) 个线程正在自旋,则后来线程直接阻塞,如果正在自旋的线程发现 Owner 发生了变化则延迟自旋时间(自旋计数)或进入阻塞,如果 CPU 处于节电模式则停止自旋,自旋时间的最坏情况是 CPU 的存储延迟(CPU A 存储了一个数据,到 CPU B 得知这个数据直接的时间差),自旋时会适当放弃线程优先级之间的差异。
shell
JDK1.6 中-XX:+UseSpinning 开启;
-XX:PreBlockSpin=10 为自旋次数;
JDK1.7 后,去掉此参数,由 jvm 控制;
乐观锁
- 乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。
- java 中的乐观锁基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
悲观锁
- 悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修 改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会 block 直到拿到锁。 java 中的悲观锁就是 Synchronized,AQS 框架下的锁则是先尝试 cas 乐观锁去获取锁,获取不到,才会转换为悲观锁,如 RetreenLock。
同步锁
synchronized 它可以把任意一个非 NULL 的对象当作锁。他属于独占式的悲观锁,同时属于可重入锁。
- **Synchronized **
锁消除
- 对于热点代码,会进行分析能否优化,可能会被优化
java
public void b(){
Object o = new Object();
// 锁的是局部对象,可以被优化
synchronize (o){
//
}
}
- 取消锁消除:-XX:-EliminateLocks
公平锁
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。 synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。
ThreadLocal
线程共享
InheritableThreadLocal
父子线程共享