提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
下文中涉及到的代码见https://download.csdn.net/download/weixin_38148824/87397823
一、线程安全问题
1, 多个线程处理共享数据时,可能会出现逻辑或者数据的错误。例如:DirtySum.java
2, 问题原因
2.1 为了加速数据处理,java或者cpu都设计了多级缓存。 线程的本地缓存,对其他线程是不可见的。可以查看jmm内存模型和cpu内存模型。
2.2 DirtySum.java 问题原因模拟图
2.3 造成多线程情况下问题的原因,通常有以下几个方面
处理共享数据
线程执行随机切换,导致程序非原子化执行
线程间缓存数据不可见,demo:VisibilityDemo.java
指令重排序 (编译器和CPU都会重排,目的是为了提高效率,demo:
ReOrderDemo.java OrderRearrangeDemo.java)
2.4 解决办法
不共享数据,如使用ThreadLocal,
对象设计成无状态, 多例对象
对线程进行同步,即加锁,本文介绍的就是这种方式
2.5 锁,可以理解为一个资源,让多线程去竞争同一个资源,得到资源的线程,执行的多个指令可以认为是原子的。另外,jvm的锁往往都有一些内存语义,对线程间缓存数据的可见性 和 指令重排,都有一定的限制(happen-before原则)。
二、java常见的锁
1.syncronized关键字
1.1 JVM提供的锁, 当两个并发线程访问同一个对象object中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块
1.2 常见使用方式,demo:SynTest.java
1.3 支持重入,锁的重入,是指当一个线程获取到锁后,释放锁之前,能够再次获取该锁。 能重入的锁demo:ReEntrySyn.java,不能重入的锁demo:MyLock.java
1.4 注意死锁问题,demo:SynDead.java,发生了死锁可以使用jstack命令查看。
1.5 释放锁时,会把线程本地缓存数据,刷回到主存,解决了数据可见性问题。
1.6 Jvm对syncronized的优化,demo:PartialSyncronized.java
2. Lock接口,主要介绍ReentrantLock
2.1 Java的一个类,常见使用方式demo: ReetrantSum.java
2.2 基于AQS实现。
2.3 释放锁时,基于AQS内部的volatitle关键字,将线程本地缓存刷刷新到主存。
2.4 与syncronized比较
Lock可以响应中断,设置超时,支持公平锁,使用更加灵活,demo:InteruptDemo.java TimeoutDemo.java
效率上的比较,竞争激烈程度不高时,syncronized稍微具优势。竞争激烈程度很高时,两者相差无几。 (待求证)
2.5 读写锁
ReentrantReadWriteLock
使用案例:ReadDemo.java WriteDemo.java ReadWriteDemo.java
2.6 公平锁
从实现来看,线程在竞争锁时,先看一下同步队列中是否有等待线程,如果有则去排队
3,CAS
3.1 比较并交换 CASSum.java
3.2 CAS 比较和交换,是由硬件(CPU)保证的原子性
3.3 与 syncronized ReentrantLock 相比,CAS是一中乐观锁。
3.4 乐观锁,是认为竞争程度不高,不需要同步(加锁的过程,需要用户态到内核态的切换,耗费大量的资源)。但是,CAS操作往往配合循环使用,会浪费大量CPU,降低程序的吞吐量。所以,竞争激烈的场景适用悲观锁,不激烈适用乐观锁
3.5 数据库中的乐观锁。例如:update table set money = 1000 where money = 900 and update_time = 1234567;
另外很多数据库 例如Mysql Hbase 都提供了CAS操作API
4,分布式锁
4.1 多进程情况下,syncronized 和 ReentrantLock 就没用了。常见的分布式锁, 基于数据库Mysql, Redis,Zookeeper等实现
4.2 基于Redis的分布锁(Redisson的实现) ,demo:RedissonLockSum.java.
加锁的脚本:
KEYS[1] : 创建lock时的key
ARGV[1] :过期时间
ARGV[2] :线程id
if (redis.call('exists', KEYS[1]) == 0) then
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
return redis.call('pttl', KEYS[1]);
释放锁的脚本:
KEYS[1] : 创建lock时的key
KEYS[1] : redisson_lock__channel:{创建lock时的key}
ARGV[1] :LockPubSub.UNLOCK_MESSAGE 0
ARGV[2] :过期时间
ARGV[3] :线程id
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
return nil;
end;
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
if (counter > 0) then
redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
else
redis.call('del', KEYS[1]);
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
return nil;
Key是有超时时间的,如果使用时未设置,会给一个默认的超时,然后有个后台线程不断的延长超时时间(如果需要)。缺点:依赖于redis的稳定性,增加redis的压力,主从切换导致的问题等。
总结
除了直接使用锁之外,java内置了很多线程安全的工具