- 并行和并发的区别?
- 并行:多个处理器同时处理多个任务
- 并发:一个处理器处理多个任务,按照细分时间交替执行,在逻辑上是同时执行的
- 线程同步和线程通信的理解?
- 线程同步:指的是当一段代码,正在被一个线程执行时,不能存在其他线程也在执行。Java给我们提供了两种方式:
- synchronize(同步监视器):
- 特点:不能修饰构造器与成员变量,能修饰类、方法和代码块;自动锁
- 以下情况会导致synchronize释放锁:
- 当代码块或方法执行完毕时
- 线程在代码块或方法中遇到return、break时
- 线程在代码块或方法中遇到未处理的异常时
- 线程在代码块或方法中遇到锁对象使用wait()方法时
- 同步锁(Lock):通过显示定义同步锁对象实现同步
- 特点:只能修饰代码块;需要自己手动的加锁和释放锁
- ReentrantLock是Lock的实现类,具有可重入性。可重入性指的是,线程可以在被加锁的ReentrantLock的锁上再次加锁
- ReentrantLock对象维持一个计数器追踪lock()方法的嵌套调用,所以一个被锁保护的代码可以调用另一个被锁保护的代码
- synchronize(同步监视器):
- 线程通信:线程之间的协调执行
-
- 线程通信是基于synchronize机制和条件变量完成的。通过查询执行条件,判断是执行还是继续等待,继续等待就执行锁对象的wait()方法。继续执行,执行完毕后,改变条件变量,并且使用notify唤醒其他线程,如此循环
- 例:一个银行账户,系统命令一直存钱和取钱
-
- 在一个程序中,至少拥有一个进程,一个进程至少拥有一个线程
- 在一个程序中,多个线程访问同一资源,如果对资源访问顺序敏感(如:加减乘除),就称存在竞态条件
- 竞态条件一般发生在代码的临界区,如:
- 当两个线程同时执行add方法,分别是add(2)和add(3),正常情况得出的结果应该是5,但是如果两个线程在获取count的值时count都为0的话,结果也就不会是5
- 解决方式:使用synchronize或者Lock显示锁
- 如果异常已经被捕获并且抛出,那么线程会继续执行
- 如果异常没有被捕获那么线程会停止执行,并抛出Tread.UncaughExceptionHandler异常,此接口是处理导致线程停止执行的未捕获异常的内嵌接口
- 在锁对象中设置thread Id值,在第一次使用时thread Id为空,JVM让其持有偏向锁,并且将thread Id设置为此线程的Id,当下一次进行访问时,会判断thread Id和此线程Id是否相同,相同则直接使用此对象;不同,则JVM就会将锁升级为轻量级锁,通过自旋循环一定次数获得锁,如果循环了一定次数没有获得锁,那么就会将锁升级为重量级锁来获得锁。此过程就是锁的升级
- 锁的升级目的是为了降低所带来的性能消耗
- 使用java.util.concurrent中的类
- 使用自动锁synchronize
- 使用手动锁Lock
- 栈:是与线程密切相关的内存区域。每个线程都拥有自己的栈内存,用于存储本地变量、方法参数以及栈调用等。每个线程在栈内存中存储的信息对于其他线程都是不可见的
- 堆:是所有线程的共享内存区域。每个对象都是在堆内存中创建的,为了提高效率线程都会从堆中放一个缓存到栈中
- 线程池的四大基本组成部分:
- 线程池管理器(ThreadPool):主要负责创建线程池、销毁线程池以及创建新任务
- 工作线程池(PoolWorker):线程池的线程,没有执行任务时处于等待状态,能够循环执行任务
- 任务接口(Task):任务必须实现的接口,目的是使工作线程调度任务的执行,它主要是任务的入口、任务结束后的收尾操作以及获得任务的执行状态等
- 任务队列(TaskQueue):存放未处理的任务队列,提供一个缓冲机制
- 在线程需要被多次使用时,反复的创建和销毁线程,对于资源消耗是很大的,而线程池能够减少线程的创建和销毁,让线程能够多次被使用,我们也可以根据需要来控制创建线程的数量,这样我们就能减少内存的使用以及资源消耗
- 当两个或两个以上的线程,因为资源的竞争而导致线程等待的状态,如:当A线程拥有独占锁a,尝试获取独占锁b时,同时B线程拥有独占锁b,尝试获取独占锁a时,此时就发生了锁的竞争,AB两个线程都拥有彼此需要的锁,这就是死锁
- 尽量使用java.util.concurrent中的类代替自己手写锁
- 尽量使用tryLock(long timeOut,TimeUnit unit)方法,设置超时时间,超时即可退出
- 降低锁的粒度,避免多个功能使用同一个锁
- 减少同步代码块的使用
- 饥饿:一个线程一直占用资源导致其他线程处于等待状态,类似死锁,但是不同于死锁的是在此线程结束后会释放资源
- 活锁:状态在一直改变,但不会执行。如:程序中存在一个资源,多个线程都将执行机会给其他线程,导致资源来回在多个线程中,但又得不到执行
- wait():Object类中的方法,使一个线程处于等待状态,会将对象锁释放
- sleep():Thread类中的方法,使一个线程在指定时间内处于休眠状态,休眠时间结束就会正常执行,不会释放对象锁
- notify():Object类中的方法,随机唤醒一个线程,由JVM控制
- notifyAll():Object类中的方法,唤醒所有线程
- 在一个进程中,执行进程代码的一个操作单元,一个进程至少有一个线程,即进程本身
- 线程的基本状态:
- 新建状态:使用new创建一个线程对象,与其他新建对象一样都只分配了内存
- 等待状态:在new创建对象之后使用start()方法之前
- 就绪状态:线程对象调用start()方法,JVM将线程放入可运行池中,等待分配CPU使用权
- 运行状态:此状态的线程持有CPU使用权,执行程序中的代码
- 阻塞状态:线程因为某些原因导致线程阻塞,停止执行。阻塞状态的线程JVM不会分配CPU的使用权,直到重新处于就绪状态,线程才有机会被分配到CPU的使用权
- 阻塞状态分为3种:
等待阻塞:线程对象调用wait()方法,线程释放对象锁,JVM将线程放入等待池中
同步阻塞:线程对象需要得到资源锁,此资源锁又被其他线程所占用时,JVM会将线程放入锁池中,等待资源锁被释放并竞争资源锁
其他阻塞:线程对象调用sleep()方法,或发送IO请求时,JVM会将线程置为阻塞状态,在sleep()休眠时间结束,或IO请求处理完毕之后,线程会重新进入就绪状态
- 阻塞状态分为3种:
- 死亡状态:run方法被执行完,或遇到未捕获的异常线程终止执行,此时线程就处于死亡状态,结束线程的生命周期
- 状态之间的关系:
- 继承Thread类,执行run()方法
- 实现Runnable接口
- 实现Callable接口
- Runnable:run()方法无返回值
- Callable:call()方法有返回值
- sheep():来自Tread类,不会释放锁,消耗完指定时间会自动恢复
- wait():来自Object类,会释放锁,需要使用notify/notifyAll进行唤醒,将等待池中的线程移到锁池中,然后进行锁的竞争
- volatile修饰的变量不会执行加锁操作,所以不会导致线程的阻塞,volatile是比synchronize更轻量级的同步机制
- 特点:
- volatile能够保证线程修改的可见性【即:当一个线程对共享内存中的变量进行修改时,其他线程能够立即获得修改之后的值】,volatile能够将新值立即同步到主内存中,每次使用时也会立即在主内存中刷新
- 提供了禁止指令重排序的优化,线程在使用volatile修饰的变量时,JVM会先执行一个操作,这个操作就是内存屏障【即:先于指令的操作必须先执行,后于指令的操作必须后执行】
- 没有被volatile修饰的变量,线程在使用时会先将内存中的变量存入CPU缓存中;volatile修饰的变量,JVM保证每次读写都在内存中执行
- 缺点:
- volatile不能保证原子性,原子性就是一个不可分割的操作
例:volatile int a=0; a++;
- a++ 分为三个步骤执行:
- 内存到寄存器
- 寄存器自增
- 写入内存
- 对于变量a而言具有修改可见性,但a++三个步骤都能够被分离开,不符合原子性的操作,所以存在线程安全问题
- a++ 分为三个步骤执行:
- volatile不能保证原子性,原子性就是一个不可分割的操作
- volatile是变量的修饰符,synchronize是修饰类、方法和代码块的
- volatile只能保证修改的可见性,不能保证原子性,synchronize既能保证修改的可见性,又能保证原子性
- volatile不会造成线程阻塞,synchronize可能会造成线程阻塞
- volatile修饰的变量不会被编译器优化,synchronize标记的会被编译器优化
- ReentrantLock使用比较灵活,但是需要手动释放锁
- ReentrantLock必须手动加锁和释放锁,synchronize不需要手动加锁和释放锁
- ReentrantLock只适用于代码块,synchronize可用于类、方法、代码块中
- synchronize是自动锁,Lock需要自己手动加锁和释放锁
- synchronize使用比较简单,当发生异常时会自动释放锁,不会产生死锁,Lock使用不当,没有使用unLock()释放锁,则会产生死锁
- Lock只能给代码块加锁,synchronize能给类、方法、代码块加锁
- Lock可以知道是否获取了对象锁,而synchronize无法判断是否得到对象锁
- 使用锁机制不同:
- synchronize:使用了悲观锁机制,即独占锁机制,认为每个线程访问资源都会对同一资源进行修改,所以规定每次只能进入一个线程。在CPU转换线程阻塞时会导致上下文的切换,当有很多线程竞争锁时,CPU频繁的上下文切换就会导致效率将低
- Lock:使用了乐观锁机制,认为进入的线程都不会对同一资源进行修改,如果产生冲突,就会进行重试
- start()用于启动线程,run()执行线程运行时的代码,start()只能运行一次,run()能够被多次调用
- 因为wait、notify和notifyAll都是锁级别的操作,所以这些方法都在Object类中,锁属于对象
- notify:将随机唤醒一个线程,由JVM进行控制
- notifyAll:将所有线程唤醒
- 等待线程由等待池移到锁池中,之后通过锁的竞争来获取锁,获得则进入就绪状态等待分配CPU的使用权,否则就在锁定池中等待进行下一次锁的竞争,如此反复
- 因为防止wait和notifyAll/notify产生竞态条件
- 假设我们有这样一个场景,消费者线程需要生产者线程写入一次数据,生产者线程需要消费者线程读取一次数据。在不使用同步机制时,当生产者线程wait执行的同时消费者线程notify也执行,消费者线程notify在等待池中未找到生产者线程,那么此时的生产者线程wait会一直处于阻塞状态
- ThreadLocal为所有线程提供线程副本,每个线程都能够独立的修改自己的线程副本,并且不会影响其他线程的副本
- 使用场景:
数据库的连接
session的管理
- stop():不安全,它会释放线程的所有锁,如果线程对象处于不连贯状态的话,那么其他线程就有可能进行检查和修改它们,且主要问题很难检测出来
- suspend():容易造成死锁,因为线程调用了suspend()方法不会释放锁,其他线程不能访问到这些资源,从而导致线程阻塞
- execute():只能执行Runnable类型的任务
- submit():Runnable类型和Callable类型的任务都能执行
- atomic主要是利用CAS(Compare And Swap 比较交换 是一种无锁机制)、volatile和native来保证原子操作,从而避免synchronize的高开销,执行效率大大提升