那些面试中必会的多线程问题(一)!!快看这!!

本文详细探讨了进程与线程的区别,强调线程是系统调度的最小单位,介绍了线程创建的多种方式,包括继承Thread、实现Runnable接口和使用Callable+Futrute。分析了线程状态,讲解了非线程安全问题和锁的解决方案,对比了synchronized与Lock,并讨论了线程池的创建与优缺点,以及线程不安全问题的ThreadLocal解决方案。
摘要由CSDN通过智能技术生成

进程与线程

1:并行编程VS并发编程
并行:同一时间点多个程序同时执行
并发:所有程序轮流执行
2:进程VS线程
(1)进程的组成单位是:

PID
状态(新建,就绪,运行中,终止,阻塞)
优先级
一组指针(资源)
记账信息(解决资源分配不均的问题)
上下文(当没有时间片这时候需要保持状态等待下次执行,这个暂存状态和后面运行状态就是一个上下文)

(2)当把一个可执行文件加载到内存中,运行起来就变成一个进程。
(3)区别

一个程序只有一个进程,但是可以有多个线程,一个进程至少有一个线程。
进程是系统分配资源的最小单位,线程是系统调度的最小单位。
进程不可以共享资源,而线程可以共享资源(打开的文件,内存)。

(4)查看线程的工具:jconsole
3:内存VS磁盘

内存一般小,磁盘比较大。 造价不同,内存造价高,磁盘造价小。 内存是以纳秒级别读写速度,磁盘读写单位是微秒,内存操作比磁盘快很多。
内存不能进行持久化,磁盘可以进行持久化。

4.线程:
1)子线程:在主线程中创建的线程
2)主线程:主要执行业务的线程
3)守护线程:(后台线程)为用户线程服务,当用户线程执行完之后,守护线程会跟随用户线程一起结束。使用场景:垃圾回收器,监控监测任务

Thread daemonTread = new Thread();
  // 设定 daemonThread 为 守护线程,default false(非守护线程)
 daemonThread.setDaemon(true);
 // 验证当前线程是否为守护线程,返回 true 则为守护线程
 daemonThread.isDaemon();

用户线程:默认创建的线程为用户线程。
注:

(1) thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。
(2) 在Daemon线程中产生的新线程也是Daemon的。
(3)不要认为所有的应用都可以分配给Daemon来进行服务,比如读写操作或者计算逻辑。

线程创建

线程创建方式:
1:继承Thread类:java是单继承,当继承Thread类后,就不能再继承其他的类。

static class MyThread extends Thread{
        @Override
        public void run() {
            //业务代码
            //打印当前线程的名称
            System.out.println("子线程名字:"+Thread.currentThread().getName());
        }
    }

//创建一个Runnable匿名类

Thread thread=new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("当前线程:"+Thread.currentThread().getName());
        }
    });

实现Runnable接口:没有返回值
3:Callable+Futrue接收线程执行后的返回值

 static  class MyCallable implements Callable<Integer> {
@Override
            public Integer call() throws Exception {
                int num = new Random().nextInt(10);
                System.out.println(String.format("线程:%s,生成随机数:%d",Thread.currentThread().getName(),num));
                return num;
            }
        }
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            //创建Callable的子对象
            MyCallable myCallable=new MyCallable();
            //使用FutrueTask接收Callable
            FutureTask<Integer> futureTask=new FutureTask<>(myCallable);
            //创建线程
            Thread thread=new Thread(futureTask);
            //执行线程
            thread.start();
            //得到线程的执行结果
            int num=futureTask.get();
            System.out.println("线程返回结果:"+num);

线程的创建不是越多越好,当创建线程是要看实际情况,任务分为:计算密集型任务,读写文件。线程的数量=CPU核数是最好的,对于读写文件理论上线程数量越多越好。

线程信息

System.out.println("thread的状态"+thread.getState());
        System.out.println("thread的优先级"+thread.getPriority());//线程优先级默认是中等级别5
        System.out.println("thread的是否未守护线程"+thread.isDaemon());
        System.out.println("thread是否存活"+thread.isAlive());
        System.out.println("thread是否被中断"+thread.isInterrupted());

run() VS start()

1:run()方法是一个对象的普通方法,它使用的是主线程来执行任务 2:start()是线程开启的方法,他可以使用新的线程来执行任务
3:start()方法只能执行一次,而run可以执行n次

线程中断:

1:自定义全局标识线程中断
2:使用Thread的intrrupted来中断
使用系统的Intrrupt()可以及时立刻终止线程,而全局变量比较温柔,不能及时终止。
Thread.intrrupted():第一次接收到的状态为true,之后就会将状态复位(false)。
普通方法.isIntrrputed():只能得到线程状态,不能进行复位

线程状态

线程的状态:
在这里插入图片描述

1;yield():用来让出CPU执行权,但是不一定会成功,要看CPU的最终选择
2:join():比较优雅的等待线程执行完成。

非线程安全问题和锁

非线程安全问题:CPU的抢占式执行,非原子性,内存不可见性,指令重排序,同时操作一个变量。
(1)CPU抢占式执行的问题是不可解决的
(2)内存不可见和指令重排序:volatile(不能解决原子性问题) 每次会清除L1中的缓存。
(3)原子性:进行加锁
在这里插入图片描述

加锁
Synchronize VS Lock

  • synchronize:

实现:

【1】操作系统层面,依靠互斥锁
【2】JVM,monitor实现
【3】java语言,将锁信息存放在对象头中(标识锁的状态,和拥有者)

使用场景:修饰代码块,静态方法,普通方法
升级过程:
无锁—》偏向锁(在一个线程访问的时候,在对象头中加该线程ID,当后续有有线程访问时,判断线程ID是否等于对象头中的偏向锁ID,不等不能拥有此锁,进行自旋的方式获取锁)–》轻量级锁—》重量级锁

  • Lock手动锁

需要进行加锁和解锁操作,注意需要将加锁操作放在try外,如果放在try里可能会造成

1.try里抛出异常,还没有加锁成功,finally就进行了解锁操作,
2.在没有锁的情况下师徒释放锁,这个时候产生的异常会将业务代码的异常吞噬,增加了调试代码的复杂性。

  • 非公平锁和公平锁:

公平锁:1;一个线程释放锁,2;主动唤醒需要得到锁的线程。
非公平锁:抢占式执行,当一个线程释放锁,另一个线程刚好执行到获取锁的代码就可以获取到锁。
使用new ReentrantLock(true)来设置公平锁,默认是false,非公平锁,synchronize是非公平锁。

Synchronize和Lock的区别:

1):synchronize自动进行加锁和释放锁,Lock需要手动 2):Synchronize是JVM层面的,Lock是java层面的
3):synchronize修饰的范围是代码块,静态方法,普通方法,Lock只能修饰代码块
4):synchronize是非公平锁,而Lock既可以是公平也可以是非公平
5):Lock的灵活性更高(tryLock)

死锁
在两个或者两个以上的线程运行中,因为资源抢占而造成线程一直等待的问题。
(1)造成死锁的4个条件;互斥条件,请求拥有,不可剥夺,环路等待
(2)其中请求拥有和环路等待可以解决(控制加锁的顺序)
(3)死锁代码:

Synchronize(lockA){
Thread.sleep(1000Synchronize(lockB){
}
}
Synchronize(lockB){
Thread.sleep(1000)
Synchronize(lockA){
}
}

线程通讯
一个线程的操作可以影响另一个线程。

(1)wait():休眠线程
(2)notify();唤醒线程
(3)notifyAll():唤醒全部线程

Synchronize(lock){    
 try{
   lock.wait();
}
}
Synchronize(lock){
  lock.notify();
}  

wait使用注意事项:

1):wait方法在执行前必须加锁,
2)Wait和notify在配合synchronize使用时一定是使用同一把锁
3)Wait和notify再配合使用时,一定要操作同一把锁。
Wait和notify使用时存在的问题:不能指定唤醒线程。

sleep VS wait

1相同: 1)都是让线程进行休眠
2)在执行过程中都可以接收到终止线程的执行的通知
2不同
1)wait必须配合synchronize使用,而sleep不用
2)Wait会释放锁,sleep不会
3)Wait是object方法,而sleep是Thread方法
4)默认情况下wait(不传递参数的情况下)会进入waiting状态,而sleep会进入timed-waiting状态
5)Wait可以主动唤醒线程,sleep不可以

** wait(0)VS sleep(0)**

1)Wait(0)表示一直休眠,sleep(0)休眠0豪秒后继续执行
2)Sleep(0)表示会重新触发一次CPU竞争

为什么wait会释放锁,而sleep不会?

sleep必须传递一个最大等待时间,也就是说sleep是可控的,而wait可以不传递参数,从设计层面上来说,如果一直让wait这个没有超时等待事件得机制不释放锁,那么线程会一直阻塞,sleep就不会存在这个问题。

为什么wait是object方法,而sleep是Thread方法?

Wait需要操作锁,而锁是对象级别的,他不是线程级别的,一个线程中可以有多把锁,为了灵活起见,就把wait设置成object。

解决wait/notify的随机唤醒问题:

LockSupper park()/unpork() 虽然不会报interrupt异常,但是依然可以监听到线程终止的指令。

线程池

线程池
(两个重要的对象:线程,工作队列)
线程池:使用池化技术来管理线程和使用线程的方法

  • 线程缺点:

1)线程的创建会开辟本地方法栈,虚拟机栈,程序计数器变成线程私有的内存,同时消耗的时候需要销毁以上三个区域,因此频繁的创建和消耗比较消耗系统资源
2)在任务量远远大于线程可以处理的任务量时,并不能有好的拒绝任务。

  • 线程池优点:

1)可以避免频繁的创建和消耗线程。
2)可以更好地管理线程的个数和资源的个数
3)拥有更多的功能,比如线程池可以进行定时任务的执行
4)线程池可以更优化的拒接不能处理的任务

  • 线程池的创建

1》创建固定个数的线程池

ExecutorService es = Exectors.newFixedThreadPool(10);

2》创建带缓存的线程池

ExecutorService es = Executors.newCachedThreadPool();

使用场景:短期有大量任务
3》创建可以执行定时任务的线程池

//创建执行定时任务的线程池
ScheduleExecutorService ses = Executors.newScheduledThreadPool(2);
//执行定时任务
ses.scheduledAtFixdRate(new Runnable(){
  Public void run(){
}
},1,3,TimeUnit.SECONDS);

参数1:线程执行任务Runnable 参数2:延迟一段时间执行 参数3:定时任务执行的频率 参数4:时间单位

ses.schedule()只会执行一次 ses.scheduleWithFixedDelay() VS
ses.scheduledAtFixdRate() 前者:以上一次任务的结束时间作为下一次任务的开始时间
后者:以上一次的开始时间作为下一次任务开始的时间

4》创建单线程执行定时任务的线程池

ScheduledExecutorService ses = Executors.newSingleThreadScheduledExecutor();

5》创建单个线程池

ExecutorService es = Executors.newSingleThreadExector();

创建单个线程池有什么用?

1)可以避免频发的创建线程和消耗线程带来的开销,
2)有任务队列可以存储多余的任务
3)当有大量的任务不能处理的时候,可以友好的执行拒绝策略
4)可以更好地管理任务

6》JDK1.8+

根据当前的硬件CPU生成对应个数的线程池,并且是异步的 同步:发请求,等待执行完成,有结果返回
异步:发请求,执行完成,另一个乘车异步处理,处理完成之后回调返回结果

7》原始的线程池创建方式

ThreadPoolExecutor
ThreadPoolExecutor tpe =new ThreadPoolExecutor(核心线程数(线程正常情况下),最大线程数量(当有大量任务的时候可以创建的最多的线程数),最大线程存活时间,单位时间,任务队列,线程工厂,拒绝策略);

注意事项:

核心线程数小于的等于最大线程数;当任务量小于核心线程数时,就会创建一个线程来执行此任务,当任务量大于等于核心线程数时会先把任务放到任务队列里,当任务队列放满时,再创建线程(最大线程数),如果最大线程也使用完,就执行拒绝策略。

拒绝策略:

> 1)默认拒绝,不执行任务抛出异常:ThreadPoolExecutor.AbortPolicy();
> 2)把当前任务交给主线程执行:ThreadPoolExecutor.CallerRunsPolicy();
> 3)丢弃最老的任务:ThreadPoolExecutor.DiscardOldestPolicy();
> 4)丢弃最新的任务:ThreadPoolExecutor.DiscardPolicy();

前六种问题

1)线程数量不可控(创建带缓存的线程池)
2)工作任务不可控,可能会导致内存溢出(OOM问题)

  • 线程池使用:
1execute(Runnable m没有返回值)
2)submit(Runnable/Callable 有返回值,返回值使用Future接收)
  • 线程池终止
 shutdown(); //结束线程池
shutdown();//立即结束线程池
  • 线程池状态
    在这里插入图片描述

线程不安全解决方案ThreadLocal

  • 解决方案

1)加锁(synchronize,Lock)
2)创建私有变量
3)ThreadLocal(既不用加锁是的排队等待,又不用每次都创建私有变量)

  • ThreadLocal

ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。

  • 方法
set():设置私有变量(出现set方法,初始化不再执行)
get():获取私有变量
remove():移除私有变量
initialValue():创建ThreadLocal时候设置默认值()

ThreadLocal的作用主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的,在多线程环境下,如何防止自己的变量被其它线程篡改。

  • 使用场景有

1:解决线程不安全问题
2:实现线程级别的数据传输
时间不对时就是SimpleDateForma(),线程池加上ThreadLocal包装SimpleDataFormat,再调用initialValue让每个线程有一个SimpleDataFormat的副本,从而解决了线程安全的问题,也提高了性能。

  • ThreadLocalMap底层结构:

ThreadLocal–>Entry[]–>Key,Value

Entry是继承WeakReference(弱引用)的,也没有看到HashMap中的next,所以不存在链表了
在这里插入图片描述

为什么需要数组呢?没有了链表怎么解决Hash冲突呢?

用数组是因为,我们开发过程中可以一个线程可以有多个TreadLocal来存放不同类型的对象的,但是他们都将放到你当前线程的ThreadLocalMap里,所以肯定要数组来存,而哈希冲突,从源码里面看到ThreadLocalMap在存储的时候会给每一个ThreadLocal对象一个threadLocalHashCode,在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,int i = key.threadLocalHashCode & (len-1)。
在这里插入图片描述

ThreadLocal实例实际上也是被其创建的类持有(更顶端应该是被线程持有),而ThreadLocal的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。

  • ThreadLocal问题:

1》不可继承性(线程本地内存),解决不可继承性,使用InheritableThreadLocal(),但是不能实现并列线程之间的数据传输(数据的设置和传输)
2》脏读:在一个线程中读取到了不属于自己的信息叫做脏读。 产生原因:线程池复用了线程,和这个线程相关的静态变量也被复用了,就造成了脏读。
3》内存泄露:ThreadLocal在保存的时候会把自己当做Key存在ThreadLocalMap中,正常情况应该是key和value都应该被外界强引用才对,但是现在key被设计成WeakReference弱引用了。这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露,解决方法:在代码的最后使用remove。
**为什么ThreadLocalMap的key要设计成弱引用? java中而引用传递的是对象的副本,如果使用强引用,当原来key原来对象失效的时候,jvm不会回收map里面的ThreadLocal,一个线程对应一块工作内存,线程可以存储多个ThreadLocal。那么假设,开启1万个线程,每个线程创建1万个ThreadLocal,也就是每个线程维护1万个ThreadLocal小内存空间,而且当线程执行结束以后,假设这些ThreadLocal里的Entry还不会被回收,那么将很容易导致堆内存溢出。

  • java的4种引用类型

1)强引用:Object o=new Object(),即使发生了OOM(内存泄露)也不会被回收。
2)软引用:当将要发生OOM时才会被回收。 3)弱引用:不管内存够不够用,下一次回收都会将次引用对象回收
4)虚引用:创建及回收,可以出发一个垃圾回收的回调。

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值