干货分享 JVM 之第 1 篇 —— Java 线程的重要知识点大全
线程与进程区别
每个正在系统上运行的程序都是一个进程。每个进程包含一到多个线程。线程是一组指令的集合,或者是程序的特殊段,它可以在程序里独立执行。也可以把它理解为代码运行的上下文。所以线程基本上是轻量级的进程,它负责在单个程序里执行多任务。通常由操作系统负责多个线程的调度和执行。
总结:进程是所有线程的集合,每一个线程是进程中的一条执行路径。
线程创建方式
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() {@Overridepublic void run() {for (int i = 0; i < 5; i++) {System.out.println("T1 线程执行:i="+i);}}});Thread t2 = new Thread(new Runnable() {@Overridepublic 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() {@Overridepublic 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: mycatTestdatasource:type: com.alibaba.druid.pool.DruidDataSource# 对应 mycat 的数据库地址,user账号只读read-only-source:jdbc-url: jdbc:mysql://127.0.0.1:8066/mycatSchemaDBdriver-class-name: com.mysql.jdbc.Driverusername: userpassword: user# 自定义可写的数据源,root账号有读写的权限write-source:jdbc-url: jdbc:mysql://127.0.0.1:8066/mycatSchemaDBdriver-class-name: com.mysql.jdbc.Driverusername: rootpassword: 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*/@Overrideprotected Object determineCurrentLookupKey() {String dataSource = DataSourceContextHolder.getDataSource();System.out.println("DynamicDataSource 获取到的 dataSource=" + dataSource);return dataSource;}/**** 配置数据源信息*/@Overridepublic 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 {@Autowiredprivate 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 值为 falseSystem.out.println("主线程已经把子线程的 flag 改为 false!");Thread.sleep(500);//主线程休眠0.5秒System.out.println("主线程中,获取到子线程的 flag =" + demo.flag);}
}//定义测试类
class ThreadVolatileDemo extends Thread {public boolean flag = true;@Overridepublic 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");}@Overridepublic 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();}@Overridepublic 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.随机生成一个valueString 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·
干货分享 JVM 之第 1 篇 —— Java 线程的重要知识点大全相关推荐
- java写的教育管理的项目_干货分享|推荐12款适合做Java后台管理系统的项目
Java是一种可以撰写跨平台应用软件的面向对象的程序设计语言.Java技术具有卓越的通用性.高效性.平台移植性和安全性,广泛应用于PC.数据中心.游戏控制台.科学超级计算机.移动电话和互联网,同时拥有 ...
- Java 开发岗面试知识点大全解析
本人简介:北京邮电大学土著硕士研究生,CSDN博客专家,熟悉的技术为Java后台开发.在2017年暑期实习校招中拿到了百度.搜狐.京东和去哪儿网的实习Offer:在2018届校园招聘中,拿到了百度.小 ...
- gprs 神奇宝典java,2016联通笔试知识点大全
答:呼叫转移是指用户在工作忙或手机无网络等无法用本机接听电话的情况下,即可将来电设置呼叫转移到另一个号码上,实现号码转移. 注:呼叫转移业务目前无需月功能费,用户设置呼叫转移后,如正常接听来电则按照呼 ...
- 聊聊JVM(九)理解进入safepoint时如何让Java线程全部阻塞
在这篇聊聊JVM(六)理解JVM的safepoint 中说了safepoint的基本概念,VM thread在进行GC前,必须要让所有的Java线程阻塞,从而stop the world,开始标记.J ...
- java 线程状态监控_干货:教你如何监控 Java 线程池运行状态
之前写过一篇 Java 线程池的使用介绍文章<线程池全面解析>,全面介绍了什么是线程池.线程池核心类.线程池工作流程.线程池分类.拒绝策略.及如何提交与关闭线程池等. 但在实际开发过程中, ...
- 求一个简单的java线程代码,Java线程代码的实现方法
1.继承Thread 声明Thread的子类 运行thread子类的方法 2.创建Thread的匿名子类 3.实现Runnable接口 声明 运行 4.创建实现Runnable接口的匿名类 5.线程名 ...
- Java线程(二):线程同步synchronized和volatile
上篇通 过一个简单的例子说明了线程安全与不安全,在例子中不安全的情况下输出的结果恰好是逐个递增的(其实是巧合,多运行几次,会产生不同的输出结果),为什么 会产生这样的结果呢,因为建立的Count对象是 ...
- 教你如何监控 Java 线程池运行状态
转载自 干货 | 教你如何监控 Java 线程池运行状态 之前写过一篇 Java 线程池的使用介绍文章<线程池全面解析>,全面介绍了什么是线程池.线程池核心类.线程池工作流程.线程池分类. ...
- Java线程怎样映射到操作系统线程
先说多线程模型,参考经典教材<Operating System Concepts , Silberschatz ,9th edition> 中文版是<操作系统概念,第9版> h ...
最新文章
- Skynet通讯遇到的奇怪问题
- 【今晚7点】:圆桌PI回归 继续聊聊开源的故事
- php+header+跳转输出,php利用header跳转怎么失效了?
- 小程序 input 换行_小程序 input双向数据绑定
- vsftp日志查看_vsftp日志xferlog格式分析(示例代码)
- matlab复杂噪声产生实验报告,matlab加入噪声 - 范文中心
- cf723d Lakes in Berland
- 前端html+css+javascript
- 使用SQLite数据库存储数据(4)-删除数据记录
- 【C++】(三) MFC入门教程 (VS 2005)
- 58-20210406华为海思Hi3516DV300的linux系统下获取IMX335的视频(eMMC模式)
- pyinstaller包含html文件,pyinstaller打包exe
- 设计模式(九)——代理模式(Proxy)
- 【洛谷P2123】皇后游戏
- 为什么说千万别用微软 IE 下载 Chrome?
- kaggle竞赛之Hungry Geese比赛
- 求负片灰度扩展幂律变换
- 【BDTC 2017】最后两天!BDTC大会抢票倒计时!
- 【毕业设计】基于STM32的心率检测器 - 单片机 嵌入式 物联网
- 进程和线程.View椭圆运动
热门文章
- tensorflow模型转为tflite
- 帝国cms怎样去掉面包屑导航里的隐藏栏目
- OFD文档标准 3.根节点文档
- 绝地求生6月28日服务器维护,绝地求生6月28日更新维护公告 6月28日吃鸡更新到几点...
- 团队作业4——第一次项目冲刺(Alpha版本)-第二篇
- 计算机硬件安装检测调试,多媒体计算机硬件教程:安装调试及维护指南
- python turtle 画老鼠_通过Turtle库在Python中绘制一个鼠年福鼠
- 普通话范文高频词及注音(第一列是出现的频率,第二列是字,第三列是注音)
- CTFShow-MISC篇详细Wp(部分)
- 公众号修改用户分组-php开发微信公众号