java并发编程

基础知识

并发编程的优缺点

为什么要使用并发编程(并发编程的优点)

  • 充分利用多核CPU的计算能力:通过并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升

  • 方便进行业务拆分,提升系统并发能力和性能:在特殊的业务场景下,先天的就适合于并发编程。现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分 。

并发编程有什么缺点

  • 并发编程的目的就是为了能提高程序的执行效率,提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如**:内存泄漏、上下文切换、线程安全、死锁**等问题。

并发编程三要素是什么?在 Java 程序中怎么保证多线程的运行安全?

  • 原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。

  • 可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。(synchronized,volatile)

  • 有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)

出现线程安全问题的原因:

线程切换带来的原子性问题

缓存导致的可见性问题

编译优化带来的有序性问题
解决办法:

JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题
synchronized、volatile、LOCK,可以解决可见性问题
Happens-Before 规则可以解决有序性问题

线程和进程区别

进程

一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程。

线程

进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。

什么是上下文切换?

多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。

概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。

上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。

Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。

守护线程和用户线程有什么区别呢?

  • 用户 (User) 线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程
  • 守护 (Daemon) 线程:运行在后台,为其他前台线程服务。也可以说守护线程是 JVM 中非守护线程的 “佣人”。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作

main 函数所在的线程就是一个用户线程啊,main 函数启动的同时在 JVM 内部同时还启动了好多守护线程,比如垃圾回收线程。

比较明显的区别之一是用户线程结束,JVM 退出,不管这个时候有没有守护线程运行。而守护线程不会影响 JVM 的退出。

注意事项:
	* setDaemon(true)必须在start()方法前执行,否则会抛出 IllegalThreadStateException 异常
	* 在守护线程中产生的新线程也是守护线程
	* 不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算逻辑
	* 守护 (Daemon) 线程中不能依靠 finally 块的内容来确保执行关闭或清理资源的逻辑。因为我们上面也说过了一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作,所以守护 (Daemon) 线程中的 finally 语句块可能无法被执行

什么是线程死锁
死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去
形成死锁的四个必要条件是什么

* 互斥条件:线程(进程)对于所分配到的资源具有排它性,即一个资源只能被一个线程(进程)占用,直到被该线程(进程)释放
*请求与保持条件:一个线程(进程)因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
* 不剥夺条件:线程(进程)已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
* 循环等待条件:当发生死锁时,所等待的线程(进程)必定会形成一个环路(类似于死循环),造成永久阻塞

如何避免线程死锁

破坏互斥条件
这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。

破坏请求与保持条件
一次性申请所有的资源。

破坏不剥夺条件
占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。

破坏循环等待条件
靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

创建线程的四种方式

继承 Thread 类;
实现 Runnable 接口;
实现 Callable 接口;
使用 Executors 工具类创建线程池

说一下 runnable 和 callable 有什么区别?

相同点
都是接口
都可以编写多线程程序
都采用Thread.start()启动线程

主要区别
Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息
注:Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。

线程的 run()和 start()有什么区别?

  • start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。

为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

  • 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。

什么是 Callable 和 Future?

Callable 接口类似于 Runnable,从名字就可以看出来了,但是 Runnable 不会返回结果,并且无法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值。

Future 接口表示异步任务,是一个可能还没有完成的异步任务的结果。所以说 Callable用于产生结果,Future 用于获取结果

什么是 FutureTask

FutureTask 表示一个异步运算的任务。FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是Runnable 接口的实现类,所以 FutureTask 也可以放入线程池中。

线程的状态和基本操作
在这里插入图片描述

Java 中用到的线程调度算法是什么?

有两种调度模型:分时调度模型和抢占式调度模型。

分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的 CPU 的时间片这个也比较好理解。

Java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU

请说出与线程同步以及线程调度相关的方法。

(1) wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;

(2)sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 InterruptedException 异常;

(3)notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关;

(4)notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;

sleep() 和 wait() 有什么区别?

两者都可以暂停线程的执行

类的不同:sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。
是否释放锁:sleep() 不释放锁;wait() 释放锁。
用途不同:Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
用法不同:wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。

为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在 Object 类里?

  • 任何对象都可以作为锁,并且 wait(),notify()等方法用于等待对象的锁或者唤醒线程,在 Java 的线程中并没有可供任何对象使用的锁,所以任意对象调用方法一定定义在Object类中

Thread 类中的 yield 方法有什么作用?
使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。

当前线程到了就绪状态,那么接下来哪个线程会从就绪状态变成执行状态呢?可能是当前线程,也可能是其他线程,看系统的分配了

线程的 sleep()方法和 yield()方法有什么区别?

(1) sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;

(2) 线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状态;

(3)sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异常;

(4)sleep()方法比 yield()方法(跟操作系统 CPU 调度相关)具有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行。

Java 中 interrupted 和 isInterrupted 方法的区别?

interrupt:用于中断线程。调用该方法的线程的状态为将被置为”中断”状态。

注意:线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException 的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。

interrupted:是静态方法,查看当前中断信号是true还是false并且清除中断信号。如果一个线程被中断了,第一次调用 interrupted 则返回 true,第二次和后面的就返回 false 了。

isInterrupted:查看当前中断信号是true还是false

notify() 和 notifyAll() 有什么区别?

notifyAll() 会唤醒所有的线程,notify() 只会唤醒一个线程。而 notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制

Java 如何实现多线程之间的通讯和协作?

Java中线程通信协作的最常见的两种方式:

一.syncrhoized加锁的线程的Object类的wait()/notify()/notifyAll()

二.ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll()

什么是线程同步和线程互斥,有哪几种实现方式?

实现线程同步的方法

同步代码方法:sychronized 关键字修饰的方法

同步代码块:sychronized 关键字修饰的代码块

使用特殊变量域volatile实现线程同步:volatile关键字为域变量的访问提供了一种免锁机制

使用重入锁实现线程同步:reentrantlock类是可冲入、互斥、实现了lock接口的锁他与sychronized方法具有相同的基本行为和语义

在监视器(Monitor)内部,是如何做线程同步的?程序应该做哪种级别的同步?

java 还提供了显式监视器( Lock )和隐式监视器( synchronized )两种锁方案

什么叫线程安全?servlet 是线程安全吗?

线程安全是编程中的术语,指某个方法在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。

Servlet 不是线程安全的,servlet 是单实例多线程的,当多个线程同时访问同一个方法,是不能保证共享变量的线程安全性的。

Struts2 的 action 是多实例多线程的,是线程安全的,每个请求过来都会 new 一个新的 action 分配给这个请求,请求完成后销毁。

SpringMVC 的 Controller 是线程安全的吗?不是的,和 Servlet 类似的处理流程。

Struts2 好处是不用考虑线程安全问题;Servlet 和 SpringMVC 需要考虑线程安全问题,但是性能可以提升不用处理太多的 gc,可以使用 ThreadLocal 来处理多线程的问题。

在 Java 程序中怎么保证多线程的运行安全?

方法一:使用安全类,比如 java.util.concurrent 下的类,使用原子类AtomicInteger
方法二:使用自动锁 synchronized。
方法三:使用手动锁 Lock。

并发理论

Java内存模型

Java中垃圾回收有什么目的?什么时候进行垃圾回收?

  • 垃圾回收是在内存中存在没有引用的对象或超过作用域的对象时进行的。

  • 垃圾回收的目的是识别并且丢弃应用不再使用的对象来释放和重用资源。

如果对象的引用被置为null,垃圾收集器是否会立即释放对象占用的内存?

不会,在下一个垃圾回调周期中,这个对象将是被可回收的。

也就是说并不会立即被垃圾收集器立刻回收,而是在下一次垃圾回收时才会释放其占用的内存

finalize()方法什么时候被调用?析构函数(finalization)的目的是什么?

1)垃圾回收器(garbage colector)决定回收某对象时,就会运行该对象的finalize()方法;
finalize是Object类的一个方法,该方法在Object类中的声明protected void finalize() throws Throwable { }
在垃圾回收器执行时会调用被回收对象的finalize()方法,可以覆盖此方法来实现对其资源的回收。注意:一旦垃圾回收器准备释放对象占用的内存,将首先调用该对象的finalize()方法,并且下一次垃圾回收动作发生时,才真正回收对象占用的内存空间

2)GC本来就是内存回收了,应用还需要在finalization做什么呢? 答案是大部分时候,什么都不用做(也就是不需要重载)。只有在某些很特殊的情况下,比如你调用了一些native的方法(一般是C写的),可以要在finaliztion里去调用C的释放函数。

重排序与数据依赖性

为什么代码会重排序?

1)垃圾回收器(garbage colector)决定回收某对象时,就会运行该对象的finalize()方法;
finalize是Object类的一个方法,该方法在Object类中的声明protected void finalize() throws Throwable { }
在垃圾回收器执行时会调用被回收对象的finalize()方法,可以覆盖此方法来实现对其资源的回收。注意:一旦垃圾回收器准备释放对象占用的内存,将首先调用该对象的finalize()方法,并且下一次垃圾回收动作发生时,才真正回收对象占用的内存空间

2)GC本来就是内存回收了,应用还需要在finalization做什么呢? 答案是大部分时候,什么都不用做(也就是不需要重载)。只有在某些很特殊的情况下,比如你调用了一些native的方法(一般是C写的),可以要在finaliztion里去调用C的释放函数。

as-if-serial规则和happens-before规则的区别

as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。

as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。

as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。

说说自己是怎么使用 synchronized 关键字,在项目中用到了吗
双重校验锁实现对象单例(线程安全)

public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public static Singleton getUniqueInstance() {
       //先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if (uniqueInstance == null) {
            //类对象加锁
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。

uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:

为 uniqueInstance 分配内存空间
初始化 uniqueInstance
将 uniqueInstance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。

说一下 synchronized 底层实现原理?
在这里插入图片描述

可以看出在执行同步代码块之前之后都有一个monitor字样,其中前面的是monitorenter,后面的是离开monitorexit,不难想象一个线程也执行同步代码块,首先要获取锁,而获取锁的过程就是monitorenter ,在执行完代码块之后,要释放锁,释放锁就是执行monitorexit指令。

为什么会有两个monitorexit呢?

这个主要是防止在同步代码块中线程因异常退出,而锁没有得到释放,这必然会造成死锁(等待的线程永远获取不到锁)。因此最后一个monitorexit是保证在异常情况下,锁也可以得到释放,避免死锁。
仅有ACC_SYNCHRONIZED这么一个标志,该标记表明线程进入该方法时,需要monitorenter,退出该方法时需要monitorexit。

synchronized可重入的原理

重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁。底层原理维护一个计数器,当线程获取该锁时,计数器加一,再次获得该锁时继续加一,释放锁时,计数器减一,当计数器值为0时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁。

什么是自旋

很多 synchronized 里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然 synchronized 里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在 synchronized 的边界做忙循环,这就是自旋。如果做了多次循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略

多线程中 synchronized 锁升级的原理是什么?

synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。

锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗

线程 B 怎么知道线程 A 修改了变量

(1)volatile 修饰变量

(2)synchronized 修饰修改变量的方法

(3)wait/notify

(4)while 轮询

synchronized、volatile、CAS 比较

(1)synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。

(2)volatile 提供多线程共享变量可见性和禁止指令重排序优化。

(3)CAS 是基于冲突检测的乐观锁(非阻塞)

synchronized 和 Lock 有什么区别?

首先synchronized是Java内置关键字,在JVM层面,Lock是个Java类;
synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。

synchronized 和 ReentrantLock 区别是什么?

ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;
ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
ReentrantLock 只适用于代码块锁,而 synchronized 可以修饰类、方法、变量等。
二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的park 方法加锁,synchronized 操作的应该是对象头中 mark word

volatile

volatile 关键字的作用

对于可见性,Java 提供了 volatile 关键字来保证可见性和禁止指令重排。 volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性,详细的可以参见 java.util.concurrent.atomic 包下的类,比如 AtomicInteger。

volatile 常用于多线程环境下的单次操作(单次读或者单次写)。

Java 中能创建 volatile 数组吗?

能,Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。意思是,如果改变引用指向的数组,将会受到 volatile 的保护,但是如果多个线程同时改变数组的元素,volatile 标示符就不能起到之前的保护作用了。

volatile 修饰符的有过什么实践?
单例模式

是否 Lazy 初始化:是

是否多线程安全:是

实现难度:较复杂

描述:对于Double-Check这种可能出现的问题(当然这种概率已经非常小了,但毕竟还是有的嘛~),解决方案是:只需要给instance的声明加上volatile关键字即可volatile关键字的一个作用是禁止指令重排,把instance声明为volatile之后,对它的写操作就会有一个内存屏障(什么是内存屏障?),这样,在它的赋值完成之前,就不用会调用读操作。注意:volatile阻止的不是singleton = newSingleton()这句话内部[1-2-3]的指令重排,而是保证了在一个写操作([1-2-3])完成之前,不会调用读操作(if (instance == null))。

public class Singleton7 {

    private static volatile Singleton7 instance = null;

    private Singleton7() {}

    public static Singleton7 getInstance() {
        if (instance == null) {
            synchronized (Singleton7.class) {
                if (instance == null) {
                    instance = new Singleton7();
                }
            }
        }

        return instance;
    }

}

final

什么是不可变对象,它对写并发应用有什么帮助?

不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,反之即为可变对象(Mutable Objects)。

不可变对象的类即为不可变类(Immutable Class)。Java 平台类库中包含许多不可变类,如 String、基本类型的包装类、BigInteger 和 BigDecimal 等。

只有满足如下状态,一个对象才是不可变的;

它的状态不能在创建后再被修改;

所有域都是 final 类型;并且,它被正确创建(创建期间没有发生 this 引用的逸出)。

不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。

Lock体系

Java Concurrency API 中的 Lock 接口(Lock interface)是什么?对比同步它有什么优势?

Lock 接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。

它的优势有:

(1)可以使锁更公平

(2)可以使线程在等待锁的时候响应中断

(3)可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间

(4)可以在不同的范围,以不同的顺序获取和释放锁

整体上来说 Lock 是 synchronized 的扩展版,Lock 提供了无条件的、可轮询的(tryLock 方法)、定时的(tryLock 带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition 方法)锁操作。另外 Lock 的实现类基本都支持非公平锁(默认)和公平锁,synchronized 只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择。

乐观锁和悲观锁的理解及如何实现,有哪些实现方式?
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁。

乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁。在 Java中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。

乐观锁的实现方式:

1、使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。

2、java 中的 Compare and Swap 即 CAS ,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。 CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置 V 的值与预期原值 A 相匹配,那么处理器会自动将该位置值更新为新值 B。否则处理器不做任何操作。

什么是 CAS

CAS 是 compare and swap 的缩写,即我们所说的比较交换。

cas 是一种基于锁的操作,而且是乐观锁。在 java 中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加 version 来获取数据,性能较悲观锁有很大的提高。

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和 A 的值是一样的,那么就将内存里面的值更新成 B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a 线程获取地址里面的值被b 线程修改了,那么 a 线程需要自旋,到下次循环才有可能机会执行。

java.util.concurrent.atomic 包下的类大多是使用 CAS 操作来实现的(AtomicInteger,AtomicBoolean,AtomicLong)。

CAS(比较交换) 的会产生什么问题?

1、ABA 问题:
比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但可能存在潜藏的问题。从 Java1.5 开始 JDK 的 atomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。

2、循环时间长开销大:
对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。

3、只能保证一个共享变量的原子操作:
当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。

什么是死锁?
当线程 A 持有独占锁a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b,并尝试获取独占锁 a 的情况下,就会发生 AB 两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。
产生死锁的条件是什么?怎么防止死锁?
产生死锁的必要条件:

1、互斥条件:所谓互斥就是进程在某一时间内独占资源。

2、请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

3、不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。

4、循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之 一不满足,就不会发生死锁。

理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和 解除死锁。

防止死锁可以采用以下的方法:

尽量使用 tryLock(long timeout, TimeUnit unit)的方法(ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁。
尽量使用 Java. util. concurrent 并发类代替自己手写锁。
尽量降低锁的使用粒度,尽量不要几个功能用同一把锁。
尽量减少同步的代码块。

死锁与活锁的区别,死锁与饥饿的区别?
死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

活锁:任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。

活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,这就是所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。

饥饿:一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。

Java 中导致饥饿的原因:

1、高优先级线程吞噬所有的低优先级线程的 CPU 时间。

2、线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。

3、线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法),因为其他线程总是被持续地获得唤醒。

多线程锁的升级原理是什么?
共有4种状态,级别从低到高依次为:无状态锁,偏向锁,轻量级锁和重量级锁状态

AQS(AbstractQueuedSynchronizer)详解与源码分析

ReentrantLock(重入锁)实现原理与公平锁非公平锁区别
ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。

在java关键字synchronized隐式支持重入性,synchronized通过获取自增,释放自减的方式实现重入。与此同时,ReentrantLock还支持公平锁和非公平锁两种方式。那么,要想完完全全的弄懂ReentrantLock的话,主要也就是ReentrantLock同步语义的学习:1. 重入性的实现原理;2. 公平锁和非公平锁。

重入性的实现原理

要想支持重入性,就要解决两个问题:1. 在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功;2. 由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功。

ReentrantLock支持两种锁:公平锁和非公平锁。何谓公平性,是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求上的绝对时间顺序,满足FIFO。

并发容器

并发容器之ConcurrentHashMap详解(JDK1.8版本)与源码分析
什么是ConcurrentHashMap?
Java 中 ConcurrentHashMap 的并发度是什么?
什么是并发容器的实现?
Java 中的同步集合与并发集合有什么区别?
SynchronizedMap 和 ConcurrentHashMap 有什么区别?
SynchronizedMap 一次锁住整张表来保证线程安全,所以每次只能有一个线程来访为 map。

ConcurrentHashMap 使用分段锁来保证在多线程下的性能。

ConcurrentHashMap 中则是一次锁住一个桶。ConcurrentHashMap 默认将hash 表分为 16 个桶,诸如 get,put,remove 等常用操作只锁当前需要用到的桶。

这样,原来只能一个线程进入,现在却能同时有 16 个写线程执行,并发性能的提升是显而易见的。

另外 ConcurrentHashMap 使用了一种不同的迭代方式。在这种迭代方式中,当iterator 被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时 new 新的数据从而不影响原有的数据,iterator 完成后再将头指针替换为新的数据 ,这样 iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。
并发容器之CopyOnWriteArrayList详解
CopyOnWriteArrayList 是什么,可以用于什么应用场景?有哪些优缺点?

并发容器之ThreadLocal详解
ThreadLocal 是什么?有哪些使用场景?

什么是线程局部变量?

ThreadLocal内存泄漏分析与解决方案

ThreadLocal造成内存泄漏的原因?
ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法

ThreadLocal内存泄漏解决方案?
每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。
并发容器之BlockingQueue详解

线程池

Executors类创建四种常见线程池
什么是线程池?有哪几种创建方式?

(1)newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

(2)newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。如果希望在服务器上使用线程池,建议使用 newFixedThreadPool方法来创建线程池,这样能获得更好的性能。

(3) newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。

(4)newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

线程池有什么优点?

降低资源消耗:重用存在的线程,减少对象创建销毁的开销。

提高响应速度。可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。当任务到达时,任务可以不需要的等到线程创建就能立即执行。

提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

附加功能:提供定时执行、定期执行、单线程、并发数控制等功能。

线程池都有哪些状态?

RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。
SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。
STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。
TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。
TERMINATED:terminated()方法结束后,线程池的状态就会变成这个

在 Java 中 Executor 和 Executors 的区别?

Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。

Executor 接口对象能执行我们的线程任务。

ExecutorService 接口继承了 Executor 接口并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。

使用 ThreadPoolExecutor 可以创建自定义线程池。

Future 表示异步计算的结果,他提供了检查计算是否完成的方法,以等待计算的完成,并可以使用 get()方法获取计算的结果。

线程池中 submit() 和 execute() 方法有什么区别?

接收参数:execute()只能执行 Runnable 类型的任务。submit()可以执行 Runnable 和 Callable 类型的任务。

返回值:submit()方法可以返回持有计算结果的 Future 对象,而execute()没有

异常处理:submit()方便Exception处理

线程池之ThreadPoolExecutor详解
Executors和ThreaPoolExecutor创建线程池的区别

newFixedThreadPool 和 newSingleThreadExecutor:
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。

newCachedThreadPool 和 newScheduledThreadPool:
主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。

ThreaPoolExecutor创建线程池方式只有一种,就是走它的构造函数,参数自己指定

你知道怎么创建线程池吗?
ThreadPoolExecutor()
ThreadPoolExecutor构造函数重要参数分析

ThreadPoolExecutor 3 个最重要的参数:

corePoolSize :核心线程数,线程数定义了最小可以同时运行的线程数量。
maximumPoolSize :线程池中允许存在的工作线程的最大数量
workQueue:当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,任务就会被存放在队列中。
ThreadPoolExecutor其他常见参数:

keepAliveTime:线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
unit :keepAliveTime 参数的时间单位。
threadFactory:为线程池提供创建新线程的线程工厂
handler :线程池任务队列超过 maxinumPoolSize 之后的拒绝策略

ThreadPoolExecutor饱和策略

ThreadPoolExecutor 饱和策略定义:

如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,ThreadPoolTaskExecutor 定义一些策略:

1.ThreadPoolExecutor.AbortPolicy:抛出RejectedExecutionException来拒绝新任务的处理。
2.ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。
3.ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。
4.ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。

并发工具

在 Java 中 CycliBarriar 和 CountdownLatch 有什么区别?

CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;CountDownLatch强调一个线程等多个线程完成某件事情。CyclicBarrier是多个线程互等,等大家都完成,再携手共进。

调用CountDownLatch的countDown方法后,当前线程并不会阻塞,会继续往下执行;而调用CyclicBarrier的await方法,会阻塞当前线程,直到CyclicBarrier指定的线程全部都到达了指定点的时候,才能继续往下执行;

CountDownLatch方法比较少,操作比较简单,而CyclicBarrier提供的方法更多,比如能够通过getNumberWaiting(),isBroken()这些方法获取当前多个线程的状态,并且CyclicBarrier的构造方法可以传入barrierAction,指定当所有线程都到达时执行的业务功能;

CountDownLatch是不能复用的,而CyclicLatch是可以复用的。

并发工具之Semaphore与Exchanger
Semaphore 有什么作用

Semaphore 就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore有一个构造函数,可以传入一个 int 型整数 n,表示某段代码最多只有 n 个线程可以访问,如果超出了 n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果 Semaphore 构造函数中传入的 int 型整数 n=1,相当于变成了一个 synchronized 了。

Semaphore(信号量)-允许多个线程同时访问: synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。

什么是线程间交换数据的工具Exchanger
Exchanger是一个用于线程间协作的工具类,用于两个线程间交换数据。它提供了一个交换的同步点,在这个同步点两个线程能够交换数据。交换数据是通过exchange方法来实现的,如果一个线程先执行exchange方法,那么它会同步等待另一个线程也执行exchange方法,这个时候两个线程就都达到了同步点,两个线程就可以交换数据。

常用的并发工具类有哪些?

  • Semaphore(信号量)-允许多个线程同时访问: synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。
  • CountDownLatch(倒计时器): CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。
  • CyclicBarrier(循环栅栏): CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await()方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。

多线程

线程基础内容

进程的特点:动态性,并发性,独立性
线程Thread:
1.进程内部的一个执行单元,它是程序中一个单元的顺序控制 流程
2.线程又被称为轻量级进程(lightweight process)
3.如果在一个进程中同时运行了多个线程,用来完成不同的工作,则称为多线程
线程特点:
1.轻量级进程
2.独立调度的基本单位
3.可并发执行
4.共享进程资源
线程的创建
1.方式1:继承java.lang.Thread类,并覆盖run()方法
2.方式2:实现java.lang.Runnable接口,并实现run()方法
方法run()称为线程体
线程的启动
1.新建的线程不会自动开始运行,必须通过start()方法启动
2.不能直接调用run()来启动线程,这样run()将作为一个普通方法立即执行,执行完毕前其他线程无法并发执行

/*
 * 1.创建线程的第一种方式:继承Thread类,重写run()方法
 * 2.启动线程
 */
public class TestThread {

	public static void main(String[] args) throws InterruptedException, ExecutionException {
		//创建callable对象
		Callable<Integer> callable = new ThreeThread();
		//因为Thread中无法放入Callable对象引入了FutureTask
		FutureTask<Integer> task = new FutureTask<>(callable);
		Thread thread2 = new Thread(task);
		//启动线程
		thread2.start();
		System.out.println("线程是否结束"+task.isDone());
		//获取线程的返回结果
		Integer num = task.get();
		System.out.println(num);
		
		TortoiseThread tortoiseThread = new TortoiseThread("wg");
		// 修改名字:方式1:继承 thread,提供有参构造,创建对象时直接赋值
		// setName 方法修改名字
		tortoiseThread.setName("乌龟");
		// 启动线程start
		tortoiseThread.start();
		// Runnable 创建线程开启
		// Runnable personThread=new PersonThread();
		// 创建 线程对象
		// 匿名内部类
		/*
		 * Thread thread=new Thread(new Runnable() {
		 * 
		 * public void run() { while(true){
		 * System.out.println("加油"+Thread.currentThread
		 * ().getName()+",优先级:"+Thread.currentThread().getPriority()); try {
		 * Thread.sleep(1000); } catch (InterruptedException e) {
		 * e.printStackTrace(); } } } });
		 */
		Runnable runnable = new Runnable() {
			private int a;

			public void run() {
				while (true) {
					System.out.println("加油" + Thread.currentThread().getName()
							+ ",优先级:" + Thread.currentThread().getPriority());
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		};
		Thread thread = new Thread(runnable);
		thread.start();
		thread.setName("preson");
		// 目前常见的代码结构
		new Thread(new Runnable() {
			public void run() {
				while (true) {
					System.out.println("哈哈哈");
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}

			}
		}).start();
		while (true) {
			// currentThread(),获取当前线程对象
			// 主线程的名称叫main
			System.out.println("兔子领先了,加油!" + Thread.currentThread().getName()
					+ ",优先级:" + Thread.currentThread().getPriority());
			Thread.sleep(2000);
		}
	}
	
}

// 1.创建线程的第一种方式:继承Thread类,重写run()方法
class TortoiseThread extends Thread {

	public TortoiseThread() {
		super();
	}

	public TortoiseThread(String name) {
		super(name);
	}

	// 重新run()
	public void run() {
		while (true) {
			System.out.println("乌龟领先了,快跑" + this.getName() + ",优先级:"
					+ this.getPriority());
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}
// 第二种线程创建方式:实现runnable 实现 run
// 也可以使用匿名内部类来实现

 class PersonThread implements Runnable{ public void run() { while(true){
System.out.println("加油"+Thread.currentThread().getName()+",优先级:"+Thread.currentThread
 * ().getPriority()); try { Thread.sleep(1000); } catch (InterruptedException e)
 * { e.printStackTrace(); } } } }
 
/*
 * 继承和实现两种方式创建线程比较: 继承: 优点:代码简单,可以直接调用start()方法启动线程,可以使用this调用线程方法
 * 缺点:java是单继承,在继承Thread 无法在继承其他类,成员变量不能共享 实现:
 * 有点:可以去继承其他类,实现多接口,可以通过匿名内部类来创建,成员变量共享 缺点:代码复杂,run 方法中不能使用this调用线程中的方法
 */
//第三种方式:实现Callable 接口,实现Call,jdk1.5:场景:
//需求:创建一个线程,该线程会产生一个随机数,获取产生的随机数
/*
 * call方法和run()方法区别:
 * 1.call有返回值
 * 2.call 申明异常处理,run方法没有声明异常只能try--catch
 */
/*
 * 与实现runnable 相比,Callable 功能更强大些
 * 方法不同
 * 可以有返回值,支持泛型的返回值
 * 可以抛出异常
 * 需要借助FutureTask,比如获取返回结果
 */ 
class ThreeThread implements Callable<Integer> {
	@Override
	public Integer call() throws Exception {
		return new Random().nextInt(10);
	}
}

线程生命周期

生命周期指的是从产生到消亡的过程:
创建—>start—>就绪状态–调度–>运行状态–>终止
1.运行状态的三个去处?
1.死亡状态。线程运行完毕进入死亡状态
2.阻塞状态,遇到阻塞事件
3.就绪状态,yield()
2.就绪状态的三个 来处
1.新生状态,调用start 方法
2.阻塞状态,结束阻塞状态
3,运行状态,调用yield()方法
导致阻塞情况有哪些?
1.等待用户的输入
2.sleep()方法休眠
3,join();

线程控制方法

优先级:只是意味着获得调度的概率低,并不是绝对先调用优秀级高后调用优先级低的进程

//线程优先级用一个
public class TestThread1 {

	public static void main(String[] args) {
		Runnable runnable = new Runnable() {
			public void run() {
				while (true) {
					System.out.println("加油" + Thread.currentThread().getName()
							+ ",优先级:" + Thread.currentThread().getPriority());
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		};
		Thread t1=new Thread(runnable);
		Thread t2=new Thread(runnable);
		Thread t3=new Thread(runnable);
		//设置线程的优先级别
		t2.setPriority(Thread.MAX_PRIORITY);
		t3.setPriority(Thread.MIN_PRIORITY);
		//启动线程
		t1.start();
		t2.start();
		t3.start();
	}
}

线程控制方法
join():阻塞指定线程等到另一线程再继续执行
sleep():
yield();暂停当前线程回到就绪状态
setDaemon():可以将指定的线程设置成后台线程
创建后台线程的线程结束时,后台线程也随之消亡;
只能在线程就把他设为后台线程
stop()强制终止线程
interrupt()并没有中断线程,线程的消亡由线程自己控制

线程同步

线程安全问题:使用同步的方式解决:方式有三:
方式一:使用同步代码块(synchronized)

/*
 * 线程安全问题
 * 	可以使用同步的方式解决这个问题
 * 1.使用同步代码块(synchronized)
 * 2.使用同步方法
 * 3.使用Lock锁,jdk1.5后提供
 * 
 * 锁:同步监视器,可以是任意对象,通常可以使用共享对象作为锁,this,还可以准备一个任意的对象(没有业务逻辑只是作为锁)
 */
public class AccountRunable implements Runnable {
	//共享帐户
	private Account account=new Account();
	@Override
	public void run() {
		synchronized (account) {
			if(account.getBalance()>=400){
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				account.getMoney(400);
				System.out.println(Thread.currentThread().getName()+",取钱成功,余额:"+account.getBalance());
			}else{
				System.out.println("余额不足");
				System.out.println(Thread.currentThread().getName()+",取钱失败,余额:"+account.getBalance());
			}
		}
	}
}

同步代码块执行过程:
1.第一个线程来到发现有锁,但是是open的状态,进去执行,同时将状态修改为close;
2.第一个线程可能在执行过程中释放CPU资源,阻塞状态,但是不会开锁
3.第二个线程获取了CPU资源,发现有锁,状态资源close 第二个线程也阻塞;
4.第一个线程重新获取cpu 资源,继续执行后续的代码,代码结束出来,状态修改成open
5.第二个线程进去重复上一线程操作
同步的优缺点:
优点:保证了线程安全
缺点:降低效率
锁:同步监视器,可以是任意对象,通常可以使用共享对象作为锁,this,还可以准备一个任意的对象(没有业务逻辑只是作为锁)

方式二: 使用同步方法
1.同步方法的锁(同步监视器)是谁?
锁是this ,意味着只要有一个方法被锁住,所有的同步方法都被锁住了
静态的同步方法的所示类本身:AccountRunnable.class
2.给线程run 方法上加synchronized 好不好?
synchronized 锁的范围太大;可以将需要同步的代码提取成一个方法对方法进行同步

public class AccountRunable implements Runnable {
	//共享帐户
	private Account account=new Account();
	@Override
	public synchronized void run() {
			if(account.getBalance()>=400){
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				account.getMoney(400);
				System.out.println(Thread.currentThread().getName()+",取钱成功,余额:"+account.getBalance());
			}else{
				System.out.println("余额不足");
				System.out.println(Thread.currentThread().getName()+",取钱失败,余额:"+account.getBalance());
			}
		}
}

方式三: 使用Lock锁,jdk1.5后提供
显示锁:所有操作都需要自己来(准备锁,自己上锁,自己开锁)
Lock是jdk1.5中提供的一种接口,常用的实现类:
ReentrantLock

public class AccountRunable implements Runnable {
	//共享帐户
	private Account account=new Account();
	//准备锁
	Lock lock=new ReentrantLock();
	
	@Override
	public void run() {
		//此处省略500行代码
		//手动上锁
		lock.lock();
		try{
			if(account.getBalance()>=400){
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				account.getMoney(400);
				System.out.println(Thread.currentThread().getName()+",取钱成功,余额:"+account.getBalance());
			}else{
				System.out.println("余额不足");
				System.out.println(Thread.currentThread().getName()+",取钱失败,余额:"+account.getBalance());
			}
			//此处省略500行
			}finally{
				//手动开锁
			lock.unlock();
			}
		}

}

线程间通信(线程与线程之间的通信)

wait ():线程在指定的锁上等待
notify():唤醒该锁上的任意一个等待线程
notifyAll():唤醒该锁上的所有等待线程
同步代码块方法实现线程同步
生产者

	private Product product;
	
	public Producer(Product product) {
		super();
		this.product = product;
	}

	@Override
	public void run() {
		
			int index=0;
			while(true){
				synchronized (product) {
					//如果仓库中有商品,生产者应该等待
					if(product.isFlag()){
						try {
							product.wait();
						} catch (InterruptedException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
					}
				if(index%2==0){
					product.setBrand("哇哈哈");
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					product.setName("牛奶");
				}else{
					product.setBrand("康师傅");
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					product.setName("方便面");
				}
				System.out.println("生产者生产:"+product.getBrand()+":"+product.getName());
				//已经生产了产品,将标记改为true
				product.setFlag(true);
				//唤醒消费者进行消费
				product.notify();
				index++;
			}
		}
	}

}

消费者

public class Consumer implements Runnable {
	private Product product;

	public Consumer(Product product) {
		super();
		this.product = product;
	}

	@Override
	public void run() {
		while (true) {
			synchronized (product) {
				//如果仓库没有产品,消费者应该等待
				
				if(!product.isFlag()){
					try {
						product.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println("消费者消费" + product.getBrand() + ":"
						+ product.getName());
				//已经消费产品
				product.setFlag(false);
				//唤醒生产进行生产
				product.notify();
			}
		}
	}

}

只进行同步并不能解决生产者和消费者交替执行的功能 ,此时,需要使用线程通信来实现该功能
同步方法实现线程同步

public class SynchProduct {

	private String name;
	private String brand;
	 
	private boolean flag;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getBrand() {
		return brand;
	}

	public void setBrand(String brand) {
		this.brand = brand;
	}

	public boolean isFlag() {
		return flag;
	}
	public void setFlag(boolean flag) {
		this.flag = flag;
	}
	//生产者
	public synchronized void producer(String name,String brand){
		if(flag){
			try {
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		this.name=name;
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		this.brand=brand;
		System.out.println("生产者生产:"+this.brand+this.name);
		//修改flag为true 
		this.flag=true;
		//唤醒消费者消费
		notify();
	}
	//消费者
	public synchronized void consumer() {
		if(!flag){
			try {
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		System.out.println("消费者"+this.brand+this.name);
		this.flag=false;
		//唤醒生产者
		notify();
	}
}

生产者,消费者,传递者
生产者—>传递者—>消费者
修改:flag 不应该使用boolean类型可以使用int
1:表示没有产品,需要生产者生产
2.表示有产品,可以传递 了,需要传递者传递
3.表示已经传递,可以 消费了,需要消费者消费
多个线程时条件判断不建议使用if 而是用while
wait()和sleep()区别?
wait()方法可以阻塞线程 ,同时还可以释放锁
sleep()方法可以阻塞线程,但是它不能释放锁
SynchProduct.java

public class SynchProduct {

	private String name;
	private String brand;
	 
	private int flag=1;//1有商品

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getBrand() {
		return brand;
	}

	public void setBrand(String brand) {
		this.brand = brand;
	}

	
	public int getFlag() {
		return flag;
	}

	public void setFlag(int flag) {
		this.flag = flag;
	}

	//生产者
	public synchronized void producer(String name,String brand){
		while(flag!=1){
			try {
				wait();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		this.name=name;
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		this.brand=brand;
		System.out.println("生产者生产:"+this.brand+this.name);
		//修改flag为2
		this.flag=2;
		//唤醒传递者
		notifyAll();
	}
	
	//传递者
		public synchronized void deliver() {
			while(flag!=2){
				try {
					wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			System.out.println("传递者"+this.brand+this.name);
			this.flag=3;
			//唤醒生产者
			notifyAll();
		}
	//消费者
	public synchronized void consumer() {
		while(flag!=3){
			try {
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println("消费者"+this.brand+this.name);
		this.flag=1;
		//唤醒生产者
		notifyAll();
	}
}

线程组

线程组的作用:
统一管理:便于对一组线程进行批量管理线程和线程组对象
安全隔离:允许线程访问有关自己 的线程组的信息,但是不允许它访问有关联线程的父线程或其他任何线程组的信息

System.out.println("当前线程组"+t1.getThreadGroup().getName());//默认为main
System.out.println("父线程组"+t1.getThreadGroup().getParent().getName());//顶级是system
public class Test {
	public static void main(String[] args) {
		/*SynchProduct product = new SynchProduct();
		Consumer consumer = new Consumer(product);
		Producer producer = new Producer(product);
		Deliver deliver=new Deliver(product);
		Thread t1=new Thread(producer);
		Thread t2=new Thread(consumer);
		Thread t3=new Thread(deliver);
		
//		t1.start();
//		t2.start();
//		t3.start();
		System.out.println("当前线程组"+t1.getThreadGroup().getName());//默认为main
		System.out.println("父线程组"+t1.getThreadGroup().getParent().getName());//顶级是system
*/		//创建线程组
		ThreadGroup tg = new ThreadGroup("消费");
		SynchProduct product = new SynchProduct();
		Consumer consumer = new Consumer(product);
		Producer producer = new Producer(product);
		Deliver deliver=new Deliver(product);
		Thread t1 = new Thread(tg, consumer, "消费者");
		Thread t2 = new Thread(tg, producer, "生产者");
		Thread t3 = new Thread(tg, deliver, "传递者");
		System.out.println("当前线程组:"+t1.getThreadGroup().getName());//当前线程组消费
		
		System.out.println("父线程组"+t1.getThreadGroup().getParent().getName());//父线程组main
		//创建线程组
		//批量操作线程优先级
		tg.setMaxPriority(8);//控制当前线程组的最大优先级
		t1.setPriority(10);
		System.out.println("当前线程优先级"+t1.getPriority());
		System.out.println("当前线程优先级"+t2.getPriority());
		//守护线程
		//tg.setDaemon(true);
		//查看当前线程组中有哪些线程是活跃的
		System.out.println(tg.activeCount());
	}
}

线程池

什么是线程池 ?

* 创建和销毁对象是非常耗费时间的
* 创建对象,需要分配内存资源等资源
* 销毁对象,虽然不需要程序员操心,但是垃圾 回收器会在后台一直跟踪并销毁
* 对于经常创建和销毁,使用 量特别大的资源,比如并发情况下的线程对性能影响很大
* 思路:创建好多个线程,放入线程池中,使用时直接获取引用,不使用时放回池中,可以避免频繁创建销毁,实现重复利用
* 生活案例:共享单程
* 技术案例:线程池,数据库连接
* JDK1.5 提供了内置线程池

线程池的好处:

  • 提高响应速度(减少了创建新线程的时间)
  • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  • 提高线程的可管理性,避免线程无限制创建从而消耗系统资源,降低系统稳定,甚至内部存溢出或者cpu耗尽

线程池的应用场合

  • 需要大量线程
  • 对性能要求苛刻
  • 接收突发性的大量请求

线程池方法

Executor:线程池顶级接口,只有一个方法
ExecutorService :真正的线程池接口

void execute(Runnable command):执行任务 /命令,没有返回值,一般用来执行Runnable
<T> Future<T>submit (callable <T> task):执行任务,有返回值,一般用来执行Callable
void shutdown():关闭线程池

AbstractExcecutorService:基本实现了ExecutorService的所有方法
ThreadPoolExecutor :默认的线程池实现类
ScheduledThreadPoolExecutor :实现周期性任务调度的线程池
Executors:工具类、线程池 的 工厂类,用于创建并返回不同类型的线程池

* Executors.newCachedThreadPool():创建一个可以根据需要创建新线程的线程池
* Executors.newFixedThreadPool():创建一个可以重用固定线程数的线程池
* Executors.newSingleThreadExecutor():创建一个只有一个 线程的线程池
* Executors.newScheduledThreadPool():创建一个线程池,它可以安排在给定延迟后运行命令或者定期执行

使用线程池执行大量的Runnable 命令

/**
 * 使用线程池执行大量的Runnable 命令
 */
public class TestPoolRunnable {
	
	public static void main(String[] args) {
		//创建可变长度的线程池
		//Executors.newCachedThreadPool();
		//创建一个只有一个线程池
		ExecutorService pool = Executors.newSingleThreadExecutor();
		//创建一个含有固定线程的线程池
		//Executors.newFixedThreadPool(10);
		//使用线程池
		for (int i = 0; i < 20; i++) {
			 Runnable runnable = new Runnable() {
				public void run() {
					System.out.println("开始。。。。");
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					System.out.println("结束。。。。");
				}
			};
			//将命令交给线程池执行
			pool.execute(runnable);
		}
		//关闭线程池 
		pool.shutdown();
	}
}

使用线程池执行大量的Callable任务

/*
 * 使用线程池执行大量的Callable任务
 */
public class TestPoolCallable {
	public static void main(String[] args) throws InterruptedException, ExecutionException {
		//创建线程池
		ExecutorService pool = Executors.newCachedThreadPool();
		//创建一个list集合
		List<Future<Integer>> list=new ArrayList<>();
		
		//使用线程池
		for (int i = 0; i <20; i++) {
			Callable<Integer> callable = new Callable<Integer>() {

				public Integer call() throws Exception {
					
					return new Random().nextInt(10);
				}
			};
			//将任务交给线程池来执行
			Future<Integer> future = pool.submit(callable);
			
			//打印随机数
			//System.out.println(future.get());
			//将future存放到集合中
			list.add(future);
		}
		//遍历集合
		for (Future<Integer> future : list) {
			System.out.println(future.get());
		}
		//关闭线程池
		pool.shutdown();
	}
}

**线程的参数 **

corePoolSize:核心池的大小
	*默认情况下,创建线程池后线程数为0,当有任务来之后,就会创建一个线程去执行任务。
	*但是当线程池中数量达到corePoolSize,就会把到达的任务放到 队列中等待
maximumPoolSize:最大线程数。
	*corePoolSize和maximumPoolSize之间的线程数会自动释放,小于等于corePoolSize的就不会释放,当大于这个值就会将任务由一个丢弃处理机制来处理。
keepAliveTime:线程没有任务时最多保持多长时间后终止
	*默认只限于corePoolSize和maximumPoolSize之间的线程
TimeUnit
	*keepAliveTime时间单位
BolckingQueue:
	*存储等待执行的任务的阻塞队列有多种队列,可以顺序,也可以链式
ThreadFactory
	*线程工厂默认是DefaultThreadFactory,Executors的静态内部类
RejectedExecutionHandler:
	*拒绝处理任务时策略,如果线程池的线程已经饱和,并且任务队列已满,对新任务采取:
		*比如抛出异常,直接舍弃,丢弃对列中最旧任务等,默认是直接抛出异常
			1.CallerRunsPolicy:
			2.DiscardOldstPolicy:
			3.DiscardPolicy
			4,AbortPolicy
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值