那些关于线程你不知道的事

本文详细探讨了线程的相关概念,包括进程、线程分类、线程安全问题以及解决策略。讲解了线程中断、线程池的工作原理、线程同步机制如synchronized、线程局部变量ThreadLocal的使用,以及并发工具类如ReentrantLock、Semaphore、CountDownLatch和CyclicBarrier的应用。还对比分析了HashMap、HashTable和ConcurrentHashMap在多线程环境下的区别。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

冯诺依曼体系结构

    CPU和输入设备或输出设备之间不能直接交互,必须依靠中间的存储部分
内存 VS 硬盘
       1.内存的读写速度远远大于磁盘,
       内存的读写速度是纳秒级,硬盘的读写速度的是微秒级
       2.内存的价格远大于硬盘
       3.内存中的数据不能持久化,重启之后就没了,而磁盘可以持久化

进程:
一个执行的任务就是一个进程
可执行任务!=进程
PID是进程的身份ID,但重启之后PID就变了

进程的本质:

PCB:进程管理模块
    1.PID  主键id,身份标识
    2.状态信息(就绪,运行,阻塞,终止)
    3.优先级
    4.记账信息(防止CPU资源分配不均)
    5.一组指针(需要使用的资源)
    6.上下文(当分配到CPU资源时执行,没有资源之后暂时保存自己当前的状态,等待下一次执行,这个过程叫一个上下文)

程序运行的方式:
并发:只有一个资源,轮流执行就叫并发
并行:所有的应用一起运行

多个进程不能共享资源
线程是系统调度资源的最小单位
线程是进程执行的最小单位,也是进程执行的实际单位
进程是系统分配资源的最小单位,线程是系统调度的最小单位
进程不可以共享资源,而线程可以
  线程可以共享的资源
    1.打开的文件
    2.内存(对象)
  线程不可以共享的资源
	上下文  记账信息  状态不能共享  线程栈信息  优先级
注意:
线程的数量并不是越多越好,当线程的数量达到某个合适的值是最好的,
有太多的线程就会出现线程之间的争抢cpu,而CPU调度是需要消耗系统资源的,
所以不是越多越好。
那么多少线程是最好的?
答:要看具体的应用场景,密集的CPU任务,IO型任务。当使用的场景是计算型任务
时,线程的数量=CPU的数量是最好的,IO型任务理论上线程数量越多越好
进程 VS 线程
1.进程是系统分配资源的最小单位,线程是系统调度的最小单位
2.一个进程中至少要包含一个线程
3.线程必须要依附于进程,线程是进程实质工作调度的最小单位

线程分类:

  1. 后台线程【守护线程】
  2. 用户线程【默认线程】

守护线程是用来服务用户线程,没有用户线程运行进程就会结束

守护线程使用场景:Java垃圾回收器

注意事项

  1. 守护线程设置必须在调用start()前,如果在之后设置不会生效,还会报错
  2. 在守护线程里面创建的线程,默认情况全都是守护线程
start VS run:
	1.run属于普通方法,而start属于启动线程的方法
	2.run方法可以执行多次,而start只能执行一次

线程中断:

  1. 使用全局自定义的变量来终止线程(在拿到终止指令之后,需要执行完当前的任务才会真正的停止线程)
  2. 使用线程提供的终止方法 interrupt来终止线程
interrupted()   VS  isInterruped() 
interrupted() 判断当前线程的中断标志位是否设置,调用后清除标志位
isInterruped() 判断对象关联的线程的标志位是否设置,调用后不清除标志位

线程非安全:

  1. cpu抢占执行
  2. 非原子性
  3. 编译器优化(代码优化,在单线程下没问题但在多线程下就会出现混乱)
  4. 内存不可见性
  5. 多个线程修改了同一个变量

解决线程安全的方案:

  1. synchronized
  2. lock

volatile的作用:

  1. 禁止指令重排序
  2. 解决内存不可见性的问题(实现原理:当操作完变量后,强制删除线程工作内存中的此变量)

synchronized注意事项:

在进行加锁操作的时候,同一组业务一定是同一个锁对象

volatile VS synchronized
   volatile可以解决内存可见性问题和禁止指令重排,但不能解决原子性问题
   synchronized是用来保证线程安全,也就是可以解决任何关于线程安全的问题
synchronized VS lock
  1.synchronized既可以修饰代码块又可以修饰静态方法或普通方法,而lock只能修饰代码块
  2.synchronized只有非公平锁的锁策略,而lock既可以是非公平锁的锁策略也可使是公平锁的锁策略
  3.Reentrantlock更加的灵活
  4.synchronized是自动加锁和释放锁的,而lock需要自己手动加锁和释放锁

synchronized实现的原理

从操作系统来说:互斥锁
从JVM来说:帮我们实现了监视器的加锁和解锁操作
从JAVA来说:锁对象 锁存放在变量的对象头

synchronized的优化:(四种状态)
无锁、偏向锁、轻量级锁、重量级锁

线程的状态:
	new(新建状态)
	Runnable(执行状态){running、ready}
	Waiting(等待状态)
	Timed-Waiting(超时等待状态)有明确结束等待时间的sleep方法
	Blocked(阻塞状态)  当拿到锁之后会进入Runnable状态
	Timinated(终止状态)

死锁

定义:在多线程编程中,因为资源抢占而造成线程无限等待

造成死锁的四个条件(同时满足):

  1. 互斥条件(一个资源只能被一个线程持有)
  2. 请求拥有条件(当拥有有一个资源之后试图请求另外一个资源)
  3. 不可剥夺条件(一个资源在被线程拥有之后,不能被其他线程剥夺)
  4. 环路等待条件(多个线程在获取资源的时候形成了一个环形链)
    解决死锁可以通过修改请求的顺序
线程的通讯机制:一个线程的动作可以让另外一个线程感知到
    wait(休眠)/notify(唤醒)/notifyall(唤醒全部)

wait为什么要加锁?
答:wait在使用的时候需要释放锁,释放之前必须有一把锁
wait为什么要释放锁?
答:wait默认是不传值的,当不传递任何值的时候表示永久等待,这样会造成一把锁被一个线程一直持有

wait(休眠)/notify(唤醒)/notifyall(唤醒全部)使用注意事项

1.使用时必须加锁
2.加锁对象和 wait/notify/notifyall的对象必须保持一致
3.一组wait/notify/notifyall的对象必须保持一致
Thread.sleep(0)  VS  wait(0)
    1.sleep他是Thread的静态方法,而wait是object的方法
    2.sleep(0)立即触发一次CPU的资源抢占,wait(0)则永久等待下去
wait VS sleep
相同点:1.都可以让当前线程休眠
       2.都必须要处理一个Interrupt异常
不同点:1.wait来自object中的一个方法,而sleep来自thread
	   2.传参不同,wait可以没有参数而sleep必须有一个大于0的参数
	   3.wait使用时必须加锁,sleep使用时不用加锁
	   4.wait使用时会释放锁,而sleep不会释放
	   5.wait默认不传参的情况会进入waiting状态,
	     而sleep会进入Timed-waiting
为什么wait放在object而不是Thread中?
答:wait必须要加锁和释放锁,而锁是属于对象级别的而非线程级别
(线程和锁是一对多的关系,也就是一个线程可以拥有多把锁)
  为了灵活起见,就把wait放在object当中
Locksupport.park()  VS lock.wait()
    相同点:1.都可以休眠
	       2.都可以传参或者不传参,并且二者的线程状态也是一样的
    不同点:1.wait必须加锁,而Locksupport.park不需要加锁
	       2.wait只能唤醒全部或随机的一个线程,
	       而Locksupport.park可以唤醒指定的线程
面试题:创建单个线程池有什么作用?
1.可以避免频繁创建和消耗线程带来的性能开销
2.有任务队列可以存储多余的任务
3.当有大量任务不能处理的时候,可以友好的执行拒绝策略
4.线程池可以更好的管理任务

线程池的优点:

  1.避免频繁创建和消耗所带来的性能开销
  2.可以优化的拒绝任务
  3.更多的功能,可以执行定时任务

线程池的七种创建方式:

1.创建固定的线程池(任务数去向无限大)
2.创建带缓存的线程池(根据任务的数量生成对应的线程数,适用于短期大量任务)
3.创建可以执行定时任务的线程池
4.创建单个可执行的定时任务的线程池
5.创建单个线程池(a.频繁的创建和消耗  b.更好的分配和执行任务,可以将任务存放到任务队列)
6.根据当前的工作环境(cpu、任务量)生成对应的线程池;
7.ThreadPoolExecutor(解决了线程数量不可控、任务数量不可控的问题)

线程池的拒绝策略:
1.默认的拒绝策略

public class ThreadDemo37 {
    public static void main(String[] args) {
        ThreadPoolExecutor executor=new ThreadPoolExecutor(5,5,0, TimeUnit.SECONDS,new LinkedBlockingDeque<>(5),new ThreadPoolExecutor.AbortPolicy());
        for (int i = 0; i < 11; i++) {
            int finalI=i;
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("任务:"+finalI+"线程名:"+Thread.currentThread().getName());
                }
            });
        }
    }
}

在这里插入图片描述

2.使用调用线程池的线程来执行任务

public class ThreadDemo38 {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 0, TimeUnit.SECONDS, new LinkedBlockingDeque<>(5), new ThreadPoolExecutor.CallerRunsPolicy());
        for (int i = 0; i < 11; i++) {
            int finalI = i;
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("任务:" + finalI + "线程名:" + Thread.currentThread().getName());
                }
            });
        }
    }
}

在这里插入图片描述
3.忽略新任务

public class ThreadDemo39 {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 0, TimeUnit.SECONDS, new LinkedBlockingDeque<>(5), new ThreadPoolExecutor.DiscardPolicy());
        for (int i = 0; i < 11; i++) {
            int finalI = i;
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("任务:" + finalI + "线程名:" + Thread.currentThread().getName());
                }
            });
        }
    }
}

在这里插入图片描述

4.忽略老任务

public class ThreadDemo40 {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 0, TimeUnit.SECONDS, new LinkedBlockingDeque<>(5), new ThreadPoolExecutor.DiscardOldestPolicy());
        for (int i = 0; i < 11; i++) {
            int finalI = i;
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("任务:" + finalI + "线程名:" + Thread.currentThread().getName());
                }
            });
        }
    }
}

在这里插入图片描述
5.自定义策略

public class ThreadDemo41 {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 0, TimeUnit.SECONDS, new LinkedBlockingDeque<>(5), new RejectedExecutionHandler() {
            @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                System.out.println("执行了自定义拒绝策略");
            }
        });
        for (int i = 0; i < 11; i++) {
            int finalI = i;
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("任务:" + finalI + "线程名:" + Thread.currentThread().getName());
                }
            });
        }
    }
}

在这里插入图片描述
ThreadPoolExecutor的执行流程图:
在这里插入图片描述

线程池的两种执行方式:

  1. execute 执行(new Runnable) 无返回值的任务,如果有OOM异常会将异常打印到控制台;
  2. submit执行(new Runnable、new Callable) 有返回值的任务,如果有OOM异常不会把异常信息打印到控制台;

线程池的关闭:

  1. shutdown:拒绝执行新任务,等待线程池中的任务队列执行完毕之后再执行关闭操作;
  2. shutdownNow:拒绝执行新任务,不会等待任务队列中的任务执行完成,直接关闭

线程池的五种状态:

  1. RUNNING (线程池创建之后的初始状态)
  2. SHUTDOWN(该状态线程池不再接收新的任务,但是会将工作队列中的任务执行结束)
  3. STOP(该状态线程池不再接收新的任务,但是不会处理工作队列中的任务,立即中断任务)
  4. TIDYING(当线程池空时会进入该状态)
  5. TERMINATED(销毁状态)

解决线程不安全的方案:

  1. 加锁(同时带来了排队执行的问题)
  2. 使用ThreadLocal(线程级别的私有变量)

ThreadLocal的使用:

  1. set():将私有变量存储到线程中
  2. get():从线程中取得私有变量
  3. remove():从线程中移除私有变量(脏读、OOM)
  4. initialvalue:初始化
  5. withinitial:初始化
问:什么情况下不会执行initialvalue方法?为什么不会执行?
答:在执行set方法之后就不会执行了;
ThreadLocal是懒加载的,当调用了get方法之后,才会尝试执行 
initialvalue方法,尝试获取一下ThreadLocal set方法的值,
如果获取到了值,那么初始化方法永远不会执行。

ThreadLocal的使用场景:

  1. 解决线程安全问题
  2. 实现线程级别的数据传递

ThreadLocal的缺点:

  1. 不可继承(子线程中不能读取到父线程的值)
  2. 脏数据(在一个线程中读到了不属于自己的数据)
  3. 内存溢出问题(当一个线程执行完之后,不会释放这个线程所占用的内存或者释放内存不及时的情况)

脏数据的解决方案:

  1. 避免使用静态属性(静态属性在线程池中会出现复用)
  2. 使用remove解决
面试题:HashMapThreadLocalMap处理hash冲突的区别?
答:HashMap使用的是链表法,而ThreadLocalMap使用的是开放寻址法;
因为开放寻址法的特点和使用场景是数据量比较少的情况下性能更好,
而HashMap里面存储的数据通常情况下比较多,这个时候通常使用链表法;

如何提升程序的性能?

  1. 多线程
  2. 单例模式(整个程序的运行中只存在一个对象)

饿汉方式

优点:不用加锁也是线程安全的
缺点:程序启动之后就会创建,但是创建完之后有可能不被使用,从而浪费了系统资源

懒汉方式
存在线程安全的问题,需要使用双重校验锁的方式,使用volatile来修饰变量

阻塞式队列
-生产者在队列为满的情况下就会休眠
-消费者在队列为空的时候进行休眠

乐观锁与悲观锁:

  1. 乐观锁:任务在一般情况下不会发生并发冲突,所以只有在进行数据更新的时候,才会检测并发冲突,如果没有冲突则执行修改,如果有冲突则返回失败。

实现:
乐观锁的策略是基于CAS的,CAS是由V(内存值)、B(预期旧值)、A(新值)组成,然后执行的时候是使用V==A来对比,如果为true则表示没有发生并发冲突,则可以直接修改,否则不能进行修改。CAS的实现是UnSafe类,而UnSafe类调用了C++的本地方法,通过调用操作系统的Atomic::cmpxchg(原子指令)来实现CAS操作的。

CAS的底层实现原理是什么?
答:Java层面CAS的实现是UnSafe类,而UnSafe类调用了C++的本地方法,通过调用操作系统的Atomic::cmpxchg(原子指令)来实现CAS操作的。

CAS的缺点:存在ABA问题,可以使用AtomicStampedReference来解决

  1. 悲观锁:它认为通常情况下会出现并发冲突,所以在一开始就会加锁

实现:
synchronized在Java中是将锁的ID存放到对象头来实现的。

JUC包下常见的类:

  1. ReentrantLock
    (a)lock写在try之前
    (b)一定要在finally里面进行unlock()
  2. Semaphore(信号量):限流功能
    (a)acquire()方法
    (b)release()方法
  3. CountDownLatch(计数器)
    用来保障一组线程同时完成某个操作的时候,
    实现:在CountDownLatch里有一个计数器,每次调用countDown方法的时候,计数器的数量-1,直到减到0之后,就可以执行await()之后的代码。
    (a)countDown()方法
    (b)await()方法
    缺点:计数器的使用是一次性的,当用完一次之后就不能使用了
  4. CyclicBarrier(循环屏障)
    内部有一个可以重复的计数器,每次执行await之前的方法时计数器-1,直至0,就能执行await之后的方法了,并会将计数器的值重置。

CyclicBarrier和CountDownLatch的区别:

CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以重复使用。

HashMap、HashTable、concurrentHashMap的区别

  1. HashMap是非线程安全的容器,它在1.7会造成死循环,JDK1.8会造成数据覆盖,而HashTable、concurrentHashMap都是线程安全的;
  2. HashTable实现线程安全的手段比较简单,它是在put方法整体加了一把锁,使用synchronized修饰,性能不高,所以使用频率较低,而concurrentHashMap是HashMap在多线程下的替代方案,他在JDK1.7的时候使用的Lock加分段锁的方案来实现线程安全问题的保障的,而在JDK1.8的时候使用了大量的CAS,volatile来实现线程的,并且在JDK1.8读写的时候不加锁(读取的数据可能不是最新的,因为读和写可以同时进行),只有在写的时候才加锁。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值