线程与进程区别
每个正在系统上运行的程序都是一个进程。每个进程包含一到多个线程。线程是一组指令的集合,或者是程序的特殊段,它可以在程序里独立执行。也可以把它理解为代码运行的上下文。所以线程基本上是轻量级的进程,它负责在单个程序里执行多任务。通常由操作系统负责多个线程的调度和执行。
总结:进程是所有线程的集合,每一个线程是进程中的一条执行路径。
线程创建方式
1、第一种继承 Thread 类,重写 run 方法
2、第二种实现Runnable接口,重写run方法
3、第三种使用匿名内部类方式
4、使用继承 Thread 类还是使用实现 Runnable 接口好?
使用实现实现Runnable接口好,原因实现了接口还可以继续继承,而继承了类不能再继承。Java 是单继承。
5、启动线程是使用调用start方法还是run方法?
注意:开启线程不是调用 run 方法,而是 start 方法。run 只是使用实例调用方法。
守护线程
Java中有两种线程,一种是用户线程,另一种是守护线程。
用户线程是指用户自定义创建的线程,主线程停止,用户线程不会停止。
守护线程当进程不存在或主线程停止,守护线程也会被停止。
使用 thread.setDaemon(true) 方法设置为守护线程。
多线程运行状态
新建状态:当用new操作符创建一个线程时, 例如new Thread(r),线程还没有开始运行,此时线程处在新建状态。 当一个线程处于新生状态时,程序还没有开始运行线程中的代码。
就绪状态:当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态。处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。
运行状态:当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法。
阻塞状态:线程运行过程中,可能由于各种原因进入阻塞状态:
1>线程通过调用sleep方法进入睡眠状态;
2>线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者;
3>线程试图得到一个锁,而该锁正被其他线程持有;
4>线程在等待某个触发条件
死亡状态:有两个原因会导致线程死亡:
1) run方法正常退出而自然死亡,
2) 一个未捕获的异常终止了run方法而使线程猝死。
为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用 isAlive 方法。如果是可运行或被阻塞,这个方法返回 true; 如果线程仍旧是 new 状态且不是可运行的, 或者线程死亡了,则返回 false。
join()方法
当在主线程当中执行到 t1.join()方法时,就认为主线程应该把执行权让给 t1。
yield()方法
Thread.yield()方法的作用:暂停当前正在执行的线程,并执行其他线程。(可能没有效果)
实际中无法保证yield()达到让步的目的,因为,让步的线程可能被线程调度程序再次选中。
优先级
在JAVA线程中,通过一个int priority来控制优先级,范围为1-10,其中10最高,默认值为5。
注意设置了优先级, 不代表每次都一定会被执行。 只是CPU调度会优先分配
作业题:在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?【 join 方法】
解题思路:依次创建 T1、T2、T3 三个线程,T1线程正常执行,T2的线程把执行权给 t1 优先执行,T3 的线程把执行权给 t2 优先执行。就是调用 join() 方法。
public class MyTest1 {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("T1 线程执行:i="+i);
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 5; i++) {
System.out.println("T2 线程执行:i="+i);
}
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 5; i++) {
System.out.println("T3 线程执行:i="+i);
}
}
});
t1.start();
t2.start();
t3.start();
}
}
什么是线程安全?
当多个线程同时共享,同一个全局变量或静态变量,做写的操作时,可能会发生数据冲突问题,也就是线程安全问题。但是做读操作是不会发生数据冲突问题。
1、线程安全解决办法?
使用多线程之间同步块 synchronized(读法:['sɪŋkrənaɪzd]) 或使用锁(lock)
2、为什么使用线程同步或使用锁能解决线程安全问题呢?
将可能会发生数据冲突问题(线程不安全问题),只能让当前一个线程进行执行。代码执行完成后释放锁,让后才能让其他线程进行执行。这样的话就可以解决线程不安全问题。
3、什么是多线程同步?
当多个线程共享同一个资源,不会受到其他线程的干扰,能够解决线程的安全问题。
内置锁
每一个Java对象都可以用作一个实现同步的锁,称为内置锁,线程进入同步代码块之前自动获取到锁,代码块执行完成正常退出或代码块中抛出异常退出时会释放掉锁
内置锁也称之为互斥锁,即线程A获取到锁后,线程B阻塞直到线程A释放锁,线程B才能获取到同一个锁。
内置锁使用 synchronized 关键字实现,synchronized关键字有两种用法:
1.修饰需要进行同步的方法(所有访问状态变量的方法都必须进行同步),此时充当锁的对象为调用同步方法的对象
2.同步代码块和直接使用 synchronized 修饰需要同步的方法是一样的,但是锁的粒度可以更细,并且充当锁的对象不一定是this,也可以是其它对象,所以使用起来更加灵活。
好处:解决了多线程的安全问题
弊端:多个线程需要判断锁,较为消耗资源、抢锁的资源。
同步代码块示例:
public void sale() {
synchronized (this) {
if (count > 0) {
System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - count + 1) + "张票");
count--;
}
}
}
同步方法示例:(在方法上修饰synchronized 称为同步方法)
public synchronized void sale() {
if (count > 0) {
System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - count + 1) + "张票");
count--;
}
}
wait()、notify()方法
因为涉及到对象锁,它们必须都放在 synchronized 中来使用。wait()、notify() 一定要在 synchronized 里面进行使用。
wait():必须暂定当前正在执行的线程,并释放资源锁,让其他线程可以有机会运行。必须使用 notify 唤醒后才能获取资源继续执行。
notify / notifyall:唤醒当前对象锁池中等待的线程,使之运行。
注意:一定要在线程同步中 synchronized 使用,并且是同一个锁的对象资源。否则会报错。
wait 与 sleep 区别
对于 sleep() 方法,我们首先要知道该方法是属于 Thread 类中的。而 wait() 方法,则是属于 Object 类中的,也就是所有的对象都可以使用 .wait() 函数。
sleep() 方法导致了程序暂停执行指定的时间,让出 cpu 给其他线程,但是他的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态。
在调用 sleep() 方法的过程中,线程不会释放对象锁。
而当调用 wait() 方法的时候,线程会释放对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify() 方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
多线程死锁
什么是多线程死锁?
答:同步中嵌套同步,导致锁无法释放。线程A和线程B要读取数据,且A和B都拥有锁,但是必须要得到对方的释放锁,才能读取数据。双方都不肯释放手中的锁给对方,导致了线程死锁。
深刻记忆法:一双筷子,两个小屁孩每个人都抢到了一只筷子,但是需要2只筷子才能吃粉,两个小屁孩都在等待对方让出筷子,但又不愿意让出自己手中的筷子给对方,这就造成了死锁。导致两个小屁孩都没法吃到粉。(最后被一个糟老头夺过碗直接手抓吃完。)
如何避免死锁?
- 加锁顺序(线程按照一定的顺序加锁):确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。
- 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁。
- 死锁检测。死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。
Threadlocal
ThreadLocal 给每个线程提供局部变量,提高线程安全性。
当使用 ThreadLocal 维护变量时,ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal 类接口很简单,只有4个方法,我们先来了解一下:
- void set(Object value)设置当前线程的线程局部变量的值。
- Object get()该方法获取当前线程所对应的线程局部变量。
- void remove() 将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
- protected Object initialValue() 返回该线程局部变量的初始值,该方法是一个 protected的 方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
ThreadLocal 的作用主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的,在多线程环境下,可以防止自己的变量被其它线程篡改。
Spring的事务主要是 ThreadLocal 和AOP去做实现的。
示例:某个项目中使用 mycat 做数据库的读写分离,
bootstrap.yml 数据源配置如下:
spring:
application:
name: mycatTest
datasource:
type: com.alibaba.druid.pool.DruidDataSource
# 对应 mycat 的数据库地址,user账号只读
read-only-source:
jdbc-url: jdbc:mysql://127.0.0.1:8066/mycatSchemaDB
driver-class-name: com.mysql.jdbc.Driver
username: user
password: user
# 自定义可写的数据源,root账号有读写的权限
write-source:
jdbc-url: jdbc:mysql://127.0.0.1:8066/mycatSchemaDB
driver-class-name: com.mysql.jdbc.Driver
username: root
password: root
数据源配置类:
package com.study.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
/**
* @author biandan
* @description 数据源配置
* @signature 让天下没有难写的代码
* @create 2021-04-26 上午 12:34
*/
@Configuration
public class DataSourceConfig {
//创建只读数据源
@Bean(name = "readOnlySource")
//对应只读属性的前缀
@ConfigurationProperties(prefix = "spring.datasource.read-only-source")
public DataSource readOnlySource(){
return DataSourceBuilder.create().build();
}
//创建可写数据源
@Bean(name = "writeSource")
//对应可写属性的前缀
@ConfigurationProperties(prefix = "spring.datasource.write-source")
public DataSource writeSource(){
return DataSourceBuilder.create().build();
}
}
使用 AbstractRoutingDataSource 实现数据源切换:
package com.study.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* @author biandan
* @description AbstractRoutingDataSource 该类充当了DataSource的路由中介, 能够在运行时, 根据某种key值来动态切换到真正的DataSource上。
* @signature 让天下没有难写的代码
* @create 2021-04-26 上午 1:18
*/
@Configuration
@Primary //自动装配时当出现多个Bean候选者时,被注解为@Primary的Bean将作为首选者
public class DynamicDataSource extends AbstractRoutingDataSource {
@Autowired
@Qualifier("readOnlySource")
private DataSource readOnlySource;
@Autowired
@Qualifier("writeSource")
private DataSource writeSource;
/**
* 这个是主要重写的方法:返回生效的数据源名称
*
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
String dataSource = DataSourceContextHolder.getDataSource();
System.out.println("DynamicDataSource 获取到的 dataSource=" + dataSource);
return dataSource;
}
/***
* 配置数据源信息
*/
@Override
public void afterPropertiesSet() {
Map<Object, Object> map = new HashMap<>();
map.put("readOnlySource",readOnlySource);
map.put("writeSource",writeSource);
setTargetDataSources(map);
setDefaultTargetDataSource(writeSource);//默认数据源
super.afterPropertiesSet();
}
}
使用 ThreadLocal 进行数据源的隔离:
package com.study.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
/**
* @author biandan
* @description 将数据源保存到本地
* @signature 让天下没有难写的代码
* @create 2021-04-26 上午 1:07
*/
@Configuration
@Lazy(false)//【饿汉模式】禁止懒加载,对象会在系统启动的时候被创建。默认为 true,设置是true就会在使用的时候才创建
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
/**
* 设置数据源
*
* @param dataSource
*/
public static void setDataSource(String dataSource) {
contextHolder.set(dataSource);
}
/**
* 获取数据源
*
* @return
*/
public static String getDataSource() {
String dataSource = contextHolder.get();
return dataSource;
}
/**
* 当前线程局部变量的值删除,目的是为了减少内存的占用
*/
public static void removeDataSource() {
contextHolder.remove();
System.out.println("*** 删除当前线程局部变量 ***");
}
}
使用 AOP 动态切换数据源:
package com.study.config;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.annotation.Order;
import java.util.Arrays;
import java.util.List;
/**
* @author biandan
* @description 使用AOP动态切换不同的数据源
* @signature 让天下没有难写的代码
* @create 2021-04-26 上午 1:30
*/
@Aspect
@Configuration
@Lazy(false)
@Order(0) //Order设定AOP执行顺序 使之在数据库事务上先执行。定义Spring IOC容器中Bean的执行顺序的优先级。参数值越小优先级越高
public class SwitchDataSourceAOP {
//这里切到我们的方法目录
@Before("execution(* com.study.service.*.*(..))")
public void pointCut(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
if(methodName.startsWith("get") ||methodName.startsWith("find") ||methodName.startsWith("query") ||
methodName.startsWith("select")){
DataSourceContextHolder.setDataSource("readOnlySource");//只读数据源
}else{
DataSourceContextHolder.setDataSource("writeSource");//切换到写的数据源
}
}
}
在业务层的调用,以 get、find、query、select 开头的方法,使用只读数据源,其它使用写数据源。因此函数的命名也要规范起来。
@Service
public class UserService {
@Autowired
private UserDao userDao;
/**
* 查询所有用户信息
* @return
*/
public List<UserEntity> findAllUsers(){
return userDao.findAllUsers();
}
/**
* 增加用户信息
* @param userEntity
* @return
*/
public Integer addUser(UserEntity userEntity){
return userDao.addUser(userEntity);
}
}
多线程有三大特性
原子性:即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。这2个操作必须要具备原子性才能保证不出现一些意外的问题。原子性其实就是保证数据一致、线程安全一部分。
可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
有序性:程序执行的顺序按照代码的先后顺序执行。比如代码:
int a = 1;//顺序1 int b = 2;//顺序2 a = a + 5;//顺序3 b = a * b;//顺序4
JVM 执行的顺序可能是:2-1-3-4 或者 1-3-2-4 或者 1-2-3-4。也就是说会保证3在4之前执行,否则程序就不可控,导致数据错乱。
Java 内存模型
共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入时,能对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。
从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:
1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
2. 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。
如上图所示,本地内存A和B有主内存中共享变量x的副本。假设初始时,这三个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。
从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM 通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。
Volatile (读[ˈvɑlətl] 窝嘞头)
什么是 Volatile?
可见性也就是说一旦某个线程修改了该被 volatile 修饰的变量,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,可以立即获取修改之后的值。
Volatile 保证了线程间共享变量的及时可见性,但不能保证原子性。不能解决线程安全问题。
在Java中为了加快程序的运行效率,对一些变量的操作通常是在该线程的寄存器或是CPU缓存上进行的,之后才会同步到主存中,而加了 volatile 修饰符的变量则是直接读写主存。
volatile 示例:下面这段代码还没加 volatile,演示效果:
/**
* @author biandan
* @description
* @signature 让天下没有难写的代码
* @create 2021-06-17 下午 11:01
*/
public class MyTest2 {
public static void main(String[] args) throws Exception {
ThreadVolatileDemo demo = new ThreadVolatileDemo();
demo.start();
Thread.sleep(500);//主线程休眠0.5秒
demo.setFlag(false);//修改 flag 值为 false
System.out.println("主线程已经把子线程的 flag 改为 false!");
Thread.sleep(500);//主线程休眠0.5秒
System.out.println("主线程中,获取到子线程的 flag =" + demo.flag);
}
}
//定义测试类
class ThreadVolatileDemo extends Thread {
public boolean flag = true;
@Override
public void run() {
System.out.println("开始执行子线程,子线程的 flag =" + flag);
while (flag) {
}
System.out.println("子线程停止执行,flag =" + flag);
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
执行效果:
说明:一开始子线程先执行,flag = true,然后进入 while 循环。然后主线程把子线程的 flag 改为了 false,但是子线程中还是它原来副本里 flag = true,说明没有获取到主存中的值。
我们加上 volatile 关键字修饰 flag。如图:
继续测试:
说明增加了 volatile 关键字后,子线程读取到 flag = false 了。
原理:volatile 关键字将解决线程之间可见性, 强制线程每次读取该值的时候都去“主内存”中取值。
既然 Volatile 不能保证原子性,那么实际项目中,什么情况下使用 Volatile 关键字?
一般是定义全局变量的属性上,才添加 volatile 关键字,用来修饰全局变量。因为它能把修改后的数据及时同步到主内存中,并且强制线程每次都去主内存中获取最新的值。
Volatile 与 Synchronized 区别
(1)volatile 具有可见性但是并不能保证原子性,不能保证线程安全。(因为线程安全必须保证原子性、可见性、有序性。volatile 只能保证后面2者。)
(2)性能方面,synchronized关键字是防止多个线程同时执行一段代码,就会影响程序执行效率,而 volatile 关键字在某些情况下性能要优于synchronized。
但是要注意 volatile 关键字是无法替代s ynchronized 关键字的,因为volatile关键字无法保证操作的原子性。
《深入理解Java虚拟机》有一段话:
观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入 volatile 关键字时,会多出一个 lock 前缀指令
lock 前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
- 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
- 它会强制将对缓存的修改操作立即写入主存;
- 如果是写操作,它会导致其他CPU中对应的缓存行无效。
比如A和B两个线程操作变量 m=0,A 已经对 m 进行 m++ 操作,这时候只是把数据放入寄存器,还没来得及写入主存,但是线程B这时候就去主存获取 m,得到 m 还是0。
什么是多线程之间通讯?
多线程之间通讯,其实就是多个线程在操作同一个资源,但是操作的动作不同。
如何解决多线程之间的通讯?
使用同步机制 synchronized 或者使用 Lock锁。
Lock 锁
在 jdk1.5 之后,并发包中新增了 Lock 接口(以及相关实现类)用来实现锁功能,Lock 接口提供了与 synchronized 关键字类似的同步功能,但需要在使用时手动获取锁和释放锁。
示例:ReentrantLock 重入锁
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author biandan
* @description
* @signature 让天下没有难写的代码
* @create 2021-06-18 上午 12:34
*/
public class MyTest3 {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
try{
lock.lock();//加锁
System.out.println("业务逻辑代码***");
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();//释放锁
}
}
}
注意要在 finally 中释放锁,不能在 try 中,避免 try 中抛出异常,导致 lock 无法释放。
重入锁
重入锁,也叫做递归锁,指的是同一线程的外层函数获得锁之后,内层递归函数仍然有获取该锁的代码,但不受影响。
ReentrantLock 和 synchronized 都是可重入锁。下面例子就是 synchronized 重入锁的例子。
private static void t5() {
Thread thread = new Thread(new Runnable() {
public synchronized void get() {
System.out.println("锁头1");
set();
}
public synchronized void set() {
System.out.println("锁头2");
}
@Override
public void run() {
get();
}
});
}
下面的例子就是 ReentrantLock 重入锁例子:
private static void t6() {
Thread thread = new Thread(new Runnable() {
ReentrantLock lock = new ReentrantLock();
public void get() {
lock.lock();
System.out.println("lock 1");
set();
lock.unlock();
}
public void set() {
lock.lock();
System.out.println("lock 2");
lock.unlock();
}
@Override
public void run() {
get();
}
});
}
Lock与 synchronized 关键字的区别
1、synchronized是 java 内置关键字,在 jvm 层面,Lock是个 java 类。
2、synchronized 无法判断是否获取锁的状态,Lock 可以判断是否获取到锁。Lock 当获取到的锁的线程被中断时,中断异常将会被抛出,同时锁会被释放。
3、Lock 接口可以尝试非阻塞地获取锁,当前线程尝试获取锁。如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁。
4、synchronized 会自动释放锁,Lock需在 finally 中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁。
5、用 synchronized 关键字的两个线程A和线程B,如果当前线程A获得锁,线程B线程等待。如果线程A阻塞,线程B则会一直等待下去。而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了
6、Lock 锁适合大量同步的代码的同步问题,synchronized 锁适合代码少量的同步问题。
乐观锁
总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或CAS操作实现。
version方式:一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数,当数据被修改时,version值会加1。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
核心SQL语句: update table set data=${data}, version=version+1 where id=#{id} and version=#{version};
CAS操作方式:即 compare and swap 或者 compare and set,涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,一般情况下是一个自旋操作,即不断的重试。
悲观锁
总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加锁(读锁、写锁、行锁等),当其他线程想要访问数据时,都需要阻塞挂起。可以依靠数据库实现,如行锁、读锁和写锁等,都是在操作之前加锁,在Java中,synchronized 的思想也是悲观锁。
分布式锁解决方案
分布式锁一般有三种实现方式:
1、数据库乐观锁
2、基于Redis的分布式锁
3、基于ZooKeeper的分布式锁
这里讲解第二种:基于 Redis 的分布式锁。
需要了解 Redis 的一个命令: setnx
setnx key value :当且仅当 key 不存在时,set一个key为 value 的字符串,返回1。若key存在,则什么都不做,返回0。锁的 value 值为一个随机生成的 UUID
处理分布式请求时,先使用 setnx 命令,在同一个 Redis 上创建相同的 key,因为 Redis 是单线程的,key 不能重复,哪个请求创建成功,就获得了锁。没有创建成功的,需要等待。获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
如何释放锁呢?
1、设置超时时间:expire key timeout。为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。
2、删除 key:delete key。
3、释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。
案例:
private static void t7() {
public String lockWithTimeout(String lockKey, Long acquireTimeout, Long timeOut) {
Jedis conn = null;
String retIdentifierValue = null;
try {
// 1.建立redis连接
conn = jedisPool.getResource();
// 2.随机生成一个value
String identifierValue = UUID.randomUUID().toString();
// 3.定义锁的名称
String lockName = "redis_lock" + lockKey;
// 4.定义上锁成功之后,锁的超时时间
int expireLock = (int) (timeOut / 1000);
// 5.定义在没有获取锁之前,锁的超时时间
Long endTime = System.currentTimeMillis() + acquireTimeout;
while (System.currentTimeMillis() < endTime) {
// 6.使用setnx方法设置锁值
if (conn.setnx(lockName, identifierValue) == 1) {
// 7.判断返回结果如果为1,则可以成功获取锁,并且设置锁的超时时间
conn.expire(lockName, expireLock);
retIdentifierValue = identifierValue;
return retIdentifierValue;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (conn != null) {
conn.close();
}
}
return retIdentifierValue;
}
/**
* 释放锁
*
* @return
*/
public boolean releaseLock(String lockKey, String identifier) {
Jedis conn = null;
boolean flag = false;
try {
// 1.建立redis连接
conn = jedisPool.getResource();
// 2.定义锁的名称
String lockName = "redis_lock" + lockKey;
// 3.如果value与redis中一直直接删除,否则等待超时
if (identifier.equals(conn.get(lockName))) {
conn.del(lockName);
System.out.println(identifier + "解锁成功......");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (conn != null) {
conn.close();
}
}
return flag;
}
}
end·