关于java:Java并发知识总结

3次阅读

共计 24485 个字符,预计需要花费 62 分钟才能阅读完成。

首先给大家分享一个 github 仓库,下面放了200 多本经典的计算机书籍,包含 C 语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生等,能够 star 一下,下次找书间接在下面搜寻,仓库继续更新中~

github 地址:https://github.com/Tyson0314/…

如果 github 拜访不了,能够拜访 gitee 仓库。

gitee 地址:https://gitee.com/tysondai/ja…

线程池

应用线程池的益处

  • 升高资源耗费。通过反复利用已创立的线程升高线程创立和销毁造成的耗费。
  • 进步响应速度。当工作达到时,工作能够不须要的等到线程创立就能立刻执行。
  • 进步线程的可管理性。对立治理线程,防止零碎创立大量同类线程而导致耗费完内存。
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler);

线程池原理

创立新的线程须要获取全局锁,通过这种设计能够尽量避免获取全局锁,当 ThreadPoolExecutor 实现预热之后(以后运行的线程数大于等于 corePoolSize),提交的大部分工作都会被放到 BlockingQueue。

ThreadPoolExecutor 的通用构造函数:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler);
  • corePoolSize:当有新工作时,如果线程池中线程数没有达到线程池的根本大小,则会创立新的线程执行工作,否则将工作放入阻塞队列。当线程池中存活的线程数总是大于 corePoolSize 时,应该思考调大 corePoolSize。
  • maximumPoolSize:当阻塞队列填满时,如果线程池中线程数没有超过最大线程数,则会创立新的线程运行工作。否则依据回绝策略解决新工作。非核心线程相似于长期借来的资源,这些线程在闲暇工夫超过 keepAliveTime 之后,就应该退出,防止资源节约。
  • BlockingQueue:存储期待运行的工作。
  • keepAliveTime:非核心线程 闲暇后,放弃存活的工夫,此参数只对非核心线程无效。设置为 0,示意多余的闲暇线程会被立刻终止。
  • TimeUnit:工夫单位

    TimeUnit.DAYS
    TimeUnit.HOURS
    TimeUnit.MINUTES
    TimeUnit.SECONDS
    TimeUnit.MILLISECONDS
    TimeUnit.MICROSECONDS
    TimeUnit.NANOSECONDS
  • ThreadFactory:每当线程池创立一个新的线程时,都是通过线程工厂办法来实现的。在 ThreadFactory 中只定义了一个办法 newThread,每当线程池须要创立新线程就会调用它。

    public class MyThreadFactory implements ThreadFactory {
        private final String poolName;
        
        public MyThreadFactory(String poolName) {this.poolName = poolName;}
        
        public Thread newThread(Runnable runnable) {return new MyAppThread(runnable, poolName);// 将线程池名字传递给构造函数,用于辨别不同线程池的线程
        }
    }
  • RejectedExecutionHandler:当队列和线程池都满了时,依据回绝策略解决新工作。

    AbortPolicy:默认的策略,间接抛出 RejectedExecutionException
    DiscardPolicy:不解决,间接抛弃
    DiscardOldestPolicy:将期待队列队首的工作抛弃,并执行当前任务
    CallerRunsPolicy:由调用线程解决该工作

线程池大小

如果线程池线程数量太小,当有大量申请须要解决,零碎响应比较慢影响体验,甚至会呈现工作队列大量沉积工作导致 OOM。

如果线程池线程数量过大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换(cpu 给线程调配工夫片,当线程的 cpu 工夫片用完后保留状态,以便下次持续运行),从而减少线程的执行工夫,影响了整体执行效率。

CPU 密集型工作(N+1):这种工作耗费的次要是 CPU 资源,能够将线程数设置为 N(CPU 外围数)+1,比 CPU 外围数多进去的一个线程是为了避免某些起因导致的工作暂停(线程阻塞,如 io 操作,期待锁,线程 sleep)而带来的影响。一旦某个线程被阻塞,开释了 cpu 资源,而在这种状况下多进去的一个线程就能够充分利用 CPU 的闲暇工夫。

I/O 密集型工作(2N):零碎会用大部分的工夫来解决 I/O 操作,而线程期待 I/O 操作会被阻塞,开释 cpu 资源,这时就能够将 CPU 交出给其它线程应用。因而在 I/O 密集型工作的利用中,咱们能够多配置一些线程,具体的计算方法:最佳线程数 = CPU 外围数 (1/CPU 利用率) = CPU 外围数 (1 + (I/ O 耗时 /CPU 耗时)),个别可设置为 2N。

敞开线程池

shutdown():

将线程池状态置为SHUTDOWN,并不会立刻进行:

  • 进行接管内部提交的工作
  • 外部正在跑的工作和队列里期待的工作,会执行完
  • 等到第二步实现后,才真正进行

shutdownNow():

将线程池状态置为STOP。希图立刻进行,事实上不肯定:

  • 跟 shutdown()一样,先进行接管内部提交的工作
  • 疏忽队列里期待的工作
  • 尝试将正在跑的工作中断(不肯定中断胜利,取决于工作响应中断的逻辑)
  • 返回未执行的工作列表

executor 框架

1.5 后引入的 Executor 框架的最大长处是把工作的提交和执行解耦。当提交一个 Callable 对象给 ExecutorService,将失去一个 Future 对象,调用 Future 对象的 get 办法期待执行后果。Executor 框架的外部应用了线程池机制,它在 java.util.cocurrent 包下,通过该框架来控制线程的启动、执行和敞开,能够简化并发编程的操作。

简介

executor 框架由 3 局部组成:工作、工作的执行、异步计算的后果

  • 工作。须要实现的接口:Runnable 和 Callable 接口。
  • 工作的执行。ExecutorService 是一个接口,用于定义线程池,调用它的 execute(Runnable)或者 submit(Runnable/Callable)执行工作。ExecutorService 接口继承于 Executor,有两个实现类 ThreadPoolExecutorScheduledThreadPoolExecutor
  • 异步计算的后果。包含 future 接口和实现 future 接口的 FutureTask,调用 future.get()会阻塞以后线程直到工作实现,future.cancel()能够勾销执行工作。

ThreadPoolExecutor 实例

应用 ThreadPoolExecutor 构造函数自定义参数的形式来创立线程池。

public class ThreadPoolExecutorDemo {
    private static final int CORE_POOL_SIZE = 5;
    private static final int MAX_POOL_SIZE = 10;
    private static final int QUEUE_CAPACITY = 100;
    private static final long KEEP_ALIVE_TIME = 1L;

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                new ThreadPoolExecutor.CallerRunsPolicy());

        for (int i = 0; i < 10; i++) {Callable worker = () -> {System.out.println(Thread.currentThread().getName());
                return "ok";
            };
            Future<String> f = executor.submit(worker);
            f.get();}
        executor.shutdown();
        while (!executor.isTerminated()) { }
        System.out.println("Finished all threads");
    }
}

Runnable 和 Callable 的区别

Runnable 工作执行后不能返回值或者抛出异样。Callable 工作执行后能够返回值或抛出异样。

Executors.callable(Runnable task);//runnable 转化为 callable
ExecutorService.execute(Runnable);
ExecutorService.submit(Runnable/Callable);//submit callable 工作有返回值

// 返回值是泛型参数 V
public interface Callable<V> {V call() throws Exception;
}

Future 和 FutureTask

Future 能够获取工作执行的后果、勾销工作。调用 future.get()会阻塞以后线程直到工作返回后果。

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 接口,而 RunnableFuture 实现了 Runnable 和 Future\<V> 接口。

execute()和 submit()

execute()办法用于提交不须要返回值的工作,所以无奈判断工作是否被线程池执行胜利与否。

submit()办法用于提交须要返回值的工作。线程池会返回一个 Future 类型的对象,通过这个 Future 对象能够判断工作是否执行胜利,并且能够通过 Futureget()办法来获取返回值,get()办法会阻塞以后线程直到工作实现,而应用 get(long timeout, TimeUnit unit)办法则会阻塞以后线程一段时间后立刻返回,无论工作是否执行完。

罕用的线程池

常见的线程池有 FixedThreadPool、SingleThreadExecutor、CachedThreadPool 和 ScheduledThreadPool。这几个都是 ExecutorService(线程池)实例。

FixedThreadPool

固定线程数的线程池。任何工夫点,最多只有 nThreads 个线程处于活动状态执行工作。

public static ExecutorService newFixedThreadPool(int nThreads) {return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}

应用无界队列 LinkedBlockingQueue(队列容量为 Integer.MAX_VALUE),运行中的线程池不会回绝工作,即不会调用 RejectedExecutionHandler.rejectedExecution()办法。

maxThreadPoolSize 是有效参数,故将它的值设置为与 coreThreadPoolSize 统一。

keepAliveTime 也是有效参数,设置为 0L,因为此线程池里所有线程都是外围线程,外围线程不会被回收(除非设置了 executor.allowCoreThreadTimeOut(true))。

不举荐应用:FixedThreadPool 不会回绝工作,在工作比拟多的时候会导致 OOM。

SingleThreadExecutor

只有一个线程的线程池。

public static ExecutionService newSingleThreadExecutor() {return new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}

应用无界队列 LinkedBlockingQueue。线程池只有一个运行的线程,新来的工作放入工作队列,线程解决完工作就循环从队列里获取工作执行。保障程序的执行各个工作。

不举荐应用:同 FixedThreadPool,在工作比拟多的时候会导致 OOM。

CachedThreadPool

依据须要创立新线程的线程池。

public static ExecutorService newCachedThreadPool() {return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
}

如果主线程提交工作的速度高于线程解决工作的速度时,CachedThreadPool 会一直创立新的线程。极其状况下,这样会导致耗尽 cpu 和内存资源。

应用没有容量的 SynchronousQueue 作为线程池工作队列,当线程池有闲暇线程时,SynchronousQueue.offer(Runnable task)提交的工作会被闲暇线程解决,否则会创立新的线程解决工作。

不举荐应用:CachedThreadPool容许创立的线程数量为 Integer.MAX_VALUE,可能会创立大量线程,从而导致 OOM。

ScheduledThreadPoolExecutor

在给定的提早后运行工作,或者定期执行工作。在理论我的项目中根本不会被用到,因为有其余计划抉择比方quartz

应用的工作队列 DelayQueue 封装了一个 PriorityQueuePriorityQueue 会对队列中的工作进行排序,工夫早的工作先被执行(即ScheduledFutureTasktime 变量小的先执行),如果 time 雷同则先提交的工作会被先执行(ScheduledFutureTasksquenceNumber 变量小的先执行)。

执行周期工作步骤:

  1. 线程从 DelayQueue 中获取已到期的 ScheduledFutureTask(DelayQueue.take())。到期工作是指 ScheduledFutureTask的 time 大于等于以后零碎的工夫;
  2. 执行这个 ScheduledFutureTask
  3. 批改 ScheduledFutureTask 的 time 变量为下次将要被执行的工夫;
  4. 把这个批改 time 之后的 ScheduledFutureTask 放回 DelayQueue 中(DelayQueue.add())。

编码标准

阿里巴巴编码规约不容许应用 Executors 去创立线程池,而是通过 ThreadPoolExecutor 的形式手动创立线程池,这样子使用者会更加明确线程池的运行机制,防止资源耗尽的危险。

Executors 创立线程池对象的弊病:

FixedThreadPool 和 SingleThreadPool。容许申请队列长度为 Integer.MAX_VALUE,可能沉积大量申请,从而导致 OOM。

CachedThreadPool。创立的线程池容许的最大线程数是 Integer.MAX_VALUE,当增加工作的速度大于线程池解决工作的速度,可能会创立大量的线程,耗费资源,甚至导致 OOM。

正确示例(阿里巴巴编码标准):

// 正例 1
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build();
//Common Thread Pool
ExecutorService pool = new ThreadPoolExecutor(5, 200,
0L, TimeUnit.MILLISECONDS, //0L keepAliveTime
new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

pool.execute(()-> System.out.println(Thread.currentThread().getName()));
pool.shutdown();//gracefully shutdown

// 正例 2
ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1, //corePoolSize threadFactory
    new BasicThreadFactory.Builder().namingPattern("example-schedule-pool-%d").daemon(true).build());

JMM

Java 内存模型:线程之间的共享变量存储在主内存里,每个线程都有本人公有的本地内存,本地内存保留了共享变量的正本,线程对变量的操作都在本地内存中进行,不能间接读写主内存中的变量。

本地内存是 JMM 的一个抽象概念,并不实在存在,它包含缓存、写缓冲区、寄存器以及其余硬件和编译器优化。

过程线程

过程是指一个内存中运行的应用程序,每个过程都有本人独立的一块内存空间,一个过程中能够启动多个线程。
线程是比过程更小的执行单位,它是在一个过程中独立的控制流,一个过程能够启动多个线程,每条线程并行执行不同的工作。

线程状态

初始(NEW):线程被构建,还没有调用 start()。

运行(RUNNABLE):包含操作系统的就绪和运行两种状态。

阻塞(BLOCKED):个别是被动的,在抢占资源中得不到资源,被动的挂起在内存,期待资源开释将其唤醒。线程被阻塞会开释 CPU,不开释内存。

期待(WAITING):进入该状态的线程须要期待其余线程做出一些特定动作(告诉或中断)。

超时期待(TIMED_WAITING):该状态不同于 WAITING,它能够在指定的工夫后自行返回。

终止(TERMINATED):示意该线程曾经执行结束。

图片起源:Java 并发编程的艺术

中断

线程中断即线程运行过程中被其余线程给打断了,它与 stop 最大的区别是:stop 是由零碎强制终止线程,而线程中断则是给指标线程发送一个中断信号,如果指标线程没有接管线程中断的信号并完结线程,线程则不会终止,具体是否退出或者执行其余逻辑取决于指标线程。

线程中断三个重要的办法:

1、java.lang.Thread#interrupt

调用指标线程的 interrupt()办法,给指标线程发一个中断信号,线程被打上中断标记。

2、java.lang.Thread#isInterrupted()

判断指标线程是否被中断,不会革除中断标记。

3、java.lang.Thread#interrupted

判断指标线程是否被中断,会革除中断标记。

private static void test2() {Thread thread = new Thread(() -> {while (true) {Thread.yield();

            // 响应中断
            if (Thread.currentThread().isInterrupted()) {System.out.println("Java 技术栈线程被中断,程序退出。");
                return;
            }
        }
    });
    thread.start();
    thread.interrupt();}

常见办法

join

Thread.join(),在 main 中创立了 thread 线程,在 main 中调用了 thread.join()/thread.join(long millis),main 线程放弃 cpu 控制权,线程进入 WAITING/TIMED_WAITING 状态,等到 thread 线程执行完才继续执行 main 线程。

public final void join() throws InterruptedException {join(0);
}

yield

Thread.yield(),肯定是以后线程调用此办法,以后线程放弃获取的 CPU 工夫片,但不开释锁资源,由运行状态变为就绪状态,让 OS 再次抉择线程。作用:让雷同优先级的线程轮流执行,但并不保障肯定会轮流执行。理论中无奈保障 yield()达到退让目标,因为退让的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。该办法与 sleep()相似,只是不能由用户指定暂停多长时间。

public static native void yield(); //static 办法

sleep

Thread.sleep(long millis),肯定是以后线程调用此办法,以后线程进入 TIMED_WAITING 状态,让出 cpu 资源,但不开释对象锁,指定工夫到后又复原运行。作用:给其它线程执行机会的最佳形式。

public static native void sleep(long millis) throws InterruptedException;//static 办法

wait()和 sleep()的区别

相同点:

  1. 使以后线程暂停运行,把机会交给其余线程
  2. 任何线程在期待期间被中断都会抛出 InterruptedException

不同点:

  1. wait() 是 Object 超类中的办法;而 sleep()是线程 Thread 类中的办法
  2. 对锁的持有不同,wait()会开释锁,而 sleep()并不开释锁
  3. 唤醒办法不完全相同,wait() 依附 notify 或者 notifyAll、中断、达到指定工夫来唤醒;而 sleep()达到指定工夫被唤醒
  4. 调用 obj.wait()须要先获取对象的锁,而 Thread.sleep()不必

创立线程的办法

  • 通过扩大 Thread 类来创立多线程
  • 通过实现 Runnable 接口来创立多线程,可实现线程间的资源共享
  • 实现 Callable 接口,通过 FutureTask 接口创立线程。
  • 应用 Executor 框架来创立线程池。

继承 Thread 创立线程 代码如下。run()办法是由 jvm 创立完操作系统级线程后回调的办法,不能够手动调用,手动调用相当于调用一般办法。

/**
 * @author: 程序员大彬
 * @time: 2021-09-11 10:15
 */
public class MyThread extends Thread {public MyThread() { }

    @Override
    public void run() {for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread() + ":" + i);
        }
    }

    public static void main(String[] args) {MyThread mThread1 = new MyThread();
        MyThread mThread2 = new MyThread();
        MyThread myThread3 = new MyThread();
        mThread1.start();
        mThread2.start();
        myThread3.start();}
}

Runnable 创立线程代码

/**
 * @author: 程序员大彬
 * @time: 2021-09-11 10:04
 */
public class RunnableTest {public static  void main(String[] args){Runnable1 r = new Runnable1();
        Thread thread = new Thread(r);
        thread.start();
        System.out.println("主线程:["+Thread.currentThread().getName()+"]");
    }
}

class Runnable1 implements Runnable{
    @Override
    public void run() {System.out.println("以后线程:"+Thread.currentThread().getName());
    }
}

实现 Runnable 接口比继承 Thread 类所具备的劣势:

  1. 资源共享,适宜多个雷同的程序代码的线程去解决同一个资源
  2. 能够防止 java 中的单继承的限度
  3. 线程池只能放入实现 Runable 或 Callable 类线程,不能间接放入继承 Thread 的类

Callable 创立线程代码

/**
 * @author: 程序员大彬
 * @time: 2021-09-11 10:21
 */
public class CallableTest {public static void main(String[] args) {Callable1 c = new Callable1();

        // 异步计算的后果
        FutureTask<Integer> result = new FutureTask<>(c);

        new Thread(result).start();

        try {
            // 期待工作实现,返回后果
            int sum = result.get();
            System.out.println(sum);
        } catch (InterruptedException | ExecutionException e) {e.printStackTrace();
        }
    }

}

class Callable1 implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        int sum = 0;

        for (int i = 0; i <= 100; i++) {sum += i;}
        return sum;
    }
}

应用 Executor 创立线程代码

/**
 * @author: 程序员大彬
 * @time: 2021-09-11 10:44
 */
public class ExecutorsTest {public static void main(String[] args) {
        // 获取 ExecutorService 实例,生产禁用,须要手动创立线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        // 提交工作
        executorService.submit(new RunnableDemo());
    }
}

class RunnableDemo implements Runnable {
    @Override
    public void run() {System.out.println("大彬");
    }
}

线程间通信

volatile

volatile 是轻量级的同步机制,volatile 保障变量对所有线程的可见性,不保障原子性。

  1. 当对 volatile 变量进行写操作的时候,JVM 会向处理器发送一条 LOCK 前缀的指令,将该变量所在缓存行的数据写回零碎内存。
  2. 因为缓存一致性协定,每个处理器通过嗅探在总线上流传的数据来查看本人的缓存是不是过期了,当处理器发现自己缓存行对应的内存地址被批改,就会将以后处理器的缓存行置为有效状态,当处理器对这个数据进行批改操作的时候,会从新从零碎内存中把数据读到处理器缓存中。

MESI(缓存一致性协定):当 CPU 写数据时,如果发现操作的变量是共享变量,即在其余 CPU 中也存在该变量的正本,会发出信号告诉其余 CPU 将该变量的缓存行置为有效状态,因而当其余 CPU 须要读取这个变量时,就会从内存从新读取。

volatile 关键字的两个作用:

  1. 保障了不同线程对共享变量进行操作时的可见性,即一个线程批改了某个变量的值,这新值对其余线程来说是立刻可见的。
  2. 禁止进行指令重排序。

指令重排序是 JVM 为了优化指令,进步程序运行效率,在不影响单线程程序执行后果的前提下,尽可能地进步并行度。Java 编译器会在生成指令系列时在适当的地位会插入 内存屏障 指令来禁止处理器重排序。插入一个内存屏障,相当于通知 CPU 和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。对一个 volatile 字段进行写操作,Java 内存模型将在写操作后插入一个写屏障指令,这个指令会把之前的写入值都刷新到内存。

synchronized

保障线程对变量拜访的可见性和排他性。synchronized 具体内容见下文锁局部。

期待告诉机制

wait/notify 为 Object 对象的办法,调用 wait/notify 须要先取得对象的锁。对象调用 wait 之后线程开释锁,将线程放到对象的期待队列,当告诉线程调用此对象的 notify()办法后,期待线程并不会立刻从 wait 返回,须要期待告诉线程开释锁(告诉线程执行完同步代码块),期待队列里的线程获取锁,获取锁胜利能力从 wait()办法返回,即从 wait 办法返回前提是线程取得锁。

期待告诉机制依靠于同步机制,目标是确保期待线程从 wait 办法返回时能感知到告诉线程对对象的变量值的批改。

synchronized

较罕用的用于保障线程平安的形式。当一个线程获取到锁时,其余线程都会被阻塞住,当持有锁的线程开释锁之后会唤醒这些线程,被唤醒的线程才有机会获取到锁。

  • 润饰实例办法,作用于以后对象实例加锁,进入同步代码前要取得以后对象实例的锁
  • 润饰静态方法,作用于以后类对象加锁,进入同步代码前要取得以后类对象的锁(类的字节码文件)
  • 润饰代码块,指定加锁对象,对给定对象加锁,进入同步代码块前要取得给定对象的锁

获取了类锁的线程和获取了对象锁的线程是不抵触的。

开释锁

当办法或者代码块执行结束后会主动开释锁,不须要做任何的操作。
当一个线程执行的代码出现异常时,其所持有的锁会主动开释。

实现原理

synchronized 通过对象外部的监视器锁(monitor)实现。每个对象都有一个 monitor,当对象的 monitor 被持有时,则它处于锁定的状态。

代码块的同步 是应用 monitorenter 和 monitorexit 指令实现的,monitorenter 指令是在编译后插入到同步代码块的开始地位,而 monitorexit 是插入到办法完结处或异样处。

public class SynchronizedDemo {public void method() {synchronized (this) {System.out.println("method start");
        }
    }
}

线程拜访同步块时,先执行 monitorenter 指令时尝试获取 monitor,过程如下:

  1. 如果 monitor 的进入数 entry count 为 0,则该线程进入 monitor,而后将进入数设置为 1,该线程即为 monitor 的所有者。
  2. 如果线程曾经占有该 monitor,只是从新进入,则进入 monitor 的进入数加 1。
  3. 如果其余线程曾经占用了 monitor,则该线程进入阻塞状态,直到 monitor 的进入数为 0,再从新尝试获取 monitor。

线程退出同步块时会执行 monitorexit 指令,monitor 的进入数减 1,如果减 1 后进入数为 0,那线程退出 monitor,不再是这个 monitor 的所有者。其余被这个 monitor 阻塞的线程能够尝试去获取这个 monitor。

Synchronized 底层是通过一个 monitor 的对象来实现,其实 wait/notify 等办法也依赖于 monitor 对象,这就是为什么只有在同步的块或者办法中能力调用 wait/notify 等办法,否则会抛出 java.lang.IllegalMonitorStateException 的异样的起因。

办法的同步 不是通过增加 monitorenter 和 monitorexit 指令来实现,而是在其常量池中增加了 ACC_SYNCHRONIZED 标识符。JVM 就是依据该标识符来实现办法的同步的:当线程调用办法时,会先查看办法的 ACC_SYNCHRONIZED 拜访标记是否被设置,如果设置了,阐明此办法是同步办法,执行线程将先获取 monitor,获取胜利之后能力执行办法体,办法执行完后再开释 monitor。在办法执行期间,其余线程无奈再取得同一个 monitor 对象。

public class SynchronizedMethod {public synchronized void method() {System.out.println("Hello World!");
    }
}

锁的状态

Synchronized 是通过对象外部的监视器来实现的。然而监视器锁实质又是依赖于底层的操作系统的 Mutex Lock 来实现的。而操作系统实现线程之间的切换这就须要从用户态转换到外围态,这个老本十分高,状态之间的转换须要绝对比拟长的工夫。这种依赖于操作系统 Mutex Lock 所实现的锁咱们称之为重量级锁。

JDK1.6 中为了缩小取得锁和开释锁带来的性能耗费,引入了偏差锁、轻量级锁、自旋锁、适应性自旋锁、锁打消、锁粗化等技术来缩小锁操作的开销。

synchronized 锁次要存在四种状态,顺次是:偏差锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的强烈而逐步降级。锁能够降级不可降级,这种策略是为了进步取得锁和开释锁的效率。

  • 偏差锁:当线程拜访同步块并获取锁时,会在对象头和锁记录中存储锁偏差的线程 id,当前该线程进入和退出同步块时,只需简略测试一下对象头的 mark word 中是否存储着指向以后线程的偏差锁,如果测试胜利,则线程获取锁胜利,否则,需再测试一下 mark word 中偏差锁标识是否是 1,是的话则应用 CAS 操作竞争锁。如果竞争胜利,则将 Mark Word 中线程 ID 设置为以后线程 ID,如果 CAS 获取偏差锁失败,则示意有竞争。当达到全局平安点时取得偏差锁的线程被挂起,偏差锁降级为轻量级锁,而后被阻塞在平安点的线程持续往下执行同步代码。
    偏差锁偏差于第一个取得它的线程,如果程序运行过程,该锁没有被其余线程获取,那么持有偏差锁的线程就不须要进行同步。引入偏差锁是为了在无多线程竞争的状况下尽量减少不必要的轻量级锁执行的开销,因为轻量级锁的获取及开释应用了屡次 CAS 原子指令,而偏差锁只在置换 ThreadID 的时候应用一次 CAS 原子指令。当存在锁竞争的时候,偏差锁会降级为轻量级锁。
    实用场景:在锁无竞争的状况下应用,在线程没有执行完同步代码之前,没有其它线程去竞争锁,一旦有了竞争就降级为轻量级锁,降级为轻量级锁的时候须要撤销偏差锁,会做很多额定操作,导致性能降落。
  • 轻量级锁
    加锁过程:线程执行同步块之前,JVM 会先在以后线程的栈帧中创立用于存储锁记录的空间,并将对象头的 mark word 复制到锁记录(displaced mark word)中,而后线程尝试应用 cas 将对象头的 mark word 替换为指向锁记录的指针。如果胜利,则以后线程取得锁,否则示意有其余线程竞争锁,以后线程便尝试应用自旋来取得锁。当自旋超过肯定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁降级为重量级锁。
    解锁过程:应用原子的 cas 操作将 displaced mark word 替换回到对象头,如果胜利则解锁胜利,否则表明有锁竞争,锁会收缩成重量级锁。

    在没有多线程竞争的前提下,应用轻量级锁能够缩小传统的重量级锁应用操作系统互斥量(申请互斥锁)产生的性能耗费,因为应用轻量级锁时,不须要申请互斥量。另外,轻量级锁的加锁和解锁都用到了 CAS 操作。如果没有竞争,轻量级锁应用 CAS 操作防止了应用互斥操作的开销。但如果存在锁竞争,除了互斥量开销外,还会额定产生 CAS 操作,因而在有锁竞争的状况下,轻量级锁比传统的重量级锁更慢!如果锁竞争强烈,那么轻量级锁将很快收缩为重量级锁!

  • 重量级锁:当一个线程获取到锁时,其余线程都会被阻塞住,当持有锁的线程开释锁之后会唤醒这些线程,被唤醒的线程才有机会获取到锁。

    synchronized 和 Lock 能保障同一时刻只有一个线程获取锁而后执行同步代码,并且在开释锁之前会将对变量的批改刷新到主存当中,保障了可见性。

  • 自旋锁:个别线程持有锁的工夫都不是太长,所以仅仅为了这一点工夫去挂起线程 / 复原线程比拟浪费资源。自旋锁就是让该线程期待一段时间,执行一段无意义的循环,不会被立刻挂起,看持有锁的线程是否会很快开释锁。如果持有锁的线程很快就开释了锁,那么自旋的效率就十分好,反之,自旋的线程就会白白消耗掉解决的资源,这样反而会带来性能上的节约。所以自旋的次数必须要有一个限度,如果自旋超过了限定次数依然没有获取到锁,则应该被挂起。
  • 自适应自旋锁:JDK 1.6 引入了更加聪慧的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋工夫及锁的拥有者的状态来决定。
  • 锁打消:虚拟机即便编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁打消。
  • 锁粗化:如果一系列的间断操作都对同一个对象重复加锁和解锁,那么会带来很多不必要的性能耗费,应用锁粗化缩小锁操作的开销。

ReentrantLock

重入锁,反对一个线程对资源的反复加锁。该锁的还反对设置获取锁时的偏心和非公平性。

应用 lock 时须要在 try finally 块进行解锁:

public static final Object get(String key) {r.lock();
    try {return map.get(key);
    } finally {r.unlock();
    }
}

原理

ReentrantLock 是通过组合自定义同步器来实现锁的获取与开释。当线程尝试获取同步状态时,首先判断以后线程是否为获取锁的线程来决定获取操作是否胜利,如果是获取锁的线程再次申请,则将同步状态值进行减少并返回 true,示意获取同步状态胜利。获取同步状态失败,则该线程会被结构成 node 节点放到 AQS 同步队列中。

如果锁被获取了 n 次,那么前 n - 1 次 tryRelease(int releases)办法必须返回 false,第 n 次调用 tryRelease()之后,同步状态齐全开释(值为 0),才会返回 true。

ReentrantLock 和 synchronized 区别

  1. 应用 synchronized 关键字实现同步,线程执行完同步代码块会主动开释锁,而 ReentrantLock 须要手动开释锁。
  2. synchronized 是非偏心锁,ReentrantLock 能够设置为偏心锁。
  3. ReentrantLock 上期待获取锁的线程是可中断的,线程能够放弃期待锁。而 synchonized 会无限期期待上来。
  4. ReentrantLock 能够设置超时获取锁。在指定的截止工夫之前获取锁,如果截止工夫到了还没有获取到锁,则返回。
  5. ReentrantLock 的 tryLock() 办法能够尝试非阻塞的获取锁,调用该办法后立即返回,如果可能获取则返回 true,否则返回 false。

锁的分类

偏心锁与非偏心锁

依照线程拜访程序获取对象锁。synchronized 是非偏心锁,Lock 默认是非偏心锁,能够设置为偏心锁,偏心锁会影响性能。

public ReentrantLock() {sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();}

共享式与独占式锁

共享式与独占式的最次要区别在于:同一时刻独占式只能有一个线程获取同步状态,而共享式在同一时刻能够有多个线程获取同步状态。例如读操作能够有多个线程同时进行,而写操作同一时刻只能有一个线程进行写操作,其余操作都会被阻塞。

乐观锁与乐观锁

乐观锁,每次拜访资源都会加锁,执行完同步代码开释锁,synchronized 和 ReentrantLock 属于乐观锁。

乐观锁,不会锁定资源,所有的线程都能拜访并批改同一个资源,如果没有抵触就批改胜利并退出,否则就会持续循环尝试。乐观锁最常见的实现就是 CAS。

乐观锁一般来说有以下 2 种形式:

  1. 应用数据版本记录机制实现,这是乐观锁最罕用的一种实现形式。给数据减少一个版本标识,个别是通过为数据库表减少一个数字类型的 version 字段来实现。当读取数据时,将 version 字段的值一起读出,数据每更新一次,对此 version 值加一。当咱们提交更新的时候,判断数据库表对应记录的以后版本信息与第一次取出来的 version 值进行比对,如果数据库表以后版本号与第一次取出来的 version 值相等,则予以更新,否则认为是过期数据。
  2. 应用工夫戳。数据库表减少一个字段,字段类型应用工夫戳(timestamp),和下面的 version 相似,也是在更新提交的时候查看以后数据库中数据的工夫戳和本人更新前取到的工夫戳进行比照,如果统一则 OK,否则就是版本抵触。

实用场景:

  • 乐观锁适宜写操作多的场景。
  • 乐观锁适宜读操作多的场景,不加锁能够晋升读操作的性能。
CAS

CAS 全称 Compare And Swap,比拟与替换,是乐观锁的次要实现形式。CAS 在不应用锁的状况下实现多线程之间的变量同步。ReentrantLock 外部的 AQS 和原子类外部都应用了 CAS。

CAS 算法波及到三个操作数:

  • 须要读写的内存值 V。
  • 进行比拟的值 A。
  • 要写入的新值 B。

只有当 V 的值等于 A 时,才会应用原子形式用新值 B 来更新 V 的值,否则会持续重试直到胜利更新值。

以 AtomicInteger 为例,AtomicInteger 的 getAndIncrement()办法底层就是 CAS 实现,要害代码是 compareAndSwapInt(obj, offset, expect, update),其含意就是,如果 obj 内的 valueexpect相等,就证实没有其余线程扭转过这个变量,那么就更新它为update,如果不相等,那就会持续重试直到胜利更新值。

CAS 三大问题:

  1. ABA 问题 。CAS 须要在操作值的时候查看内存值是否发生变化,没有发生变化才会更新内存值。然而如果内存值原来是 A,起初变成了 B,而后又变成了 A,那么 CAS 进行查看时会发现值没有发生变化,然而实际上是有变动的。ABA 问题的解决思路就是在变量后面增加版本号,每次变量更新的时候都把版本号加一,这样变动过程就从A-B-A 变成了1A-2B-3A

    JDK 从 1.5 开始提供了 AtomicStampedReference 类来解决 ABA 问题,原子更新带有版本号的援用类型。

  2. 循环工夫长开销大。CAS 操作如果长时间不胜利,会导致其始终自旋,给 CPU 带来十分大的开销。
  3. 只能保障一个共享变量的原子操作。对一个共享变量执行操作时,CAS 可能保障原子操作,然而对多个共享变量操作时,CAS 是无奈保障操作的原子性的。

    Java 从 1.5 开始 JDK 提供了 AtomicReference 类来保障援用对象之间的原子性,能够把多个变量放在一个对象里来进行 CAS 操作。

并发工具

在 JDK 的并发包里提供了几个十分有用的并发工具类。CountDownLatch、CyclicBarrier 和 Semaphore 工具类提供了一种并发流程管制的伎俩。

CountDownLatch

CountDownLatch 用于某个线程期待其余线程 执行完工作 再执行,与 thread.join()性能相似。常见的利用场景是开启多个线程同时执行某个工作,等到所有工作执行完再执行特定操作,如汇总统计后果。

public class CountDownLatchDemo {
    static final int N = 4;
    static CountDownLatch latch = new CountDownLatch(N);

    public static void main(String[] args) throws InterruptedException {for(int i = 0; i < N; i++) {new Thread(new Thread1()).start();}

       latch.await(1000, TimeUnit.MILLISECONDS); // 调用 await()办法的线程会被挂起,它会期待直到 count 值为 0 才继续执行; 期待 timeout 工夫后 count 值还没变为 0 的话就会继续执行
       System.out.println("task finished");
    }

    static class Thread1 implements Runnable {

        @Override
        public void run() {
            try {System.out.println(Thread.currentThread().getName() + "starts working");
                Thread.sleep(1000);
            } catch (InterruptedException e) {e.printStackTrace();
            } finally {latch.countDown();
            }
        }
    }
}

运行后果:

Thread-0starts workingThread-1starts workingThread-2starts workingThread-3starts workingtask finished

CyclicBarrier

CyclicBarrier(同步屏障),用于一组线程相互期待到某个状态,而后这组线程再 同时 执行。

public CyclicBarrier(int parties, Runnable barrierAction) {}public CyclicBarrier(int parties) {}

参数 parties 指让多少个线程或者工作期待至某个状态;参数 barrierAction 为当这些线程都达到某个状态时会执行的内容。

public class CyclicBarrierTest {
    // 申请的数量
    private static final int threadCount = 10;
    // 须要同步的线程数量
    private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5);

    public static void main(String[] args) throws InterruptedException {
        // 创立线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(10);

        for (int i = 0; i < threadCount; i++) {
            final int threadNum = i;
            Thread.sleep(1000);
            threadPool.execute(() -> {
                try {test(threadNum);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();} catch (BrokenBarrierException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();}
            });
        }
        threadPool.shutdown();}

    public static void test(int threadnum) throws InterruptedException, BrokenBarrierException {System.out.println("threadnum:" + threadnum + "is ready");
        try {
            /** 期待 60 秒,保障子线程齐全执行完结 */
            cyclicBarrier.await(60, TimeUnit.SECONDS);
        } catch (Exception e) {System.out.println("-----CyclicBarrierException------");
        }
        System.out.println("threadnum:" + threadnum + "is finish");
    }

}

运行后果如下,能够看出 CyclicBarrier 是能够重用的:

threadnum:0is ready
threadnum:1is ready
threadnum:2is ready
threadnum:3is ready
threadnum:4is ready
threadnum:4is finish
threadnum:3is finish
threadnum:2is finish
threadnum:1is finish
threadnum:0is finish
threadnum:5is ready
threadnum:6is ready
...

当四个线程都达到 barrier 状态后,会从四个线程中抉择一个线程去执行 Runnable。

CyclicBarrier 和 CountDownLatch 区别

CyclicBarrier 和 CountDownLatch 都可能实现线程之间的期待。

CountDownLatch 用于某个线程期待其余线程 执行完工作 再执行。CyclicBarrier 用于一组线程相互期待到某个状态,而后这组线程再 同时 执行。
CountDownLatch 的计数器只能应用一次,而 CyclicBarrier 的计数器能够应用 reset()办法重置,可用于解决更为简单的业务场景。

Semaphore

Semaphore 相似于锁,它用于管制同时拜访特定资源的线程数量,管制并发线程数。

public class SemaphoreDemo {public static void main(String[] args) {final int N = 7;        Semaphore s = new Semaphore(3);        for(int i = 0; i < N; i++) {new Worker(s, i).start();}    }    static class Worker extends Thread {private Semaphore s;        private int num;        public Worker(Semaphore s, int num) {this.s = s;            this.num = num;}        @Override        public void run() {            try {                s.acquire();                System.out.println("worker" + num +  "using the machine");                Thread.sleep(1000);                System.out.println("worker" + num +  "finished the task");                s.release();} catch (InterruptedException e) {e.printStackTrace();            }        }    }}

运行后果如下,能够看出并非依照线程拜访程序获取资源的锁,即

worker0 using the machineworker1 using the machineworker2 using the machineworker2 finished the taskworker0 finished the taskworker3 using the machineworker4 using the machineworker1 finished the taskworker6 using the machineworker4 finished the taskworker3 finished the taskworker6 finished the taskworker5 using the machineworker5 finished the task

原子类

根本类型原子类

应用原子的形式更新根本类型

  • AtomicInteger:整型原子类
  • AtomicLong:长整型原子类
  • AtomicBoolean:布尔型原子类

AtomicInteger 类罕用的办法:

public final int get() // 获取以后的值
public final int getAndSet(int newValue)// 获取以后的值,并设置新的值
public final int getAndIncrement()// 获取以后的值,并自增
public final int getAndDecrement() // 获取以后的值,并自减
public final int getAndAdd(int delta) // 获取以后的值,并加上预期的值
boolean compareAndSet(int expect, int update) // 如果输出的数值等于预期值,则以原子形式将该值设置为输出值(update)public final void lazySet(int newValue)// 最终设置为 newValue, 应用 lazySet 设置之后可能导致其余线程在之后的一小段时间内还是能够读到旧的值。

AtomicInteger 类次要利用 CAS (compare and swap) 保障原子操作,从而防止加锁的高开销。

数组类型原子类

应用原子的形式更新数组里的某个元素

  • AtomicIntegerArray:整形数组原子类
  • AtomicLongArray:长整形数组原子类
  • AtomicReferenceArray:援用类型数组原子类

AtomicIntegerArray 类罕用办法:

public final int get(int i) // 获取 index=i 地位元素的值
public final int getAndSet(int i, int newValue)// 返回 index=i 地位的以后的值,并将其设置为新值:newValue
public final int getAndIncrement(int i)// 获取 index=i 地位元素的值,并让该地位的元素自增
public final int getAndDecrement(int i) // 获取 index=i 地位元素的值,并让该地位的元素自减
public final int getAndAdd(int i, int delta) // 获取 index=i 地位元素的值,并加上预期的值
boolean compareAndSet(int i, int expect, int update) // 如果输出的数值等于预期值,则以原子形式将 index=i 地位的元素值设置为输出值(update)public final void lazySet(int i, int newValue)// 最终 将 index=i 地位的元素设置为 newValue, 应用 lazySet 设置之后可能导致其余线程在之后的一小段时间内还是能够读到旧的值。

援用类型原子类

  • AtomicReference:援用类型原子类
  • AtomicStampedReference:带有版本号的援用类型原子类。该类将整数值与援用关联起来,可用于解决原子的更新数据和数据的版本号,能够解决应用 CAS 进行原子更新时可能呈现的 ABA 问题。
  • AtomicMarkableReference:原子更新带有标记的援用类型。该类将 boolean 标记与援用关联起来

AQS

AQS 定义了一套多线程访问共享资源的同步器框架,许多并发工具的实现都依赖于它,如罕用的 ReentrantLock/Semaphore/CountDownLatch。

原理

AQS 应用一个 volatile 的 int 类型的成员变量 state 来示意同步状态,通过 CAS 批改同步状态的值。

private volatile int state;// 共享变量,应用 volatile 润饰保障线程可见性

同步器依赖外部的同步队列(一个 FIFO 双向队列)来实现同步状态的治理,以后线程获取同步状态失败时,同步器会将以后线程以及期待状态(独占或共享)结构成为一个节点(Node)并将其退出同步队列并进行自旋,当同步状态开释时,会把首节中的后继节点对应的线程唤醒,使其再次尝试获取同步状态。

Condition

任意一个 Java 对象,都领有一组监视器办法(定义在 java.lang.Object 上),次要包含 wait()、wait(long timeout)、notify()以及 notifyAll()办法,应用这些办法的前提是曾经获取对象的锁,和 synchronized 配合应用。Condition 接口也提供了相似 Object 的监视器办法,与 Lock 配合能够实现期待 / 告诉模式。Condition 是依赖 Lock 对象。

Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void conditionWait() throws InterruptedException {lock.lock();
    try {condition.await();
    } finally {lock.unlock();
    }
}
public void conditionSignal() throws InterruptedException {lock.lock();
    try {condition.signal();
    } finally {lock.unlock();
    }
}

个别将 Condition 对象作为成员变量。当调用 await()办法后,以后线程会开释锁进入期待队列。其余线程调用 Condition 对象的 signal()办法,唤醒期待队列首节点的线程。

实现原理

每个 Condition 对象都蕴含着一个期待队列,如果一个线程胜利获取了锁之后调用了 Condition.await()办法,那么该线程将会开释同步状态、唤醒同步队列中的后继节点,而后结构成节点退出期待队列。只有当线程再次获取 Condition 相关联的锁之后,能力从 await()办法返回。

图片起源:Java 并发编程的艺术

在 Object 的监视器模型上,一个对象领有一个同步队列和期待队列。Lock 通过 AQS 实现,AQS 能够有多个 Condition,所以 Lock 领有一个同步队列和多个期待队列。

图片起源:Java 并发编程的艺术

线程获取了锁之后,调用 Condition 的 signal()办法,会将期待队列的队首节点移到同步队列中,而后该节点的线程会尝试去获取同步状态。胜利获取同步状态之后,线程将 await()办法返回。

图片起源:Java 并发编程的艺术

其余

Daemon Thread

在 Java 中有两类线程:

  • User Thread(用户线程)
  • Daemon Thread(守护线程)

只有以后 JVM 实例中尚存在任何一个非守护线程没有完结,守护线程就全副工作;只有当最初一个非守护线程完结时,守护线程随着 JVM 一起完结工作。

Daemon 的作用是为其余线程的运行提供便当服务,守护线程最典型的利用就是垃圾收集。

将线程转换为守护线程能够通过调用 Thread 对象的 setDaemon(true)办法来实现。

参考资料

线程中断

synchronized 实现原理

指令重排导致单例模式生效

本文曾经收录到 github 仓库,此仓库用于分享 Java 相干常识总结,包含 Java 根底、MySQL、Spring Boot、MyBatis、Redis、RabbitMQ、计算机网络、数据结构与算法等等,欢送大家提 pr 和 star!

github 地址:https://github.com/Tyson0314/…

如果 github 拜访不了,能够拜访 gitee 仓库。

gitee 地址:https://gitee.com/tysondai/Ja…

正文完
 0