Java并发编程——基础概念

本文深入探讨Java并发编程的核心概念,包括内存模型JMM、可见性、有序性和原子性问题及解决方案,通过实例讲解死锁、活锁和饥饿等活跃性问题,并分析线程生命周期与同步机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、内存模型JMM(Java Memory Model)

        我们以一个最简单的例子开始

int i = 5;
i = i + 1;

        i=i+1这条语句,虽然看起来只有一步,但是从微观的角度可以将它分解为以下几步

        (1)从内存中读取i=5,并复制到cpu缓存中

        (2)将cpu缓存中i的值+1,现在cpu缓存中i=6,而内存中i=5

        (3)将cpu缓存中的i刷新到内存中,此时i=6

        简单一句话就是,赋值会先刷到缓存再刷到内存,取值会直接从内存取。

2、导致并发问题的三个原因,以及JVM提供的解决方案

2.1、可见性问题

2.1.1、问题描述

        由上面内存模型的例子,我们可以引出一个问题,如果我们两个线程执行i=i+1这个操作,期望的最终结果i=7,但是如果两个线程同时执行了(1)呢?我们知道多核CPU中的缓存是独立的、不共享的。两个线程会同时将i=5刷到自己的缓存中,并分别执行i=i+1,再刷回内存,结果是i=6,这就是缓存一致性问题,也就是线程间缓存不可见导致的可见性问题

2.1.2、解决方案------volatile+Happens-Before原则

        首先介绍一下内存屏障(Memory Barrier)的概念:

        内存屏障也叫内存栅栏,是一条cpu指令,用来影响数据可见性。内存屏障分为两种:

(1)写内存屏障(Store-Barrier)

        在写一个变量时加内存屏障,将写入的变量立即从缓存刷入内存

(2)读内存屏障(Load-Barrier)

        在读一个变量时加内存屏障,使该变量的缓存失效饼从内存中读取

        volatile是“易变的”的意思,修饰在属性(常量/成员变量)上表示通知JVM该属性可能不太稳定,会在读写该变量的时候增加内存屏障,在写该变量时直接将缓存刷入内存,在读该变量时使缓存失效并直接从内存读。

        接下来介绍一下Happens-Before原则:

        Happens-Before的意思是,如果A Happens-Before B,则A的操作结果对B可见,它一共有8个原则:

(1)程序次序规则:

        一个线程内一段代码的执行结果是有序的。即两行代码先后执行,先执行的代码产生的结果对后执行的代码可见。

(2)管程锁定规则:

        对一个锁的解锁Happens-Before于后续对这个锁的加锁。即上一轮的加锁解锁产生的结果对下一轮加锁解锁中的操作可见。

(3)volatile变量规则:

        对一个volatile变量的写操作Happens-Before于后续对这个变量的读操作。

(4)线程启动规则:

        主线程启动的操作Happens-Before于子线程。即主线程在启动子线程前的操作对子线程可见。

(5)线程终止规则:

        子线程终止前的操作Happens-Before于主线程。即子线程的操作对主线程可见。

(6)线程中断规则:

         调用interrupt方法Happens-Before于检测到中断事件。即对一个线程执行interrupt方法的结果,对被中断线程检测到中断状态Thread.isInterrupted之前可见

(7)传递规则:

        A Happens-Before B,B Happens-Before C,则 A Happens-Before C.

(8)对象终结规则:

        一个对象的初始化操作Happens-Before于销毁操作。

2.2、有序性问题

2.2.1、问题描述

        双重锁实现单例模式:

public class Singleton {
    private Singleton() { }

    private static Singleton instance;

    public static Singleton getInstance(){
        if(instance==null){
            synchronized (Singleton.class){
                if(instance==null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

        这段代码中new Singleton()的执行顺序本来应该是这样的:

        JVM开一块内存空间--->在这块内存空间上初始化Singleton对象--->把内存空间地址赋值给instance

        但其实编译器在编译过程中会对指令进行重排序,执行顺序有可能是这样的:

        JVM开一块内存空间--->把内存空间地址赋值给instance--->在这块内存空间上初始化Singleton对象

        虽然两种执行顺序在单线程中结果是正常的,但是后者在多线程中会出现问题:

        假如A线程执行到了new Singleton(),A把空间地址赋值给instance,这时候B线程进来了,instance==null就是false,但是实际上对象还没初始化,就会造成调用方法空指针

2.2.2、解决方案------volatile+Happens-Before原则

        volatile还有禁用编译优化的功能

2.3、原子性问题

2.3.1、问题描述

        最经典的取钱问题,假如一个账户只有500元,A和B两个人同时对这个账户进行取500元的操作,分别对应一个进程里两个不同的线程,而账户里的钱对应共享资源

        A取钱:检测账户里有500元---->取出500元

        B取钱,检测账户里有500元---->取出500元

        如果A和B同时执行检测账户余额的操作,就会同时执行取钱的操作,此时账户余额就会出现-500的情况

        原子性:我们把一个或多个操作在CPU中执行的过程中不被中断的特性叫原子性,可以认为原子性问题就是由多个线程(A、B两个人)同时对共享资源(账户)进行操作(取钱)导致的。

        取钱过程分为检测余额和取钱两步,这两步是不可分割的,但是由于A和B是两个不同的线程,就有可能在检测余额的时候出现“线程切换”的操作,破坏了取钱这个过程应具有的原子性

2.3.2、解决方案------synchronized关键字

       首先synchronized是一种互斥锁,即同一时刻只能有一个线程访问临界资源

        synchronized是同步的意思,用来告诉JVM多个线程必须同步执行该段代码。

        synchronized可以修饰成员方法和静态方法,分别加对象锁和类锁;还可以修饰方法内的方法块,需要自己指定加锁类型,注意对同一个变量的读写操作一定要加同一种锁

package com.lcy.thread.part41;

/**
 * 功能描述:
 *
 * @author liuchaoyong
 * @version 1.0
 * @date 2019-08-04 14:55
 */
public class Test {

    //修饰类方法
    private static synchronized void test1(){
        //修饰代码块,加类锁
        synchronized (Test.class){
            
        }
    }

    //修饰成员方法
    private synchronized void test2(){        
        //修饰代码块,加对象锁
        synchronized (this){
            
        } 
    }

}

    2.3.2.1、粗粒度锁

        粗粒度锁,即用同一把锁保护多个不同的临界资源,优点就是实现容易,缺点就是所有操作串行,性能低:

package com.lcy.thread.part41;

/**
 * 功能描述:
 *
 * @author liuchaoyong
 * @version 1.0
 * @date 2019/9/3 09:44
 */
public class Account {
    
    //保护锁
    private final Object lock = new Object();
    
    //账户余额
    private Integer balance;

    //账户信息
    private String userInfo;

    //存款
    private void addBalance(Integer amt) {
        synchronized (lock) {
            balance += amt;
        }
    }

    //取款
    private void subBalance(Integer amt){
        synchronized (lock){
            if(balance > amt){
                balance -= amt;
            }
        }
    }

    //设置账户信息
    private void setUserInfo(String newUserInfo){
        synchronized (lock){
            userInfo = newUserInfo;
        }
    }

    //查看账户信息
    private String getUserInfo(){
        synchronized (lock){
            return userInfo;
        }
    }

}

    2.3.2.2、细粒度锁

        使用不同的锁保护不同的临界资源,叫细粒度锁,优点就是对不同临界资源的操作并行化,性能高,缺点就是容易产生死锁:

package com.lcy.thread.part41;

/**
 * 功能描述:
 *
 * @author liuchaoyong
 * @version 1.0
 * @date 2019/9/3 09:44
 */
public class Account {

    //账户锁
    private final Object balLock = new Object();
    
    //用户信息锁
    private final Object infoLock = new Object();

    //账户余额
    private Integer balance;

    //账户信息
    private String userInfo;

    //存款
    private void addBalance(Integer amt) {
        synchronized (balLock) {
            balance += amt;
        }
    }

    //取款
    private void subBalance(Integer amt){
        synchronized (balLock){
            if(balance > amt){
                balance -= amt;
            }
        }
    }

    //设置账户信息
    private void setUserInfo(String newUserInfo){
        synchronized (infoLock){
            userInfo = newUserInfo;
        }
    }

    //查看账户信息
    private String getUserInfo(){
        synchronized (infoLock){
            return userInfo;
        }
    }

}

3、并发编程中应该注意的三个问题

3.1、安全性问题

        就是保证程序运行结果的正确性,上面导致并发问题的三个原因,就都是安全性问题。

        导致安全性问题的原因就是数据竞争,也就是存在多个线程对同一数据进行读写的情况,这时你就要注意安全性问题。

3.2、活跃性问题

3.2.1、死锁

    3.2.1.1、问题描述

        假如现实生活中有这样一个场景,现在有两个人都要炒菜,却只有一口锅和一把铲子,A先拿了锅,B先拿了铲子,A在等B用完铲子才能炒菜,而B也在等A用完锅才能炒菜,A和B会一直等待下去,就发生了死锁。

        一组相互竞争资源的线程,由于相互等待对方释放自己执行下一步所需的临界资源,导致永久阻塞的现象称为死锁。

    3.2.1.2、解决方案------预防死锁,破坏死锁条件

    3.2.1.2.1、占用且等待条件

        线程已经取得了一个共享资源,在对下一个被占用的共享资源发出申请时被阻塞,此时该线程并不释放已经取得的共享资源

        破坏占用且等待条件很简单,就是一次性申请所有资源。对应到做饭的情景就是,锅和铲子都放在厨房,只有一个人能进厨房

        

package com.lcy.thread.part41;

import java.util.ArrayList;
import java.util.List;

/**
 * 功能描述:
 *
 * @author liuchaoyong
 * @version 1.0
 * @date 2019/9/3 17:21
 */
public class Kitchen {

    private List<Object> kitchen = new ArrayList<>();

    private Object pot = new Object();

    private Object spatula = new Object();

    synchronized List<Object> getTool(){

        if(kitchen.contains(pot) || kitchen.contains(spatula)){
            return null;
        }
        kitchen.add(pot);
        kitchen.add(spatula);
        return kitchen;
    }

    synchronized void backTool(Object pot,Object spatula){

        kitchen.remove(pot);
        kitchen.remove(spatula);

    }


}

    3.2.1.2.2、不可抢占条件

        线程取得的共享资源不能被其他线程释放

        破坏不可抢占条件需要用到java并发包里的相关类,我们到时候再说。对应到做饭的情景就是,一个人拿了锅再去拿铲子,如果铲子拿不到锅也不要了。

    3.2.1.2.3、循环等待条件

        当前线程会占用下一个线程的至少一种资源

        破坏循环等待条件可以把资源排序,规定线程从小到大申请资源。对应到做饭的情景就是,规定做饭必须先拿铲子再拿锅。

3.2.2、活锁

    3.2.2.1、问题描述

        现实生活中可能会有这样的情况,两个人面对面走,快要撞上的时候,同时互相谦让走到另一条道上,结果还是过不去,如此反复。。。

        线程之间并没有阻塞,但就是无法满足继续执行下去的条件,一直循环尝试->失败->尝试->失败的操作,这种情况称为活锁。

    3.2.2.2、解决方案

        失败之后设置一个随机等待时间再尝试

3.2.3、饥饿

    3.2.3.1、问题描述

        大家都知道,过马路要等红绿灯,如果有一天有个路口的红绿灯突然坏了,一边一直是绿灯,另一边一直是红灯,等红灯的人和车就要一直等一直等(如果都遵纪守法的话)。

        线程因执行优先级较低或永久等待,无法得到cpu的运行时间块,而无法继续执行下去的状态称为饥饿

    3.2.3.2、解决方案

        保证资源分配的公平性,使用基于先来后到原则的公平锁,在java并发包里有相关类,我们后面会讲到

4、线程的生命周期

4.1、线程的生命周期

(1)初始化状态(NEW)

        new Thread();JVM仅仅为其分配内存

(2)可运行状态(RUNNABLE)

        Thread.start();表示线程已经可以运行了,但是什么时候运行取决于JVM线程调度器的调度

(3)运行状态(RUNNING)

        线程获得CPU时间块,执行方法体

(4)阻塞状态(BLOCKED)

        线程在等待获取共享资源的锁的时候

(5)无时间限制等待状态(WAITING)

        在线程获取锁的时候,主动调用wait()等方法,会进入等待被唤醒的状态,并释放对应的锁

(6)有时间限制的等待状态(TIMED_WAITING)

        在线程获取锁的时候,主动调用wait(long millis)等方法,会进入等待被唤醒的状态,并释放对应的锁,如果一定时间内没有被唤醒,则自己主动苏醒。

(7)终止状态(TERMINATED)

        线程执行完毕或异常终止

4.2、wait()、notify()、notifyAll()的使用方法

    我们以最简单的生产者消费者模型为背景来介绍,下面的例子主要就是两个线程对一个数的增减

生产者Producer,抢到锁后,数小于等于0就执行+5然后唤醒等待线程,sychronized结束才会释放锁;大于0就不执行:

package com.lcy.thread.part08;

/**
 * 功能描述:
 *
 * @author liuchaoyong
 * @version 1.0
 * @date 2019/9/10 09:29
 */
public class Producer implements Runnable {

    private Test test;

    public Producer(Test test) {

        this.test = test;

    }

    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        while (true) {
            synchronized (test) {
                if (test.i <= 0) {
                    test.i += 5;
                    System.out.println(threadName +"生产...剩余" + test.i);

                    System.out.println(threadName +"去唤醒...");
                    test.notify();
                }
            }
        }
    }
}

消费者Consumer,抢到锁后,小于等于0就释放锁等待被唤醒;大于0就执行-1:

package com.lcy.thread.part08;

/**
 * 功能描述:
 *
 * @author liuchaoyong
 * @version 1.0
 * @date 2019/9/10 09:29
 */
public class Consumer implements Runnable {

    private Test test;

    public Consumer(Test test) {
        this.test = test;
    }

    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        while (true) {
            synchronized (test) {
                if (test.i <= 0) {
                    System.out.println(threadName +"等待...");
                    try {
                        test.wait();
                        System.out.println(threadName +"醒了...");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                test.i--;
                System.out.println(threadName +"消费...剩余" + test.i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

测试类:

package com.lcy.thread.part08;

/**
 * 功能描述:
 *
 * @author liuchaoyong
 * @version 1.0
 * @date 2019/9/10 09:35
 */
public class Test {

    public int i = 5;

    public static void main(String[] args) {

        Test test = new Test();

        Thread thread = new Thread(new Consumer(test));
        Thread thread1 = new Thread(new Producer(test));
        thread.start();
        thread1.start();

    }

}

        其实要注意的只有一点,就是调用某个对象的wait()、notifyAll()的线程,一定要获取到该对象的锁才可以,否则会报java.lang.IllegalMonitorStateException异常

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wheat_Liu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值