文章目录
说到锁,你们能想起java种有哪些锁的概念?
乐观锁,悲观锁,公平锁,非公平锁,同步锁(代码块),读写锁,可重入锁,lock锁等等。。。
一、 锁的介绍
对于锁的理解 -确保线程安全
公平/非公平锁
公平:不可以插队
非公平:可以插队,这是正常的业务逻辑 – 大部分锁都是
乐观/悲观锁
乐观锁:乐观,操作的时候不加锁,等最后的时候判断是否需要事务回滚。
- 关系型数据库:添加一个字段version,操作前先获取,结束后再获取version,两次值不同说明发生了变化。
- 非关系型数据库(redis):watch监控命令就是有个乐观锁
悲观锁:悲观,一开始就加锁。
正是由于有了悲观锁,所以才会有其他锁的概念。
二、锁
1.synchronized同步锁
同步锁synchronized,是一种悲观锁
synchronized关键字:表示“同步”的。它可以对“多行代码”进行“同步”——将多行代码当成是一个完整的整体;
一个线程如果进入到这个代码块中,会全部执行完毕,执行结束后,其它线程才会执行。这样可以保证这多行的代码作为完整的整体,被一个线程完整的执行完毕。
使用方法有两种:
- 同步方法
- 同步代码块
但是有一个致命的缺点就是数据同步的问题,尤其是遇到减法的时候,可能会导致超卖等真实问题。
同步代码块
用于方法中的某个区块中,表示只对这个区块的资源实行互斥访
问
synchronized(同步锁){
需要同步操作的代码
}
1. 锁对象 可以是任意类型。
2. 多个线程对象 要使用同一把锁。
例子:
Object lock = new Object();
synchronized (lock) {
if(ticket>0){//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(50);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name+"正在卖:"+ticket--);
}
}
同步方法
保证A线程执行该方法的时候,其他线程只能在方法外等着
public synchronized void method(){
里面是可能会产生线程安全问题的代码
}
对于非static方法,同步锁就是this当前调用者对象。
对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。
public synchronized void sellTicket(){
if(ticket>0){//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name+"正在卖:"+ticket--);
}
}
2.lock锁
从api文档可知,lock是一个接口,有三个实现类,分别是:
- ReentrantLock 可重入锁
- ReentrantReadWriteLock.ReadLock 读锁
- ReentrantReadWriteLock.WriteLock 写锁
ReentrantLock 可重入锁
我们正常使用lock锁,是new 一个可重入锁对象使用的
ReentrantLock有两个实现类
- ReentrantLock() 默认非公平锁
- ReentrantLock(boolean fair) true为公平,false非公平
例子
Lock lock = new ReentrantLock();
业务方法(){
lock.lock(); //加锁
try{
线程不安全的操作
}catch(){
} finally{
lock.unlock(); //解锁
}
}
ReentrantReadWriteLock 读写锁
本身是ReadWriteLock接口的唯一实现类
读可以被多线程同时读,但是写只能被一个线程写
第一步:创建读写锁
ReadWriteLock lock = new ReentrantReadWriteLock();
第二步:对写的操作
lock.writeLock().lock();
lock.writeLock().unlock();
第三步:对读的操作
lock.readLock().lock();
lock.readLock().unlock();
3.java.util.(concurrent).locks
这个locks下面有三个接口
- Condition 并发锁
- Lock 锁
- ReadWriteLock 读写锁
4.Synchronized和lock锁的区别
Synchronized 是关键字 无法判断获取锁的状态 自动加放锁 线程阻塞一直等待 可重入锁,非公平,不可终端, 是和少量代码的同步问题
Lock 是类 可判断获取锁的状态 手动加放锁 lock.tryLock()可以尝试获取锁,不会一直等待 可重入锁 适合大量代码
三、线程的通信
线程之间的通信其实就是线程支架你的等待唤醒和通知唤醒
比如,想线程之间交替执行,就需要实现线程之间的通信
有两个版本,第一个是Synchronized版本 ,另一个是JUC版本
1.Synchronized版本
使用的是wait和notify方法来实现
这里需要注意一个问题,不能用if,单线程个数超过2个的时候,会出现虚假唤醒,也就是同时唤醒多个线程,导致数据不对。。。所以等待应该总是出现在循环while中,防止虚化唤醒
class Data {
private int number = 0;
public syschronized void increment {
while(number != 0) {
this.wait(); // 等待,放锁
}
number ++; // 业务
this.notifyAll(); // 唤醒通知其他线程,+1的业务操作执行完毕
}
public syschronized void decrement {
while(number == 0) {
this.wait(); // 等待,放锁
}
number --; // 业务
this.notifyAll(); // 唤醒通知其他线程,-1的业务操作执行完毕
}
}
class A {
main() {
Data data = new Data();
new Thread(()-> for循环10执行data.increament());
new Thread(()-> for循环10执行data.decreament());
}
}
结果就是 一直10101010这样
2.JUC版本
JUC版本就是使用Lock中的Condition的await和signal方法
class Date {
private int number = 0;
Lock lock - new ReetrantLock();
Condition condition = lock.new Condition();
public void increment() {
lock.lock();
try{
while(number != 0) {
condition.await(); // 等待
}
number++;
condition.signalAll(); // 唤醒
}
} catch(){}
finally{
lock.unlock();
}
}
减法decrement方法同理
Synchronized版本和JUC版本的区别就是:
JUC版本可以精准唤醒某一个线程,那就是使用signal而不是用signalAll,例子
使用多个condition对象,同步对象点击不同的await和signal方法来实现
如 condition2.signal那唤醒的就是用condition2.await的线程
四、自动释放锁 - jdk新特性
jdk7的新特性,自动释放锁- AutoCloseable
问题产生:
在开发中锁的释放是必须要做的事情,所以会把它放在 finally 中执行。但是在开发中往往很多时候都会忘记释放锁或者忘记把锁的释放放入 finally 中,就会造成死锁的现象,这就是一个危险的操作和行为。
问题解决:
使用 AutoCloseable 接口覆盖 close 方法。JDK 在1.7 之后出现了自动关闭类的功能,该功能的出现为各种关闭资源提供了相当大的帮助。实现 AutoCloseable 接口覆盖 close 方法,把原来要写 finally 中释放资源的动作放入到 close 方法中让 JVM 自动地去执行。
自动释放资源实现:
- 实现 AutoCloseable 接口。
- 接口的实现类要重写 close 方法。
- 将要关闭的资源定义在 try 中,程序执行完毕之后,资源将会自动关闭。
public class MyLockDemo implements AutoCloseable {
static Lock lock = new ReentrantLock();
public static void main(String[] args) {
try {
lock.lock();
System.out.println("1-----加锁成功");
System.out.println("1-----开始执行业务逻辑");
Thread.sleep(3000);
System.out.println("3-----业务执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void close() throws Exception {
lock.unlock();
System.out.println("4-----释放锁资源");
}
}
五、案例
假如我要使用一个广告节目单,分为两个步骤:
步骤一:生成节目单
步骤二:发布节目单
判断条件:最新的发布时间 不能 在生成时间之后
private static Date newestPublicDate = new Date();
// 时间判断的方法
public void updateNewestPublicDate(Date createTime) {
synchronized (newestPublicDate) {
if (newestPublicDate.after(createTime)) {
String errMsg = MyPlanConstant.PC_CREATETIME_BEFORE_PUBLICTIME;
logger.error(errMsg);
throw new PlanException(EamErrorType.PC_ITEM_CREATE_ERROR.getValue(), errMsg);
}
newestPublicDate = createTime;
}
}
/**
* 生成节目单
* 加一个互斥锁
*
* @param itemDto
* @return
*/
@Override
@SaveLog
@Transactional
public synchronized List<ItemCache> moreCreateItem(ItemDto itemDto) {
// 业务逻辑,存储生成时间在redis
}
/**
*发布节目单
*/
@SaveLog
@Override
@Transactional
public void saveItem(ItemDto itemDto) {
// 最新发布时间和生产时间做判断
}
测试用例:
public void test(int i) {
System.out.println("初始" + myCommonUtil.SDF2.format(newestPublicDate));
Thread thread1 = new Thread(() -> {
Date createTime1 = new Date();
try {
createTime1 = myCommonUtil.SDF2.parse("2022-07-22 00:00:00");
} catch (ParseException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "启动");
this.updateNewestPublicDate(createTime1, "甲");
System.out.println(Thread.currentThread().getName() + "结束");
System.out.println("甲线程" + myCommonUtil.SDF2.format(newestPublicDate));
});
thread1.setName("甲线程");
thread1.start();
Thread thread2 = new Thread(() -> {
Date createTime2 = new Date();
try {
createTime2 = myCommonUtil.SDF2.parse("2022-07-21 00:00:00");
} catch (ParseException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "启动");
this.updateNewestPublicDate(createTime2, "乙");
System.out.println(Thread.currentThread().getName() + "结束");
System.out.println("乙线程" + myCommonUtil.SDF2.format(newestPublicDate));
});
thread2.setName("乙线程");
thread2.start();
Thread thread3 = new Thread(() -> {
Date createTime2 = new Date();
try {
createTime2 = myCommonUtil.SDF2.parse("2022-07-26 00:00:00");
} catch (ParseException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "启动");
this.updateNewestPublicDate(createTime2, "丙");
System.out.println(Thread.currentThread().getName() + "结束");
System.out.println("丙线程" + myCommonUtil.SDF2.format(newestPublicDate));
});
thread3.setName("丙线程");
thread3.start();
Thread thread4 = new Thread(() -> {
Date createTime2 = new Date();
try {
createTime2 = myCommonUtil.SDF2.parse("2022-07-27 00:00:00");
} catch (ParseException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "启动");
this.updateNewestPublicDate(createTime2, "订");
System.out.println(Thread.currentThread().getName() + "结束");
System.out.println("订线程" + myCommonUtil.SDF2.format(newestPublicDate));
});
thread4.setName("订线程");
thread4.start();
Thread thread5 = new Thread(() -> {
Date createTime2 = new Date();
try {
createTime2 = myCommonUtil.SDF2.parse("2022-07-20 00:00:00");
} catch (ParseException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "启动");
this.updateNewestPublicDate(createTime2, "a");
System.out.println(Thread.currentThread().getName() + "结束");
System.out.println("a线程" + myCommonUtil.SDF2.format(newestPublicDate));
});
thread5.setName("a线程");
thread5.start();
Thread thread6 = new Thread(() -> {
Date createTime2 = new Date();
try {
createTime2 = myCommonUtil.SDF2.parse("2022-07-29 00:00:00");
} catch (ParseException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "启动");
this.updateNewestPublicDate(createTime2, "b");
System.out.println(Thread.currentThread().getName() + "结束");
System.out.println("b线程" + myCommonUtil.SDF2.format(newestPublicDate));
});
thread6.setName("b线程");
thread6.start();
Thread thread7 = new Thread(() -> {
Date createTime2 = new Date();
try {
createTime2 = myCommonUtil.SDF2.parse("2022-07-30 00:00:00");
} catch (ParseException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "启动");
this.updateNewestPublicDate(createTime2, "c");
System.out.println(Thread.currentThread().getName() + "结束");
System.out.println("c线程" + myCommonUtil.SDF2.format(newestPublicDate));
});
thread7.setName("c线程");
thread7.start();
}