前言
- JAVA线程状态经常有人搞混,说5种6种甚至7种都有。其实5种是操作系统的线程状态,JAVA有6种,Thread源码的枚举类型statue有提现。
- NEW:
被创建,还没有调用start()方法; - RUNNABLE:
运行中,JAVA中把操作系统的就绪(ready),运行(running)统称为”运行中“。
线程对象被创建后,其他线程(如main)调用了该对象的start方法,该状态的线程位于可运行线程池中,等待被线程调度选择,获取cpu权限,此时是就绪(ready)。
就绪状态的线程获取cpu时间片后变为运行中(running)状态。 - BlOCKED:表示线程进入等待状态,也就是线程因为某种原因放弃了 CPU 使用权。
- 等待阻塞:运行的线程执行了Thread.sleep() 、wait()、 join() 等方法, JVM 会把当前线程设置为等待状态,当 sleep 结束、join 线程终止或者线程被唤醒后,该线程从等待状态进入到阻塞状态,重新抢占锁后进行线程恢复;
- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被其他线程锁占用了,那么jvm会把当前的线程放入到锁池中 ;
- 其他阻塞:发出了 I/O请求时,JVM 会把当前线程设置为阻塞状态,当 I/O处理完毕则线程恢复;
- WAITING:
等待状态,没有超时时间,要被其他线程或者有其它的中断操作。
无条件等待,当线程 调用wait()、join()、LockSupport.park() 不加超时时间的方法之后所处的状态,如果没有被唤醒或等待的线程没有结束,那么将一直等待,当前状态的线程不会被分配CPU资源和持有锁; - TIMED_WAITING:
超时等待状态,超时以后自动返回;
有条件的等待,当线程调用 sleep(long)、wait(long)、join(long)、LockSupport.park(long)、LockSupport.parkNanos(long)、LockSupport.parkUntil(long)方法之后所处的状态,在指定的时间没有被唤醒或者等待线程没有结束,会被系统自动唤醒,正常退出。 - TERMINATED:
终止状态,表示当前线程执行完毕 。
执行完了run()方法。其实这只是Java语言级别的一种状态,在操作系统内部可能已经注销了相应的线程,或者将它复用给其他需要使用线程的请求,而在Java语言级别只是通过Java代码看到的线程状态而已。创建
- 继承Thread类,重新run()
定义Runnable接口的实现类,重新run(),然后new Thread(new xxx())
run()方法的返回值是void,
创建Callable接口的实现类,并实现call()
call()方法是有返回值的,返回值是泛型,和Future,FutureTask配合可以用来获取异步执行的结果。
线程池创建
- newFixedThreadPool(int nThreads)固定长度,当线程发生未预期的错误而结束时,线程池会补充一个新的线程。
- newCachedThreadPool()可缓存的线程池,若线程池规模超过了处理需求,自动回收空闲线程,当需求增加,自动添加新线程,线程池规模不受任何限制。
- newSingleThreadExecutor()单线程的Executor,他创建单个工作线程来执行任务,若线程异常结束,会创建一个新的线程代替,特定时确保任务在队列中顺序串行执行。
- newScheduledThreadPool(int corePoolSize)固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。
状态切换
《并发编程的艺术》
- 代码示例
package shen.example.demo.mutithread;
import java.util.concurrent.locks.LockSupport;
/**
* 线程状态demo
*/
public class ThreadStatusDemo {
public static void main(String[] args) throws InterruptedException {
//创建一个线程
System.out.println("创建子线程new");
Thread thread = new Thread(()->{
//2.线程状态:RUNNABLE
System.out.println(Thread.currentThread().getState());
System.out.println("子线程运行中...开始调用park()");
LockSupport.park();
//睡眠
try {
System.out.println("子线程运行中...调用sleep(long)");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取同步锁
System.out.println("子线程运行中...进入同步代码块");
synchronized (ThreadStatusDemo.class){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//1.线程状态:NEW
System.out.println(thread.getState());
System.out.println("子线程启动start");
//启动线程
thread.start();
//主线程睡眠,等待子线程thread调用park()
Thread.sleep(500);
//3.线程状态:WAITING
Thread.sleep(500);
System.out.println(thread.getState());
//唤醒子线程,使子线程调用sleep(long)
LockSupport.unpark(thread);
//4.TIMED_WAITING
System.out.println(thread.getState());
//主线程获取当前类锁
synchronized (ThreadStatusDemo.class){
Thread.sleep(1200);//等待子线程进入同步代码块阻塞
//5.BLOCKED
System.out.println(thread.getState());
}
Thread.sleep(1200);
System.out.println("子线程运行结束");
//5.TERMINATED
System.out.println(thread.getState());
}
}
- 程序计数器为什么私有?
- 为了多线程中线程切换后能够恢复到正确的执行位置。
- 字节码解释器通过改程序计数器来依次执行指令,从而实现代码的流程控制。
- 虚拟机栈为什么私有
- java方法执行时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用信息等,从方法调用到完成,对应栈帧入栈出栈。
- 本地方法栈为什么私有
- 与虚拟机栈类似,区别为native方法服务
- 什么是上下文切换
- CPU分配时间片轮转模拟多核心,当一个时间片用完重新进入就绪状态让其他线程执行,这就是一次上下文切换。
- 任务从保存到加载的过程。
死锁的四个条件
- 互斥
- 请求与保持
- 不剥夺
- 循环等待
package com.example.demo.Lock;
/**
* TODO
*
* @author Skiray
* @date 2021/6/17 10:51
*/
public class DeadLock {
private static Object re1 = new Object();
private static Object re2 = new Object();
public static void main(String[] args){
new Thread(()->{
synchronized (re1){
System.out.println(Thread.currentThread().getName() + "get re1");
try{
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get re2");
synchronized (re2){
System.out.println(Thread.currentThread() + "get re2");
}
}
},"线程 1 ").start();
new Thread(()->{
synchronized (re2){
System.out.println(Thread.currentThread()+"get re2");
try{
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get re1");
synchronized (re1){
System.out.println(Thread.currentThread() + "get re1");
}
}
},"线程2").start();
}
}
sleep与 wait
- 区别sleep没有释放锁,wait释放锁
- wait常用于线程间交互/通信,sleep常用于暂停。
- wait需要手动唤醒,notify,notifyAll。
- sleep执行完成自动苏醒,超时等待wait(long xx)也会自动苏醒。
- 为什么调用start时会执行run
- new一个Thread,线程进入新建状态,start后,启动线程并进入就绪,等分配到时间片就可运行。start会执行线程的相应准备工作,然后自动执行run内容。直接执行run会把run方法当成一个main线程下的普通方法去执行,并不会在某个线程中执行。
- synchronized
- 保证修饰的方法或代码块在任意时刻只能有一个线程执行。早期版本属于重量级锁。低效。
- 因监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock实现的,java的线程是映射到OS的原生线程之上的,挂起或唤醒线程都需要OS帮忙,而OS实现线程间切换需要从用户态转换到内核态,相对比较耗时,耗成本。
- 6之后引入了自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等减少锁的开销。现在开源很多用了synchronized。
synchronized使用
修饰实例方法
对当前对象实例加锁,进入同步代码前需要获得当前对象实例的锁。 synchronized void f(){}
静态方法
给当前类加锁,会作用于类的所有对象实例,进入同步代码前要获得当前calss的锁。因静态成员变量不属于任何实例对象,是类成员(不管new多少,static只有一份),所以,一线程A调用一个实例对象的非静态synchronized()方法,线程B需要调用这个实例对象所属类的静态synchronized方法是允许的,不会发送互斥现象,因访问静态synchronized()方法占用的锁是当前类的锁,而非静态synchronized()方法占用的锁是当前实例的锁。
代码块
指定加锁对象,对给定对象/类加锁。synchronized(this)表示进入同步代码块前要获得给定对象的锁,synchronized(类.class)表示进入同步代码块前要获得当前class的锁。 synchronized(this){}
synchronized可保证方法或代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时还保证贡献变量的内存可见性,java每个对象都可作为锁,这是synchronized实现同步的基础;普通同步锁当前实例对象;静态同步方法锁当前类的class对象;同步方法块锁括号里的对象。
synchronized与volatile
volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可访问该变量,其他线程被阻塞。volatile仅能使用在变量级别:synchronized可使用在变量、方法和类级别volatile仅能实现变量的修改可见性;不能保证原子性;synchronized可保证变量的修改可见性和原子性。volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。volatile编辑的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
synchronized与Lock
首先synchronized是java关键字,Lock是java类;synchronized无法判断是否获取锁的状态;Lock可判断是否获取到锁;synchronized会自动释放锁(a、线程执行完同步代码会释放锁b、线程执行过程中发生异常会释放锁)Lock需要在finally中手动释放(unlock())容易造成线程死锁。synchronized锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平;
直接lock.unlock 会报IllegalMonitorStateException异常。 直接;
总结
- synchronized加static和synchronized(class)代码块都是给Class类上锁。
- synchronized关键字加到实例方法是给对象实例上锁。
- 尽量不用synchronized(String a )字符串缓冲池具有缓存功能。
package com.example.demo.Lock;
/**
* TODO
*
* @author Skiray
* @date 2021/6/17 17:18
*/
//双重校验锁实现单例(线程安全)
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance(){
// 先判断是否已经实例化过,没有才加锁
if (null == uniqueInstance ){
synchronized (Singleton.class){
if (null== uniqueInstance){
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
volatile关键字修饰也很重要,uniqueInstance = new Singleton(); 分三步
- 1为uniqueInstance分配空间;
- 2初始化 uniqueInstance;
- 3将 uniqueInstance 指向分配的内存地址;
但是JVM具有指令重排的特性,执行顺序不一定是123.指令重排多线程下会导致一个线程还没有初始化。如线程1执行1和3,此时线程2调用方法,发下uniqueInstance不为空 null == uniqueInstance不成立,因此返回uniqueInstance,但此时uniqueInstance还没初始化。
构造方法可synchronized 修饰吗?
- 不,构造方法本身就是线程安全的。
- 为什么要CPU缓存?
- 解决内存cpu速度不匹配的问题。
- synchronized 和 volatile区别
- volatile是线程同步的轻量级实现,性能好,只能用于变量。
- volatile能保证数据的可见性,但不能保证数据的原子性,synchronized都能保证。
- volatile主要用于解决变量在多个线程之间的可见性,synchronized解决的是多个线程之间访问资源的同步性。
- ThreadLocal
- 类的作用是创建线程私有的变量。
- 若创建了一个ThreadLocal变量,访问这个变量的每个线程都会有这个变量的本地副本,可get/set获取默认值或将其更改为当前线程所存的副本的值,避免了线程安全的问题。
- Runnable 和 Callbale
- Runnbale没有返回值,或抛出检查异常
- execute 和submit区别
- execute体检不需要返回值的任务,无法判断是否成功。
- submit提交返回值任务,线程会返回一个Future类,通过Fulture可判断任务是否执行成功,get获取返回值
wait/notify/notifyAll
- wait()将当前运行的线程挂起(让其他进入阻塞状态),直到notify或者notifyAll方法来唤醒线程。
- wait(Long timeout):没手动唤醒,时间到自动唤醒。
wait是一个本地方法,底层通过监视器锁的对象完成,没有获取到monitor对象的权限会报错,获取monitor权限java中只能通过synchronized关键字完成。 必须再同步范围内使用,否则抛IllegaMonitorStateException异常 这三个方法作用是线程间的协作,位于Object类中。wait等待其实是对象mointor,由于每个对象都有一个内置的monitor对象,所以每个类都理应由wait/notify方法
sleep/yield/join
- sleep暂停当前线程指定实现(ms)、区别:wait依赖于同步,sleep可以直接使用。深层区别:sleep方法只是暂时让出cpu执行权限,并不释放锁。而wait方法则需要释放锁。
- yield暂停当前线程,以便其他线程有机会执行,不过不能指定暂停的时间,也不能保证当前线程马上停止。yield方法只是将Running状态转变为Runnable状态。
- join方法左作用是父线程等待子线程执行完成后再执行,就是将异步执行的线程合并为同步的线程
操作系统
进程间五种通信方式
- 管道:速度慢,容量有限,只有父子进程能通讯。
- FIFO:任何进程间都能通讯,但速度慢。
- 消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题。
- 信号量:不能传递复杂消息,只能用来同步。
共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存。
死锁条件
互斥条件:一个资源每次只能被一个线程使用;
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放;
- 不剥夺条件:进程已经获得的资源,在未使用完之前,不能强行剥夺;
循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
避免死锁
破坏“请求和保持”条件:让进程在申请资源时,一次性申请所有需要用到的资源,不要一次一次来申请,当申请的资源有一些没空,那就让线程等待。不过这个方法比较浪费资源,进程可能经常处于饥饿状态。还有一种方法是,要求进程在申请资源前,要释放自己拥有的资源。
- 破坏“不可抢占”条件:允许进程进行抢占,方法一:如果去抢资源,被拒绝,就释放自己的资源。方法二:操作系统允许抢,只要你优先级大,可以抢到。
- 破坏“循环等待”条件:将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序提出(指定获取锁的顺序,顺序加锁)。
线程池
三大方法
工具类创建
-
七大参数
public ThreadPoolExecutor(int corePoolSize, // 核心线程数,最小线程数,常驻核心线程数 int maximumPoolSize //最大线程数 long keepAliveTime, // 空闲线程存活时间 TimeUnit unit, // 时间单位 BlockingQueue<Runnable> workQueue, // 任务队列,尚未执行的任务 ThreadFactory threadFactory, // 线程工厂,创建线程 RejectedExecutionHandler handler){ // 拒绝策略 }
四种拒绝策略
JDK内置四种拒绝策略,均实现; RejectedExecutionHandle接口
- AbortPolicy():默认,直接抛出RejectedExecutionException异常阻止系统正常运行。
- CallerRunsPolicy:不抛异常,将任务会退给调用者,降低任务流量。
- DiscardOldesPolicy:抛弃队列中等待最久的任务。
- DiscardPolicy:丢弃,不处理也不抛出异常。
- 在工作中单一的/固定数的/可变的三种创建线程池的方法哪个用的多?超级大坑
- 我们只是用自定义的ThreadPoolExecutor()
线程池数量怎么确定
- 一般来说,如果是CPU密集型应用,则线程池大小设置为N+1。
- 一般来说,如果是IO密集型应用,则线程池大小设置为2N+1。
- 在IO优化中,线程等待时间所占比例越高,需要越多线程,线程CPU时间所占比例越高,需要越少线程。这样的估算公式可能更适合:最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目