注:

1.笔记为个人归纳整理,尽力保证准确性,如有错误,恳请指正

2.写文不易,转载请注明出处

3.本文首发地址 https://blog.leapmie.com/archives/c02a6ed1/

4.本系列文章目录详见《Java八股文纯享版——目录》

5.文末可关注公众号,内容更精彩

Java创建线程的方法

方式一:继承Thread类的方式

继承于Thread类,重写Thread类中的run()方法,创建子类对象,调用start()方法。

public class Demo {
public static void main(String[] args) {
Thread t = new MyThread();
t.start();
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println(“1”);
}
}

方式二:实现Runnable接口的方式

创建实现Runnable接口的类,实现run()方法,创建对象,以此对象作为参数传入Thread类的构造器中,调用Thread类的start()方法。

对比Thread优点:Thread是继承的形式,一个类只能继承一个类,所以Runnable接口实现的方式更灵活。

public class RunnableDemo {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnableThread());
t.start();
}
}
class MyRunnableThread implements Runnable { @Override
public void run() {
System.out.println("abc");
}
}

方式三:实现Callable接口

实现Callable接口,传入FutureTask构造器创建对象,把FutureTask构造器对象传入Thread构造器,调用Thread类的start()方法。

FutureTask是Futrue接口的唯一的实现类,Future接口可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。

对比Runnable优点:

Callable功能更强大些,实现的call()方法相比run()方法,可以返回值方法,可以抛出异常,支持泛型的返回值。

public class CallableDemo {
public static void main(String[] args) {
MyCallableThread myCallableThread = new MyCallableThread();
FutureTask<String> futureTask = new FutureTask<>(myCallableThread);
new Thread(futureTask).start();
try {
String result = futureTask.get();
System.out.println("result:" + result);
} catch (Exception e) {
e.printStackTrace();
}
}
}
class MyCallableThread implements Callable {
@Override
public String call() throws Exception {
System.out.println("callable > call");
return "hello";
}
}

锁的类型

取锁方式分类:悲观锁、乐观锁

悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,通常用于写多的场景。

典型代表为Java中的synchronized、ReentrantLock等独占锁。

乐观锁

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,大部分情况下不需要上锁,通常用于读多的场景。

典型代表为CAS机制,java.util.concurrent.atomic包下面的原子变量类使用CAS实现。

锁的性质分类:不可重入锁、可重入锁、共享锁、排它锁

不可重入锁

【例】无。(Java提供的都是可重入锁,不可重入锁非常容易导致死锁。)

只判断这个锁有没有被锁上,只要被锁上申请锁的线程都会被要求等待,实现简单。

可重入锁

【例】ReentrantLock、ReentrantReadWriteLock

可重入锁:不仅判断锁有没有被锁上,还会判断锁是谁锁上的,当就是自己锁上的时候,那么他依旧可以再次访问临界资源,并把加锁次数加一(计数用于正确解锁)。

Java提供的锁都是可重入锁。不可重入锁非常容易导致死锁。

共享锁

【例】ReentrantReadWriteLock

线程可以同时获取锁。ReentrantReadWriteLock对于读锁是共享的。在读多写少的情况下使用共享锁会非常高效

排它锁

【例】ReentrantLock

多线程不可同时获取的锁,与共享锁对立。与重入锁不矛盾可以是并存属性。

取锁时是否先参与排队分类:公平锁、非公平锁

公平锁

【例】ReentrantLock(boolean fair)可以配置为公平锁

线程试图获取锁时,先按尝试获取锁的时间顺序排队

非公平锁

【例】ReentrantLock默认是非公平锁

线程试图获取锁时,如果当前锁没有线程占有,则跟排队获取锁的线程一起竞争锁而无序按顺序排队,则为非公平锁。如果竞选失败,依然要排队。

根据锁的状态划分:偏向锁、轻量级锁、重量级锁

偏向锁

一段同步代码一直被一个线程所访问,那么该线程会自动获取锁

轻量级锁

当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能

重量级锁

当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低

根据锁粒度划分:分段锁等

分段锁

【例】jdk8的ConcurrentHashMap实现方式

分段锁是一种锁思想,对数据分段加锁已提高并发效率,比如jdk8之前的ConcurrentHashMap,jdk8后采用CAS+synchronized。当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在哪一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。

常用的并发方案

Lock

Lock lock = new XxxLock();
lock.lock();
// work
lock.unlock();

Synchronized

使用方便,常用的解决方案,支持方法、静态方法、代码块的锁

1. 修饰实例方法(修饰静态方法一样用法)

public class Demo {
public synchronized void methodOne() { }
}

2.修饰代码块

有时如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方法对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了。

我们可以使用如下几种对象来作为锁的对象:

1)成员锁(锁的对象是变量)

public Object synMethod(Object a1) {
synchronized(a1) {
// 操作
}
}

2)成员锁(锁的对象是变量)

synchronized(this) {
for (int j = 0; j < 100; j++) {
i++;
}
}

3)当前类的 class 对象锁

synchronized(AccountingSync.class) {
for (int j = 0; j < 100; j++) {
i++;
}
}

实现原理:

在 Java 中,每个对象都会有一个 monitor 对象,使用synchronized关键字在编译后是在方法前增加monitor.enter指令,在方法退出和异常处插入monitor.exit指令,通过对一个对象监视器(monitor)进行获取从而达到只能一个线程访问。

在 Java 中,针对每个类也有一个锁,可以称为“类锁”,所以每个类只有一个类锁,所以synchronized关键字可以传入class,对于静态方法,由于此时对象还未生成,所以只能采用类锁。

底层进阶:

操作系统本身并不支持 monitor 机制,monitor机制是由编程语言实现,java的monitor由JVM实现。操作系统中semaphore 信号量 和 mutex 互斥量是最重要的同步原语,monitor是对semaphore 和mutex 的进一步封装,提供简洁易用的接口。

Volatile

volatile 只能保证可见性而不能保证原子性(准确的说是不能保证复合操作的原子性),要非常小心的使用才能确保线程安全,通常在某些特定场合下使用,如双重检查锁模式(常用于单例模式或延迟赋值的场景)

(volatile可使线程每次读取变量时都从主内存中读取,保证变量对于各线程的可见性,但是对于多CPU的计算机,CPU中有一层高速缓存——寄存器,volatile 不能保证其它 cpu 的缓存同步刷新,因此无法保证原子性。)

双重检查锁模式:

public class Singleton {
private volatile static Singleton uniqueSingleton; // 1. 为变量添加volatile修饰符 private Singleton() {
} public static Singleton getInstance() {
if (null == uniqueSingleton) { //2. 第一重检查
synchronized (Singleton.class) { // 3. synchronized加锁
if (null == uniqueSingleton) { // 4. 第二重检查
uniqueSingleton = new Singleton();
}
}
}
return uniqueSingleton;
}
}

CAS (Compare And Set)

java.util.concurrent.automic包中实现

CAS是最常见的乐观锁之一,应用于小概率需要锁资源的场景,常用于高并发的“查询并修改”的场景,通过“先查询判断再更新”的方式保证数据一致性。

场景例:

现有变量余额money=100,线程1需要扣款20元,线程2需要扣款30元,执行顺序如图:

业务上最终应该money=50,但由于并发问题最终money=70,在CAS方案中,在修改前需要先进行判断,对应思路的伪代码如下

Get money = 100
If(money == 100) {
Set money = money -100
}

显然实际使用中并不能如上简单实现,因为以上操作并非原子操作,实际的Java代码需要调用Unsafe类的compareAndSwap系列方法,如compareAndSwapInt。该方法实际通过JNI调用底层C语言实现,最终CPU指令 cmpxchg,通过CPU实现原子性及“查询并修改”。

compareAndSet只会返回成功或失败,CAS的常规使用示例:

public final int incrementAndGet() {
while(true) {
//获取当前值
int current = get();
//设置期望值
int next = current + 1;
//调用Native方法compareAndSet,执行CAS操作
if (compareAndSet(current, next))
//成功后才会返回期望值,否则无线循环
return next;
}
}

从底层来说CAS也是有排他锁,但是相对synchronized的排他时间短非常短,在多线程情况下性能会比较好。

CAS使用注意事项:

CAS当需要等待获取锁时会自旋等待(即while true),非常消耗CPU资源,所以避免在高频取锁的场景中使用CAS。

ReentrantLock底层原理AQS

ReentrantLock通过AQS(AbstractQueuedSynchronizer)实现。AQS底层是通过CLH双向队列实现。

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

它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。

AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。

不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:

  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
  • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。

ThreadLocal

通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。最常见的ThreadLocal使用场景为用来解决数据库连接、Session管理等。

内部结构

ThreadLocal提供四个方法

  1. public T get() { }
  2. public void set(T value) { }
  3. public void remove() { }
  4. protected T initialValue(){ }

内部是通过ThreadLocalMap实现

使用注意事项

1)脏数据

线程复用会造成脏数据。由于线程池会复用Thread对象,因此Thread类的成员变量threadLocals也会被复用。如果在线程的run()方法中不显式调用remove()清理与线程相关的ThreadLocal信息,并且下一个线程不调用set()设置初始值,就可能get()到上个线程设置的值

2)内存泄露

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏

大白话一点,ThreadLocalMap的key是弱引用,GC时会被回收掉,那么就有可能存在ThreadLocalMap的情况,这个Object就是泄露的对象。

其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。

解决办法

解决以上两个问题的办法很简单,就是在每次用完ThreadLocal后,及时调用remove()方法清理即可。

Java内存模型(JMM)

JMM(Java Memory Model)是一个抽象的概念,JMM是和多线程相关的,定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

Java内存模型中分为主内存和各线程的本地内存,共享变量存储于主内存中,各线程操作共享变量前先把变量复制一份副本到本地内存,线程对变量副本运算完后再刷新到主内存。

JMM三大特性:

  1. 可见性
  2. 原子性
  3. 有序性

JMM关于同步的规定:

  1. 线程解锁前,必须把共享变量的值刷新回主内存;
  2. 线程加锁前,必须读取主内存的最新值到自己的工作内存;
  3. 加锁解锁是同一把锁;

(区别概念“JVM内存结构”,JVM内存结构描述的是Java程序执行过程中,由JVM管理的不同数据区域,如虚拟机栈、Java堆,本地方法栈等, 各个区域有其特定的功能。)

如何保证变量的可见性

主要实现可见性的方式有三种:

  • volatile,注意一点 volatile不能保证操作的原子性
  • Synchronized,synchronized互斥锁对应的内存间交互操作为lock和unlock。在对一个变量进行unlock操作之前,必须把变量值同步回主内存。
  • final,被final关键字修饰的变量在构造器中一旦初始化完成,并且没有发生 this 逃逸(其他线程通过this引用访问到初始化了一半的对象),那么其他线程就能看见final字段的值。

内存屏障

大多数现代计算机为了提高性能而采取乱序执行,内存屏障是一个指令级别的同步点,有内存屏障的地方,会禁止指令重排序。

语义上,内存屏障之前的所有写操作都要写入内存;内存屏障之后的读操作都可以获得同步屏障之前的写操作的结果。因此,对于敏感的程序块,写操作之后、读操作之前可以插入内存屏障。

内存屏障用于解决可见性及有序性问题,内存屏障通过防止指令重排保证有序性,通过内存屏障前后的刷新主存保证可见性(可见安全)。

Volatile、Lock、synchronized、final都是通过内存屏障实现。

  • lock:解锁时,jvm会强制刷新cpu缓存,导致当前线程更改,对其他线程可见。
  • volatile:标记volatile的字段,每次读取都是直接读内存。
  • final:即时编译器在final写操作后,会插入内存屏障,来禁止重排序,保证可见性

Happen-Before原则

happen before的含义是指操作对后续的操作都是可见的,比如 A happen before B 的意思并不是说 A 操作发生在 B 操作之前,而是说 A 操作对于 B 操作一定是可见的。

happen before原则是JMM中重要的一个原则,主要用于明确有序性。

Java的happen before原则规定了八种规则,以明确有序性的满足条件,相反在这八种规则以外意味着不能确定其执行顺序。八种规则如下:

  • 程序次序规则:在一个线程内,按照代码执行,书写在前面的操作先行发生于书写在后面的操作。
  • 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  • 线程启动原则:Thread对象的start()方法先行发生于此线程的每一个动作
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()方法返回值手段检测到线程已经终止执行
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

如对于一段代码添加了synchronized字段,各个线程在执行这段代码时需要获取锁,那么符合"管理锁定规则",可以确保其执行顺序,所以代码段中的变量可以不添加volatile关键字。

线程池

创建线程池

Java中已经提供了创建线程池的一个类:Executor,我们创建时,一般使用它的子类:ThreadPoolExecutor.

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

  • corePoolSize就是线程池中的核心线程数量,这几个核心线程,即使在没有用的时候,也不会被回收。

  • maximumPoolSize就是线程池中可以容纳的最大线程的数量。

    很多人以为它的作用是这样的:”当线程池中的任务数超过 corePoolSize 后,线程池会继续创建线程,直到线程池中的线程数小于maximumPoolSize“,其实这种理解是完全错误的。它真正的作用是:当线程池中的线程数等于 corePoolSize 并且 workQueue 已满,这时就要看当前线程数是否大于 maximumPoolSize,如果小于maximumPoolSize 定义的值,则会继续创建线程去执行任务, 否则将会调用相应的任务拒绝策略来拒绝这个任务。

  • keepAliveTime,就是线程池中除了核心线程之外的其他的最长可以保留的时间。

    除了核心线程即使在无任务的情况下也不能被清除,其余的都是有存活时间的,keepAliveTime意思就是非核心线程可以保留的最长的空闲时间。

    当ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true时,keepAliveTime同样会作用于非核心线程。

  • util,就是计算这个时间的一个单位,共7种取值:

    • TimeUnit.DAYS; //天
    • TimeUnit.HOURS; //小时
    • TimeUnit.MINUTES; //分钟
    • TimeUnit.SECONDS; //秒
    • TimeUnit.MILLISECONDS; //毫秒
    • TimeUnit.MICROSECONDS; //微妙
    • TimeUnit.NANOSECONDS; //纳秒
  • workQueue,就是等待队列,任务可以储存在任务队列中等待被执行,执行的是FIFIO原则(先进先出)。

    • ArrayBlockingQueue   //基于数组的先进先出队列,此队列创建时必须指定大小;
    • LinkedBlockingQueue //基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
    • synchronousQueue  //这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
  • threadFactory,线程工厂,用来为线程池创建线程,当我们不指定线程工厂时,线程池内部会调用 Executors.defaultThreadFactory()创建默认的线程工厂,其后续创建的线程优先级都是 Thread.NORM_PRIORITY。如果我们指定线程工厂,我们可以对产生的线程进行一定的操作。

  • rejectHandler,拒绝执行策略

    • ThreadPoolExecutor.AbortPolicy: // 丢弃任务并抛出RejectedExecutionException异常。
    • ThreadPoolExecutor.DiscardPolicy: // 也是丢弃任务,但是不抛出异常。
    • ThreadPoolExecutor.DiscardOldestPolicy:// 丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
    • ThreadPoolExecutor.CallerRunsPolicy:// 由调用线程处理该任务

线程池的创建方式

java.util.concurrent包下的Executors提供四种线程池:

  • NewCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  • NewFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  • NewScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
  • NewSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 new ThreadPoolExecutor 实例的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。Executors 返回线程池对象的弊端如下:

  • FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM。
  • CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。

怎么设置CPU最佳线程数

最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目

线程等待时间(非CPU运行时间,比如IO)所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。

例如proxy代理应用的线程数量可以开到很大,因为本身不占用太多CPU运算。

例如解码等应用的线程数量只能与CPU核数相近,因为解码需要大量CPU运算。

线程状态

  • 新建状态(New): 线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
  • 就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
  • 运行状态(Running): 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
  • 阻塞状态(Blocked): 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
  • 等待阻塞 -- 通过调用线程的wait()方法,让线程等待某工作的完成。
  • 同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
  • 其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
  • 死亡状态(Dead): 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

yield()方法

yield()方法只是提出申请释放CPU资源,至于能否成功释放由JVM决定。由于这个特性,一般编程中用不到此方法,但在很多并发工具包中,yield()方法被使用,如AQS、ConcurrentHashMap、FutureTask等。

join()方法

thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。

比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。

例如在main中调用线程t的t.join,则会等待t方法执行完后再执行main的方法。

t.join();      // 调用join方法,等待线程t执行完毕
t.join(1000);  // 等待 t 线程,等待时间是1000毫秒。

notify/notifyAll()与wait()

wait()和notify()都是定义在Object类中,为什么如此设计。因为synchronized中的这把锁可以是任意对象,所以任意对象都可以调用wait()和notify(),并且只有同一把锁才能对线程进行操作,不同锁之间是不可以相互操作的,所以wait和notify属于Object。

wait、notify要放在sychronized同步块中,否则会抛出IllegalMonitorStateException。如果不在同步块中,调用this.wait()时当前线程都没有取得对象的锁,又谈何让对象通知线程释放锁、或者来竞争锁呢?如果确实不放到同步块中,则会产生 Lost-wake的问题,即丢失唤醒。

调用wait方法可以让当前线程进入等待唤醒状态,该线程会处于等待唤醒状态直到另一个线程调用了object对象的notify方法或者notifyAll方法。

notify()唤醒等待的线程,如果监视器种只有一个等待线程,使用notify()可以唤醒。但是如果有多条线程notify()是随机唤醒其中一条线程,与之对应的就是notifyAll()就是唤醒所有等待的线程。

线程间通信的几种方式

方式一:使用 volatile 关键字

大致意思就是多个线程同时监听一个变量,当这个变量发生变化的时候 ,线程能够感知并执行相应的业务。

方式二:使用Object类的wait() 和 notify() 方法

Object类提供了线程间通信的方法:wait()、notify()、notifyaAl(),它们是多线程通信的基础,而这种实现方式的思想自然是线程间通信。(注意必须作用于synchronized中。可以简单理解lock不能锁对象,而wait、notify是对象的方法,所以是要配合synchronized使用)

方式三:使用JUC工具类 CountDownLatch

CountDownLatch***基于AQS框架,相当于也是维护了一个线程间共享变量state

方式四:使用 ReentrantLock 结合 Condition

显然这种方式使用起来并不是很好,代码编写复杂,而且线程B在被A唤醒之后由于没有获取锁还是不能立即执行,也就是说,A在唤醒操作之后,并不释放锁。这种方法跟 Object 的 wait() 和 notify() 一样。

方式五:基本LockSupport实现线程间的阻塞和唤醒

LockSupport 是一种非常灵活的实现线程间阻塞和唤醒的工具,使用它不用关注是等待线程先进行还是唤醒线程先运行,但是得知道线程的名字。

如何控制多线程执行顺序

方式一 join方法

public static void main(String[] args) {
thread1.start();
thread1.join();
thred2.start();
thread2.join();
thread3.start();
}

join方法的底层是调用对象的wait方法,wait方法的意思是阻塞当前线程,而此处的当前线程并非指thread1子线程本身,而是调用thread1.join()的主线程。所以当在主线程调用thread1.join()时,主线程阻塞,等待thread1执行完毕后继续执行thread2的任务,实现顺序执行。

拓展:注意join方法是使调用者当前的线程阻塞,所以可以实现线程嵌套,如先创建threadA,然后在threadA中再运行threadB并调用threadB.join()方法时,是阻塞threadA,让threadB执行完毕。

方式二 Excutors.newSingleThreadExecutor()

ExecutorService executor = Excutors.newSingleThreadExecutor();
executor.execute(thread1);
executor.execute(thread2);
executor.execute(thread3);

newSingleThreadExecutor是单线程运行无限队列的线程池,所以每个时间段只有一个线程可以运行,而后续加入的线程将进入队列排队,从而实现顺序执行。

并行与并发的区别

理解一

并发是对需求侧的描述,并行才是对实现侧的描述,这两根本不是同一个范畴的东西,更不可能是互斥的关系。

举个栗子:

每天中午12:00一大波人来到食堂门口,这是并发访问(需求场景)。

然后食堂开了12个打菜窗口给来吃饭的人打菜,这是并行处理(实现方式)。

即使开了12个窗口,也不能同时满足几千人,所以大家要排队(实现方式)。

所以正确的描述上述场景的句子应该是:“食堂每天中午会收到大量并发访问的请求,于是食堂通过开12个窗口的方式并行地处理这些请求,即便如此,仍然无法同时满足所有的请求,所以食堂仍然要求大家排队等待”。

你看,不管是并发,还是并行,还是排队,在上述场景里是同时存在的,其实并不互斥。

理解二

并行与并发不是一个维度的概念,并行是指多个节点能同时进行的能力或场景,并发是指在一个节点中同时发生的场景。

例如在互联网架构中,一个服务可以部署多个节点的集群,同一时刻每个服务都在处理任务,这是并行的状态。如果有大量请求落到一个节点中,则该节点会出现并发场景。

协程与线程的区别

线程

线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。

协程

协程是一种用户态的轻量级线程,协程的调度完全由用户控制,一个进程可轻松创建数十万计的协程。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

(在python的爬虫、go语言中使用协程的频率较高)


[目录]《Java八股文纯享版——目录》

[上一篇]《Java八股文纯享版——篇①:Java基础》


Java八股文纯享版——篇②:并发编程的更多相关文章

  1. Java八股文纯享版——篇①:Java基础

    注: 1.笔记为个人归纳整理,尽力保证准确性,如有错误,恳请指正 2.写文不易,转载请注明出处 3.本文首发地址 https://blog.leapmie.com/archives/b8fe0da9/ ...

  2. Java 面试知识点解析(二)——高并发编程篇

    前言: 在遨游了一番 Java Web 的世界之后,发现了自己的一些缺失,所以就着一篇深度好文:知名互联网公司校招 Java 开发岗面试知识点解析 ,来好好的对 Java 知识点进行复习和学习一番,大 ...

  3. Java多线程学习(七)并发编程中一些问题

    本节思维导图: 关注微信公众号:"Java面试通关手册" 回复"Java多线程"获取思维导图源文件和思维导图软件. 多线程就一定好吗?快吗?? 并发编程的目的就 ...

  4. (转)《深入理解java虚拟机》学习笔记10——并发编程(二)

    Java的并发编程是依赖虚拟机内存模型的三个特性实现的: (1).原子性(Atomicity): 原子性是指不可再分的最小操作指令,即单条机器指令,原子性操作任意时刻只能有一个线程,因此是线程安全的. ...

  5. Python之路(第三十八篇) 并发编程:进程同步锁/互斥锁、信号量、事件、队列、生产者消费者模型

    一.进程锁(同步锁/互斥锁) 进程之间数据不共享,但是共享同一套文件系统,所以访问同一个文件,或同一个打印终端,是没有问题的, 而共享带来的是竞争,竞争带来的结果就是错乱,如何控制,就是加锁处理. 例 ...

  6. JAVA工程师面试题【来自并发编程网】

    基础题: Java线程的状态 进程和线程的区别,进程间如何通讯,线程间如何通讯 HashMap的数据结构是什么?如何实现的.和HashTable,ConcurrentHashMap的区别 Cookie ...

  7. Python之路(第三十五篇) 并发编程:操作系统的发展史、操作系统的作用

    一.操作系统发展史 第一阶段:手工操作 —— 真空管和穿孔卡片 ​ 第一代之前人类是想用机械取代人力,第一代计算机的产生是计算机由机械时代进入电子时代的标志,从Babbage失败之后一直到第二次世界大 ...

  8. (转)《深入理解java虚拟机》学习笔记9——并发编程(一)

    随着多核CPU的高速发展,为了充分利用硬件的计算资源,操作系统的并发多任务功能正变得越来越重要,但是CPU在进行计算时,还需要从内存读取输出,并将计算结果存放到内存中,然而由于CPU的运算速度比内存高 ...

  9. Python之路(第三十七篇)并发编程:进程、multiprocess模块、创建进程方式、join()、守护进程

    一.在python程序中的进程操作 之前已经了解了很多进程相关的理论知识,了解进程是什么应该不再困难了,运行中的程序就是一个进程.所有的进程都是通过它的父进程来创建的.因此,运行起来的python程序 ...

随机推荐

  1. UiPath文本操作Get OCR Text的介绍和使用

    一.Get OCR Text操作的介绍 使用OCR屏幕抓取方法从指示的UI元素或图像中提取字符串及其信息.执行屏幕抓取操作时,还可以自动生成此活动以及容器.默认情况下,使用Google OCR引擎. ...

  2. MOEAD实现、基于分解的多目标进化、 切比雪夫方法-(python完整代码)

    确定某点附近的点 答:每个解对应的是一组权重,即子问题,红点附近的四个点,也就是它的邻居怎么确定呢?由权重来确定,算法初始化阶段就确定了每个权重对应的邻居,也就是每个子问题的邻居子问题.权重的邻居通过 ...

  3. Linux的文件路径和访问文件相关命令

    Linux的绝对和相对路径 绝地路径 绝对路径:以根作为起来的路径 相对路径 相对路径:以当前位置作为起点 文件操作命令 显示当前工作目录: pwd命令 pwd:显示文件所在的路径 基名:basena ...

  4. web文本划线的极简实现

    开篇 文本划线是目前逐渐流行的一个功能,不管你是小说阅读网站,还是卖教程的的网站,一般都会有记笔记或者评论的功能,传统的做法都是在文章底部加一个评论区,优点是简单,统一,缺点是不方便对文章的某一段或一 ...

  5. POI导出复杂Excel,合并单元格(2)

    /** * 导出excel (HSSFWorkbook) */ @GetMapping("/testExport") public void testExport1(HttpSer ...

  6. TypeScript 接口继承

    1.TypeScript 接口继承 和类一样,接口也可以通过关键字 extents 相互继承.接口继承,分为:单继承和多继承,即继承多个接口.另外,接口也可以继承类,它会继承类的成员,但不包括具体的实 ...

  7. npm相关知识整理

    语义化版本 major: 重大变化,不兼容老版本 minor: 新增功能,兼容老版本 patch: 修复bug,兼容老版本 依赖版本号 * 匹配最新版本的依赖 ^ 匹配最近的大版本依赖,比如^1.2. ...

  8. 输入一个url全过程详解

    1. 用户在浏览器中输入url,浏览器接收到url. 2.浏览器接收到这个url之后,会根据这个url会先查看缓存,如果有缓存且没有过期的话直接提供给客户端,完成页面渲染. 3.否则浏览器就会通过DN ...

  9. 21条最佳实践,全面保障 GitHub 使用安全

    GitHub 是开发人员工作流程中不可或缺的一部分.无论你去哪个企业或开发团队,GitHub 都以某种形式存在.它被超过8300万开发人员,400万个组织和托管超过2亿个存储库使用.GitHub 是世 ...

  10. 【一本通提高博弈论】[ZJOI2009]取石子游戏

    [ZJOI2009]取石子游戏 题目描述 在研究过 Nim 游戏及各种变种之后,Orez 又发现了一种全新的取石子游戏,这个游戏是这样的: 有 n n n 堆石子,将这 n n n 堆石子摆成一排.游 ...