目录
1.JAVA 线程实现/创建方式
1. 1继承 Thread 类
1.2实现 Runnable 接口
1.3、ExecutorService、 Callable、 Future 有返回值线程
1.4基于线程池的方式
二、4 种线程池
2.1newCachedThreadPool
2.1newFixedThreadPool
2.2newScheduledThreadPool
2.3newSingleThreadExecutor
三、线程生命周期(7种状态)
新建状态(NEW)
1.初始化状态(start)
3.就绪状态(RUNNABLE)
3.运行状态(RUNNING)
4.等待状态(o.wait->等待对列) :
5.睡眠状态(sleep)
6.阻塞状态(BLOCKED)
1)同步阻塞(lock->锁池)
2)其他阻塞(/join)
7.线程死亡(DEAD)
1)正常结束
2)异常结束
3)调用 stop
四. 终止线程 4 种方式
1.正常运行结束
2.使用退出标志退出线程
3.Interrupt 方法结束线程
4、stop 方法终止线程(线程不安全)
五、sleep 与 wait 区别
六、JAVA 锁
1. 乐观锁
2、悲观锁
3、自旋锁
4、Synchronized同步锁
5、ReentrantLock
非公平锁
公平锁
6. AtomicInteger
7. ReadWriteLock 读写锁
10、共享锁和独占锁
11、重量级锁、轻量级锁、偏向锁
七、Join 等待其他线程终止
八、线程上下文切换
九、上下文、寄存器、程序计数器、PCB-“切换桢”
十、线程池原理
1、Java 线程池工作过程
十一、volatile 关键字
十二、ThreadLocal 作用(线程本地存储)
十四、什么是 CAS
ABA问题
十五、什么是 AQS(抽象的队列同步器)
进程和线程的区别
多线程和单线程的区别
Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。 启动线程的唯一方
法就是通过 Thread 类的 start()实例方法。 start()方法是一个 native 方法,它将启动一个新线
程,并执行 run()方法。
public class MyThread extends Thread {
public void run() {
System.out.println("MyThread.run()");
}
}
MyThread myThread1 = new MyThread();
myThread1.start();
如果自己的类已经 extends 另一个类,就无法直接 extends Thread,此时,可以实现一个
Runnable 接口。
public class MyThread extends OtherClass implements Runnable {
public void run() {
System.out.println("MyThread.run()");
}
}
//启动 MyThread,需要首先实例化一个 Thread,并传入自己的 MyThread 实例:
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
/**
//事实上,当传入一个 Runnable target 参数给 Thread 后, Thread 的 run()方法就会调用
target.run()
public void run() {
if (target != null) {
target.run();
}
}
**/
有返回值的任务必须实现 Callable 接口,类似的,无返回值的任务必须 Runnable 接口。执行Callable 任务后,可以获取一个Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务返回的 Object 了,再结合线程池接口 ExecutorService 就可以实现传说中有返回结果的多线程了。
//创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(taskSize);
// 创建多个有返回值的任务
List list = new ArrayList();
for (int i = 0; i < taskSize; i++) {
Callable c = new MyCallable(i + " ");
// 执行任务并获取 Future 对象
Future f = pool.submit(c);
list.add(f);
}
// 关闭线程池
pool.shutdown();
// 获取所有并发任务的运行结果
for (Future f : list) {
// 从 Future 对象上获取任务的返回值,并输出到控制台
System.out.println("res: " + f.get().toString());
}
线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。
// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
while(true) {
threadPool.execute(new Runnable() { // 提交多个线程任务,并执行
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running ..");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
Java 里面线程池的顶级接口是 Executor,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 ExecutorService。
创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。 调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。 因此,长时间保持空闲的线程池不会使用任何资源。
创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。
创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3);
scheduledThreadPool.schedule(newRunnable(){
@Override
public void run() {
System.out.println("延迟三秒");
}
}, 3, TimeUnit.SECONDS);
scheduledThreadPool.scheduleAtFixedRate(newRunnable(){
@Override
public void run() {
System.out.println("延迟 1 秒后每三秒执行一次");
}
},1,3,TimeUnit.SECONDS);
Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程) ,这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去!
当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配内存,并初始化其成员变量的值。这是网上很多人说的新建状态,但是我个人认为它不属于线程的声明周期,而是类的生命周期的一部分。对象和线程是分离来理解的。
线程的生命周期,经过初始化状态(start)、就绪(Runnable ready-to-run)、运行(Running)、等待(wait)、睡眠状态(sleep)、阻塞(Blocked)和死亡(Dead)5 种状态。
当线程调用start的之后,进入到初始化状态
当线程调用start的之后,没有抢占到锁或者准备去抢占锁,该状态为就绪状态。
如果处于就绪状态的线程获得了 CPU也就是说抢占到了锁,开始执行 run()方法的线程执行体,则该线程处于运行状
态。
运行(running)的线程执行 o.wait()方法, JVM 会把该线程放入等待队列(waitting queue)
中。
调用sleep方法之后,抱锁等待的一个状态。
阻塞状态是指线程因为某种原因放弃了 cpu 使用权,转为就绪状态。直到线程进入可运行(runnable)状态,才有机会再次获得 cpu转为运行(running)状态。阻塞的情况分三种:
运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线
程放入锁池(lock pool)中。
运行(running)的线程执行t.join()方法,或者I/O 等待,JVM 会把该线程置为阻塞状态。 join()等待线程终止或者超时、或者 I/O处理完毕时,线程重新转入可运行(runnable)状态。
线程会以下面三种方式结束,结束后就是死亡状态。
run()或 call()方法执行完成,线程正常结束。
线程抛出一个未捕获的 Exception 或 Error。
直接调用该线程的 stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用。
程序运行结束,线程自动结束
一般 run()方法执行完,线程就会正常结束,然而,常常有些线程是伺服线程。它们需要长时间的运行,只有在外部某些条件满足的情况下,才能关闭这些线程。使用一个变量来控制循环,例如:最直接的方法就是设一个 boolean 类型的标志,并通过设置这个标志为 true 或 false 来控制 while循环是否退出,代码示例:
public class ThreadSafe extends Thread {
public volatile boolean exit = false;
public void run() {
while (!exit){
//do something
}
}
}
定义了一个退出标志 exit,当 exit 为 true 时, while 循环退出, exit 的默认值为 false.在定义 exit时,使用了一个 Java 关键字 volatile,这个关键字的目的是使 exit 同步,也就是说在同一时刻只能由一个线程来修改 exit 的值。
使用 interrupt()方法来中断线程有两种情况:
1. 线程处于阻塞状态: 如使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时,会使线程处于阻塞状态。当调用线程的 interrupt()方法时,会抛出 InterruptException 异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让我们有机会结束这个线程的执行。 通常很多人认为只要调用 interrupt 方法线程就会结束,实际上是错的, 一定要先捕获 InterruptedException 异常之后通过 break 来跳出循环,才能正常结束 run 方法。
2. 线程未处于阻塞状态: 使用 isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置 true,和使用自定义的标志来控制循环是一样的道理。
public class ThreadSafe extends Thread {
public void run() {
while (!isInterrupted()){ //非阻塞过程中通过判断中断标志来退出
try{
Thread.sleep(5*1000);//阻塞过程捕获中断异常来退出
}catch(InterruptedException e){
e.printStackTrace();
break;//捕获到异常之后,执行 break 跳出循环
}
}
}
}
调用之后,会抛出 ThreadDeatherror 的错误,释放所有锁,导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性。
sleep()方法是thread内部方法,指定睡眠时间,让出cpu,但持有锁不释放
wait()方法是对象方法,放弃锁,进入等待;
乐观锁是一种乐观思想,认为读多写少,读的时候不会上锁,但是在写时先读出当前版本号,然后加锁操作(比较两个版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。java 中的乐观锁基本都是通过 CAS 操作实现的, CAS 是一种更新的原子操作。
悲观锁就是悲观思想,认为读少写多,读写都上锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试 cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如 RetreenLock。
自旋锁原理 如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。线程自旋是需要消耗 cup 的,所以需要设定一个最大自旋等待时间。
Synchronized 核心组件
1) Wait Set:哪些调用 wait 方法被阻塞的线程被放置在这里;
2) Contention List: 竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
3) Entry List: Contention List 中那些有资格成为候选资源的线程被移动到 Entry List 中;
4) OnDeck:任意时刻, 最多只有一个线程正在竞争锁资源,该线程被成为 OnDeck;
5) Owner:当前已经获取到所资源的线程被称为 Owner;
6) !Owner:当前释放锁的线程。
Synchronized 是非公平锁。 Synchronized 在线程进入 ContentionList 时, 等待的线程会先尝试自旋获取锁,如果获取不到就进入 ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁资源
方法锁:类中非静态方法上的锁;用this(当前实例化的对象)做锁
对象锁:方法块中传入的对象,持有了该对象的锁才能进入
类锁 : 静态方法上的锁,持有.class类的锁才能进入
ReentantLock 继承接口 Lock 并实现了接口中定义的方法, 他是一种可重入锁, 除了能完成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。
判断可重入锁:synchronized和ReentrantLock都是可重入锁,也就是在同一个线程每进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
JVM 按随机、就近原则分配锁的机制则称为不公平锁, ReentrantLock 在构造函数中提供了是否公平锁的初始化方式,默认为非公平锁。
公平锁指的是锁的分配机制是公平的,通常先对锁提出获取请求的线程会先被分配到锁,ReentrantLock 在构造函数中提供了是否公平锁的初始化方式来定义公平锁。
提供原子操作的 Integer 的类,这是由硬件提供原子操作指令实现的。
其底层就是volatile和CAS 共同作用的结果
1.首先使用了volatile 保证了内存可见性。
2.然后使用了CAS(compare-and-swap)算法 保证了原子性。
读锁:同时读,不能同时写;
写锁:不能同时写,不能同时读;
独占锁:ReentrantLock 就是以独占方式实现的互斥锁,独占锁是一种悲观锁。
共享锁:共享锁则允许多个线程同时获取锁,如: ReadWriteLock。 共享锁则是一种乐观锁
锁升级:随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁,锁升级是单向的不可逆。
拿synchronized来讲,刚开始在只有一个线程执行同步块的时候使用的是偏向锁(解决了获取锁和释放锁的消耗),在多个线程执行同步块的时候升级为轻量级锁(使用的是自旋锁,解决了获取锁的资源消耗),当多个线程同一时间访问同一把锁的时候,就会升级为重量级锁,锁的升级是不可逆的。
分段锁:并非一种实际的锁,而是一种思想 ConcurrentHashMap 使用的就是分段锁
主线程生成并启动子线程,需要用到子线程返回的结果,也就是需要主线程在子线程结束后再结束,这时候就要用到 join() 方法。
利用了时间片轮转的方式, CPU 给每个任务都服务一定的时间,然后把当前任务的状态保存下来,在加载下一任务的状态后,继续服务下一任务, 任务的状态保存及再加载, 这段过程就叫做上下文切换。时间片轮转的方式使多个任务在同一颗 CPU 上执行变成了可能。
上下文:是指某一时间点 CPU 寄存器和程序计数器的内容
寄存器:寄存器通过对常用值(通常是运算的中间值)的快速访问来提高计算机程序运行的速度。
程序计数器:用于表明指令序列中 CPU 正在执行的位置
PCB-“切换桢”:上下文切换之后的线程执行的位置的记录
线程池主要由 4 个组成部分:
1. 线程池管理器:用于创建并管理线程池
2. 工作线程:线程池中的线程
3. 任务接口:每个任务必须实现的接口,用于工作线程调度其运行
4. 任务队列:用于存放待处理的任务,提供一种缓冲机制
线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面
有任务,线程池也不会马上执行它们。
2. 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
a) 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
c) 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要
创建非核心线程立刻运行这个任务;
d) 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池
会抛出异常 RejectExecutionException。
3. 当一个线程完成任务时,它会从队列中取下一个任务来执行。
4. 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运
行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它
最终会收缩到 corePoolSize 的大小
变量可见性、禁止重排序
在访问 volatile 变量时不会执行加锁操作,因此也就不会使执行线程阻塞,保证了原子性。
但是不能执行i++操作,因为本质上 i++是读、写两次操作
在某些场景下可以代替 Synchronized。
(1)对变量的写操作不依赖于当前值(比如 i++),或者说是单纯的变量赋值(boolean
flag = true) 。
(2)该变量没有包含在具有其他变量的不变式中, 也就是说,不同的 volatile 变量之间,不
能互相依赖。
ThreadLocal 的作用保持线程变量。
存储变量使用的是treadlocal中的table数组,而数组具体存储的是TreadLocalMap里面的entry对象。
ThreadLocalMap : ThreadLocal类中的静态内部类
(实质存储变量的位置,在Thread中有类型为ThreadLocal.ThreadLocalMap类型的属性threadLocals)
Entry : ThreadLocalMap中的静态内部类,弱引用ThreadLocal(存储变量的结构)
ThreadLoca造成内存溢出的:
entry继承了weakreference是一个弱引用,但是弱引用只针对于key;
所有当key被设置为null之后,可以被gc回收,而value确实强引用,无法被gc。这样就导致类内存溢出。
解决办法:调用remove方法移除对象;
最常见的 ThreadLocal 使用场景为 用来解决 数据库连接、 Session 管理等。
private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}
private static ThreadLocal connectionHolder = new ThreadLocal() {
public Connection initialValue() {
return DriverManager.getConnection(DB_URL);
}
};
public static Connection getConnection() {
return connectionHolder.get();
cas首先获取当前预期值,以及预期更新值,然后判断当前预期值与当前值是否相等,相等的话将内存值更新为预期更新值,返回true。否则返回false进行重复之前的操作。
乐观锁机制,当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂
起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。
getAndIncrement 采用了 CAS 操作,每次从内存中读取数据然后将此数据和+1 后的结果进行
CAS 操作,如果成功就返回结果,否则重试直到成功为止。
public class AtomicInteger extends Number implements java.io.Serializable { private volatile int value; public final int get() { return value; } public final int getAndIncrement() { for (;;) { //CAS 自旋,一直尝试,直达成功 int current = get(); int next = current + 1; if (compareAndSet(current, next)) return current; } } public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); } } |
比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但是不代表这个过程就是没有问题的。
版本号(version)的方式来解决 ABA 问题
AQS 定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock /Semaphore /CountDownLatch。
它维护了一个 volatile int state(代表共享资源)和一个 FIFO 线程等待队列(多线程争用资源被
阻塞时会进入此队列)。这里 volatile 是核心关键词,具体 volatile 的语义,在此不述。 state 的
访问方式有三种:
getState()
setState()
compareAndSetState()
羊群效应
这里说一下羊群效应,当有多个线程去竞争同一个锁的时候,假设锁被某个线程占用,那么如果有成千上万个线程在等待锁,有一种做法是同时唤醒这成千上万个线程去去竞争锁,这个时候就发生了羊群效应,海量的竞争必然造成资源的剧增和浪费,因此终究只能有一个线程竞争成功,其他线程还是要老老实实的回去等待。AQS的FIFO的等待队列给解决在锁竞争方面的羊群效应问题提供了一个思路:保持一个FIFO队列,队列每个节点只关心其前一个节点的状态,线程唤醒也只唤醒队头等待线程。其实这个思路已经被应用到了分布式锁的实践中
线程同步,并发操作怎么控制
线程同步:简单来说就是使用synchronized加锁控制,只有一个线程能够对同步块进行读写操作;其他线程只能等待该线程释放锁资源;
并发控制:多个线程放完数据的时候通过加锁来控制,保证被访问资源的一致性
进程:是执行中的一段程序
线程:单个进程中执行的每个任务就是一个线程。
多线程:在单核CPU中,CPU被分成一个时间轮转片以轮转的方式给线程分配CPU。
单线程:一个线程获取CPU然后去执行执行,不存在其他线程竞争CPU资源
在单核CPU的情况下采用多线程不会提高程序的执行速度,反而会降低速度,但是对于用户来说,可以减少用户的响应时间。