创建线程的方式
- 线程概念:操作系统能够进行运算调度的最小单位
线程是操作系统能够进行运算调度的最小单位,被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
- 创建线程的三种方法:
1.继承Thread类
2.实现Runnable接口
3.实现Callable接口 - 具体实现:
//1.继承Thread
public class ThreadDome extends Thread {
@Override
public void run() {
for (int i = 0; i <= 10; i++) {
System.out.println("学习JAVA");
}
}
public static void main(String[] args) {
ThreadDome td = new ThreadDome();
td.start();
}
}
-------------------------------------------------------------------
//2.实现Runnable
public class ThreadDome02 implements Runnable {
@Override
public void run() {
for (int i = 0; i <= 10; i++) {
System.out.println("学习JAVA");
}
}
public static void main(String[] args) {
//创建Runnable接口的实现对象
ThreadDome02 threadDome02 = new ThreadDome02();
//创建线程,通过线程对象开启线程
Thread td = new Thread(threadDome02);
td.start();
}
}
----------------------------------------------------------------------
//3.实现Callable接口
public class ThreadDome03 implements Callable<Boolean> {
@Override
public Boolean call(){
for (int i=0;i<=10;i++){
System.out.println("学习JAVA");
}
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建目标对象
ThreadDome03 td = new ThreadDome03();
//创建执行对象
ExecutorService es = Executors.newFixedThreadPool(1);
//提交执行
Future<Boolean> result = es.submit(td);
//获取执行结果
boolean re = result.get();
//关闭服务
es.shutdown();
}
}
- 线程6种状态之间的转换
解决线程安全的方法
1.不跨线程共享变量,线程共享的变量修改为方法局部变量;
2.使状态变量不可变,使用final修饰,将变量变为常量;
3.在任何访问状态变量的时候使用同步,使用Synchronized修饰方法,或使用同步代码块;
4.每个共享的可变变量都需要唯一一个确定的锁保护,使用Lock锁。
线程池技术
- 线程池优点:
降低资源消耗
提高响应速度
方便管理
核心:线程复用、可以控制最大并发数、管理线程 - 三大方法
ExecutorService t1 = Executors.newSingleThreadExecutor();//开启单个线程的线程池
ExecutorService t2 = Executors.newFixedThreadPool(5);//创建固定数量的线程池,此处固定为5
ExecutorService t3 = Executors.newCachedThreadPool();//开启可伸缩的线程池,遇强则强、遇弱则弱
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolDome {
public static void main(String[] args) {
ExecutorService t1 = Executors.newSingleThreadExecutor();//开启单个线程的线程池
ExecutorService t2 = Executors.newFixedThreadPool(10);//创建固定数量的线程池
ExecutorService t3 = Executors.newCachedThreadPool();//开启可伸缩的线程池,遇强则强、遇弱则弱
try{
//开启单个线程的线程池
for (int i=0;i<=10;i++){
t1.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"--->"+"AA");
}
});
}
//创建固定数量的线程池
for (int i=0;i<=10;i++){
t2.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"--->"+"AA");
}
});
}
//开启可伸缩的线程池,遇强则强、遇弱则弱
for (int i=0;i<=10;i++){
t3.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"--->"+"AA");
}
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
t1.shutdown();
t2.shutdown();
t3.shutdown();
}
}
}
- 七大参数
newSingleThreadExecutor()、 newFixedThreadPool()、newCachedThreadPool()三种创建线程池的方法在底层都是调用同一种方法,该方法中包含7种参数,如下所示:
public ThreadPoolExecutor(int corePoolSize,//核心线程池大小
int maximumPoolSize,//最大核心线程池大小
long keepAliveTime,//超时没有调用就会释放,即线程超时时间
TimeUnit unit,//超时单位
BlockingQueue<Runnable> workQueue,//阻塞队列
ThreadFactory threadFactory,//线程工厂,用来创建线程,一般不动
RejectedExecutionHandler handler//拒绝策略) {........}
- 线程池创建线程原则:
(1)如果没有空闲的线程执行该任务且当前运行的线程数少于corePoolSize,则添加新的线程执行该任务。
(2)如果没有空闲的线程执行该任务且当前的线程数等于corePoolSize同时阻塞队列未满,则将任务入队列,而不添加新的线程。
(3)如果没有空闲的线程执行该任务且阻塞队列已满同时池中的线程数小于maximumPoolSize,则创建新的线程执行任务。
(4)如果没有空闲的线程执行该任务且阻塞队列已满同时池中的线程数等于maximumPoolSize,则根据构造函数中的handler指定的策略来拒绝新的任务。
参考文章:链接: 点击跳转.
- 四种拒绝策略
拒绝策略 | 报错方式 |
---|---|
new ThreadPoolExecutor.AbortPolicy() | 当阻塞队列被填满时,下一个线程进来执行时,该策略会直接抛出异常,阻止系统正常工作; |
new ThreadPoolExecutor.CallerRunsPolicy() | 如果线程池的线程数量达到上限,该策略会把任务队列中的任务放在调用者线程当中运行; |
new ThreadPoolExecutor.DiscardPolicy() | 队列满了,丢掉任务,不会抛出异常 |
new ThreadPoolExecutor.DiscardOldestPolicy() | 队列满了,该策略会丢弃任务队列中最老的一个任务,也就是当前任务队列中最先被添加进去的,马上要被执行的那个任务,并尝试再次提交,不会抛出异常 |
- 最大线程数的定义和优化方向
CPU密集型优化方向:利用RunTime.getRunTime().availProcessors()动态获取CPU最大核心数,以此语句,作为自定义线程池方法中最大线程参数;
IO密集型优化方向:判断程序中十分消耗IO的线程数量,以此数量的2倍,来定义自定义线程池方法中最大线程参数;
阻塞队列
- 常用四中API
方式 | 抛出异常 | 有返回值,不抛出异常 | 阻塞等待 | 超时等待 |
---|---|---|---|---|
添加 | add | offer | put | offer(有参) |
移除 | remove | pull | take | poll(有参) |
检查队首元素 | element | peek | 无 | 无 |
synchronized底层原理
- 如果synchronized操作的是有static修饰的静态对象,实际上是操作其对应的类模板;
- 如果synchronized操作的是非静态类产生的对象,实际上是操作的非静态类对应的实例对象;
//synchronized操作的是有static修饰的静态对象代码块
synchronized (MyStudent.class){ .....}
------------------------------------------------------------
//synchronized操作的是非静态类产生的对象
Mystudent stu = new MyStudent();
synchronized (stu){ .....}
- synchronized底层给对象加锁操作,实际上是操作对象布局(对象组成)中的对象头信息,来进行加锁的,对象布局结构如下:
1.mark word+klass pointer(类信息)组成对象头信息,其中包括:有关堆对象的布局、类型、GC状态、同步状态、标识哈希码的基本信息等。
2.其中对象头中的类型指针(Klass pointer),用于指向元空间当前类的类元信息,比如调用类中的方法,通过类型指针找到元空间中的该类,再找到相应的方法。
3.开启指针压缩时类型指针只用4个字节存储,否则需要8个字节存储。
4.实例数据,是指被锁的对象中所有全局变量数据,其中包括:基本类型数据、引用类型(句柄)、数组长度等
数据补充位,为使对象头+实力数据+数据补充位三者字节数之和,为8字节的整数倍而进行数据补充。
- 利用Java Object Layout工具包,将对象的字节码信息输出,如下图所示:
特别注意:mark word中的value值由于Inter的CPU数据小端模式,故需要进行反向数据读取(从右往左,从下往上)。
synchronized锁膨胀机制
- JDK1.6之前,synchronized锁的机制比较繁重,每一次加锁动作都会在用户态(访问CPU权限受限,不会造成CPU压力过大)和内核态(访问CPU权限不受限,会造成CPU压力过大)之间切换,系统占用率比较大,从JDK1.6开始,synchronized锁的机制被优化,
引入synchronized锁膨胀机制,该机制不可逆,只能升级不能降级。
synchronized锁的优化实际操作的是对象头mark word中:是否偏向锁和锁标志位这两个信息,首先被synchronized修饰的对象被创建时,是处于无锁状态,此时是否偏向锁信息为0,锁标志位信息为01;当有且仅有一个线程来操作该对象时,是否偏向锁信息会被设置为1,锁标志位信息被设置为01,证明此时为偏向锁;当有多个线程想要操作该对象时,并且竞争不激烈,此时锁标志位信息会被设置为00,证明此时为轻量级锁;没有获得锁的线程进行自旋操作,当线程竞争激烈时,自旋会达到阈值(10),此时锁标志位被设置为10,证明此时为重量级锁。 - 对象头中mark word信息图:
- synchronized锁膨胀机制原理图
- 轻量级锁中线程自旋原理图
1.无锁线程会由JVM在当前线程的栈帧中创建锁记录空间(Lock Record),用于存储对象头中的Mark Word信息;
2.当对象中的Lock Record指针指向相应线程中的锁记录空间(Lock Record),如果成功指向则获取锁成功,同时线程中锁记录空间中的Owner也会指向对象的Mark Word;
3.当对象中的Lock Record指针没有指向相应线程中的锁记录空间(Lock Record),则获得锁失败,需要继续自旋,知道达到自旋阈值(默认为10,可以使用虚拟机参数-XX:PreBlockSpin进行修改),升级为重量级锁。
CAS底层原理
- CAS是CPU的并发原语(CPU指令),作用就是比较当前工作内存中的值和主内存中的值,如果这个值是期望的值,那么执行操作,如果不是期望值就一直循环判断,因为底层采用自旋锁。
- Java端使用的compareAndSet,在底层调用UnSafe类中的compareAndSwap来进行内存操作的;Java无法操作内存,但是可以通过调用UnSafe类中各种native方法,来实现调用底层C++方法来操作内存。
- CAS缺点:
1.由于底层是自旋锁,循环时会耗时;
2.CAS操作是底层CPU的指令操作,一次性只能保证一个共享变量的原子性;
ABA问题;
CAS中ABA问题解决
- CAS底层对应的思想就是乐观锁,只要期望值相同就会修改,不会进行每次修改后的版本号的比对;
- 解决方法,引入原子引用,加入版本号的判断,例如:AtomicStampedReference(初始值,版本号)
AQS原理
- AQS:全程AbstractQueuedSynchronizer,时JDK提供的一个同步框架,内部维护着FIFO双向队列,即CLH同步队列。
- AQS依赖它来完成同步状态管理(voliate修饰的state,用于标志是否持有锁)。如果获取同步状态state失败时,会将当前线程及等待信息等构建成一个Node,将Node放到FIFO队列中,同时阻塞当前线程,当线程将同步状态state释放时,会把FIFO队列中的首节唤醒,使其获得同步状态的state,另很多JUC包下的锁都是基于AQS实现的。
- 底层实现,使用CAS、自旋锁和park()方法进行判断和加锁。
- 原理图
ReentrantLock
参考文章:
链接: 点击跳转.
JUC中的各种锁
- 悲观锁:对外界的修改持保守态度,在整个数据处理中,将数据处于锁定状态。悲观锁适用于读少写多的场景。
- 乐观锁:与悲观锁相反,假设数据一般情况下不会发生冲突,只有在数据进行提交更新时,才会正式对数据的冲突与否进行检验,如果发生冲突了,则返回错误信息,让用户决定如何处理。一共分为三个阶段:数据读取、写入校验、数据写入。乐观锁适用于读多写少的场景,这样可以提高系统的并发量。
- 可重入锁:也叫递归锁,是指同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。ReentrantLock和Synchronized否是可重入锁。可重入锁的好处是可以一定程度上避免死锁。
- 自旋锁:是采用让当前线程不停的在循环体中执行,当循环条件被其他线程改变时才能进入临界区(被上锁的代码端),自旋锁只是当前线程不停的执行循环体,不进行线程状态的改变,所以相应速度更快。但是线程数不断增加时,性能下降明显,因为每个线程多需要执行,会占用CPU时间片。如果线程竞争不激烈,并且保持锁的时间段。,适合使用自旋锁。
- 独享锁:是指该锁一次只能被一个线程锁持有,ReentrantLock和Synchronized都是独享锁。
- 共享锁:是指该锁可以被多个线程所持有。ReentrantReadWriteLock,其读锁是共享锁,其写锁是独享锁。读锁的共享锁可以保证并发读是非常高效的,读写、写读、写写的过程都是互斥的。独享锁和共享锁都是通过AQS(AbstractQueuedSynchronizer)来实现的,通过实现不同的方法,来实现独享和共享。
- 互斥锁\读写锁:独享锁\共享锁就是广义上的说法,互斥锁\读写锁是具体的实现。互斥锁在Java中的具体实现就是ReentrantLock,读写锁在Java中的具体实现就是ReentrantReadWriteLock。
- 阻塞锁:让线程进入阻塞状态进行等待,当获得相应的信号(唤醒、到达时间)时,才能进入线程的准备就绪状态,准备就绪状态中的所有线程,通过竞争,进入运行状态。
- 公平锁:是指多个线程按照申请锁的顺序来获取锁。
- 非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序执行的,可能后申请先获得锁,可能造成优先级反转或者饥饿的现象,对于ReentrantLock(boolean fair)默认时非公平锁,非公平锁的优点是吞吐量比公平锁大,Synchronized是非公平锁。