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域正确初始化的值