本章学习内容:Synchronized、Lock、Volatile

锁的出现就是为了保护共享资源的独占性,避免多个线程同时操作一个共享资源

学习之前,先来了解三个锁的区别:

image-20220112201608179
  • java内存模型

    1. 每个线程都有自己的本地内存空间(即jvm栈中的帧),线程执行时,先把主内存的数据拷贝到工作内存,同步代码结束后,再把工作内存中的数据更新到主内存中,这样就能够保证并发场景下,主内存中的共享变量数据的一致性;
    2. 锁的两种特性:
      • 互拆性(mutual exclusion):互拆即一次只允许一个线程持有某个特定的锁;
      • 可见性(visibility):一个线程修改了变量其它线程可以立马知道;
    3. java内存模型三种特性:
      • 原子性(Atomicity):原子不可分割,原子操作不可中断(要么成功,要么失败,即使多线程下,一个操作一旦开始,不受其它线程干扰)。java的concurrent包下提供了一些原子类(AtomicInteger、AtomicLong、AtomicReference);
      • 可见性(Visibility):一个线程修改了共享变量值,其它线程能够得知这个修改,其它线程对这个修改是可见的(实现:java内存模型即以主内存为传递媒介方式实现可见性——变量修改后将新值同步到主内存,在变量读取前从主内存刷新变量值);
      • 有序性(Ordering):java提供关键字Volatile和Synchronized来保证线程之间操作的有序性,即保证变量赋值操作的顺序与程序代码中的执行顺序一致(好比:a=1,b=2。按照代码顺序是先对a赋值再对b赋值,但由于指令重排序,很可能b就先赋值了,所以普通变量不能够保证有序性。而Volatile可以禁止指令重排序。而Synchronized根据其规则”一个变量在同一时刻只允许一个线程对其进行锁操作”决定了持有同一个对象锁的两个或多个同步块只能串行执行);
  • synchronized, lock和volatile区别(可见性、原子性、有序性)

    属性 Synchronized lock volatile 解释
    可见性 变量被操作之后,能够快速写入内存,并提醒其他线程重读,加锁是通过一个一个执行保证了可见性
    原子性 × 做的过程中,不要有相关的来打扰,不相关的我们也不关心,加锁是通过一个一个执行保证了流程不会被相关的打扰
    有序性 在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性
  • synchronized与lock区别

    类别 synchronized Lock
    存在层次 Java的关键字,在jvm层面上 是一个接口类
    锁的释放 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 在finally中必须释放锁,不然容易造成线程死锁
    锁的获取 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 分情况而定,Lock有多个锁获取的方式,大致就是可以尝试获得锁,线程可以不用一直等待
    锁状态 无法判断 可以判断
    锁类型 可重入 不可中断 非公平 可重入 可判断 可公平(两者皆可)
    性能 少量同步 大量同步
  • synchronized与volatile区别

    类别 volatile synchronized
    解决问题 变量在多线程之间的可见性 多个线程之间访问共享资源的同步性
    可修饰 变量 方法、代码块
    多线程下 不会阻塞 会阻塞
    能否保证原子性 能,将私有内存和公有内存中的数据做同步
  • 锁类型

    锁类型 描述
    可重入锁 Synchronized和ReentrantLock都属于可重入锁,即某对象中有两个带有Synchronized或ReentrantLock的方法a和方法b,当一个线程获取到该对象锁后,在同步方法a里面可以调用同步方法b。表明了锁的分配机制:基于线程的分配而不是基于方法调用的分配。
    中断锁 即可以响应中断的锁(线程a在执行带锁的代码片段,线程b在等待线程a执行完释放锁后去获取锁执行,但此时线程等了一段时间不想等了,线程b可以释放锁去做其它的事情),Synchronized不可中断,ReentrantLock可中断(调用方法:lockInterruptibly())。
    公平锁 即尽量以请求锁的顺序来获取锁(比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁),Synchronized不公平,ReentrantLock公平(new ReentrantLock(true)即为公平锁)。
    读写锁 对资源读取和写入的时候拆分为2部分处理,读的时候可以多线程一起读,写的时候必须同步地写
  • 线程状态

    wait():释放占有的对象锁,线程进入等待池,释放cpu,而其他正在等待的线程即可抢占此锁,获得锁的线程即可运行程序。而sleep()不同的是,线程调用此方法后,会休眠一段时间,休眠期间,会暂时释放cpu,但并不释放对象锁。也就是说,在休眠期间,其他线程依然无法进入此代码内部。休眠结束,线程重新获得cpu,执行代码。wait()和sleep()最大的不同在于wait()会释放对象锁,而sleep()不会!

    notify(): 该方法会唤醒因为调用对象的wait()而等待的线程,其实就是对对象锁的唤醒,从而使得wait()的线程可以有机会获取对象锁。调用notify()后,并不会立即释放锁,而是继续执行当前代码,直到synchronized中的代码全部执行完毕,才会释放对象锁。JVM则会在等待的线程中调度一个线程去获得对象锁,执行代码。需要注意的是,wait()和notify()必须在synchronized代码块中调用。

    notifyAll():则是唤醒所有等待的线程。

    这三个方法都属于Object。

    查看源图像
    线程状态 描述
    新建 新建线程对象,并没有调用start()方法之前
    就绪 调用start()方法之后线程就进入就绪状态,但是并不是说只要调用start()方法线程就马上变为当前线程,在变为当前线程之前都是为就绪状态。值得一提的是,线程在睡眠和挂起中恢复的时候也会进入就绪状态哦
    运行 线程被设置为当前线程,开始执行run()方法。就是线程进入运行状态
    阻塞 线程被暂停,比如说调用sleep()方法后线程就进入阻塞状态
    死亡 线程执行结束

Synchronized

1、概述

2、作用域

  1. 对象实例:可以防止多个线程同时访问这个对象的synchronized方法,如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程就不能同时访问这个对象中任何一个synchronized方法。这时,不同的对象实例的synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法;
  2. :可以防止多个线程同时访问这个类所创建的对象中的synchronized方法。它可以对这个类创建的所有对象实例起作用;

3、代码

synchronized修饰方法

import java.util.stream.IntStream;
/**
 * 当synchronized修饰方法时,
 * 下面模拟多线程下面共用一个对象实例,只要当一个线程获取到锁的时候,其它线程只能阻塞等待,直到线程释放锁,下一个线程获取锁,再进行下一步;
 * 当多线程下每个线程创建一个对象实例,不阻塞等待,模拟的多线程皆可以访问该对象下的所有带synchronized关键字的方法;
 */
public class SynchronizedUseMethod {
    public synchronized void toDoFirst(int i) throws InterruptedException {
        System.err.println(String.format("TODO first :[%s]",i));
        Thread.sleep(3000);
        System.out.println(String.format("TODO first :[%s] finish",i));
    }
    public synchronized void toDOSecond(int i) throws InterruptedException {
        System.err.println(String.format("TODO second :[%s]",i));
        Thread.sleep(3000);
        System.out.println(String.format("TODO second :[%s] finish",i));
    }
    public static void test1(){
        SynchronizedUseMethod synchronizedUse = new SynchronizedUseMethod();
        IntStream.range(0,10).forEach(i->{
            if (i%2==0) {
                new Thread(()-> {
                    try {
                        synchronizedUse.toDoFirst(i);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }).start();
            }else{
                new Thread(()-> {
                    try {
                        synchronizedUse.toDOSecond(i);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }).start();
            }
        });
    }
    public static void test2(){
        IntStream.range(0,10).forEach(i->{
            if (i%2==0) {
                new Thread(()-> {
                    try {
                        SynchronizedUseMethod synchronizedUse1 = new SynchronizedUseMethod();
                        synchronizedUse1.toDoFirst(i);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }).start();
            }else{
                new Thread(()-> {
                    try {
                        SynchronizedUseMethod synchronizedUse2 = new SynchronizedUseMethod();
                        synchronizedUse2.toDOSecond(i);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }).start();
            }
        });
    }
    public static void main(String[] args) {
        test2();
    }
}

synchronized修饰代码块时,括号内为this,当前对象时,同上

import java.util.stream.IntStream;
/**
 * 当synchronized修饰代码块时,括号内为this,当前对象时,
 * 下面模拟多线程下面共用一个对象实例,只要当一个线程获取到锁的时候,其它线程只能阻塞等待,直到线程释放锁,下一个线程获取锁,再进行下一步;
 * 当多线程下每个线程创建一个对象实例,不阻塞等待,模拟的多线程皆可以访问该对象下的所有带synchronized关键字代码块的方法;
 */
public class SynchronizedUseCodeBlock {
    public void toDoFirst(int i) throws InterruptedException {
        synchronized (this){
            System.err.println(String.format("TODO first :[%s]",i));
            Thread.sleep(3000);
            System.out.println(String.format("TODO first :[%s] finish",i));
        }
    }
    public void toDOSecond(int i) throws InterruptedException {
        synchronized(this){
            System.err.println(String.format("TODO second :[%s]",i));
            Thread.sleep(3000);
            System.out.println(String.format("TODO second :[%s] finish",i));
        }
    }
    public static void test1(){
        SynchronizedUseCodeBlock synchronizedUse = new SynchronizedUseCodeBlock();
        IntStream.range(0,10).forEach(i->{
            if (i%2==0) {
                new Thread(()-> {
                    try {
                        synchronizedUse.toDoFirst(i);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }).start();
            }else{
                new Thread(()-> {
                    try {
                        synchronizedUse.toDOSecond(i);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }).start();
            }
        });
    }
    public static void test2(){
        IntStream.range(0,10).forEach(i->{
            if (i%2==0) {
                new Thread(()-> {
                    try {
                        SynchronizedUseCodeBlock synchronizedUse1 = new SynchronizedUseCodeBlock();
                        synchronizedUse1.toDoFirst(i);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }).start();
            }else{
                new Thread(()-> {
                    try {
                        SynchronizedUseCodeBlock synchronizedUse2 = new SynchronizedUseCodeBlock();
                        synchronizedUse2.toDOSecond(i);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }).start();
            }
        });
    }
    public static void main(String[] args) {
        test1();
    }
}

当synchronized修饰代码块时,括号内为类模板时

import java.util.stream.IntStream;
/**
 * https://blog.csdn.net/qq_28082757/article/details/91542030
 * 当synchronized修饰代码块时,括号内为类模板时,
 * 下面模拟多线程下面共用一个对象实例,只要当一个线程获取到锁的时候,其它线程只能阻塞等待,直到线程释放锁,下一个线程获取锁,再进行下一步;
 * 当多线程下每个线程创建一个对象实例,只要当一个线程获取到锁的时候,其它线程只能阻塞等待,直到线程释放锁,下一个线程获取锁,再进行下一步;
 */
public class SynchronizedUseClass {
    public void toDoFirst(int i) throws InterruptedException {
        synchronized (SynchronizedUseClass.class){
            System.err.println(String.format("TODO first :[%s]",i));
            Thread.sleep(3000);
            System.out.println(String.format("TODO first :[%s] finish",i));
        }
    }
    public void toDOSecond(int i) throws InterruptedException {
        synchronized(SynchronizedUseClass.class){
            System.err.println(String.format("TODO second :[%s]",i));
            Thread.sleep(3000);
            System.out.println(String.format("TODO second :[%s] finish",i));
        }
    }
    public static void test1(){
        SynchronizedUseClass synchronizedUse = new SynchronizedUseClass();
        IntStream.range(0,10).forEach(i->{
            if (i%2==0) {
                new Thread(()-> {
                    try {
                        synchronizedUse.toDoFirst(i);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }).start();
            }else{
                new Thread(()-> {
                    try {
                        synchronizedUse.toDOSecond(i);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }).start();
            }
        });
    }
    public static void test2(){
        IntStream.range(0,10).forEach(i->{
            if (i%2==0) {
                new Thread(()-> {
                    try {
                        SynchronizedUseClass synchronizedUse1 = new SynchronizedUseClass();
                        synchronizedUse1.toDoFirst(i);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }).start();
            }else{
                new Thread(()-> {
                    try {
                        SynchronizedUseClass synchronizedUse2 = new SynchronizedUseClass();
                        synchronizedUse2.toDOSecond(i);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }).start();
            }
        });
    }
    public static void main(String[] args) {
        test1();
    }
}

Lock

1、概述

  • 1
  • Lock接口主要方法:
    1. lock():获取锁,如果锁被暂用则一直等待;
    2. unlock():释放锁;
    3. tryLock(): 注意返回类型是boolean,如果获取锁的时候锁被占用就返回false,否则返回true;
    4. tryLock(long time, TimeUnit unit):比起tryLock()就是给了一个时间期限,保证等待参数时间;
    5. lockInterruptibly():用该锁的获得方式,如果线程在获取锁的阶段进入了等待,那么可以中断此线程,先去做别的事。

2、代码

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.IntStream;
/**
 * Lock:可重入所锁,可判断,可公平
 * https://blog.csdn.net/u012403290/article/details/64910926
 * https://www.cnblogs.com/iyyy/p/7993788.html
 */
public class MyLock {
    private final Lock lock = new ReentrantLock();//new ReentrantLock(true)为公平锁
    /**
     * 无论是线程共用对象实例还是各自用对象实例都会阻塞,直到上一个线程释放锁
     * @param i
     */
    public void useLock(int i) {
        try {
            lock.lock();
            System.err.println(String.format("[%s]_useLock获取锁成功",i));
            Thread.sleep(3000);
            System.out.println(String.format("[%s]_useLock finish!!!",i));
        }catch (InterruptedException e){
            e.printStackTrace();
            System.err.println(String.format("[%s]_useLock 释放锁失败!!!",i));
        }finally {
            lock.unlock();
            System.err.println(String.format("[%s]_useLock 释放锁成功!!!",i));
        }
    }
	//获取锁的时候锁被占用就返回false
    public void useTryLock(int i) {
        try {
            if (lock.tryLock()) {
                System.err.println(String.format("[%s]_useTryLock获取锁成功",i));
                Thread.sleep(3000);
                System.out.println(String.format("[%s]_useTryLock finish!!!",i));
            }else{
                System.err.println(String.format("当前锁被占用,无法获取"));
            }
        }catch (InterruptedException e){
            e.printStackTrace();
            System.err.println(String.format("[%s]_useTryLock 释放锁失败!!!",i));
        }finally {
            lock.unlock();
            System.err.println(String.format("[%s]_useTryLock 释放锁成功!!!",i));
        }
    }
	//设置的获取锁的等待时间结束后,锁还是被占用就返回false
    public void useTryLockTime(int i) {
        try {
            if (lock.tryLock(3000, TimeUnit.MILLISECONDS)) {
                System.err.println(String.format("[%s]_useTryLock获取锁成功",i));
                Thread.sleep(3000);
                System.out.println(String.format("[%s]_useTryLock finish!!!",i));
            }else{
                System.err.println(String.format("当前锁被占用,无法获取"));
            }
        }catch (InterruptedException e){
            e.printStackTrace();
            System.err.println(String.format("[%s]_useTryLock 释放锁失败!!!",i));
        }finally {
            lock.unlock();
            System.err.println(String.format("[%s]_useTryLock 释放锁成功!!!",i));
        }
    }

    private static void test(){
        MyLock myLock = new MyLock();
        IntStream.range(0,12).forEach(i->{
            int q = i % 3;
            if (q==0) {
                new Thread(()->myLock.useLock(i)).start();
            }
            if (q==1) {
                new Thread(()->myLock.useTryLock(i)).start();
            }
            if (q==2) {
                new Thread(()->myLock.useTryLockTime(i)).start();
            }
        });
    }
    public static void main(String[] args) {
        test();
    }
}

Volatile

1、概述

2、代码

下面代码不能保证原子性

扩展:关于volatile变量的可见性,经常被误解,认为一下描述成立:“volatile变量对所有线程时立即可见的,对volatile变量所有写操作都能立刻反应到其他线程之中,也就是说,volatile变量在各个线程中是一致的,所以基于volatile变量的运算在并发下是安全的”。这句话的论据部分没有错,但是结论有问题。不要忘记了在Java里面的运算并非原子操作,这导致volatile变量的运算在并发的情况下一样是不安全的。

/**
 * volatile变量自增运算测试
 * 这段代码发起了30个线程,每个线程对num变量进行了1000次自增操作,如果这段代码能够正确并发的话,那么最后的输出结果应该是30000。
 * 但是输出结果并不是我们期望的这样,而且基本上每次运行都会得到不同的结果。这是为什么呢?
 * 问题就出在 “num++” 之中,这句代码看似只有一行,但是在Class文件中是由4条指令构成的
 * cankao:https://blog.csdn.net/qq_28082757/article/details/91047241
 */
public class VolatileTest {
    public static volatile int num = 0;

    /**
     * 自增1
     */
    public static void increase(){
        num++;
    }
    //  创建线程数量
    private static final int THREADS_COUNT = 30;

    public static void main(String[] args) {
    	// 先创建一个容量为 30 的 Thread 数组 
        Thread[] threads = new Thread[THREADS_COUNT];
        for (int i = 0; i < THREADS_COUNT; i++) {
        	// 对每一个 Thread 进行实例化,并使用匿名内部类实现 run 方法
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                   // 每个线程调用1000 次自增方法
                    for (int j = 0; j < 1000; j++) {
                        increase();
                    }
                }
            });
            threads[i].start();
        }
		
		//如果在IDEA里面进行调试的话,这里的判断条件改为2,因为IDEA会启动一个 Monitor Ctrl-Break 线程。
        while (Thread.activeCount() > 1){
            Thread.yield();
        }
        System.out.println(num);
    }
}

通过java的concurrent包下提供了一些原子类AtomicInteger来保证原子性

/**
 *AtomicInteger位于java.util.concurrent包下,使用了AtomicInteger代替了int后,程序输出了正确的结果,这一切都要归功于incrementAndGet方法的原子性
 */
public class AtomicTest {
    public static AtomicInteger num = new AtomicInteger(0);
    
    /**
     * 自增1
     */
    public static void increase(){
        num.incrementAndGet();
    }
    
    //  创建线程数量
    private static final int THREAD_COUNT = 30;

    public static void main(String[] args) {
  	    // 先创建一个容量为 30 的 Thread 数组 
        Thread[] threads  = new Thread[THREAD_COUNT];
        for (int i = 0; i < THREAD_COUNT; i++) {
            // 对每一个 Thread 进行实例化,并使用匿名内部类实现 run 方法
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
             	    // 每个线程调用1000 次自增方法
                    for (int j = 0; j < 1000; j++) {
                        increase();
                    }
                }
            });
            threads[i].start();
        }

        while (Thread.activeCount() > 2){
            Thread.yield();
        }
        System.out.println(num);
    }

乐观锁和悲观锁

描述
乐观锁 从名字就可以看出,乐观锁就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断在此期间别人有没有去更新这个数据,具体方法可以使用版本号机制和CAS算法。
悲观锁 乐观锁总是假设最好的情况,而悲观锁总是假设最坏的情况,每次拿数据的时候都认为别人会修改,所以每次拿数据都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系数据库就是悲观锁机制(行锁、表锁、读锁、写锁都是在操作之前先上锁)。java中的Synchronized和ReentrantLock独占锁就是悲观锁的实现。

1、乐观锁——版本号机制

  • 描述:版本号机制一般是在数据表中加上一个数据库版本号version字段,它代表数据被修改的次数,当数据被修改时,version的值会加1。当一个线程要更新数据值时,在读取数据的同时也会读取version字段的值,在提交更新的时候,如果之前读取的version值与当前数据表中version字段值相等时才更新,同时将version值加1。否则重新读取最新数据和version值,尝试更新,直到更新成功。
  • 举例:假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100
    1. 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 );
    2. 在操作员 A 操作的过程中,操作员B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20 );
    3. 操作员 A 完成了修改工作,将数据版本号加一( version=2 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 ;
    4. 操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。

2、乐观锁——CAS算法


文章作者: LJH
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 LJH !
  目录