Java学习笔记——多线程I

本文深入探讨Java多线程编程的两大核心问题:线程间的通信与同步,并详细解析Java内存模型(JMM)机制,包括内存可见性问题及解决办法、happens-before规则、volatile关键字的使用以及锁的内存语义。
摘要由CSDN通过智能技术生成

Java学习笔记——多线程I

本系列文章为自己在java学习过程中的一些心得记录,一些错误、理解上的偏差也希望大家批评指正

概述

使用多线程编程的好处

并发编程核心的目的 —— 让程序运行的更快

具体而言可以从三个层面进行理解

​ 1)更多的处理器核心 —— 一个线程一个时刻只能在一个核心上运行
​ 2)更快的响应时间 —— 对数据一致性要求不高的复杂的业务逻辑交给多线程去做
​ 3)更好的编程模型 —— java本身提供了良好、考究并且一致的编程模型。程序员本身不用在多线程化上浪费过多精力,就可以编写出良好的多线程程序

多线程编程的两个核心问题

多线程编程的两个核心需要关注的问题——线程之间的通信 、 线程之间的同步

线程间的通信

Java通过共享内存模型的方式实现线程通信。

下面简单描述一下Java内存模型(JMM)的机制

在这里插入图片描述

线程之间的通信,通过JMM控制线程与之内存之间的交互来实现。

需要注意的问题

内存可见性的问题:

问题引出 —— 从源代码 -》指令序列的重排序

​ 1)编译器优化(不改变单线程执行结果 -> 可以重排序)

​ 2)指令级并行的重排序(ILP,不存在数据依赖性,可能重排序)

​ 3)读写缓冲区 -》 不能保证线程之间刷写主内存的顺序

解决方式 ——

​ 1)底层 —— JMM通过添加内存屏障的方式禁止特定的重排序

​ 2) 从编程的角度来看,通过happens- before规则,保证变量的可见性

线程的同步问题
顺序一致性模型

理论参考模型:

​ 1)一个线程中的所有操作必须按照程序的顺序来执行

​ 2)所有线程都只能看到一个单一的操作执行顺序,每个操作都必须原子执行、并立即对所有线程可见

JMM内存模型对内存一致性的保证

正确同步的程序将具有顺序一致性 —— 程序的执行结果与顺序一致性模型中的执行结果一致

Java内存模型——JMM

如上文中提到,java内存模型对于程序员提供了保障 —— 只要按照规则对于程序进行正确同步,将得到与顺序一致性模型相同的结果。

而java内存模型也对底层进行适当的放松,允许一定的指令重排、读写缓冲区的存在(提高了程序的执行效率)

happens- before原则

happens- before是JMM内存模型中最为重要的一个概念,如果需要保证一个操作的执行结果对另一个操作课件,就必须存在happens- before关系

具体规则如下:

​ 1)单线程,前面的hp后面的
​ 2)一个锁的解锁 hp 后续的加锁
​ 3)volatile的写 hp 后续对这个变量的读
​ 4)传递性
​ 5)线程A调用ThreadB.start(),A中的ThreadB.start() hp B中的任何操作
​ 6)线程A ThreadB.join() B中的任意操作 hp A的ThreadB.join()

三个重要的机制
volatile

volatile 关键字可以理解为一个轻量级的Synchronized 保障了可见性、并禁止与其他变量操作的重排序

可见性 —— 可以理解为对一个volatile的读,总是能看到对这个变量最后的写操作

禁止重排序 —— 可以参考单例模式double-check中volatile的作用

内存语义

读操作 —— 把本地内存中的值设置为无效、去共享内存中读

写操作 —— 将该线程本地内存中的值,刷新到主内存中

典型应用场景(单例模式:double-check、Concurrent包下的多数共享变量)

这里我们简单介绍一下,锁在JMM中的作用

根据h-p原则:一个锁的解锁 hp 后续的加锁

因此,被加锁/通过sychronized同步的方法(代码块)可以保障可见性/原子性/有序性

锁释放与获取的内存语义

​ 线程A释放锁,线程B获取锁 ,实质上是线程A通过主内存向线程B发送消息

​ 线程A释放锁 == 线程A向将要获取这个所得线程发出消息

​ 线程B获取锁 == 线程B接受了某个线程发出的释放锁的消息

以JUC下的Lock包为例,介绍一下锁的释放与获取的内存语义

锁的获取

// 非公平锁
	//调用lock()方法
	Lock lock = new ReentrantLock();
    lock.lock();

	//Reetrantlock 类下 lock方法
	public void lock() {
        sync.lock();	//调用sync的lock方法
    }

	//sync为静态内部类,继承了AQS
	 abstract static class Sync extends AbstractQueuedSynchronizer {
         abstract void lock();
     }

	//实现类 - 非公平锁的实现
	static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;
        final void lock() {
            if (compareAndSetState(0, 1))	// 该方法为AQS类实现的方法
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

	//AQS类中的cas方法改变state变量
	protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

// 公平锁
	final void lock() {
        acquire(1);
    }
	
	//AQS中的acquire方法
	public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

	//Sync中的tryAcquire方法
	protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();		//读volatile变量
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
	

锁的释放

    Lock lock = new ReentrantLock(true);
    lock.lock();
    try {
        //相应的业务逻辑
    }catch (Exception e){
        e.printStackTrace();
    }finally {
        lock.unlock();	//释放锁的操作
    }
	//ReetrantLock类
	public void unlock() {
        sync.release(1);
    }

	//AQS类
	public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

	//sync(继承AQS)
	protected final boolean tryRelease(int releases) {
        int c = getState() - releases;
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null);
        }
        setState(c);	// 本质还是volatile的写操作
        return free;
    }

Concurrent包的实现示意图

CAS同时具有volatile读与写的内存语义

Concurrent包中类的基本实现思路:

​ 1)共享变量声明为volatile

​ 2)使用CAS进行原子更新,实现线程同步

​ 3)配合volatile、CAS内存语义,实现线程间通信

例子见学习笔记中关于ConcurrentHashMap的介绍以及本系列笔记中后续关于Lock接口的介绍

摘自Java并发的编程艺术
在这里插入图片描述

final
关于final关键字

final关键字的作用:

用于修饰类(不可以被继承)、方法(不可以被重写)、变量(不可以更改(见代码示例))

// 局部变量
public class FinalDemo {
    public static void main(String[] args) {
        final int[] arr = {2, 3, 5, 7, 11};	// 修饰引用数据类型,地址不可变、具体的值可以改变
        // arr = null; 编译器无法通过
        arr[1] = 4;
        System.out.println(arr[1]); // 4
    }
}

// 类变量
class Programmer{
    //static final boolean isHandsome;    //编译不通过
    static final boolean SMART = true;  //要么声明时显示赋值
    static final boolean isHandsome;
    static {
        isHandsome = true;      // 要么在静态代码块中赋值
    }
}

// 实例成员变量
class Architect{
    //final boolean isHandsome; // 不进行显示赋值,编译不通过
    final boolean isHandsome = true; // 声明时赋值
    final boolean isSmart ;
    final boolean isTall ;
    {
        isSmart = true;     // 非静态代码块中复制
    }
    public Architect(){
        isTall = true;     //构造器中赋值
    }
}
Bonus:String、StringBuilder、StringBuffer

三者的区别:

String不可变、StringBuilder、StringBuffer是可变的

从运行速度上讲:StringBuilder > StringBuffer > String

String适合适用于少量的字符串操作的情况;StringBuilder适用于适用于单线程下在字符缓冲区进行大量操作的情况;StringBuffer适用多线程下在字符缓冲区进行大量操作的情况。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];	//底层为final类型的数组,因此不可变
    ...
}

关于String的拼接

底层是new了一个StringBuilder 进行相应的拼接操作,最后调用toString()方法,重新赋值给String

关于StringBuilder

// 空参构造器(初始化数组)
	public StringBuilder() {
        super(16);
    }
    
// 父类中的方法
	AbstractStringBuilder(int capacity) {
        value = new char[capacity];	//初始化长度为16的数组
    }

// 其他构造方法
	public StringBuilder(String str) {
        super(str.length() + 16);	// char[] 数组会有所余量
        append(str);
    }

// append方法
	public StringBuilder append(StringBuffer sb) {
        super.append(sb);
        return this;
    }
	
	public AbstractStringBuilder append(StringBuffer sb) {
        if (sb == null)
            return appendNull();
        int len = sb.length();
        ensureCapacityInternal(count + len);	// 确保是否需要扩容
        sb.getChars(0, len, value, count); // 拷贝元素
        count += len;
        return this;
    }

关于StringBuffer (相关方法添加了synchronized ,线程安全但效率较低)

@Override
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}
final 域的重排序规则

​ 1)构造函数内对于一个final域的写入,域随后把这个被构造对象的引用赋值给一个引用变量,不能重排序

​ 2)初始读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序

  return this;
}

关于StringBuffer (相关方法添加了synchronized ,线程安全但效率较低)

```java
@Override
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}
final 域的重排序规则

​ 1)构造函数内对于一个final域的写入,域随后把这个被构造对象的引用赋值给一个引用变量,不能重排序

​ 2)初始读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序

核心就是保证,构造函数返回后,任意线程都将保证能看到final域正确初始化的值

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值