关于java:Java并发编程八股文

42次阅读

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

Java 并发

大家好,我是大彬。最近在面试,看了很多面经,将常见的 Java 并发编程常见面试题总结了一下,如果对你有帮忙,能够 珍藏和点赞 后续还会持续更新新的面试题目哦!

文章目录如下:

首先给大家分享一个 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

线程池的类型有哪些?实用场景?

常见的线程池有 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))。

实用场景:实用于解决 CPU 密集型的工作,确保 CPU 在长期被工作线程应用的状况下,尽可能的少的调配线程,即实用执行长期的工作。须要留神的是,FixedThreadPool 不会回绝工作,在工作比拟多的时候会导致 OOM。

SingleThreadExecutor

只有一个线程的线程池。

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

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

实用场景:实用于串行执行工作的场景,一个工作一个工作地执行。在工作比拟多的时候也是会导致 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())。

实用场景:周期性执行工作的场景,须要限度线程数量的场景。

过程线程

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

线程的生命周期

初始(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();}

创立线程有哪几种形式?

  • 通过扩大 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("大彬");
    }
}

什么是线程死锁?

多个线程同时被阻塞,它们中的一个或者全副都在期待某个资源被开释。因为线程被无限期地阻塞,因而程序不可能失常终止。

如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会相互期待而进入死锁状态。

上面通过例子阐明线程死锁,代码来自并发编程之美。

public class DeadLockDemo {private static Object resource1 = new Object();// 资源 1
    private static Object resource2 = new Object();// 资源 2

    public static void main(String[] args) {new Thread(() -> {synchronized (resource1) {System.out.println(Thread.currentThread() + "get resource1");
                try {Thread.sleep(1000);
                } catch (InterruptedException e) {e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程 1").start();

        new Thread(() -> {synchronized (resource2) {System.out.println(Thread.currentThread() + "get resource2");
                try {Thread.sleep(1000);
                } catch (InterruptedException e) {e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) {System.out.println(Thread.currentThread() + "get resource1");
                }
            }
        }, "线程 2").start();}
}

代码输入如下:

Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1

线程 A 通过 synchronized (resource1) 取得 resource1 的监视器锁,而后通过 Thread.sleep(1000); 让线程 A 休眠 1s 为的是让线程 B 失去执行而后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠完结了都开始希图申请获取对方的资源,而后这两个线程就会陷入相互期待的状态,这也就产生了死锁。

线程死锁怎么产生?怎么防止?

死锁产生的四个必要条件

  • 互斥:一个资源每次只能被一个过程应用(资源独立)
  • 申请与放弃:一个过程因申请资源而阻塞时,对已取得的资源放弃不放(不开释锁)
  • 不剥夺:过程已取得的资源,在未应用之前,不能强行剥夺(争夺资源)
  • 循环期待:若干过程之间造成一种头尾相接的循环期待的资源敞开(死循环)

防止死锁的办法:

  • 第一个条件 “ 互斥 ” 是不能毁坏的,因为加锁就是为了保障互斥
  • 一次性申请所有的资源,毁坏 “ 占有且期待 ” 条件
  • 占有局部资源的线程进一步申请其余资源时,如果申请不到,被动开释它占有的资源,毁坏 “ 不可抢占 ” 条件
  • 按序申请资源,毁坏 “ 循环期待 ” 条件

线程 run 和 start 的区别?

调用 start() 办法是用来启动线程的,轮到该线程执行时,会主动调用 run();间接调用 run() 办法,无奈达到启动多线程的目标,相当于主线程线性执行 Thread 对象的 run() 办法。
一个线程对线的 start() 办法只能调用一次,屡次调用会抛出 java.lang.IllegalThreadStateException 异样;run() 办法没有限度。

线程都有哪些办法?

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 办法

volatile 底层原理

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

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

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

volatile 关键字的两个作用:

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

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

AQS 原理

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

AQS 应用一个 volatile 的 int 类型的成员变量 state 来示意同步状态,通过 CAS 批改同步状态的值。当线程调用 lock 办法时,如果 state=0,阐明没有任何线程占有共享资源的锁,能够取得锁并将 state=1。如果 state=1,则阐明有线程目前正在应用共享变量,其余线程必须退出同步队列进行期待。

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

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

synchronized 的用法有哪些?

  1. 润饰一般办法:作用于以后对象实例,进入同步代码前要取得以后对象实例的锁
  2. 润饰静态方法:作用于以后类,进入同步代码前要取得以后类对象的锁,synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁
  3. 润饰代码块:指定加锁对象,对给定对象加锁,进入同步代码库前要取得给定对象的锁

Synchronized 的作用有哪些?

原子性:确保线程互斥的拜访同步代码;
可见性:保障共享变量的批改可能及时可见,其实是通过 Java 内存模型中的“对一个变量 unlock 操作之前,必须要同步到主内存中;如果对一个变量进行 lock 操作,则将会清空工作内存中此变量的值,在执行引擎应用此变量前,须要从新从主内存中 load 操作或 assign 操作初始化变量值”来保障的;
有序性:无效解决重排序问题,即“一个 unlock 操作后行产生 (happen-before) 于前面对同一个锁的 lock 操作”。

synchronized 底层实现原理?

synchronized 同步代码块的实现是通过 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始地位,monitorexit 指令则指明同步代码块的完结地位。当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor 对象存在于每个 Java 对象的对象头中,synchronized 锁便是通过这种形式获取锁的,也是为什么 Java 中任意对象能够作为锁的起因) 的持有权。

其外部蕴含一个计数器,当计数器为 0 则能够胜利获取,获取后将锁计数器设为 1 也就是加 1。相应的在 执行 monitorexit 指令后,将锁计数器设为 0
,表明锁被开释。如果获取对象锁失败,那以后线程就要阻塞期待,直到锁被另外一个线程开释为止

synchronized 润饰的办法并没有 monitorenter 指令和 monitorexit 指令,获得代之的的确是 ACC_SYNCHRONIZED 标识,该标识指明了该办法是一个同步办法,JVM 通过该 ACC_SYNCHRONIZED 拜访标记来分别一个办法是否申明为同步办法,从而执行相应的同步调用。

ReentrantLock 是如何实现可重入性的?

ReentrantLock 外部自定义了同步器 Sync,在加锁的时候通过 CAS 算法,将线程对象放到一个双向链表中,每次获取锁的时候,查看以后保护的那个线程 ID 和以后申请的线程 ID 是否 统一,如果统一,同步状态加 1,示意锁被以后线程获取了屡次。

ReentrantLock 和 synchronized 区别

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

wait()和 sleep()的区别

相同点:

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

不同点:

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

wait(),notify()和 suspend(),resume()之间的区别

  • wait() 使得线程进入阻塞期待状态,并且开释锁
  • notify()唤醒一个处于期待状态的线程,它个别跟 wait()办法配套应用。
  • suspend()使得线程进入阻塞状态,并且不会主动复原,必须对应的 resume() 被调用,能力使得线程从新进入可执行状态。suspend()办法很容易引起死锁问题。
  • resume()办法跟 suspend()办法配套应用。

suspend()不倡议应用 ,suspend() 办法在调用后,线程不会开释曾经占有的资 源(比方锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。

Runnable 和 Callable 有什么区别?

  • Callable 接口办法是 call(),Runnable 的办法是 run();
  • Callable 接口 call 办法有返回值,反对泛型,Runnable 接口 run 办法无返回值。
  • Callable 接口 call()办法容许抛出异样;而 Runnable 接口 run()办法不能持续上抛异样;

volatile 和 synchronized 的区别是什么?

  1. volatile 只能应用在变量上;而 synchronized 能够在类,变量,办法和代码块上。
  2. volatile 至保障可见性;synchronized 保障原子性与可见性。
  3. volatile 禁用指令重排序;synchronized 不会。
  4. volatile 不会造成阻塞;synchronized 会。

线程执行程序怎么管制?

假如有 T1、T2、T3 三个线程,你怎么保障 T2 在 T1 执行完后执行,T3 在 T2 执行完后执行?

能够应用 join 办法 解决这个问题。比方在线程 A 中,调用线程 B 的 join 办法示意的意思就是:A 期待 B 线程执行结束后(开释 CPU 执行权),在继续执行。

代码如下:

public class ThreadTest {public static void main(String[] args) {Thread spring = new Thread(new SeasonThreadTask("春天"));
        Thread summer = new Thread(new SeasonThreadTask("夏天"));
        Thread autumn = new Thread(new SeasonThreadTask("秋天"));

        try
        {
            // 春天线程先启动
            spring.start();
            // 主线程期待线程 spring 执行完,再往下执行
            spring.join();
            // 夏天线程再启动
            summer.start();
            // 主线程期待线程 summer 执行完,再往下执行
            summer.join();
            // 秋天线程最初启动
            autumn.start();
            // 主线程期待线程 autumn 执行完,再往下执行
            autumn.join();} catch (InterruptedException e)
        {e.printStackTrace();
        }
    }
}

class SeasonThreadTask implements Runnable{

    private String name;

    public SeasonThreadTask(String name){this.name = name;}

    @Override
    public void run() {for (int i = 1; i <4; i++) {System.out.println(this.name + "来了:" + i + "次");
            try {Thread.sleep(100);
            } catch (InterruptedException e) {e.printStackTrace();
            }
        }
    }
}

运行后果:

春天来了: 1 次
春天来了: 2 次
春天来了: 3 次
夏天来了: 1 次
夏天来了: 2 次
夏天来了: 3 次
秋天来了: 1 次
秋天来了: 2 次
秋天来了: 3 次

乐观锁肯定就是好的吗?

乐观锁防止了乐观锁独占对象的景象,进步了并发性能,但它也有毛病:

  • 乐观锁只能保障一个共享变量的原子操作。如果多一个或几个变量,乐观锁将变得力不从心,但互斥锁能轻易解决,不论对象数量多少及对象颗粒度大小。
  • 长时间自旋可能导致开销大。如果 CAS 长时间不胜利而始终自旋,会 给 CPU 带来很大的开销。
  • ABA 问题。CAS 的核心思想是通过比对内存值与预期值是否一样而判 断内存值是否被改过,但这个判断逻辑不谨严,如果内存值原来是 A,起初被一条线程改为 B,最初又被改成了 A,则 CAS 认为此内存值并 没有产生扭转,但实际上是有被其余线程改过的,这种状况对依赖过程值的情景的运算后果影响很大。解决的思路是引入版本号,每次变量更新都把版本号加一。

守护线程是什么?

守护线程是运行在后盾的一种非凡过程。它独立于管制终端并且周期性地执行某种工作或期待解决某些 产生的事件。在 Java 中垃圾回收线程就是非凡的守护线程。

线程间通信形式

volatile

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

synchronized

保障线程对变量拜访的可见性和排他性。

期待告诉机制

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

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

ThreadLocal

线程本地变量。当应用 ThreadLocal 保护变量时,ThreadLocal 为每个应用该变量的线程提供独立的变量正本,所以每一个线程都能够独立地扭转本人的正本,而不会影响其它线程。

ThreadLocal 原理

每个线程都有一个 ThreadLocalMap(ThreadLocal 外部类),Map 中元素的键为 ThreadLocal,而值对应线程的变量正本。

调用 threadLocal.set()–> 调用 getMap(Thread)–> 返回以后线程的 ThreadLocalMap<ThreadLocal, value>–>map.set(this, value),this 是 ThreadLocal

public void set(T value) {Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {return t.threadLocals;}

void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);
}

调用 get()–> 调用 getMap(Thread)–> 返回以后线程的 ThreadLocalMap<ThreadLocal, value>–>map.getEntry(this),返回 value

    public T get() {Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {@SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();}

threadLocals 的类型 ThreadLocalMap 的键为 ThreadLocal 对象,因为每个线程中可有多个 threadLocal 变量,如 longLocal 和 stringLocal。

public class ThreadLocalDemo {ThreadLocal<Long> longLocal = new ThreadLocal<>();

    public void set() {longLocal.set(Thread.currentThread().getId());
    }
    public Long get() {return longLocal.get();
    }

    public static void main(String[] args) throws InterruptedException {ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();
        threadLocalDemo.set();
        System.out.println(threadLocalDemo.get());

        Thread thread = new Thread(() -> {threadLocalDemo.set();
            System.out.println(threadLocalDemo.get());
        }
        );

        thread.start();
        thread.join();

        System.out.println(threadLocalDemo.get());
    }
}

ThreadLocal 并不是用来解决共享资源的多线程拜访的问题,因为每个线程中的资源只是正本,并不共享。因而 ThreadLocal 适宜作为线程上下文变量,简化线程内传参。

ThreadLocal 内存透露的起因?

每个 Thread 都有⼀个 ThreadLocalMap 的外部属性,map 的 key 是 ThreaLocal,定义为弱援用,value 是强援用类型。GC 的时候会⾃动回收 key,而 value 的回收取决于 Thread 对象的生命周期。个别会通过线程池的形式复用 Thread 对象节俭资源,这也就导致了 Thread 对象的生命周期比拟长,这样便始终存在一条强援用链的关系:Thread –> ThreadLocalMap–>Entry–>Value,随着工作的执行,value 就有可能越来越多且无奈开释,最终导致内存透露。

解决⽅法:每次使⽤完 ThreadLocal 就调⽤它的 remove()⽅法,手动将对应的键值对删除,从⽽防止内存透露。

currentTime.set(System.currentTimeMillis());
result = joinPoint.proceed();
Log log = new Log("INFO",System.currentTimeMillis() - currentTime.get());
currentTime.remove();

ThreadLocal 应用场景有哪些?

ThreadLocal 实用场景:每个线程须要有本人独自的实例,且须要在多个办法中共享实例,即同时满足实例在线程间的隔离与办法间的共享。比方 Java web 利用中,每个线程有本人独自的 Session 实例,就能够应用 ThreadLocal 来实现。

锁的分类

偏心锁与非偏心锁

依照线程拜访程序获取对象锁。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?

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 存在的问题?

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 working
Thread-1starts working
Thread-2starts working
Thread-3starts working
task 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 machine
worker1 using the machine
worker2 using the machine
worker2 finished the task
worker0 finished the task
worker3 using the machine
worker4 using the machine
worker1 finished the task
worker6 using the machine
worker4 finished the task
worker3 finished the task
worker6 finished the task
worker5 using the machine
worker5 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 标记与援用关联起来

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

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

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

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

Java 并发

大家好,我是大彬。最近在面试,看了很多面经,将常见的 Java 并发编程常见面试题总结了一下,如果对你有帮忙,能够 珍藏和点赞 后续还会持续更新新的面试题目哦!

文章目录如下:

线程池

线程池:一个治理线程的池子。

为什么应用线程池?

  • 升高资源耗费。通过反复利用已创立的线程升高线程创立和销毁造成的耗费。
  • 进步响应速度。当工作达到时,工作能够不须要的等到线程创立就能立刻执行。
  • 进步线程的可管理性。对立治理线程,防止零碎创立大量同类线程而导致耗费完内存。
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

线程池的类型有哪些?实用场景?

常见的线程池有 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))。

实用场景:实用于解决 CPU 密集型的工作,确保 CPU 在长期被工作线程应用的状况下,尽可能的少的调配线程,即实用执行长期的工作。须要留神的是,FixedThreadPool 不会回绝工作,在工作比拟多的时候会导致 OOM。

SingleThreadExecutor

只有一个线程的线程池。

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

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

实用场景:实用于串行执行工作的场景,一个工作一个工作地执行。在工作比拟多的时候也是会导致 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())。

实用场景:周期性执行工作的场景,须要限度线程数量的场景。

过程线程

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

线程的生命周期

初始(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();}

创立线程有哪几种形式?

  • 通过扩大 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("大彬");
    }
}

什么是线程死锁?

多个线程同时被阻塞,它们中的一个或者全副都在期待某个资源被开释。因为线程被无限期地阻塞,因而程序不可能失常终止。

如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会相互期待而进入死锁状态。

上面通过例子阐明线程死锁,代码来自并发编程之美。

public class DeadLockDemo {private static Object resource1 = new Object();// 资源 1
    private static Object resource2 = new Object();// 资源 2

    public static void main(String[] args) {new Thread(() -> {synchronized (resource1) {System.out.println(Thread.currentThread() + "get resource1");
                try {Thread.sleep(1000);
                } catch (InterruptedException e) {e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程 1").start();

        new Thread(() -> {synchronized (resource2) {System.out.println(Thread.currentThread() + "get resource2");
                try {Thread.sleep(1000);
                } catch (InterruptedException e) {e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) {System.out.println(Thread.currentThread() + "get resource1");
                }
            }
        }, "线程 2").start();}
}

代码输入如下:

Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1

线程 A 通过 synchronized (resource1) 取得 resource1 的监视器锁,而后通过 Thread.sleep(1000); 让线程 A 休眠 1s 为的是让线程 B 失去执行而后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠完结了都开始希图申请获取对方的资源,而后这两个线程就会陷入相互期待的状态,这也就产生了死锁。

线程死锁怎么产生?怎么防止?

死锁产生的四个必要条件

  • 互斥:一个资源每次只能被一个过程应用(资源独立)
  • 申请与放弃:一个过程因申请资源而阻塞时,对已取得的资源放弃不放(不开释锁)
  • 不剥夺:过程已取得的资源,在未应用之前,不能强行剥夺(争夺资源)
  • 循环期待:若干过程之间造成一种头尾相接的循环期待的资源敞开(死循环)

防止死锁的办法:

  • 第一个条件 “ 互斥 ” 是不能毁坏的,因为加锁就是为了保障互斥
  • 一次性申请所有的资源,毁坏 “ 占有且期待 ” 条件
  • 占有局部资源的线程进一步申请其余资源时,如果申请不到,被动开释它占有的资源,毁坏 “ 不可抢占 ” 条件
  • 按序申请资源,毁坏 “ 循环期待 ” 条件

线程 run 和 start 的区别?

调用 start() 办法是用来启动线程的,轮到该线程执行时,会主动调用 run();间接调用 run() 办法,无奈达到启动多线程的目标,相当于主线程线性执行 Thread 对象的 run() 办法。
一个线程对线的 start() 办法只能调用一次,屡次调用会抛出 java.lang.IllegalThreadStateException 异样;run() 办法没有限度。

线程都有哪些办法?

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 办法

volatile 底层原理

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

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

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

volatile 关键字的两个作用:

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

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

AQS 原理

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

AQS 应用一个 volatile 的 int 类型的成员变量 state 来示意同步状态,通过 CAS 批改同步状态的值。当线程调用 lock 办法时,如果 state=0,阐明没有任何线程占有共享资源的锁,能够取得锁并将 state=1。如果 state=1,则阐明有线程目前正在应用共享变量,其余线程必须退出同步队列进行期待。

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

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

synchronized 的用法有哪些?

  1. 润饰一般办法:作用于以后对象实例,进入同步代码前要取得以后对象实例的锁
  2. 润饰静态方法:作用于以后类,进入同步代码前要取得以后类对象的锁,synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁
  3. 润饰代码块:指定加锁对象,对给定对象加锁,进入同步代码库前要取得给定对象的锁

Synchronized 的作用有哪些?

原子性:确保线程互斥的拜访同步代码;
可见性:保障共享变量的批改可能及时可见,其实是通过 Java 内存模型中的“对一个变量 unlock 操作之前,必须要同步到主内存中;如果对一个变量进行 lock 操作,则将会清空工作内存中此变量的值,在执行引擎应用此变量前,须要从新从主内存中 load 操作或 assign 操作初始化变量值”来保障的;
有序性:无效解决重排序问题,即“一个 unlock 操作后行产生 (happen-before) 于前面对同一个锁的 lock 操作”。

synchronized 底层实现原理?

synchronized 同步代码块的实现是通过 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始地位,monitorexit 指令则指明同步代码块的完结地位。当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor 对象存在于每个 Java 对象的对象头中,synchronized 锁便是通过这种形式获取锁的,也是为什么 Java 中任意对象能够作为锁的起因) 的持有权。

其外部蕴含一个计数器,当计数器为 0 则能够胜利获取,获取后将锁计数器设为 1 也就是加 1。相应的在 执行 monitorexit 指令后,将锁计数器设为 0
,表明锁被开释。如果获取对象锁失败,那以后线程就要阻塞期待,直到锁被另外一个线程开释为止

synchronized 润饰的办法并没有 monitorenter 指令和 monitorexit 指令,获得代之的的确是 ACC_SYNCHRONIZED 标识,该标识指明了该办法是一个同步办法,JVM 通过该 ACC_SYNCHRONIZED 拜访标记来分别一个办法是否申明为同步办法,从而执行相应的同步调用。

ReentrantLock 是如何实现可重入性的?

ReentrantLock 外部自定义了同步器 Sync,在加锁的时候通过 CAS 算法,将线程对象放到一个双向链表中,每次获取锁的时候,查看以后保护的那个线程 ID 和以后申请的线程 ID 是否 统一,如果统一,同步状态加 1,示意锁被以后线程获取了屡次。

ReentrantLock 和 synchronized 区别

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

wait()和 sleep()的区别

相同点:

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

不同点:

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

wait(),notify()和 suspend(),resume()之间的区别

  • wait() 使得线程进入阻塞期待状态,并且开释锁
  • notify()唤醒一个处于期待状态的线程,它个别跟 wait()办法配套应用。
  • suspend()使得线程进入阻塞状态,并且不会主动复原,必须对应的 resume() 被调用,能力使得线程从新进入可执行状态。suspend()办法很容易引起死锁问题。
  • resume()办法跟 suspend()办法配套应用。

suspend()不倡议应用 ,suspend() 办法在调用后,线程不会开释曾经占有的资 源(比方锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。

Runnable 和 Callable 有什么区别?

  • Callable 接口办法是 call(),Runnable 的办法是 run();
  • Callable 接口 call 办法有返回值,反对泛型,Runnable 接口 run 办法无返回值。
  • Callable 接口 call()办法容许抛出异样;而 Runnable 接口 run()办法不能持续上抛异样;

volatile 和 synchronized 的区别是什么?

  1. volatile 只能应用在变量上;而 synchronized 能够在类,变量,办法和代码块上。
  2. volatile 至保障可见性;synchronized 保障原子性与可见性。
  3. volatile 禁用指令重排序;synchronized 不会。
  4. volatile 不会造成阻塞;synchronized 会。

线程执行程序怎么管制?

假如有 T1、T2、T3 三个线程,你怎么保障 T2 在 T1 执行完后执行,T3 在 T2 执行完后执行?

能够应用 join 办法 解决这个问题。比方在线程 A 中,调用线程 B 的 join 办法示意的意思就是:A 期待 B 线程执行结束后(开释 CPU 执行权),在继续执行。

代码如下:

public class ThreadTest {public static void main(String[] args) {Thread spring = new Thread(new SeasonThreadTask("春天"));
        Thread summer = new Thread(new SeasonThreadTask("夏天"));
        Thread autumn = new Thread(new SeasonThreadTask("秋天"));

        try
        {
            // 春天线程先启动
            spring.start();
            // 主线程期待线程 spring 执行完,再往下执行
            spring.join();
            // 夏天线程再启动
            summer.start();
            // 主线程期待线程 summer 执行完,再往下执行
            summer.join();
            // 秋天线程最初启动
            autumn.start();
            // 主线程期待线程 autumn 执行完,再往下执行
            autumn.join();} catch (InterruptedException e)
        {e.printStackTrace();
        }
    }
}

class SeasonThreadTask implements Runnable{

    private String name;

    public SeasonThreadTask(String name){this.name = name;}

    @Override
    public void run() {for (int i = 1; i <4; i++) {System.out.println(this.name + "来了:" + i + "次");
            try {Thread.sleep(100);
            } catch (InterruptedException e) {e.printStackTrace();
            }
        }
    }
}

运行后果:

春天来了: 1 次
春天来了: 2 次
春天来了: 3 次
夏天来了: 1 次
夏天来了: 2 次
夏天来了: 3 次
秋天来了: 1 次
秋天来了: 2 次
秋天来了: 3 次

乐观锁肯定就是好的吗?

乐观锁防止了乐观锁独占对象的景象,进步了并发性能,但它也有毛病:

  • 乐观锁只能保障一个共享变量的原子操作。如果多一个或几个变量,乐观锁将变得力不从心,但互斥锁能轻易解决,不论对象数量多少及对象颗粒度大小。
  • 长时间自旋可能导致开销大。如果 CAS 长时间不胜利而始终自旋,会 给 CPU 带来很大的开销。
  • ABA 问题。CAS 的核心思想是通过比对内存值与预期值是否一样而判 断内存值是否被改过,但这个判断逻辑不谨严,如果内存值原来是 A,起初被一条线程改为 B,最初又被改成了 A,则 CAS 认为此内存值并 没有产生扭转,但实际上是有被其余线程改过的,这种状况对依赖过程值的情景的运算后果影响很大。解决的思路是引入版本号,每次变量更新都把版本号加一。

守护线程是什么?

守护线程是运行在后盾的一种非凡过程。它独立于管制终端并且周期性地执行某种工作或期待解决某些 产生的事件。在 Java 中垃圾回收线程就是非凡的守护线程。

线程间通信形式

volatile

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

synchronized

保障线程对变量拜访的可见性和排他性。

期待告诉机制

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

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

ThreadLocal

线程本地变量。当应用 ThreadLocal 保护变量时,ThreadLocal 为每个应用该变量的线程提供独立的变量正本,所以每一个线程都能够独立地扭转本人的正本,而不会影响其它线程。

ThreadLocal 原理

每个线程都有一个 ThreadLocalMap(ThreadLocal 外部类),Map 中元素的键为 ThreadLocal,而值对应线程的变量正本。

调用 threadLocal.set()–> 调用 getMap(Thread)–> 返回以后线程的 ThreadLocalMap<ThreadLocal, value>–>map.set(this, value),this 是 ThreadLocal

public void set(T value) {Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {return t.threadLocals;}

void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);
}

调用 get()–> 调用 getMap(Thread)–> 返回以后线程的 ThreadLocalMap<ThreadLocal, value>–>map.getEntry(this),返回 value

    public T get() {Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {@SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();}

threadLocals 的类型 ThreadLocalMap 的键为 ThreadLocal 对象,因为每个线程中可有多个 threadLocal 变量,如 longLocal 和 stringLocal。

public class ThreadLocalDemo {ThreadLocal<Long> longLocal = new ThreadLocal<>();

    public void set() {longLocal.set(Thread.currentThread().getId());
    }
    public Long get() {return longLocal.get();
    }

    public static void main(String[] args) throws InterruptedException {ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();
        threadLocalDemo.set();
        System.out.println(threadLocalDemo.get());

        Thread thread = new Thread(() -> {threadLocalDemo.set();
            System.out.println(threadLocalDemo.get());
        }
        );

        thread.start();
        thread.join();

        System.out.println(threadLocalDemo.get());
    }
}

ThreadLocal 并不是用来解决共享资源的多线程拜访的问题,因为每个线程中的资源只是正本,并不共享。因而 ThreadLocal 适宜作为线程上下文变量,简化线程内传参。

ThreadLocal 内存透露的起因?

每个 Thread 都有⼀个 ThreadLocalMap 的外部属性,map 的 key 是 ThreaLocal,定义为弱援用,value 是强援用类型。GC 的时候会⾃动回收 key,而 value 的回收取决于 Thread 对象的生命周期。个别会通过线程池的形式复用 Thread 对象节俭资源,这也就导致了 Thread 对象的生命周期比拟长,这样便始终存在一条强援用链的关系:Thread –> ThreadLocalMap–>Entry–>Value,随着工作的执行,value 就有可能越来越多且无奈开释,最终导致内存透露。

解决⽅法:每次使⽤完 ThreadLocal 就调⽤它的 remove()⽅法,手动将对应的键值对删除,从⽽防止内存透露。

currentTime.set(System.currentTimeMillis());
result = joinPoint.proceed();
Log log = new Log("INFO",System.currentTimeMillis() - currentTime.get());
currentTime.remove();

ThreadLocal 应用场景有哪些?

ThreadLocal 实用场景:每个线程须要有本人独自的实例,且须要在多个办法中共享实例,即同时满足实例在线程间的隔离与办法间的共享。比方 Java web 利用中,每个线程有本人独自的 Session 实例,就能够应用 ThreadLocal 来实现。

锁的分类

偏心锁与非偏心锁

依照线程拜访程序获取对象锁。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?

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 存在的问题?

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 working
Thread-1starts working
Thread-2starts working
Thread-3starts working
task 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 machine
worker1 using the machine
worker2 using the machine
worker2 finished the task
worker0 finished the task
worker3 using the machine
worker4 using the machine
worker1 finished the task
worker6 using the machine
worker4 finished the task
worker3 finished the task
worker6 finished the task
worker5 using the machine
worker5 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 标记与援用关联起来

小伙伴们感觉有用的话,点赞加珍藏,反对一下!

正文完
 0