阻塞悲观锁-synchronized

程序运行环境

maven

<dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.10</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>
    </dependencies>

logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration >
    <statusListener class="ch.qos.logback.core.status.NopStatusListener" />

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%date{HH:mm:ss} [%t] %logger - %m%n</pattern>
        </encoder>
    </appender>


    <logger name="com.laowa" level="debug" additivity="false">
        <appender-ref ref="STDOUT"/>
    </logger>
</configuration>

线程安全问题

  • 一个程序运行多个线程本身是没有问题的,问题出现在多个线程访问共享资源(多个线程对共享资源读写操作时发生指令交错),一段代码内如果存在对共享资源的多线程读写操作,这段程序称为临界区
  • 多个线程在临界区执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
@Slf4j
public class Demo {
    static int counter = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                counter++;
            }
        },"t1");

        Thread t2 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                counter++;
            }
        },"t2");

        t1.start();
        t2.start();

        t1.join();
        t2.join();
        log.debug("counter={}",counter);

    }
}

在这里插入图片描述
预期结果为10000,实际结果为6979

synchronized解决方案

synchronized是阻塞式的解决方案,俗称对象锁,他会采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其他线程再想获取这个对象锁就会被阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文的切换

@Slf4j
public class Demo {
    static int counter = 0;
    static Object lock = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            synchronized (lock){
                for (int i = 0; i < 5000; i++) {
                    counter++;
                }
            }

        },"t1");

        Thread t2 = new Thread(()->{
            synchronized (lock){
                for (int i = 0; i < 5000; i++) {
                    counter++;
                }
            }

        },"t2");

        t1.start();
        t2.start();

        t1.join();
        t2.join();
        log.debug("counter={}",counter);

    }
}

在这里插入图片描述
synchronized实际是用对象锁保证了临界区内代码的原子性,临界区的代码对外是不可分割的,不会被线程切换所打断

面向对象改进

@Slf4j
public class Demo {
    public static void main(String[] args) throws InterruptedException {
        Room room = new Room();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                room.increment();
            }
        },"t1");

        Thread t2 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                room.decrement();
            }
        },"t2");

        t1.start();
        t2.start();

        t1.join();
        t2.join();
        log.debug("counter={}",room.getCounter());
    }
}
class Room{
    private int counter = 0;
    public void increment(){
    	//锁住自身对象,不用额外创建一个属性用于加锁
        synchronized (this){
            counter++;
        }
    }
    public void decrement(){
        synchronized (this){
            counter--;
        }
    }
    public int getCounter(){
        return counter;
    }
}

方法锁

public synchronized void m(){
}
等价于
public void m(){
	synchronized(this){
	}
}

两个方式都是对当前对象实例加锁

class Test{
	public synchronized static void m(){
	}
}
等价于
class Test{
	public static void m(){
		synchronized(Test.class){
		}
	}
}

两个方式都是锁住当前类的class对象

线程安全性分析

变量的线程安全性

  1. 成员变量和静态变量:如果被多个线程共享了,并且同时有读写操作,则需要考虑线程安全
  2. 局部变量:如果局部变量的作用范围逃离了方法的作用范围(作为返回值和参数)

常见的线程安全类

  • String
  • Integer
  • StringBuffer(StringBuilder线程不安全)
  • Random
  • Vector
  • HashTable(HashMap线程不安全)
  • java.util.concurrent包下的类

这里所述的线程安全是指,多个线程调用他们同一个实例的某个方法时,是线程安全的。也可以他们的每个方法是原子的,但是多个方法组合在一起可能出现线程问题

多个方法组合导致线程不安全例子

Hashtable table = new Hashtable();
new Thread(()->{
	if(table.get("key")!=null){
		table.put("key",1);
	}
}).start();
new Thread(()->{
	if(table.get("key")!=null){
		table.put("key",2);
	}
}).start();

两个线程可能同时进入if条件中,同时执行了put操作,其中一个put操作就会失效

不可变类的线程安全性

String、Integer等都是不可变类,因为其内部的状态不可改变
在这里插入图片描述
对象内部的操作都不会在原有的对象内部进行改变,而是创建新的对象,内部的字符串不会收到任何的改动(没有写操作,不会有线程安全问题)

synchronized工作原理(Monitor)

Java对象头

普通对象:Mark Word(32bit)+Klass Word(32bit)
数组对象:Mark Word(32bit)+Klass Word(32bit)+array length(32bit)

其中Mark Word在不同状态下的内容为:
在这里插入图片描述

Monitor工作原理

Monitor翻译为监视器管程,每个对象都会关联一个Monitor,synchronized的加锁就基于这个Monitor(不论synchornized是加在对象、类、方法上,实际都是对某个对象加了锁)
在这里插入图片描述
从字节码层面,synchornized在修饰同步代码块和修饰同步方法时,采用的方式不同

修饰同步代码块

public final class Demo{
    public static void main(String[] args) {
        Demo.getInstance();
    }
    private Demo(){}
    private static Demo instance = null;
    public static Demo getInstance(){
        if(instance==null){
            synchronized (Demo.class){
                if(instance==null){
                    instance = new Demo();
                }
            }
        }
        return instance;
    }
}

在这里插入图片描述
修饰同步方法

public final class Demo{
    public static void main(String[] args) {
        Demo.getInstance();
    }
    private Demo(){}
    private static Demo instance = null;
    public static synchronized Demo getInstance(){
        if(instance==null){
            instance = new Demo();
        }
        return instance;
    }
}

在这里插入图片描述

等待/通知机制

场景

当一个线程的运行需要建立在某个变量的变化,或某个线程的运行基础之上(join是建立在父子线程的关系上,而且与锁无关),例如:

@Slf4j
public final class Demo{
    static boolean flag = true;
    static Object lock = new Object();
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            synchronized (lock){
                while (flag){
                    try {
                        // 使用sleep方法不会释放锁,当前线程就无效的占用资源
                        // 并且使用sleep时间不好掌控,如果太长导致对flag的变化不能即使的响应,如果太短则一直占用cpu空转,造成资源浪费
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 进行下一步操作
            }
        },"t1");

        Thread t2 = new Thread(()->{
            flag = false;
        },"t2");
    }
}

解决这样一个生产者-消费者的同步问题,使用sleep的方式就会有如下两个缺陷:

  1. 难以保证及时性,如果睡得太久,就无法对flag的改变做出及时的响应
  2. 难以降低开销,如果睡眠时间减少,线程能够及时做出反应,但是空转导致一直占用处理器资源,早曾无端的资源浪费

解决方案

Object中有两个方法:wait、notify;这两个方法就可以解决这个问题:当一个对象调用wait,当前执行的线程会被挂起,同时释放掉对象锁;只有另一个线程在调用该对象的notify方法,重新唤醒wait中的线程,这样就可以保证线程能够在满足执行需求时迅速的开始行动

@Slf4j
public final class Demo{
    static boolean flag = true;
    static Object lock = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            synchronized (lock){
                while (flag){
                    try {
                        // 挂起,释放当前锁,等待其他线程的唤醒
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 进行下一步操作
                log.debug("ok");
            }
        },"t1");

        Thread t2 = new Thread(()->{
            // 调用notify的线程需要先获取对象的锁
            synchronized (lock){
                flag = false;
                lock.notify();
            }

        },"t2");

        t1.start();
        t2.start();

        t1.join();
        t2.join();
    }
}

相关方法

方法名称描述
notify()通知一个在对象上等待的线程,使其从wait()方法返回;返回的前提是这个线程抢到了对象锁(即只有抢到锁的一个线程才会被唤醒)
notifyAll()通知所有在该对象上等待的线程
wait()线程进入WAITING状态,只有等待另外线程调用notify或者线程中断才会返回
wait(long)等待一段时间,当时间达到参数指定的毫秒数还没有通知,就会返回
wait(long,int)对超时时间进行更细粒度的控制,可以达到纳秒

wait/notify原理

在这里插入图片描述

  • 当Owner线程发现条件不满足,调用wait方法,即可进入WaitSet变为WAITING状态
  • 其他线程调用notify方法时,会通知WaitSet,随机挑选一个线程进入EntryList进行锁竞争
  • 被唤醒的线程不会立即获得锁,仍然需要进入EntryList变成BLOCK状态

等待/通知范式

优化前的代码

@Slf4j
public final class Demo{
    static boolean flag1 = true;
    static boolean flag2 = true;
    static Object lock = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            synchronized (lock){
                if(flag1){
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                if(flag1){
                    log.debug("1操作失败");
                }else{
                    log.debug("执行操作1");
                }
            }
        },"t1");
        Thread t2 = new Thread(()->{
            synchronized (lock){
                if(flag2){
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                if(flag2){
                    log.debug("2操作失败");
                }else{
                    log.debug("执行操作2");
                }
            }
        },"t2");

        Thread t3 = new Thread(()->{
            synchronized (lock){
                flag2 = false;
                lock.notify();
            }
        },"t2");

        t1.start();
        t2.start();
        Thread.sleep(1000);
        t3.start();

    }
}

如果t3线程在唤醒时,t1争抢到了锁,就会出现这样的矛盾:t3的目的是唤醒t2,却错误的唤醒了t1,这种矛盾称为虚假唤醒;这样会导致t2没能正常工作,同时t1也无法再正常工作了

于是推出这样一种范式:

等待方
synchronized(lock){
	while(条件不满足){
		lock.wait()
	}
	处理操作
}

通知方
synchronized(lock){
	改变条件
	lock.notifyAll();
}

对应如上代码

@Slf4j
public final class Demo{
    static boolean flag1 = true;
    static boolean flag2 = true;
    static Object lock = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            synchronized (lock){
                while(flag1){
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("执行操作1");
            }
        },"t1");
        Thread t2 = new Thread(()->{
            synchronized (lock){
                while(flag2){
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("执行操作2");

            }
        },"t2");

        Thread t3 = new Thread(()->{
            synchronized (lock){
                flag2 = false;
                lock.notifyAll();
            }
        },"t2");

        t1.start();
        t2.start();
        Thread.sleep(1000);
        t3.start();

    }
}

这样一来,通过notifyAll可以保证所有线程都被唤醒;在while循环中,没有满足继续执行条件的线程会再次进入等待

Park/Unpark

LockSupport工具类中的park和unpark方法也能够对线程进行阻塞和唤醒

@Slf4j
public final class Demo{
    static boolean flag1 = true;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            while(flag1){
                LockSupport.park();
            }
            log.debug("执行操作1");
        },"t1");

        t1.start();
        Thread.sleep(1000);
        flag1 = false;
        LockSupport.unpark(t1);
    }
}

与wait/notify对比

  • wait/noify必须配合Object Monitor一起使用,即必须获取对象锁,而park/unpark不用
  • park/unpark是以线程为单位来阻塞和唤醒线程,而wait/notify只能随机唤醒一个线程;park/unpark比wait/notify更精确
  • park/unpark可以先执行unpark,后park仍然可以接受唤醒;而wait/notify不能先执行notify,后wait不能接受唤醒

原理

每个线程都关联一个Parker对象,由三部分组成:_counter,_cond,_mutex

调用park

  1. 检查_counter,如果_counter为0,获得_mutex互斥锁
  2. 线程进入_cond条件变量阻塞
  3. 设置_counter=0

调用unpark

  1. 设置_counter=1
  2. 唤醒_cond条件变量中的线程
  3. 设置_counter为0

活跃性

死锁

假设有两个线程:
t1获得了A锁,想要获取B锁
t2获得了B锁,想要获得A锁
两个线程就会陷入矛盾,互不相让

@Slf4j
public final class Demo{
    static Object lockA = new Object();
    static Object lockB = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            synchronized (lockA){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lockB){

                }
            }
        },"t1");

        Thread t2 = new Thread(()->{
            synchronized (lockB){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lockA){

                }
            }
        },"t2");
        t1.start();
        t2.start();
    }
}

避免死锁的方法

  1. 避免一个线程同时获取多个锁
  2. 避免一个线程在锁内同时占有多个资源,尽量保证每个锁只占用一个资源
  3. 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制
  4. 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况

活锁

活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束

@Slf4j
public final class Demo{
    static volatile int count = 10;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            while(count>0){
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count--;
                log.debug("t1-{}",count);
            }
        },"t1");

        Thread t2 = new Thread(()->{
            while(count<20){
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count++;
                log.debug("t2-{}",count);
            }
        },"t2");
        t1.start();
        t2.start();
    }
}

饥饿

一个线程由于优先级太低,每次锁竞争都被其他线程抢走锁,导致该线程一直得不到运行

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值