sleep()方法和wait()方法的区别和共同点?
- 相同点:两者都可以暂停线程的执行,都会让线程进入等待状态
- 不同点:
- sleep方法没有释放锁,wait方法释放了锁
- sleep方法属于 Thread 类的静态方法,作用于当前线程;而wait方法是Object类的实例方法,作用于对象本身
- 执行sleep方法后,可以通过超时或interrupt方法唤醒休眠中的线程;执行wait方法后,只能通过notify或notifyAll方法唤醒等待线程
-
Thread.sleep(0)的作用是什么?
由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情 况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动 触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。
说说synchronized的实现原理?
在Java中,每个对象都隐式的包含一个监视器(montior)对象。加锁的过程其实就是竞争监视器的过程,当线程进入字节码monitorenter 指令后,线程将持有监视器对象,执行monitorexit 后释放监视器对象,其他线程没有拿到该对象的,就需要 阻塞 等待获取该对象
说一下synchronized锁升级过程?
- 偏向锁:在JDK1.8之后,sync其实默认是轻量级锁,但是可以通过JVM配置,为sync的对象加上偏向锁。当出于偏向锁的状态时,markwork会记录下当前线程ID
- 升级到轻量级锁:当下一个线程参与到偏向锁竞争时,会先判断争抢锁的线程ID是否和原来保存的ID相同,如果不相同,会立即撤销偏向锁,升级为轻量级锁。
- 每个线程在自己的栈中生成有一个LockRecord(LR),然后每个线程通过CAS操作尝试将被锁对象的markwork指向自己的LR,被指向的即为获取到锁的
- 升级为重型锁:若锁竞争加剧(如自旋次数或自旋线程数超过阈值),就会升级为重量级锁。
- 重型锁可以认为直接对应底层操作系统中的互斥量,使用互斥量来进行锁的竞争,由于直接使用底层操作系统调度,会消耗大量性能,所以称之为重型锁
Java里线程有那些状态?
- 初始(NEW):新创建了一个线程对象,但还没有调用start()方法
- 运行(RUNNABLE):Java中将就绪(ready)和运行中(running)统称为“运行”。在线程创建后,由其他线程调用此线程start()方法,该线程就开始等待CPU调度,此时即为“就绪”,当获取CPU使用权,“就绪”状态就变为“运行”状态
- 阻塞(BLOCKED):表示线程被锁阻塞
- 等待(WAITING):处于这种状态的线程不会被分配CPU执行时间,需要等待其他线程做出特定操作唤醒(通知或中断)
- 超时等待(TIMED_WAITING)超时等待可以在指定时间后自行返回“就绪”态。
- 终止(TERMINATED) 表示线程执行完毕。
悲观锁与乐观锁?
- 悲观锁:在对数据进行修改前,先对数据进行加锁。之所以叫悲观锁,就是因为悲观的认为数据被并发修改的概率大,所需需要先加锁再操作
- 乐观锁:相对于悲观锁而言,乐观锁乐观的认为被修改数据不会发生并发修改问题,所以乐观锁不会使用锁机制去主动的对数据进行上锁,而是在数据修改之后,才正式对数据是否冲突进行校验(使用版本号的方式),如果发现了冲突,则提示错误,由用户决定下一步操作(撤销修改或者覆盖等)
并发编程三要素?
- 原子性
一个或多个操作,要么同时执行,要么同时失败。不允许执行期间被其他线程操作打断
- 可见性
指多个线程操作共享变量时,其中一个线程对象变量进行修改后,其他线程应该能立即看到修改的结果
- 有序性
程序的执行按照代码定义的顺序执行,不允许后定义的代码先执行。
创建线程的几种方式?
- 继承 Thread 类来创建线程类
- 通过实现 Runnable 接口创建线程类
- 通过Callable和Future创建线程
- 通过线程池创建线程
线程池的优点?
- 重复使用存在的线程,减少创建销毁线程的开销
- 有效控制最大并发数,提高系统资源使用率,避免过多的资源竞争,从而避免大量阻塞
- 提供定时执行,定期执行,单线程,并发控制等功能
CyclicBarrier和CountDownLatch的区别?
- CountDownLatch就是一个线程进行等待,直到他所等待的其他线程都执行完毕,并且调用countDown()方法通知后,他才可以继续执行,CountDownLatch的计数器只能使用一次。
- CyclicBarrier是所有线程都进行等待,直到所有线程都准备好并且进入await()方法后,大家一起继续向下执行,CyclicBarrier的计数器可重复使用。
什么是CAS?
CAS是compare and swap的缩写,即为比较并交换
CAS是一种实现锁的操作,属于一种方法论,实现了乐观锁
具体操作是包含三个操作数,内存位置(V),原值(A)和新值(B),只有当V处的值和A一样,那么才会将V的值更新为B。
CAS是指的这样比较并交换的单次操作,所以并不一定会成功,所以通过了自旋的方式,来保证CAS执行成功。
java.util.concurrent.atomic 包下的类大多是使用CAS操作来实现的(AtomicInteger,AtomicBoolean,AtomicLong)。
CAS的问题?
- ABA问题
一个线程将A改为B,接着又改回A,此时CAS认为内存地址V中没有变化,但其实已经变化过了,解决办法为使用版本号标识数据,而不是单独判断数据是否等于预期值。
- CPU使用率增加
由于CAS不保证成功,所以需要自旋保证成功,而自旋又是无意义操作,若产生大量自旋会导致CPU被无意义消耗
- 只保证单个操作原子性(不保证代码块的原子性)
CAS只能保证单个数据修改的原子性,若需要保证多个数据修改操作的原子性,就需要进行加锁处理了
什么是自旋锁?
自旋锁是SMP架构中的一种low-level的同步机制。
当线程A想要获取一把自旋锁而该锁又被其它线程锁持有时,线程A会在一个循环中自旋以检测 锁是不是已经可用了。
什么是多线程上下文切换?
即使是单核CPU也能执行多线程操作,CPU通过分配时间片来实现上下文切换机制。时间片是CPU分配给各个线程的运行时间,因为时间片非常短,所以CPU就会不停的切换线程执行。
上下文切换过程中,CPU会停止处理当前线程运行的程序,并保存下当前程序运行的位置(保存在线程私有的程序计数器中),然后切换到其他线程执行任务。
保存上一个任务的状态,以便下一次切换回这个任务时,可以再次加载这个任务的状态。
从任务保存到再加载的过程就是一次上下文切换。
什么是线程与进程?
- 进程:在操作系统中能够独立运行,并且作为资源分配的基本单位。表示为运行中的程序。系统运行一个程序就是一个进程从创建、运行到销毁的过程
- 线程:比进程更小的执行单位,能够完成进程中的一个功能,一个进程在执行过程中可以产生多个线程
- 不同:同类的多个线程共享进程的堆与方法区资源,但是每个线程有自己的私有程序计数器、虚拟机栈和本地方法栈,操作系统在产生一个线程,或是在线程之间调度的消耗比进程小得多。
程序计数器为什么是线程私有的?
字节码解释器通过程序计数器类读取指令,从而实现代码的流程控制。在多线程环境下,程序计数器用于记录当前线程执行的位置,从而当上下文切换时能知道线程执行到哪了。
如果计数器是共享的话,就会导致其他线程覆盖掉当前线程的指令,导致被唤醒线程不知道执行到哪了。
虚拟机栈和本地方法栈为什么是线程私有的?
虚拟机栈和本地方法栈存有局部变量表,操作数栈和常量池引用等,是独属于本线程的资源,为了不被其他线程访问到局部变量,虚拟机栈和本地方法栈是线程私有的。
并发与并行的区别?
并发是指多个任务交替执行,并行是指真正意义上的“同时进行”
真正的并行只能出现在多核CPU下,单核CPU只能通过切换时间片来达到并发的效果。
什么是线程死锁,如何避免死锁?
多个线程同时被阻塞,它们中的一个或者多个都在等待某个资源被释放,而它们需要获取的锁被另一个线程阻塞。由于线程的无限期阻塞,导致程序无限期等待的问题,就是线程死锁。
死锁是程序无法自行解决的,必须要通过外部干扰来解决死锁
假设 A 持有 资源1,B 持有 资源2, 它们同时都阻塞的想申请对方的资源,那么这两个线程就会互相等待而进入死锁状态。
如何避免死锁:
- 避免一个线程同时获取多个锁
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
- 尝试使用超时解锁,从而规避无限期等待
为什么调用start()方法会执行run()方法,为什么不直接调用run()方法?
new一个线程后,线程进入初始状态,当调用start方法后,将线程变为就绪状态,等待CPU调度分配到时间片后开始运行。start方法会执行线程准备工作,然后自动调用run方法,这才算是真正的多线程的操作。
而run方法只是一个普通方法,如果main线程之间调用run方法,程序就会吧run方法当成main线程的一个普通方法执行,压入main的虚拟机栈执行,并不会在其他线程中去执行他,所以不属于多线程操作。
总结:调用 start 方法可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普 通方法调用,还是在主线程里执行。
什么是线程安全问题?如何解决?
线程安全问题是指:在某一线程从开始访问到结束访问某一资源期间,该资源数据被其他线程所修改,那么对于当前线程而言,该线程就发生了线程安全问题,表现形式为数据不一致,数据缺失等
线程安全问题发生的条件:
- 多线程环境下,即包括自己在内存在多个线程
- 多线程环境下存在共享资源,并且多线程操作该共享资源
- 多线程为该资源的操作是非原子性的
线程安全问题解决思路
- 尽量不使用共享变量,将不必要的共享变量变成局部变量来使用
- 使用加锁机制确保同时只有一个线程获取到共享变量
- 室友ThreadLocal为每个线程创建共享变量的副本,各个线程独立操作,互不影响
什么是活锁?
加锁机制是每个线程争抢资源,而活锁是每个线程都想将资源让给对方,可能会存在资源一直让来让去,导致资源一直在线程间跳动而无法真正让某一个线程拿到资源并执行,这就是活锁问题。
什么是线程饥饿问题?如何解决?
线程饥饿是指某一个线程因为一直无法获取到资源,导致程序一直无法执行。
解决办法:与死锁相比,饥饿现象还是有可能自行恢复的。可以对线程设置不同的执行优先级来规避饥饿问题
什么是线程阻塞问题?如何解决?
阻塞是指当一个线程占有了共享资源后,所有其他需要此资源的线程都要进入临界区挂起等待,一直不能工作,直到该线程获取到此临界资源,这种情况就是阻塞。如果一个线程一直不释放资源,那么其他线程就得一直阻塞等待。
阻塞问题的本质就是锁的性能问题
解决方法:可以通过减少锁持有时间,读写锁分离,减小锁粒度等方式来优化锁的性能。
synchronized 关键字和 volatile 关键字的区别
- volatile关键字是对变量进行上锁,锁住的是单个变量,而synchronized能够对方法及代码块进行上锁
- volatile主要用于解决共享变量的数据可见性,而synchronized主要用于保证访问数据的同步性(sync也能保证可见性)
- volatile只能保证单个变量操作的原子性(如i++等操作),sync能保证被锁住的整个代码块的原子性
- volatile是线程同步的轻量级实现,性能高于sync
- 多线程访问volatile修饰的变量不会发生阻塞,而访问sync修饰的资源会发生阻塞(主要是因为volatile采用CAS加锁)
说一说几种常见线程池和应用场景
-
FixedThreadPool:可重用固定线程数线程池(适用于负载较重的服务器)
- 使用无界队列LinkedBlockingQueue作为工作队列,线程池中若有空闲线程则立即执行,若无则入工作队列,待有空闲线程后,任务依次出队执行
-
SingleThreadExecutor:只会创建一个线程执行任务(适用于需要保证执行顺序的任务,且无多线程需求的场景)
- 使用无界队列LinkedBlockingQueue作为工作队列,若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先 出的顺序执行队列中的任务。
-
CachedThreadPool:能根据需求调整线程数的线程池(使用与执行很多短期异步任务的服务器)
- 使用没有容量的SynchronousQueue作为线程池的工作队列,但 CachedThreadPool的maximumPool是无界的。线程池的线程数量不确定,但**若有空闲线程可以复用,则会优先使用可复用的线程。若所有线 程均在工作,又有新的任务提交,则会创建新的线程处理任务。**所有线程在当前任务执行完毕 后,将返回线程池进行复用。
-
ScheduledThreadPool:继承自ThreadPoolExecutor。它主要用来在给定的延迟之后运行 任务,或者定期执行任务。使用DelayQueue作为任务队列
线程池的几种工作队列?
- ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原 则对元素进行排序。
- LinkedBlockingQueue:是一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量 通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了 这个队列
- SynchronousQueue:是一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用 移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于Linked-BlockingQueue,静 态工厂方法Executors.newCachedThreadPool使用了这个队列。
- PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
Hashtable的size()方法中明明只有一条语句”return count”,为什么还要做同步?
- 这个问题其实类比与I++问题,虽然语句只有一条,但是在底层操作却不是只有一条指令
- 同一时间只能有一个线程访问被同步的方法,但是非同步方法就可以多线程访问,这就会导致,只有一个线程访问的put方法,同时有多个线程访问size方法,put修改了size,而size方法由于没有做同步,就可能会读到旧值,线程不安全
同步方法和同步代码块哪一个是更好的选择?
同步代码块意味着同步的范围比同步方法小,加锁有一条原则就是:同步的范围越小越好。
照这个角度看,似乎是同步代码块比同步方法好
但是在Java中,有一种优化策略:锁粗化。 这种方法就是将同步范围变大。
例如StringBuffer,他是一个线程安全的类,所以最常用的方法append()是一个同步方法,我们操作stringbuffer是,通常会频繁的append,这就意味着,我们可能会频繁的加锁解锁,可这是耗费资源的。
因此,JVM就会对多次append方法进行一个锁粗化(注意是JVM对同步方法进行锁粗化,而不是StringBuffer本身对append方法进行了锁粗化),将多次append操作扩展到append操作的头尾,变成一个大的同步块,这样就减少了加锁解锁的次数。