使用锁的目的
多个外部线程同时来竞争使用同一资源时,会彼此影响,导致混乱,然后锁的目的,就是将资源的使用做排它性处理,使同一时间,仅一个线程能访问资源
如何理解线程安全的类
1:无成员变量/成员变量不存在变化的类,就是无状态类 ----- 这种类是线程安全的
2:如果成员变量状态变化是原子的(即没有中间变迁过程,变化不需要时间,没有中间态) ---- 那么它一样是线程安全的
锁的本质
锁要解决的问题是 ------- 资源数据会不一致
锁要达成的目标是 ------- 让资源使用起来,像原子性一样
锁达成目标的手段 ------- 让使用者访问资源时,只能排队,一个一个地去访问资源
分布式下实现原理
由于服务部署在不同的服务器上,JVM无法发挥作用,此时只得去寻求大家都公认的服务来做见证人,以协调资源,常见的公证人 ------》 mysql/zk/file/redis
然后我们可以通过公证人发出信号,来协调分布式的访问者,排队访问资源
注意:
1:任何一个能够提供【是/否】信号量的事物,都可以来做公证人
2:发出锁信号量的动作,本身必须是原子性的
Jedis实现
使用redis单线程的特性实现,同一时刻只有一个线程在执行,其余线程阻塞
1:jar包依赖
<!-- config jedis data and client jar -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.7.1.RELEASE</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.8.1</version>
</dependency>
2:生成对应的工厂配置类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import redis.clients.jedis.JedisPoolConfig;
@Configuration
public class RedisConfig {
//jedis连接池
@Bean
public JedisPoolConfig jedisPoolConfig() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(10);
jedisPoolConfig.setMaxTotal(10000);
return jedisPoolConfig;
}
//jedis连接工厂
@Bean
public JedisConnectionFactory jedisConnectionFactory(JedisPoolConfig jedisPoolConfig) {
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory();
//redis服务所在服务器ip
jedisConnectionFactory.setHostName("127.0.0.1");
//redis服务所在服务器端口
jedisConnectionFactory.setPort(6379);
jedisConnectionFactory.setUsePool(true);
jedisConnectionFactory.setPoolConfig(jedisPoolConfig);
return jedisConnectionFactory;
}
}
3:redisLock工具类
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
import javax.annotation.Resource;
import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
@Service
public class RedisLock implements Lock {
private static final String KEY = "LOCK_KEY";
@Resource
private JedisConnectionFactory factory;
//每个线程单独私有
private ThreadLocal<String> local = new ThreadLocal<>();
@Override
//阻塞式的加锁
public void lock() {
//1.尝试加锁
if(tryLock()){
return;
}
//2.加锁失败,当前任务休眠一段时间
try {
Thread.sleep(10);//性能浪费
} catch (InterruptedException e) {
e.printStackTrace();
}
//3.递归调用,再次去抢锁
lock();
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
//阻塞式加锁,使用redis的setNx命令返回OK的加锁成功,并生产随机值
public boolean tryLock() {
//产生随机值,标识本次锁编号,并放入本地线程变量,解锁时根据编号释放对应的锁
String uuid = UUID.randomUUID().toString();
Jedis jedis = (Jedis) factory.getConnection().getNativeConnection();
/**
* key:我们使用key来当锁
* uuid:唯一标识,这个锁是我加的,属于我
* NX:设入模式【SET_IF_NOT_EXIST】--仅当key不存在时,本语句的值才设入
* PX:给key加有效期
* 1000:有效时间为 1 秒
*/
String ret = jedis.set(KEY, uuid,"NX","PX",1000);
//设值成功--抢到了锁
if("OK".equals(ret)){
local.set(uuid);//抢锁成功,把锁标识号记录入本线程--- Threadlocal
return true;
}
//key值里面有了,我的uuid未能设入进去,抢锁失败
return false;
}
//错误解锁方式
public void unlockWrong() {
//获取redis的原始连接
Jedis jedis = (Jedis) factory.getConnection().getNativeConnection();
String uuid = jedis.get(KEY);//现在锁还是自己的
//uuid与我的相等,证明这是我当初加上的锁
if (null != uuid && uuid.equals(local.get())){//现在锁还是自己的
//进来时锁刚好失效了,新的线程持有新的锁
//最后进行删锁时,删除的已经是别的线程所持有的锁
jedis.del(KEY);
}
}
//正确解锁方式
public void unlock() {
//读取lua脚本
String script = FileUtils.getScript("unlock.lua");
//获取redis的原始连接
Jedis jedis = (Jedis) factory.getConnection().getNativeConnection();
//通过原始连接连接redis执行lua脚本
jedis.eval(script, Arrays.asList(KEY), Arrays.asList(local.get()));
}
//-----------------------------------------------
@Override
public Condition newCondition() {
return null;
}
@Override
public boolean tryLock(long time, TimeUnit unit)
throws InterruptedException {
return false;
}
}
备注:解锁时,使用了lua 脚本具有原子性操作的特性,解决锁释放时存在的安全隐患问题
4:读取文档工具类
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
public class FileUtils {
//无成员变量 --- 无状态
public static String getScript(String fileName){
String path = FileUtils.class.getClassLoader().getResource(fileName).getPath();
return readFileByLines(path);
}
public static String readFileByLines(String fileName) {
FileInputStream file = null;
BufferedReader reader = null;
InputStreamReader inputFileReader = null;
String content = "";
String tempString = null;
try {
file = new FileInputStream(fileName);
inputFileReader = new InputStreamReader(file, "utf-8");
reader = new BufferedReader(inputFileReader);
// 一次读入一行,直到读入null为文件结束
while ((tempString = reader.readLine()) != null) {
content += tempString;
}
reader.close();
} catch (IOException e) {
e.printStackTrace();
return null;
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e1) {
}
}
}
return content;
}
public static void main(String[] args) {
String path = FileUtils.class.getClassLoader().getResource("unlock.lua").getPath();
String script = FileUtils.readFileByLines(path);
System.out.println(script);
}
}
5:unlock.lua 脚本文件
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
然后就可进行使用