java 线程安全解决方案-java怎么解决线程死锁
1.什么是线程安全
当多个线程访问某个类(对象或方法)时,该对象对应的公共数据区总能正确运行,则该类(对象或方法)是线程安全的。
线程安全代码是在多个线程并发执行时起作用的代码
如果一段代码可以确保共享数据在被多个线程访问时被正确操作,那么它就是线程安全的。
二、创建线程的方式
继承Thread类创建线程
实现Runnable接口创建线程
使用 Callable 和 Future 创建线程
使用线程池创建(使用 java.util.concurrent.Executor 接口)
1.继承Thread类创建线程类
(1)定义一个Thread类的子类,重写这个类的run方法。 run方法的方法体表示线程要完成的任务。 因此,run()方法被称为执行体。
(2)创建Thread子类的实例,即创建线程对象。
(3)调用线程对象的start()方法启动线程。
2.通过实现Runnable接口创建线程类
(1)定义runnable接口的实现类,重写接口的run()方法。 run()方法的方法体也是线程的线程执行体。
(2)创建Runnable实现类的一个实例,并以这个实例为Thread的目标创建一个Thread对象,这才是真正的线程对象。
(3)调用线程对象的start()方法启动线程。
3.通过Callable和Future创建线程
(1)创建Callable接口的实现类,实现call()方法。 call()方法会作为线程执行体,有返回值。
(2)创建Callable实现类的实例,使用FutureTask类封装Callable对象。 FutureTask 对象封装了 Callable 对象的 call() 方法的返回值。
(3)使用FutureTask对象作为Thread对象的目标来创建并启动一个新的线程。
(4)调用FutureTask对象的get()方法获取子线程执行完成后的返回值。
4.使用线程池创建(使用java.util.concurrent.Executor接口)
3.Runnable和Callable的区别
注意:Callalble接口支持返回执行结果,需要调用FutureTask.get()获取。 该方法会阻止主进程继续执行。 如果不调用,则不会被阻塞。
4.wait方法和sleep方法的区别 5.synchronized和ReentrantLock的区别
1)Lock是接口,synchronized是Java中的关键字,synchronized是内置语言实现;
2)synchronized发生异常时,线程占用的锁会自动释放,不会出现死锁。 如果Lock异常,如果不主动释放java 线程安全解决方案,极有可能造成死锁,所以需要调用finally中的unLock方法来释放锁;
3)Lock可以让等待锁的线程响应中断,使用synchronized只会让等待的线程永远等待,无法响应中断
4)通过Lock可以知道锁是否已经成功获取,synchronized就不行了
5)Lock可以提高多线程进行读操作的效率。
ReentrantLock是Lock的实现类,是一个互斥的同步器。 多线程高竞争情况下,ReentrantLock比synchronized有更好的性能
在底层实现上,synchronized是JVM层面的锁,一个Java关键字,通过monitor对象(monitorenter和monitorexit)完成。 对象只能在同步块或同步方法中调用wait/notify方法。 ReentrantLock是从jdk1.5以来提供的API级锁(java.util.concurrent.locks.Lock)。
是否可以手动释放:
Synchronized不需要用户手动释放锁。 synchronized代码执行完后,系统会自动让线程释放锁。 ReentrantLock 需要用户手动释放锁。 如果不手动释放锁,可能会导致死锁。 一般使用lock()和unlock()方法来配合try/finally语句块,release的使用更加灵活。
是否可中断
synchronized是一种不可中断的锁类型,除非被锁代码出现异常或者正常执行完成; ReentrantLock是可以打断的,可以通过trylock(long timeout, TimeUnit unit)设置超时方法或者把lockInterruptibly()放在代码块中,调用interrupt方法打断。
是公平锁吗
如果synchronized是非公平锁ReentrantLock,可以选择公平锁或者非公平锁。 可以在构造新的 ReentrantLock 时通过传入布尔值来选择。 如果为空,则对于非公平锁默认为 false,对于公平锁默认为 true。
锁是否可以绑定条件Condition
synchronized不能绑定; ReentrantLock通过绑定Condition结合await()/singal()方法实现线程的精准唤醒,而不是像synchronized那样通过Object类的wait()/notify()/notifyAll()方法随机唤醒一个线程 Either wake up所有线程。
锁定对象
synchronzied锁是对象,锁保存在对象头中。 根据对象头数据,识别是否有线程获取锁/竞争锁; ReentrantLock锁是线程,根据传入线程和int类型状态获取锁。 /争夺。
两者的共同点:
1.两者都是用来协调多线程对共享对象和变量的访问
2.都是可重入锁,同一个线程可以多次获取同一个锁
3.可见性和互斥性均有保障
两者的区别:
1.ReentrantLock显示锁的获取和释放,synchronized是隐式获取和释放锁
2、ReentrantLock可以响应中断,可以轮回。 同步不能响应中断。 用于处理锁
不可用性提供了更大的灵活性
3、ReentrantLock是API层面的,synchronized是JVM层面的
4.ReentrantLock可以实现公平锁
5.ReentrantLock可以通过Condition绑定多个条件
6.底层实现不同,synchronized是同步阻塞,采用悲观并发策略,lock是同步非阻塞
plug,使用乐观并发策略
7.Lock是接口,synchronized是Java中的关键字,synchronized是内置语言
完成。
8、当异常发生时,synchronized会自动释放线程持有的锁,所以不会造成死锁;
Lock发生异常时,如果不通过unLock()主动释放锁,很可能造成死锁。
因此,在使用Lock时,需要在finally块中释放锁。
9.Lock可以让等待锁的线程响应中断,synchronized不行。 使用同步时,
等待线程将永远等待,无法响应中断。
10、通过Lock可以知道锁是否已经成功获取,synchronized不能。
11、Lock可以提高多线程读操作的效率,即实现读写锁等。
六、CAS免锁技术(简单了解即可,详见Java_jayxu博客无捷径-CSDN博客_cas中CAS详解) 七、volatile关键字的作用及原理
对于用 volatile 关键字修饰的变量,编译器和运行时都会注意到该变量是共享的,因此对该变量的操作不会与其他内存操作重新排序。 易失性变量不会缓存在寄存器中,否则对其他处理器不可见,因此读取易失性变量将始终返回最近写入的值。
访问volatile变量时不进行加锁操作,因此不会阻塞执行线程,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。 在读写非易失性变量时,每个线程首先将变量从内存中复制到CPU缓存中。 如果计算机有多个 CPU,每个线程可能在不同的 CPU 上处理,这意味着每个线程可能被复制到不同的 CPU 缓存中。 声明的变量是volatile,JVM保证每次都是从内存中读取变量,跳过CPU缓存这一步。
可见性是指当多个线程访问同一个变量时,一个线程修改变量的值,其他线程可以立即看到修改后的值。 Java中的volatile关键字提供了一个功能,就是被它修饰的变量在被修改后可以立即同步到主存,被它修饰的变量在每次使用前从主存中刷新。 因此可以使用volatile来保证多线程运行时变量的可见性。
Volatile 不是原子的,但具有可见性,并且在一定程度上具有秩序。
八、ThreadLocal和ThreadPoolExectutor
ThreadLocal称为线程变量,也就是说ThreadLocal中填充的变量属于当前线程,该变量与其他线程是隔离的。 ThreadLocal 在每个线程中创建变量的副本,因此每个线程都可以访问自己的内部副本变量。
使用场景
在跨层传递对象时,使用ThreadLocal可以避免多次传递java 线程安全解决方案,打破层与层之间的约束。 事务操作的线程间数据隔离,用于存储线程事务信息。 数据库连接、Session会话管理。九、公共线程池
Java通过Executors提供了四种线程池,分别是:
newCachedThreadPool 创建一个可缓存的线程池。 如果线程池的长度超过处理需要,可以灵活回收空闲线程。 如果没有可回收线程,则会创建一个新线程。
newFixedThreadPool 创建一个定长线程池,可以控制最大并发线程数,超出的线程会在队列中等待。
newScheduledThreadPool 创建一个支持定时和周期性任务执行的定长线程池。
newSingleThreadExecutor创建一个单线程线程池,只使用唯一的工作线程执行任务,保证所有任务按指定顺序(FIFO、LIFO、优先级)执行。
10.分布式环境下如何保证线程安全
避免并发
在分布式环境中,如果出现并发问题,很难通过技术解决,或者解决方案非常昂贵,所以我们首先要思考是否可以通过一定的策略和业务设计来避免并发。 例如,通过合理的时间调度,可以避免共享资源的访问冲突。 此外,在并行任务的设计中,可以采用适当的策略来保证任务之间不共享资源。 比如上一篇博文提到的例子,我们需要用多线程或者分布式集群来计算一堆客户的相关性。 统计值,由于客户的统计值是共享数据,有并发的潜力。 但是从业务的角度,我们可以分析客户之间的数据是不共享的,所以我们可以设计一个规则,保证一个客户的计算工作和数据访问只会由一个线程或者一台worker机器来完成,而不是一个客户端的计算工作分配给多个线程来完成。 这个规则很容易设计,例如可以使用哈希算法。
时间戳
分布式环境下的并发并不能保证时序,无论是通过远程接口的同步调用还是异步消息,都容易导致一些需要时序的业务在高并发时产生错误。 例如系统A需要将某个值的变化同步给系统B,由于通知的时间问题,过期的值会覆盖有效值。 对于这个问题,常用的方法是使用时间戳的方法。 系统A每次向系统B发送一个变更,都需要带上一个时间戳,可以标记时机。 系统B收到通知后,将时间戳与现有时间戳进行比较,只有当通知的时间戳大于现有时间戳时才更新。 这种方法比较简单,但关键是调用方一般需要保证时间戳的计时有效性。
连载
有时可以通过序列化可能导致并发问题的操作来满足数据一致性的要求,牺牲性能和可扩展性。 比如分布式消息系统不能保证消息的顺序,但是可以通过将分布式消息系统改成单体系统来保证消息的顺序。 另外,当接收方无法处理调用顺序时,可以先通过队列缓存调用信息,然后串行处理这些调用。
数据库
分布式环境下的共享资源无法通过Java中的同步方式或加锁实现线程安全,但数据库是分布式服务器的共享点,可以利用数据库的高可靠一致性机制来满足要求。 例如,唯一索引可以用来解决重复数据的产生或者并发过程中重复任务的执行; 另外,一些update计算操作尽量通过SQL完成,因为程序段计算完再更新后可能会出现脏复制的问题,但是通过一个sql完成计算和update,更新操作的一致性可以通过数据库的锁机制来保证。
行锁
有些事务比较复杂,无法通过单条sql解决,存在并发问题。 这时候就需要使用行锁来解决了。 通常,行锁可以通过以下方式实现:
对于Oracle数据库,可以使用select ... for update方法。这种方法有潜在的危险,即如果没有commit,这行数据将被锁定,其他涉及到这行数据的任务将被执行暂停,因此应谨慎使用
添加一个字段来标记表中的锁。 在每次操作之前,使用更新锁字段完成类似于竞争锁的操作。 操作完成后,重新设置update lock field表示锁已经归还。这种方式比较安全,但是缺点是操作这些update lock field是额外的性能消耗
统一触发通路
当一条数据可能被多个触发点或多个业务涉及时,就会存在并发问题带来的隐患。 因此,通过前期的架构和业务设计,我们可以尝试统一触发渠道。 缺少触发通道降低了并发的可能性。 也有利于对并发问题的分析判断。
高并发下如何保证线程安全?
对于商城这样的系统,单点登录、购物车、订单都是有并发的。
使用AtomicInteger、synchronized、Lock、ThreadLocal等类在代码层面保证线程安全; 如果功能需要自主多线程处理,那么也会使用线程池ThreadPool来提高并发效率。
对于高并发处理,会用到Redis分布式锁(setnx)。 当服务器的承载量达到一定数量后,所有后续的请求都会加入到队列中进行处理。
负载均衡:在代码层面针对不同业务进行读写分离; 而集群和主从复制是在数据库上进行的。 相应的,应用服务器上的各个服务器采用lvs+keepalive的方式进行服务器集群; 如果硬件资源足够,那么可以使用越来越多的分散集群节点来提高并发性和系统稳定性。
Redis 是一种开源的高级键值存储,是构建高性能、可扩展且线程安全的 Web 应用程序的完美解决方案。
Redis具有三个主要特点:
Redis 数据库完全在内存中,仅使用磁盘进行持久化。
与许多键值数据存储相比,Redis 具有更丰富的数据类型集合(列表、字符串、排序、集合、哈希)。
Redis 可以将数据复制到任意数量的从服务器。