干货分享 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() {
            @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只筷子才能吃粉,两个小屁孩都在等待对方让出筷子,但又不愿意让出自己手中的筷子给对方,这就造成了死锁。导致两个小屁孩都没法吃到粉。(最后被一个糟老头夺过碗直接手抓吃完。)

 

如何避免死锁?

  1. 加锁顺序(线程按照一定的顺序加锁):确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。
  2. 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁。
  3. 死锁检测。死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。每当一个线程获得了锁,会在线程和锁相关的数据结构中(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个功能:

  1. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
  2. 它会强制将对缓存的修改操作立即写入主存
  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·

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值