JAVA线程

线程基础

线程的基本概念

进程与线程的区别

进程:正在运行的程序,是操作系统分配系统资源(CPU、内存)的最小单位

线程:进程是由多个线程组成的,是操作系统调度CPU的最小单元,每个线程可以单独执行指令

进程更加重量级,操作系统创建和销毁进程需要更多的时间和资源,进程的相互通信更加复杂

线程更加轻量级,操作系统创建和销毁线程消耗时间和资源更少,同一个进程的线程可以共享内存空间,通信更容易

为什么要多线程?

  • 压榨CPU资源,执行高性能运算

  • 同时执行多个程序指令,相互不会影响

  • 服务器可以同时服务多个用户,互不影响

并行与并发

并发:同时执行多个任务,一个CPU内核会在多个线程间来回切换执行程序指令,不是真正同时执行

并行:同时执行多个任务,多个CPU内核,一个内核执行一个线程,线程中指令是同时执行的

同步与异步

同步:多个指令是排队执行的,效率比较低

异步:多个指令同时执行(借助线程),效率比较高

线程的实现方法

Java的实现有4种:

1、继承Thread类

1) 继承Thread类

2) 重写run方法

3) 调用start启动线程

2、实现Runnable接口

1) 实现Runnable接口

2) 实现run方法

3) 创建Thread对象,传入Runnable对象

4) 调用start启动

3、实现Callable接口

前两种都实施run方法,没有返回值,Callable接口的方法有返回值

1) 实现Callable接口

2) 实现call方法,返回结果

3) 创建FutureTask对象,传入Callable对象

4) 创建Thread对象,传入FutureTask对象

5) 调用Thread的start方法

6) 通过FutureTask的get方法获得返回结果

4、通过线程池创建

线程的生命周期

  • 新建

  • 就绪

  • 运行

  • 阻塞

  • 死亡

线程的常用方法

方法名说明
* start()启动线程
stop()停止线程,可能导致重要资源无法释放,出现死锁等问题
interrupt()中断线程,可以配合异常处理停止线程
* run()执行线程的核心指令
setName(String)设置名字
getName()获得名字
* sleep(long)Thread的静态方法,让当前线程睡眠一定时间(毫秒)
* setPriority(int)设置优先级,从低到高,1到10,优先级高的线程抢占cpu几率更高
yield()放弃占用CPU一会,马上回到就绪状态
suspend()禁用,当前线程挂起(暂停)
resume()禁用,当前线程恢复
* setDaemon(boolean)设置后台线程,默认是false
join()合并其它线程,让其它线程先执行完,再执行自己的代码

如何停止线程?

  • stop 禁用,可能导致重要资源无法释放,出现死锁等问题
  • 等待run方法执行结束
  • 在run方法加入条件,中途停止run方法
  • 执行interrupt方法,进行异常处理的时候停止线程

什么是后台线程?

后台线程是一种特殊线程,这种线程是为其它线程服务的,如果没有存活的其它线程,后台线程会自动死亡

后台线程主要应用场景:GC 垃圾收集器 就是一种后台线程

sleep和wait的区别

都可以让线程进入阻塞状态

区别:

  • 调用对象不同:sleep是线程调用的,wait是锁对象(Object)调用的

  • 释放锁不同:sleep不会让线程释放锁,wait会让线程释放锁

  • 唤醒机制不同:sleep只能等睡眠时间结束,wait可以等时间结束也可以通过notify方法唤醒

线程安全

并发编程的三大特性

  • 原子性:线程执行的程序指令能完整的执行,全部执行,或全部不执行

  • 可见性:对于某个数据,某个线程进行的修改,其它所有线程都可见

  • 有序性:线程中的程序指令是按最初编写的顺序执行的

原子性

线程安全问题:多线程的调度是抢占式的,无法保证程序代码能完整执行,则会出现数据不一致的问题

解决线程安全问题:通过上锁机制

synchronized关键字

可以给方法或代码上锁,让线程有序的完整的执行

方法上锁(同步方法),当第一个线程执行方法时,持有锁,其它线程无法进入方法,线程执行完后,自动释放锁,其它线程才能进入

public synchronized 返回值 方法名(...){
   
}

代码上锁(同步代码块)

public 返回值 方法名(...){
   synchronized(锁对象){
       代码块....
   }
}

任何成员变量对象都可以作为锁对象

  • 可以创建新对象作为锁

  • 成员方法还可以使用this作为锁

  • 静态方法可以使用 类名.class 作为锁

同步方法如果是非静态的,默认将this作为锁,如果是静态,默认将类名.class作为锁

synchronized能保证线程安全,但会带来性能上的损失

为什么synchronized会降低性能

synchronized属于互斥锁,一个线程持有锁时,会阻塞其它线程

线程的两个状态:

  • 用户态

    JVM能够管理的状态

  • 内核态

    JVM不能管理,由操作系统管理

上下文切换

线程在抢占资源时,发现资源上锁,线程从用户态转为内核态进行等待,线程获得锁,重新执行前会从内核态转换为用户,转换过程会降低性能,

切换的过程中需要保存或读取程序计数器的代码行数和寄存器的数据,比较消耗时间

synchronized的优化

jdk1.6对synchronized关键字进行了优化

  • 锁消除

    如果jvm发现同步方法或同步块中没有线程竞争的资源,会消除锁

  • 锁膨胀

    如果jvm发现在大量循环中使用锁,会优化将锁放到循环外部

  • 锁升级

    锁的状态:

    • 无锁

      没有任何线程竞争情况下,不会加锁

    • 偏向锁

      如果只有一个线程使用锁,锁会在对象头中记录线程的id,如果是这个线程就直接放行

    • 轻量级锁

      出现少量竞争情况下,会通过CAS乐观锁机制进行线程的调度,不会出现上下文切换,会出现自旋等待(消耗cpu)

    • 重量级锁

      出现大量竞争情况下,会转换为重量级锁(互斥锁),线程出现上下文切换

    synchronized在1.6后,上锁的过程叫锁升级: 无锁 ---> 偏向锁 ---> 轻量级锁 ---> 重量级锁

    只能升不能降级

synchronized的原理

自动上锁和释放锁实现的原理,一旦给方法或代码块加synchronized,JVM会启动Monitor监视器监控上锁的代码,线程进入后,监视器中计数器加1,其它线程进入时,监视器的计数器不为0,就不允许其它线程进入,线程执行完代码后,计数器减1,监视器再让其它线程进入

ReentantLock 类

是java.util.concurrent.lock 包提供工具类

ReentantLock 重入锁(递归锁)

重入锁: 发生方法递归情况下,持有锁的线程,可以重新持有该锁

非重入锁:方法递归的情况,持有锁的线程,不能重新持有该锁

创建方法1:

ReentantLock lock = new ReentrantLock();

创建方法2:

ReentantLock lock = new ReentrantLock(true/false);

布尔值由于指定该锁是公平或非公平锁,true公平,false非公平(默认)

公平锁: 会维护等待线程的队列,锁释放后,优先让等待时间长的线程拿到锁,降低线程的饥饿,也会降低程序的效率

非公平锁: 所有线程都去抢锁,谁抢到谁执行,有的线程会一直饥饿,效率高

使用方法:需要手动上锁和释放锁

lock.lock(); //上锁
try{
    上锁的业务代码
}finally{
    lock.unlock(); //释放锁
}

主要方法

方法名作用
lock()上锁
unlock()释放锁
getQueuedLength()获得公平锁线程排队长度
boolean tryLock()上锁并获得上锁是否成功
boolean tryLock(long,TimeUnit)在一定时间内上锁并获得上锁是否成功
Condition newCondition()获得条件对象
boolean isFair()是否公平锁

synchronized 和 ReentrantLock的区别

1) 上锁机制不同:synchronized 是JVM自动上锁和释放锁,ReentrantLock需要代码手动上锁和释放锁

2) 锁的类型不同:synchronized 是非公平锁,ReentrantLock 可以设置公平锁或非公平锁

3) 性能不同:ReentrantLock高于synchronized

4) 功能不同:ReentrantLock提供非常丰富的方法,功能大大强于synchronized

5) 编程难度不同:synchronized 更加简单,ReentrantLock更复杂

可见性

对于共享资源,一个线程修改后,其它的线程可以看到修改后的状态

原因:CPU有多个内核,每个内核中都有独立的存储单元(寄存器、L1L2L3缓存),每个内核都执行线程,线程中的数据会从主内存中缓存到不同的内核中,线程修改一个内核中的数据,另一个内核不能及时修改

volatile关键字的作用:用于修饰变量,保证变量的可见性

被修饰的变量只保存在主内存中,所有线程都直接读写主内存,避免了可见性问题

有序性

程序指令是按编写的顺序执行的

JVM会对程序指令进行优化,可能导致程序指令重排序

做菜: 买菜、洗碗、洗菜、切菜、炒菜 ----> 买菜、切菜、洗菜、炒菜、洗碗

Object obj = new Object();

创建对象的过程:

1) 分配内存创建对象

2) 对属性初始化

3) 将内存地址赋值给引用

指令重排可能出现: 1) 3) 2) 可能将没有完成初始化的对象交给用户,导致问题

x //静态实例 volatile 防止指令重排
private static volatile MySingleton instance = null;

原子类

常用的原子类:

  • AtomicInteger 原子整数

  • AtomicLong 原子长整数

  • AtomicBoolean 原子布尔

AtomicInteger 用法

AtomicInteger count = new AtomicInteger(初始值);

自增: getAndIncrement() 或 IncrementAndGet()

自减: getAndDecrement() 或 DecrementAndGet()

原子类实现的原理:

使用乐观锁实现的

悲观锁,认为线程竞争比较激烈,会给代码上锁,线程会出现上下文切换,效率比较低

乐观锁,认为线程竞争比较少,不给代码上锁,线程不会出现上下文切换,效率高

Java中的乐观锁机制是CAS: 比较和交换 Compare And Swap

对变量进行修改时,先读取变量的原始值,要修改时,再读取变量当前内存中的值,如果当前值和原始值相同,就用新值覆盖原始值;如果当前值和原始值不同,就表示出现其它线程修改了该值,放弃修改。

CAS机制可能出现ABA问题:假设原始值是A,线程1将其改为B,线程2将其改为A,前面的线程发现值相同,以为没有线程并发问题出现

如何解决ABA问题:引入版本号机制,给变量加版本号,每次修改版本号加1,比较时判断原始值和当前值是否相同还要判断版本号是否改变

线程通信

线程的等待和通知

并发编程中可以通过锁对象控制线程,如:让线程等待,通知线程执行

注意:线程的等待和通知必须由锁对象完成,否则出现线程状态异常 IllegalMonitorStateException

任何对象都可以作为锁,方法是Object类定义,等待和通知必须是同一个锁对象完成

方法名说明
wait()让当前持有锁的线程等待,直到被锁的notify唤醒(自动释放锁)
wait(long)线程等待一定时间,到时候会自动唤醒
notify()随机选择一个等待的线程唤醒
notifyAll()唤醒所有等待该锁的线程

JUC工具类

java.util.concurrent 并发包

JUC下提供大量的并发开发工具类

常用的有:

  • ReentrantLock 重入锁

  • 阻塞队列

  • CountDownLatch类

  • Semaphone类

  • CyclicBarrier类

ReentrantLock的等待和通知

使用Condition接口 ,翻译为条件

创建方法

Condition condition = Lock对象.newCondition()

下面的方法必须包含在 lock的try-finally中使用

方法名说明
await()让当前持有锁的线程等待,直到被锁的singal唤醒(自动释放锁)
await(long)线程等待一定时间,到时候会自动唤醒
singal()随机选择一个等待的线程唤醒
singalAll()唤醒所有等待该锁的线程

阻塞队列

BlockingQueue 是一系列特殊的集合,这种集合会有临界值,达到临界值后自动让线程等待,也会自动唤醒线程

常用实现类:

ArrayBlockingQueue 数组结构的阻塞队列

LinkedBlockingQueue 链表结构的阻塞队列

创建

new ArrayBlockingQueue(临界值)

用法

方法说明
put(T)添加数据到末尾,达到临界值后自动阻塞线程,小于临界值后会自动唤醒线程
T take()从头部删除一个数据,如果空了自动阻塞线程,非空后会自动唤醒线程
int size()数据个数

CountDownLatch 类

作用:一个或多个线程等待其它线程工作执行完再执行自己的任务

创建:

new CountDownLatch(倒数次数)

主要方法:

方法说明
await()让当前线程等待
countDown()倒数一次,次数-1,当次数为0,自动唤醒等待的线程
int getCount()获得当前倒数次数

PS: CountDownLatch对象只能使用一次

Semaphore类

信号量,可以用于控制执行任务的数量,主要用于限流(限制并发量)

创建方法:

new Semaphore(信号量大小)
new Semaphore(信号量大小,是否公平锁)

用法

方法说明
void accquired()请求信号量,信号量会减1,为0时就会阻塞
void release()释放信号量,信号量会加1,会唤醒一个阻塞的线程

CyclicBarrier类

循环栏栅,让多个线程等待一个线程,线程准备好后一起执行,类似CountDownLatch,区别是:可以重复使用

AQS

AbstractQueuedSynchronizer 抽象队列同步器

大量并发包中的工具类使用了AQS,基于AQS实现工具类功能的开发

使用了AQS的类:

  • CountDownLatch

  • Semaphone

  • ReentrantLock

AQS是一个抽象类,用于开发并发编程的类,包含:

1) 成员变量

    /**
     * The synchronization state.
     */
    private volatile int state; //同步器状态

对于CountDownLatch来说,state就是倒数数量

对于Semaphore来说,就是信号量数

对于ReentrantLock,是上锁的状态

2) 双向链表

保存处于等待状态的线程

对于Semaphore和ReentrantLock来说,可以保存公平锁的等待顺序

线程池

线程池概述

作用:线程属于重要的系统资源,创建线程会消耗大量的服务器资源,线程池能够对线程资源进行回收,重复使用,以降低对服务器资源的消耗

相关的API:

线程池顶层的接口: Executor

主要方法: execute(Runnable) 启动线程执行任务,执行完回收线程

子接口:ExecutorService

主要方法: shutdown() 停止线程池,会让其中线程继续把任务做完

shutdownNow() 停止线程池,中断正在执行的线程

实现类:ThreadPoolExecutor 具体线程池的实现类

工具类:Executors 用于快速创建线程池

创建线程池的几种方法

使用Executors工具类创建线程池

  • ExecutorService newCachedThreadPool()

    获得长度不限的线程池

  • ExecutorService newFixedThreadPool(数量)

    获得长度固定的线程池

  • ExecutorService newSingleThreadExecutor()

    获得长度单一的线程池

  • ScheduledExecutorService newScheduledThreadPool(数量)

    获得可以调度的线程池

手动创建线程池

阿里规范要求必须手动创建线程池,不使用Executors

使用ThreadPoolExecutor类创建,构造方法的参数:

1) corePoolSize 核心线程数 ,线程池中初始的线程数量

2) maxPoolSize 最大线程数,线程池的极限

3) keepAliveTime 存活时间,允许线程空闲的时间

4) timeUnit 时间单位

5) workQueue 工作队列,阻塞队列用于保存执行任务Runnable

6) threadFactory 线程工厂,用于创建线程

7) rejectHandler 拒绝策略,处理暂时执行不了的任务

线程池的配置

1) 核心线程数,要考虑当前任务情况:是计算密集型(大量使用CPU)还是IO密集型(大量进行数据IO)

计算密集性需要充分利用CPU内核,线程数量=CPU内核,获得方法:Runtime.getRuntime().availableProcessors();

IO密集型,耗时的IO操作会造成线程长时间阻塞,需要更多的线程,线程数量=CPU内核 * n (n >=2 n的大小取决于IO操作耗时情况和请求总数) 具体通过压力测试

2) 最大线程数,可以设置和核心线程数一样,就避免频繁创建线程和销毁线程的消耗

3) 存活时间,如果核心线程数和最大线程数一样,可以设置为0,如果不一样,尽量设置长一点,避免频繁创建和销毁

4) 阻塞队列可以使用LinkedBlockingQueue,频繁添加和删除任务的效率高

线程池的原理

线程池如何回收线程?在线程执行完任务后,不死亡

ThreadPoolExecutor类中有内部类Worker代表工作线程,内部包装Thread

所有工作线程保存在HashSet

/**
* Set containing all worker threads in pool. Accessed only when
* holding mainLock.
*/
private final HashSet<Worker> workers = new HashSet<Worker>();

线程池工作原理: execute方法

1) 执行execute方法传入Runnable对象

2) 判断当前线程有没有达到核心线程数

3) 没有就添加核心线程,有再判断是否达到最大线程数

4) 达到最大值就执行拒绝策略

5) 添加线程时,创建Worker工作线程,启动工作线程

6) 执行runWorker方法,会循环调用getTask()从工作队列取任务

7) 取到任务就执行,如果任务取空,线程就被阻塞,直到有新任务

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值