什么是线程
什么是进程
- 正在进行的程序,是系统进行资源分配的基本单位
- 目前操作系统都是支持多进程,可以同时执行多个进程,通过进程ID(PID)区分
- 单核CPU在同一时刻,只能运行一个进程;宏观并行、微观串行
什么是线程
- 线程,又称轻量级进程;进程中的一条执行路径,也是CPU的基本调度单位
- 一个进程由一个或多个线程组成,彼此间完成不同的工作,同时执行,称为多线程
- 迅雷是一个进程,当中的多个下载任务即是多个线程。(真正负责代码执行的是线程)
- Java虚拟机是一个进程,当中默认包含主线程(main),可通过代码创建多个独立线程,与main并发执行
进程和线程的区别
- 进程是操作系统资源分配的基本单位,而线程是CPU的基本调度单位
- 一个程序运行后至少有一个进程
- 一个进程可以包含多个线程,但是至少需要有一个线程,否则这个进程是没有意义
- 进程间不能共享数据段地址,但同进程的线程之间可以
知乎大佬
类似进程是资源分配的最小单位,线程是CPU调度的最小单位这样的回答感觉太抽象,都不太容易让人理解。
做个简单的比喻:进程=火车,线程=车厢
- 线程在进程下行进(单纯的车厢无法运行)
- 一个进程可以包含多个线程(一辆火车可以有多个车厢)
- 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
- 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
- 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
- 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
- 进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
- 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-“互斥锁”
- 进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量”
作者:biaodianfu
链接:https://www.zhihu.com/question/25532384/answer/411179772
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
线程的组成
- 任何一个线程都具有基本的组成部分:
- CPU时间片:操作系统(OS)会为每个线程分配执行时间
- 运行数据:
- 堆空间:存储线程需使用的对象,多个线程可以共享堆中的对象
- 栈空间:存储线程需使用得局部变量,每个线程都拥有独立的栈
- 线程的逻辑代码
线程的特点
- 线程抢占式执行
- 效率高
- 可防止单一线程长时间独占CPU
- 在单核CPU中,宏观上同时执行,微观上顺序执行
创建线程
- 创建线程三种方式
- 通过继承Thread类,重写Thread的run()方法,将线程运行的逻辑放在其中
- 通过实现Runnable接口,实例化Thread类
- 实现Callable接口
-
继承Thread类(main方法为主线程,其中实现的线程为子线程;谁抢到CPU谁执行)
- 继承Thread类
- 覆盖run()方法
- 创建子类对象
- 调用start()方法
- 获取和修改线程名称
- 获取线程ID和线程名称
- 在Thread的子类中调用this.getId()或this.getName()(必须继承Thread类)
- 使用Thread.currentThread().getId()和Thread.currentThread().getName()(Thread.currentThread()获取当前线程)
- 修改线程名称
- 调用线程对象的setName()方法(必须在启动之前修改线程名称)
- 使用线程子类的构造方法赋值(推荐)
- 获取线程ID和线程名称
-
实现Runnable接口
- 实现Runnable接口
- 覆盖run()方法
- 创建实现类对象 1、2、3不可用匿名内部类实现
- 创建线程对象
- 调用start()方法
线程的状态
-
(New 初始状态)线程对象被创建,即为初始状态。只是在堆中开辟内存,与常规对象无异
调用start()
-
(Ready 就绪状态)调用start()之后,进入就绪状态。等待OS选中,并分配时间片
OS选中->
<-时间片到期
-
(Running 运行状态) 获得时间片之后,进入运行状态,如果时间片到期,则回到就绪状态
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I6cgFH3y-1608218745974)(C:\Users\wenwen\AppData\Roaming\Typora\typora-user-images\image-20201113121651405.png)]
线程常见方法
- 睡眠
- public static void sleep(long millis)
- 当前线程主动休眠millis毫秒
- 让步
- public static void yield()
- 当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片、
- 可以尽可能的保证线程的交替输出
- 加入
- public final void join()
- 谁调用该方法,则加入该线程,并阻塞当前线程,直到加入线程执行完毕 ,当前线程继续执行
- 优先级
- 线程对象.setPriority()
- 线程优先级为1-10,默认为5,优先级越高,表示获取CPU机会越多
- 守护线程
- 线程对象.setDaemon(true);设置为守护线程
- 线程有两类:用户线程(前台线程)、守护线程(后台线程)
- 如果程序中所有前台线程都执行完毕了,后台线程会自动结束
- 垃圾回收器线程属于守护线程
线程安全
- ABA问题
- 多线程安全问题
- 当多线程并发访问临界资源时,如果破坏了原子操作,可能会造成数据不一致
- 临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可保证其正确性
- 原子操作:不可分割的多部操作,被视作一个整体,其顺序和步骤不可打乱或缺省
在程序应用中,如何保证线程的安全性?(同步)
-
同步方式1
-
同步代码块:
synchronized(临界资源对象){ //对临界资源对象加锁
//代码(原子操作)
}
-
注意:
- 每个对象都有一个互斥锁标记,用来分配给线程的
- 只有拥有对象互斥锁标记的线程,才能进入对该对象加锁的同步代码块
- 线程退出同步代码块时,会释放相应的互斥锁标记
-
-
同步方式2
-
同步方法:
synchronized 返回值类型 方法名称(形参列表0){
//对当前执行方法对象(this)加锁
//如果时静态方法 synchronized static 返回值 方法名(){} 锁的时该类
//代码(原子操作)
}
-
注意:
- 只有拥有对象互斥锁标记的线程,才能进入该对象加锁的同步方法中
- 线程退出同步方法时,会释放相应的互斥锁标记
-
同步规则
- 注意:
- 只有在调用包含同步代码块的方法,或者同步方法时,才需要对象的锁标记
- 如果用不包含同步代码块的方法,或普通方法时,则不需要锁标记,可直接调用
- 已知JDK中线程安全的类:
- StringBuffer
- Vector
- Hashtable
- 以上类中的公开方法,均为synchonized修饰的同步方法
经典问题
死锁
-
当第一个线程拥有A对象锁标记,并等待B对象锁标记,同时第二个线程拥有B对象所标记,并等待A对象所标记时,产生死锁
-
一个线程可以同时拥有多个对象的锁标记,当线程阻塞时,不会释放已经拥有的锁标记,由此可能造成死锁
-
死锁的四个条件;
1.互斥条件:一个资源每次只能被一个进程使用。
2.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
3.不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
4.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
生产者、消费者:
- 若干个生产者在生产产品,这些产品将提供给若干个消费者去消费,为了使生产者和消费者能并发执行,在两者之间设置一个能存储多个产品的缓冲区,生产者将生产的产品放入缓冲区中,消费者从缓冲区中取走产品进行消费,显然生产者和消费者之间必须保持同步,即不允许消费者到一个空的缓冲区中去产品,也不允许生产者向一个满的缓冲区中放入产品
package com.productconsume;
public class Bread {
private int id ;
private String productName;
public Bread() {
}
public Bread(int id, String productName) {
this.id = id;
this.productName = productName;
}
public int getId() {
return id;
}
public String getProductName() {
return productName;
}
@Override
public String toString() {
return "Bread{" +
"id=" + id +
", productName='" + productName + '\'' +
'}';
}
}
package com.productconsume;
public class BreadCon {
//存放面包的数组
private Bread[] cons= new Bread[5];
//存放面包的位置
private int index = 0;
//存放面包
public synchronized void input(Bread b) throws InterruptedException {
//判断容器有没有位置
while (index>=5)
this.wait();
cons[index] = b;
System.out.println(Thread.currentThread().getName()+ "生产了:"+b.getId());
index++;
//唤醒消费者
this.notifyAll();
}
//取出面包
public synchronized void output() throws InterruptedException {
while (index<=0)
this.wait();
index--;
Bread b = cons[index];
System.out.println(Thread.currentThread().getName()+ "消费了:"+b.getId()+ " 生产者:"+b.getProductName());
cons[index] = null;
//唤醒生产者
this.notifyAll();
}
}
package com.productconsume;
public class Product implements Runnable{
private BreadCon con;
public Product(BreadCon con){
super();
this.con = con;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
con.input(new Bread(i,Thread.currentThread().getName()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
package com.productconsume;
public class Consume implements Runnable{
private BreadCon con;
public Consume(BreadCon con) {
super();
this.con = con;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
con.output();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
package com.productconsume;
public class Test {
public static void main(String[] args) {
//容器
BreadCon con = new BreadCon();
//生产和消费
Product prodcut = new Product(con);
Consume consume = new Consume(con);
//创建线程对象
Thread chenchen = new Thread(prodcut, "晨晨");
Thread bingbing = new Thread(consume, "冰冰");
Thread liangliang = new Thread(prodcut, "亮亮");
Thread xiaoxiao = new Thread(consume, "小小");
//启动线程
chenchen.start();
bingbing.start();
liangliang.start();
xiaoxiao.start();
}
}
线程通信
-
等待:
-
public final void wait()
-
public final void wait(long timeout)
-
必须在对obj加锁的同步代码块中。在一个线程中,调用obj.wait()时,此线程会释放其拥有的所有锁标记。同时此线程阻塞在o的等待队列中。释放锁,进入等待队列
-
-
通知:
- public final void notify()
- public final void notifyAll()
小结
- 线程的创建:
- 方式1:继承Thread类
- 实现Runnable接口(一个任务Task),传入给Thread对象并执行
- 线程安全:
- 同步代码块:为方法中的局部代码(原子操作)加锁
- 同步方法:为方法中的所有代码(原子操作)加锁
- 线程间的通信:
- wait()/wait(long timeout):等待
- notify()/notifyAll():通知
线程池
线程池概念
- 问题:
- 线程是宝贵的内存资源、单个线程约占1MB空间,过多分配易造成内存溢出
- 频繁的创建及销毁线程会增加虚拟机回收频率、资源开销,造成程序性能下降
- 线程池:
- 线程容器,可设定线程分配的数量上限
- 将预先创建的线程对象存入池中,并重用线程池中的线程对象
- 避免了频繁的创建和销毁
- 将任务提交给线程池,由线程池分配线程、运行任务,并在当前任务结束后复用线程
线程池的创建
-
Executor:线程池的根接口,execute()
-
ExecutorService:接口包含管理线程池的一些方法,submit(Runnable\ Callable)提交 shutdown()关闭
- ThreadPoolExecutor实现类
- ScheduledThreadPoolExecutor子类
- ThreadPoolExecutor实现类
-
Executors:创建线程池的工具类
- 创建固定线程个数线程池
- 创建缓存线程池,
- 创建单线程池
- 创建调度线程池 调度:周期、定时执行
Callable接口
-
public interface Callable{
public V call() throws Exception;
}
-
与Runnable接口类似,实现之后代表一个线程任务
-
Callable具有泛型返回值、可以声明异常
-
基本使用
public class Demo1 { public static void main(String[] args) throws Exception { //功能需求:使用Callable实现1-100的和\ //1、创建Callable对象 Callable<Integer> integerCallable = new Callable<Integer>(){ @Override public Integer call() throws Exception { System.out.println(Thread.currentThread().getName()+"计数开始"); int sum = 0; for (int i = 1; i <= 10; i++) { sum += i; Thread.sleep(200); } return sum; } }; //2、把Callable对象转成可执行任务 FutureTask<Integer> integerFutureTask = new FutureTask<>(integerCallable); //3、创建线程 Thread thread = new Thread(integerFutureTask); //4、启动线程 thread.start(); //5、获取结果 Integer integer = integerFutureTask.get(); System.out.println("结果是:"+ integer); } }
-
结合线程池:
public class Demo2 { public static void main(String[] args) throws Exception { //1、创建线程池 ExecutorService es = Executors.newCachedThreadPool(); //2、提交任务 Future:表示将要执行任务的结果 Future<Integer> future = es.submit(new Callable<Integer>() { @Override public Integer call() throws Exception { System.out.println(Thread.currentThread().getName()+"开始:"); int sum = 0; for (int i = 1; i <= 10; i++) { sum += i; Thread.sleep(200); } return sum; } }); Future<Integer> future2 = es.submit(new Callable<Integer>() { @Override public Integer call() throws Exception { System.out.println(Thread.currentThread().getName()+"开始:"); int sum = 0; for (int i = 11; i <= 20; i++) { sum += i; Thread.sleep(200); } return sum; } }); //3、获取任务结果 System.out.println(future.get()); System.out.println(future2.get());; //4、关闭线程池 es.shutdown(); } }
Future接口
- Future:表示将要完成任务的接口
- 表示ExecutorService.submit()所返回的状态结果,就是call()的返回值
- 方法:V get()以阻塞形式等待Future中的异步处理结果(call()的返回值)
什么是同步?什么是异步?
- 同步:
- 形容一次方法调用,同步一旦开始,调用者必须等待该方法返回,才能继续
- 单挑执行路径
- 异步
- 形容一次方法调用,异步一旦开始,像是一次消息传递,调用者告知以后立刻返回。二者竞争时间片,并发执行
- 多条执行路径
Lock接口
- JDK5加入,与synchronized比较,显示定义,结构更灵活
- 提供更多实用性方法,功能更强大、性能更优越
- 常用方法:
- void lock()//获取锁,如锁被占用,则等待
- boolean tryLock()//尝试获取锁(成功返回true。失败返回false,不阻塞)
- void unlock()//释放锁
重入锁(Lock实现类)
- ReentrantLock:Lock接口的实现类,与synchronized一样具有互斥锁功能
- 步骤:
- 创建重入锁对象
- 显示开启锁
- 显示释放锁
读写锁
- ReentrantReadWriteLock:
- 一种支持一写多读的同步锁,读写分离,可分别分配读锁、写锁
- 支持多次分配读锁,使用多个读操作进行并发执行
- 互斥规则:
- 写-写:互斥,阻塞
- 读-写:互斥,读阻塞写、写阻塞读
- 读-读:不互斥、不阻塞
- 在读操作远远高于写操作的环境中,可在保障线程安全的情况下,提高运行效率
- 步骤:
- 创建读写锁
- 获取读锁
- 获取写锁
- 写读取方法
- 上锁
- 解锁
- 写写入方法
- 上锁
- 解锁
- 效率:
- 互斥锁运行时间20s,读写锁运行时间3s(2次写,18次读)
线程安全集合
- Collection体系集合中,除Vector以外的线程安全集合
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TrFLYpUV-1608218745977)(C:\Users\wenwen\AppData\Roaming\Typora\typora-user-images\image-20201115104827498.png)]
- Map安全集合
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r3IO5oTr-1608218745978)(C:\Users\wenwen\AppData\Roaming\Typora\typora-user-images\image-20201115105459290.png)]
Collections工具类中提供了多个可以获得线程安全集合的方法
-
public static Collection synchronizedCollection(Collection c)
-
public static List synchronizedList(List list)
-
public static Set synchronizedSet(Set s)
-
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)
-
public static SortedSet synchronizedSortedSet(SortedSet s)
-
public static <K,V> SortedMap<K,V> synchronizedSortedMapt(SortedMap<K,V> m)
-
JDK1.2提供,接口统一、维护性高,但性能没有提升,均以synchronized实现
-
现在不常用,性能不高
CopyOnWriteArrayList
- 线程安全的ArrayList,加强版的读写分离
- 写有锁,读无锁,读写之间不阻塞,优于读写锁
- 写入时,先copy一个容器副本、在添加新元素,最后替换引用
- 缺点:浪费空间
- 使用方式与ArrayList无异
CopyOnWriteArraySet(有序)
- 线程安全的Set,底层使用CopyOnWriteArrayList实现
- 唯一不同在于,使用addIfAbsent()添加元素,会遍历数组
- 如存在元素,则不添加(扔掉副本)
- 重复依据:equals方法
Queue接口(队列)
- Collection的子接口,表示队列FIFO(First In First Out)先进先出
- 常用方法:
- 抛出异常:
- boolean add(E e)//顺序添加一个元素(到达上限后,再添加则会抛出异常)
- E remove()//获得第一个元素并移除(如果队列没有元素时,则抛出异常)
- E element()//获得第一个元素但不移除(如果队列没有元素时,则抛出异常)、
- 返回特殊值:推荐使用
- boolean offer(E e)//顺序添加一个元素(到达上限后,再添加则会返回false)
- E poll()//获得第一个元素并移除(如果队列没有元素时,则返回null)
- E peek()//获得第一个元素但不移除(如果队列没有元素时,则返回null)
- 抛出异常:
ConcurrentLinkQueue
- 线程安全、可高效读写的队列,高并发下性能最好的队列
- 无锁、CAS比较交换算法(硬件所支持的算法),修改的方法包含三个核心参数(V,E,N)
- V:要更新的变量、E:预期值、N:新值
- 只有当V==E时,V=N;否则表示已被更新过,则取消当前操作
BlockingQueue接口(阻塞队列)
-
Queue的子接口,阻塞的队列,增加了两个线程状态为无期限等待的方法
-
方法:
- void put (E e )//将指定元素插入此队列中,如果没有可用空间,则等待
- E take()//获取并移除此队列头部元素,如果没有可用元素,则等待
-
实现类:
- ArrayBlockQueue:
- 数组结构实现,有界队列(手工固定上限)
- LinkedBlockingQueue:
- 链表结构实现,有界队列(默认上限Integer.MAX_VALUE)
- ArrayBlockQueue:
-
使用阻塞队列实现生产者和消费者
public class Demo1 {
public static void main(String[] args) {
//1、创建队列
ArrayBlockingQueue<Integer> integers = new ArrayBlockingQueue<>(5);
//2、创建两个线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
try {
integers.put(i);
System.out.println(Thread.currentThread().getName()+"生产了"+i+"号面包");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"龙龙");
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
try {
Integer take = integers.take();
System.out.println(Thread.currentThread().getName()+"消费了"+take+"号面包");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"丽丽");
thread.start();
thread2.start();
}
ConcurrentHashMap
- 初始容量默认为16段(Segment),使用分段锁设计
- 不对整个Map加锁,而是为每个Segment加锁
- 当多个对象存入同一个Segment时,才需要互斥
- 最理想状态为16个对象分别存入16个Segment,并行数量16
- 使用方法与HashMap无异
- JDK1.8之后采用CAS
总结
- ExecutorService线程池接口、Executors工厂
- Callable线程任务、Future异步返回值
- Lock、ReentrantLock重入锁、ReentrantReadWriteLock读写锁
- CopuOnWriteArrayList线程安全的ArrayList
- CopyOnWriteArraySet线程安全的Set
- ConCurrentLinkedQueue线程安全的Queue
- ArrayBlockingQueue线程安全的阻塞Queue(生产者、消费者)
- ConcurrentHashMap线程安全的HashMap