线程
1.线程基础
- 线程是操作系统中能给进行运算调度的最小单位,它被包含在进程中,是进程中实际的运作单位;
- 线程是进程的子集,一个进程可以有多个线程,每条线程并发执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片内存空间
//获取cpu核数
System.out.println(Runtime.getRuntime().availableProcessors());
2.线程的实现方式
- 继承Thread类
- 实现Runnable接口
java不支持类的多继承,但是允许实现多个接口,所以继承了Thread类就不能在继承其他类了,而且Thread类实际上也是实现了Runnable接口,所以最好使用Runnable接口实现线程。
- 实现callable接口(concurrent包下)call方法
3.线程的状态(生命周期)
- NEW 新建状态,线程被创建出来,但尚未启动的线程
- RUNNABELE 就绪状态,表示可运行的状态,它可能正在运行,或者在排队等待系统给他分配CPU资源
- BLOCKED 阻塞等待锁的状态,表示处于阻塞状态的线程正在等待监视器锁,比如等待执行synchronized代码块或者使用synchronized标记的方法。
- WAITING 等待状态,一个处于等待状态的线程,正在等待另一个线程执行某个特定的动作,比如,一个线程调用了Object.wait()方法,那它就在等待另一线程调用Object.notify()或者Objiect.notifyAll()方法
- TIME_WAITING 计时等待状态,和等待状态类似,它只是多了超时时间,比如调用了有超时时间设置的方法Object.wait(long timeout)和Thread.join(long timeout)。
- TERMINATED 终止状态,表示线程已经执行完成。
4.线程常用的几个方法
- join()
在一个线程中调用另一个线程的join方法,这时候当前线程就会让出执行权给另一个线程,直到另一个线程执行完,或者过了超时时间再继续执行当前线程。
- yield()
yield()是一个本地方法(native修饰的方法),yield()方法表示给程序调度器一个当前线程愿意让出CPU使用权的暗示,但线程调度器可能会忽略这个暗示
- slepp()
sleep()方法属于Thread类,让当前下线程让出CPU执行权给其他线程,但是不会释放对象锁和监控状态,到了指定时间后可恢复为可运行状态(注意,线程睡眠到期自动苏醒,并返回到可运行状态,不是运行状态。sleep()中指定的时间是不会运行的最短时间,因此不能保证睡眠到期后立即执行)
在企业开发终休眠一般会使用java.util.concurrent包下TimeUnit这个方法
TimeUnit.DAYS.sleep(1);//休眠一天
TimeUnit.SECONDS.sleep(1);//休眠一秒钟
- wait()
wait()属于Object类,与sleep()的区别是当前线程会释放锁,进入等待此对象的等待锁池;(注意它必须包含在synchronzied语句中,无论wait()还是notofy()都需要首先获得目标的一个监视器)
5.线程常被问到的问题
- start()方法与run()方法的区别?
从Thread源码看,start()方法属于Thread自身的方法,并且使用synchronized来保证线程安全;run()方法为Runnable的抽象方法,重写run()方法其实就是此线程要执行的业务方法
- BLOCKED(阻塞)与WAITING(等待)区别?
BLOCKED可以理解为当前线程还处于活跃状态,只是阻塞等待其他线程释放某个锁资源;而WAITTING是线程自身调用Object.wait()或者Thread.join()方法而进入等待状态。当线程自身调用Object.wait()方法而进入等待状态后,需等待另一线程执行Object.notify()或者Object.notifyall()方法后才能被唤醒
- 线程优先级
通过Thread.setPriority()来设置线程优先级
线程池
线程池:三大方法;七大参数;四种拒绝策略
1.什么是线程池
线程池是为了避免线程的频繁创建和销毁带来的性能消耗,而建立的一种池化技术,它是把自己创建的线程放入池中,当有任务来临的时候可以重用已有的线程,无需等待创建线程的过程,这样可以提高程序响应的速度
2.线程池的好处
1.降低资源消耗
2.提高响应速度
3.方便管理
线程复用,可控制最大并发量,管理线程
3.线程池的创建(ThreadPoolExecutor)
线程池的创建不允许使用Executor去创建,而是通过ThreadPoolExecutor的方式。这样的处理方式可以更加明确线程池的运行规则,避免资源消耗风险。
JAVA的executors工具类提供了五种类型线程池创建的常用方法:
- newFiexedThreadPool() 创建固定数目的线程池
- newCachedThreadPool() 创建可缓冲的线程池
- newSingleThreadPoolExecutor() 创建单线程化的线程池
- newScheduledThreadPool() 创建定时的周期性的任务执行的线程池**
- newWorkStealingPool() 工作窃取线程
底层也是new了一个ThreadPoolExecutor()
- ThreadPoolExecutor创建线程池(正确姿势)
//直接调用ThreadPoolExecutor的构造函数来创建线程池。在创建时给BlockQueue指定容量
private static ExecutorService executor = new ThreadPoolExecutor(10,10,60L,
TimeUnit.SECONDS,
new ArryBlockingQueue(10))
- ThreadPoolExecutor核心参数:
- 第一个参数:corePoolSize 核心线程数
- 第二个参数:maximumPoolSize 线程池最大可创建线程数
- 第三个参数:keepAliveTime 表示线程存活时间,当线程池空闲时并且超过了此时间,多余的线程就会毁掉。
- 第四个参数:unit 表示存活时间的单位,他是配合KeepAliveTime参数共同使用的。
- 第五个参数:workQueue表示线程池执行的任务队列,当线程池的所有线程都在处理任务时,如果来领新的任务就会缓存到此队列中排队等待执行。
- 第六个参数:设置线程池的工厂方法
- 第七个参数 RejectedExecutionHandler 表示线程池拒绝策略,当线程池的任务已经在缓存队列workQueue中存储满了之后,并且不能创建新的线程来执行任务时,就会用到此拒绝策略,它属于一种限流保护机制。
- 线程池执行流程:
当一个任务被提交到线程池,会先判断当前线程的数量是否小于线程池的核心线程数,如果小于则创建线程执行任务,不小于则放入队列中,如果队列满了,则判断当前线程数量是否小于线程池的最大线程数量,小于则创建线程来执行任务,大于则调用拒绝策略。
- 线程池拒绝策略
java自带的线程池拒绝策略又四种,默认的为AbortPolicy:终止策略,线程池回抛出异常并终止执行。
当一个线程池的corePoolSize为1,maxmiunPoolSize为3,队列最多可存储两个任务,3+2=5,所以当6个任务来的时候,此时线程池就忙不过来了,就会抛异常。
多线程的锁机制
1.java多线程提供的锁机制常见的有
- synchronized(synchronized重量级锁)
synchronized实现多线程的同步只需要在需要同步的对象方法上加方法,类或者代码块中加入该关键字,就能保证在同一时刻最多只有一个线程访问统一对象的同步代码块
synchronized的实现机制依赖于JVM,因此随着java版本的不断升级而提高。
- reentrantLock
可重入锁,这个锁可以被线程多次重复进入进行获取操作
在并发量较小的多线程应用程序中与synchronized性能相差无几,但在高并发的条件下synchronizd性能会迅速下降十几倍,而reentrantLock的性能依然能维持一个水准,因此建议在搞并发量的情况下使用reentrantLock
reentrantLock引入两个概念公平锁和非公平锁。
公平锁指的是分配锁的机制时公平的,通常先对锁提出获取请求的线程会先被分配锁。反之,JVM按随机就近原则分配锁的机制称为非公平锁,reentrantLock
2.synchronized与reentrantLock的实现原理
synchronized对对象进行加锁,在JVM中,对象在内存中分为三个区域,对象头,实例数据和对齐填充。在对象头中保存了锁的标志位和指向monitor对象的内存地址
synchronized属于独占式悲观锁,是通过JVM隐式实现的,加锁的过程就是竞争monitor对象的过程,当线程进入字节码monitorEnter指令之后,线程将持有monitor对象,执行monitorExit时,释放monitor对象,当其他线程没有获取到,monitor对象时,则需要阻塞等待获取该对象。
reentrantLock是Lock的默认实现方式之一,它是基于AQS(队列同步器)实现的,它的默认是通过非公平锁实现,在他内部有一个state状态字段用于表示锁是否被占用,如果是0表示锁未被占用,此时线程就可以把state改为1,并成功获取锁,而其他未获得锁的线程只能去排队等待获取锁资源。
3.synchronized与reentrantLock区别:
- synchronized是JVM隐式实现的,而reentrantLock是java语言提供的API
- ReentrantLock可设置为公平锁,而synchronized不行
- reentrantLock只能修饰代码块,而synchronized可用于修饰方法,类,代码块
- reentrantLock需要手动加锁和释放锁,如果忘记释放锁,会造成资源被永久占用,而synchronized无需手动释放锁
- Lock必须配对,否者会造成死锁(例如加了两个锁,就必须解两次锁)
4.synchronized锁的是谁?
我们可以从synchronized加索的位置分为对象锁和类锁
1.synchronized加在普通方法前或者synchronized(this)锁的是当前实例对象,如果多线程访问,多线程持有多个对象,每个线程控制自己的对象锁,线程间异步执行,互不影响。
2.synchronized(类名.class)和加载静态方法前,金泰代码块,锁的是当前整个class,对多个线程持有一个类锁,排队执行。
一般能缩小代码段范围尽量缩小,能在代码片段上加索就不要在整个方法上加索,缩小锁的粒度。
5.jdk1.6synchronized的优化
锁升级
无锁=》偏向锁=》轻量级锁=》适应性自旋锁=》重量级锁偏向锁:是指在无竞争的状态下设置的一种锁状态;偏向锁会偏向于第一个获取它的线程。当锁对象第一次被获取到之后,会在此对象头中设置01的标识,标识偏向锁模式,并在对象头中记录下此线程的ID,如果是持有偏向锁的线程再次进入的话,不再进行任何同步操作,直到另一线程尝试获取此锁的时候,偏向锁才会结束,偏向锁可以提高带有同步但无竞争的程序性能。
偏向锁:偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能
- 偏向锁的升级:当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID,因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致,如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。
- 轻量级锁:轻量级锁是相对重量级锁而言的,在JDK1.6之前synchronized是通过操作系统的互斥量来实现的,这种实现方式需要在用户态和核心态之间切换,有很大性能消耗,这种传统实现锁的方式称为重量级锁;而轻量级锁是通过CAS来实现的,它对比的是线程和对象的Mark Word(对象头中的一个区域),如果更新成功则表示当前线程拥有此锁;如果失败,虚拟机会先检查对象的Mark Work是否指向当前线程的栈帧,如果是则表示当前线程已经拥有此锁,否则,则说明此锁已经被其他线程占用,当两个以上线程争抢此锁时,轻量级锁就膨胀为重量级锁,这就是锁升级的过程;锁膨胀后当虚拟机发现有空闲的monitor对象,会进行锁降级。
- 锁消除
- 锁粗化
6.锁问题
用synchronized修饰的普通方法,锁对象是方法的调用者
用synchronized修饰的静态方法,锁住的是Class唯一的一个模板
ThreadLocal(线程局部变量)
ThreadLocal为每一个线程提供一个变量的剧本。每个线程可以单独访问自己的剧本,而不与其他线程共享,当线程结束后GC
实现原理:ThreadLocal类中有一个map (ThreadLocalMap),key为线程对象,value为对应副本值。
1.ThreadLocal常用方法:
1 . public T get() 获取当前线程的副本变量值。
2 . public void set(T value) 保存当前线程的副本变量值。
3 . public void remove() 移除当前前程的副本变量值。
4 . public static ThreadLocal withInitial(Supplier supplier) 初始化变量
生产者消费者问题
生产者和消费者问题是线程模型中的经典问题:生产者和消费者在同一时间段内共用同一个存储空间,生产者往存储空间中添加产品,消费者从存储空间中取走产品,当存储空间为空时,消费者阻塞,当存储空间满时,生产者阻塞。
- 1.传统模式wait()和notify()方法的实现
public class pcDemo01 {
public static void main(String[] args) {
final Data date = new Data();
new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 10; i++) {
try {
date.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"A").start();
new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 10; i++) {
try {
date.decement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"B").start();
}
}
class Data {
private int number = 0;
public synchronized void increment() throws InterruptedException {
while (number != 0) {
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName() + "==>" + number);
this.notifyAll();
}
public synchronized void decement() throws InterruptedException {
while (number == 0) {
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName() + "==>" + number);
this.notifyAll();
}
}
- JUC版生产者消费再模式(Lock实现)
public class ProducerConsumeDemo {
public static void main(String[] args) {
final Data date = new Data();
new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 10; i++) {
try {
date.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"A").start();
new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 10; i++) {
try {
date.decement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"B").start();
new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 10; i++) {
try {
date.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"C").start();
new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 10; i++) {
try {
date.decement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"D").start();
}
}
class Data {
private int number = 0;
Lock lock = new ReentrantLock();
Condition condition =lock.newCondition();
// condition.await(); 等待
// condition.signalAll(); 唤醒全部
public void increment() throws InterruptedException {
try {
lock.lock();
while (number != 0) {
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName() + "==>" + number);
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void decement() throws InterruptedException {
try {
lock.lock();
while (number == 0) {
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName() + "==>" + number);
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
可通过创建多个Condition对象实现精准通知和唤醒
JUC
JUC指的java.util包下的三个工具类:
java.util.concurrent;
java.util.concurrent.atomic;
java.util.concurrent.locks
- java.util.concurrent包下常用类:ConcurrentHashMap ;ThreadPoolExecutor;
- java.util.concurrent.locks包下常用的类:ReentarntLock(独享锁)
- java.util.concurrent.atomic包下的常用类:AtomicInter,AtomicLong 基本数据类型的原子类
1.JUC下集合问题
(1).解决Arrlist在并发情况下异常问题
ArryList线程不安全,在并发情况下对ArryList进行操作会抛出java.util.ConcurrentModificationException异常(并发修改异常)
public class ListTest {
public static void main(String[] args) {
final List<String> list = new ArrayList<String>();
for (int i = 1; i <= 10; i++) {
new Thread(new Runnable() {
public void run() {
list.add(UUID.randomUUID().toString().substring(0, 5));
System.out.println(list);
}
},String.valueOf(i)).start();
}
}
}
1.可以用Vector代替Arrylist
2.用Collection工具类下的synchronizedList方法(Collections.synchronizedList)
3.使用CopyOnWriteArrayList代替ArryList
CopyOnWriteArrayList属于juc包下
CopyOnWrite写入时复制, 在写入的时候避免覆盖造成数据问题
CopyOnWrite比Vector厉害之处在于Vector底层使用synchronized修饰,效率低,CopyOnWrite底层使用的Lock锁。
(2).阻塞队列
Javan内存模型(JMM)
1.什么是JMM
JMM即为JAVA 内存模型(java memory model)。因为在不同的硬件生产商和不同的操作系统下,内存的访问逻辑有一定的差异,结果就是当你的代码在某个系统环境下运行良好,并且线程安全,但是换了个系统就出现各种问题。Java内存模型,就是为了屏蔽系统和硬件的差异,让一套代码在不同平台下能到达相同的访问结果
2.内存划分
JMM规定了内存主要分为主内存和工作内存两种。此处的主内存和工作内存跟JVM内存划分(堆,栈)是在不通层次上进行的。
3.内存交互操作
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的
- lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
- unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
- use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
- assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
- store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
- write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
volatile关键字
volatile详细描述地址
volatile是java虚拟机提供轻量级的同步机制。
1.保证可见性
当某个线程用volatile修饰某个变量被修改时,JMM会强制将这个修改更新到主内存中,并且让其他线程工作内存中存储的副本失效
2.不保证原子性
volatile变量并不能保证其操作的原子性
3.禁止指令重排
指令重排是指JVM在编译Java代码的时候,或者CPU在执行JVM字节码的时候,对现有的指令顺序进行重新排序。重排序在单线程下一定能保证结果的正确性,但是在多线程环境下,可能发生重排序,影响结果
指令重排的目的是为了在不改变程序执行结果的前提下,优化程序的运行效率。需要注意的是,这里所说的不改变执行结果,指的是不改变单线程下的程序执行结果。
Volatile是一个特殊的修饰符,只有成员变量才能使用它,当一个共享变量被Volatile修饰时,他会保证修改的值会立即被更新到主内存,当有线程需要读取时,它回去内存之读取新值
所有有变量都存储在主内存中,每个线程都有自己的工作内存,线程不能直接读写主内存中的数据,所以操作都在工作内存中进行
内存间的交互操作有很多,和volatile有关的操作有:
- read(读取):作用于主内存变量,把一个主内存变量传输到线程的工作内存中,以便随后的load使用
- load(载入):作用于工作 内存变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
- use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,当虚拟机遇到一个需要使用变量的字节码指定时,将会执行这个操作
CAS
CAS (Compare And Swap),即比较并交换;==》(比较当前工作内存中的值和主内存中的值;如果这个值是我们期望的值就执行操作。)
CAS是乐观锁的一种实现方式,是一种轻量级锁,线程在读取数据时不进行加锁,在准备写数据时,比较原始值是否修改,若未被修改则写回,若已被修改,则重新执行读取流程。
CAS容易出现ABA问题:
例如:如果线程T1读取A之后,发生两次写入,先是线程T2写回了B再又线程T3写回了A,此时线程T1在写回比较时,值还是A,就无法判断是否发生过修改。
ABA问题不一定影响结果,但需要防范,解决办法可以增加额外标志或者时间戳,引入原子引用JUC工具包提供了这样的类
可重入锁
1.synchronized版
public class Test01 {
public static void main(String[] args) {
final Phone phone = new Phone();
new Thread(new Runnable() {
@Override
public void run() {
phone.sms();
}
},"A").start();
new Thread(new Runnable() {
@Override
public void run() {
phone.sms();
}
},"B").start();
}
}
// synchronized可重入锁
class Phone {
public synchronized void sms() {
System.out.println(Thread.currentThread().getName()+"==》sms");
call();
}
public synchronized void call() {
System.out.println(Thread.currentThread().getName()+"==》call");
}
}
3.Lock版
public class Test01 {
public static void main(String[] args) {
final Phone phone = new Phone();
new Thread(new Runnable() {
@Override
public void run() {
phone.sms();
}
},"A").start();
new Thread(new Runnable() {
@Override
public void run() {
phone.sms();
}
},"B").start();
}
}
// synchronized可重入锁
class Phone {
ReentrantLock lock = new ReentrantLock();
public void sms() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"==》sms");
call();
}finally {
lock.unlock();
}
}
public void call() {
lock.lock();
try{
System.out.println(Thread.currentThread().getName()+"==》call");
}finally {
lock.unlock();
}
}
}