目录
1 能确保线程安全的代码写法?
1.1 为什么多线程并发不安全?
A:
操作系统中进程拥有资源,而线程则不拥有资源。一个进程可以创建多个线程。这些线程共享进程中的资源。当多线程并发执行。对同一个数据进行修改,就可能会造成数据的不一致。
线程执行的数据是由“工作内存“”获取的。而工作内存的数据来源与“主内存”。其中工作内存起到的作用类似于“缓存”。假设现在有个任务需将data=0,执行“+1”操作。开启两个线程执行。正常执行的结果data应该为2。实际data存在1 或者 2。原因线程1执行获取的data数据由主内存加载到工作内存。此时线程2也执行同样的逻辑,线程各自执行完毕后将结果写入工作内存,再各自写回”主内存”,导致线程1的结果被线程2覆盖。因此出现结果为“1”。在并发编程中称为不可见性。
1.2 并发不安全的解决思路。
A:
(1)给共享资源加锁,使的资源在同一时间节点上只由一个线程占用。
(2) 让线程也拥有资源,不取共享进程中的资源。
1.3 编码思路
A:
(1)多实例、多副本(ThreadLocal) ThreadLocal可以为每个线程维护一个私有的本地变量。
(2)使用锁机制 synchronize、lock方式。
(3)使用JUC并发包的工具。
2 三个线程t1、t2、t3如何顺序执行?
public static void main(String[] args) {
Thread t1 = new Thread(() -> System.out.println("t1"));
Thread t2 = new Thread(() -> {
try {
// 引用t1线程,等待线程1执行完毕再执行
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2");
});
Thread t3 = new Thread(() -> {
try {
// 引用t2线程,等待线程1执行完毕再执行
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t3");
});
// 顺序不强制
t3.start();
t1.start();
t2.start();
}
3 synchronized、volatile的原理?
3.1 synchronized是什么? 如何保证有序性?
A:
(1)是阻塞式的同步,当一个线程获得对象锁,进入同步块,其他线程访问都必须阻塞在同步块外面等待,进行线程阻塞和唤醒的代价较高。
(2)synchronized经过编译,会在同步块的前后分别形成“monitorenter”、“monitorexit”两个字节码指令。在执行“monitorenter”指令时,首先尝试获取对象锁。如果对象没被锁定或当前线程已经拥有对象锁,把锁的计数器加1.相对的,在执行“monitorexit”指令时会将计数器减1,当计数器为0时,锁释放。
3.2 volatile是什么?如何保证有序性?
A:
volatile修饰的变量(类的成员变量、类的静态成员变量)具有两层语义。
(1)保证了不同线程对同一变量进行操作的可见性,即一个线程修改了变量值,新值对其他线程来说是可感知的,volatile关键字会强制将修改值写入主内存,并使其他线程的工作内存值失效。
(2)禁止指令重排。当程序执行到volatile修饰的变量在读、写操作时,在其前面的操作的更改必然全部执行,且结果对后面的操作可见,在其后面的操作肯定未执行。
4 无锁、偏向锁、轻量级锁、重量级锁的区别?
A:
从JDK1.6版本后,synchronized也在不断优化锁的机制,某些情况下也并非是很重量级的锁。优化机制包括“自适应锁”、“自旋锁”、“锁消除”、“锁粗化”、“轻量级锁”、“偏向锁”
锁的状态从低到高依次为 “无锁” -> ”偏向锁” -> “轻量级锁” -> “重量级锁”,升级的过程就是从低到高。
自旋锁:由于大部分时候,锁占用的时间很短。共享变量的锁定时间也很短。所以没有必要挂起线程。用户态和内核态的来回上下文切换影响性能。自旋的概念就是让线程执行一个忙循环,可以理解为就是啥也不干,防止从用户态转入内核态。自旋锁可以通过设置-XX:+UseSpining来开启,自旋的默认次数10次,可以使用-XX:PreBlockSpin设置。
锁消除:锁消除指的是JVM检测到一些同步的代码块,完全不存在数据竞争的情况,并不需要加锁,会进行锁消除。
自适应锁:自适应锁就是自适应的自旋锁,自旋的时间不是固定时间,而是由前一次在同一个锁上的自旋时间和锁的持有者状态来决定。
锁粗化:锁粗化指的是有很多操作都是对同一个对象进行加锁,就会把锁的同步范围扩展整个操作序列之外。
偏向锁:当线程访问同步块获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程ID,之后这个线程再次进入同步块时不再需要CAS来加锁和解锁。偏向锁会永远偏向第一个获得锁的线程,如果后续没有其他线程获得过这个锁,持有锁的线程就永远不需要进行同步,反之,担忧其他线程竞争偏向锁时,持有偏向锁的线程会释放偏向锁。通过设置-XX:+UseBiasedLocking开启偏向锁。
轻量级锁:JVM对象的对象头中包含一些锁的标志位,代码进入同步块的时候,JVM将会使用CAS方式来尝试获取锁,如果更新成功则会把对象头中的状态位标记为轻量级锁,如果更新失败,则线程尝试自旋获取锁。
5 正确的启动和停止线程?
A:
启动:Runneable和Thread调用的run()方法都是在当前线程执行,调用start()方法的时候,则是另外启动一个线程运行run()里的逻辑。
停止:
(1)使用退出标志,使线程正常退出。
(2)使用stop方法强行终止线程(此方法与suspend、resume都可能造成资源未正常释放的问题),不推荐。
(3)使用interrupt方法中断线程。
6 JDK提供了那种现成的线程池?
A:
newSingleThreadExecutor:创建一个单线程的线程池,此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
newFixedThreadPool:创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。
newCachedThreadPool:创建一个大小无限的线程池,此线程池不会对象池大小做限制。线程池大小完全依赖于操作系统能够创建的最大线程大小。
newSingleThreadExecutor:此线程池支持定时以及周期性执行任务的需求。