七、多线程
JAVA怎么保证线程安全?锁在项目中具体怎么使用?线程安全在三个方面体现
1.原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作, (atomic,synchronized);
2.可见性:一个线程对主内存的修改可以及时地被其他线程看到, (synchronized,volatile);
3.有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察 结果一般杂乱无序,(happens-before原则)。
1.Java如何保证原子性: 锁和同步 常用的保证Java操作原子性的工具是锁和同步方法(或者同步代码块)。使 用锁,可以保证同一时间只有一个线程能拿到锁,也就保证了同一时间只有一个 线程能执行申请锁和释放锁之间的代码。与锁类似的是同步方法或者同步代码块。使用非静态同步方法时,锁住的是 当前实例;使用静态同步方法时,锁住的是该类的Class对象;使用静态代码块 时,锁住的是synchronized关键字后面括号内的对象。无论使用锁还是synchronized,本质都是一样,通过锁来实现资源的排它 性,从而实际目标代码段同一时间只会被一个线程执行,进而保证了目标代码段 的原子性。这是一种以牺牲性能为代价的方法。
2.Java如何保证可见性: Java提供了volatile关键字来保证可见性。由于JMM是基于共享内存实现线 程通信的,所以会存在缓存一致性的问题。当使用volatile修饰某个变量时,它 会保证对该变量的修改会立即被更新到内存中,并且将其它缓存中对该变量的缓 存设置成无效,因此其它线程需要读取该值时必须从主内存中读取,从而得到最 新的值。
3.Java如何保证顺序性: 编译器和处理器对指令进行重新排序时,会保证重新排序后的执行结果和代 码顺序执行的结果一致,所以重新排序过程并不会影响单线程程序的执行,却可 能影响多线程程序并发执行的正确性。Java中可通过volatile在一定程序上保证顺序性,另外还可以通过 synchronized和锁来保证顺序性。synchronized和锁保证顺序性的原理和保证原子性一样,都是通过保证同 一时间只会有一个线程执行目标代码段来实现的。除了从应用层面保证目标代码段执行的顺序性外,JVM还通过被称为 happens-before原则隐式地保证顺序性。两个操作的执行顺序只要可以通过 happens-before推导出来,则JVM会保证其顺序性,反之JVM对其顺序性不作 任何保证,可对其进行任意必要的重新排序以获取高效率。
什么是线程安全,怎么保证线程安全,线程安全的三个原则是什么?
有没有其他方法保证线程安全?
有。尽可能避免引起非线程安全的条件——共享变量。如果能从设计上避免 共享变量的使用,即可避免非线程安全的发生,也就无须通过锁或者 synchronized以及volatile解决原子性、可见性和顺序性的问题。
还有不可变对象 可以使用final修饰的对象保证线程安全,由于final修饰的引用型变量(除String外)不 可变是指引用不可变,但其指向的对象是可变的,所以此类必须安全发布,即不能对外提供 可以修改final对象的接口。锁在项目中使用场景?
JAVA怎么避免死锁?
避免死锁
编写一个会导致死锁的代码如下:
1、加锁顺序 当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。
2、加锁时限 另外一个可以避免死锁的方法是在尝试获取锁的时候加一个超时时间,这也就意味着在 尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的 时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机 的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应 用在没有获得锁的时候可以继续运行(译者注:加锁超时后可以先继续运行干点其它事情, 再回头来重复之前加锁的逻辑)。
3、死锁检测死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超 时也不可行的场景。每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记 下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。那么当检测出死锁时,这些线程该做些什么呢?一个可行的做法是释放所有锁,回退,并且等待一段随机的时间后重试。这个和简单的 加锁超时类似,不一样的是只有死锁已经发生了才回退,而不会是因为加锁的请求超时了。虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁(编者 注:原因同超时类似,不能从根本上减轻竞争)。
一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就 像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同 一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级。
顺便复习一下操作系统死锁:
死锁预防:限制申请方式:
互斥:原来独占的资源变得共享,可能会造成程序不确定性。
占用并等待:必须保证当一个进程请求一个资源的时候,它不持有任何其他资 源。(要么全部拿到,要么一点也不占有)它开始执行之前需要进程请求并分配其所有的资源,允许进程 请求资源当且仅当进程没有占有任何资源的时候 资源利用率低,可能发生饥饿 无抢占 如果进程占有某些资源,并请求其他不能被立即分配的资源, 则释放当前正占有的资源 被抢占资源添加到资源列表中 只有当它能够获得旧的资源以及它请求的新的资源,进程可以得到执行。
循环等待:对所有资源类型进行排序,并要求每个进程按照资源的顺序进行申请。
死锁避免:银行家算法,如果发现分配了资源之后就可能死锁,就不分配资源了。
死锁检测:允许进入死锁状态,主要是通过检测算法看看是否产生了死锁,然后让相应线程 进行回滚。
死锁恢复:杀死所有进程,或者根据优先级杀死部分进程,从而解除死锁。
ThreadLocal具体怎么使用?使用在什么场景?
彻底理解ThreadLocal
ThreadLocal-面试必问深度解析
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供 独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其 它线程所对应的副本。从线程的角度看,目标变量就象是线程的本地变量,这也是类名 中“Local”所要表达的意思。ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思 路很简单:在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本, Map中元素的键为线程对象,而值对应线程的变量副本。ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每 一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因 为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全 的变量封装进ThreadLocal。个人理解:每一个ThreadLocal内部有一个静态内部 类:ThreadLocalMap,Map里面存储线程本地线程对象(key)和线程的变量副 本(value)但是,Thread内部的Map是由ThreadLocal维护的,由 ThreadLocal负责向map获取和设置线程的变量值。所以对于不同的线程,每次 获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离, 互不干扰。
使用场景:还记得Hibernate的session获取场景吗?
private static final ThreadLocal<Session> threadLocal = new ThreadLocal<S ession>();
//获取Session 4 public static Session getCurrentSession(){
Session session = threadLocal.get();
//判断Session是否为空,如果为空,将创建一个session,并设置到本地线程变量中
try {
if(session ==null&&!session.isOpen()){
if(sessionFactory==null){
rbuildSessionFactory();
// 创建Hibernate的SessionFactory
}else{
session = sessionFactory.openSession();
}
}
threadLocal.set(session);
} catch (Exception e) {
// TODO: handle exception18 }
return session;
}
为什么每个线程访问数据库都应当是一个独立的Session会话?
如果多个线程共享同一个Session会话,有可能其他线程关闭连接了,当前线程再执行提交 时就会出现会话已关闭的异常,导致系统异常。
此方式能避免线程争抢 Session,提高并发下的安全性。
使用ThreadLocal的典型场景正如上面的数据库连接管理,线程会话管理等场景,只适用于独立变量副本的情况,如果变量为全局共享的,则不适用在高并 发下使用。
自己使用的一个场景:
@Component
public class HostHolder {
private static ThreadLocal<User> users = new ThreadLocal<User>();
public User getUser() {
return users.get();
}
public void setUser(User user) {
users.set(user);
}
public void clear() {
users.remove();
}
}
主要是用来判断当前用户是否登录。即在某些页面比如发帖等页面需要判断 当前用户是否登录,若没有登录则需要跳转到登录页面。
总结:
每个ThreadLocal只能保存一个变量副本,如果想要上线一个线程能 够保存多个副本以上,就需要创建多个ThreadLocal。
ThreadLocal内部的ThreadLocalMap键为弱引用,会有内存泄漏的 风险。适用于无状态,副本变量独立后不影响业务逻辑的高并发场景。
如果业务逻辑强依赖于副本变量,则不适合用ThreadLocal解决,需要另寻解决方案。