(Java.util.concurrent)
在学习之前,让我们先回忆一下什么是进程/线程?
进程
:进行是一个具有一定独立功能的程序关于某个数据集合的一次运行活动,它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元
线程
: 通常在一个进程中科院包含若干个线程,当然一个进程至少有一个线程,不然没有存在意义了。线程可以利用进程所拥有的资源,在引入线程的操作系统中,通常把进程作为分配资源的基本单位,而吧线程作为独立运行和独立调度的基本单位,由于线程比进程小,基本上不拥有系统资源,故,对他的调度所付出的开销就会小很多,能更高效的提高系统多个程序间并发执行的程度。
并发和并行都是完成多任务更加有效率的方式,但还是有一些区别的,并发(concurrency),并行(parallellism),可见他们的确是有区别的。下面通过一些具体的例子进行说明。
例子一:
? 假设一个有三个学生需要辅导作业,帮每个学生辅导完作业是一个任务
? 顺序执行:老师甲先帮学生A辅导,辅导完之后再取给B辅导,最后再去给C辅导,效率低下 ,很久才完成三个任务
? 并发:老师甲先给学生A去讲思路,A听懂了自己书写过程并且检查,而甲老师在这期间直接去给B讲思路,讲完思路再去给C讲思路,让B自己整理步骤。这样老师就没有空 着,一直在做事情,很快就完成了三个任务。与顺序执行不同的是,顺序执行,老师讲完思路之后学生在写步骤,这在这期间,老师是完全空着的,没做事的,所以效率低下。
? 并行:直接让三个老师甲、乙、丙三个老师“同时”给三个学生辅导作业,也完成的很快。
例子二:
? 顺序执行:你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
? 并发:你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。
? 并行:你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并
理解:
解释一:并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
解释二:并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
解释三:在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如hadoop分布式集群
普通解释:
并发:交替做不同事情的能力
并行:同时做不同事情的能力
专业术语:
并发:不同的代码块交替执行
并行:不同的代码块同时执行
并发和并行的意义:
并发和并行都可以处理“多任务”,二者的主要区别在于是否是“同时进行”多个的任务。
但是 涉及到任务分解(有先后依赖的任务就不能做到并行)、任务运行(可能要考虑互斥、锁、共享等)、结果合并
java.util.concurrent
java.util.concurrent.atomic
java.util.concurrent.locks
锁的状态都是存在Java对象头里面的,
32bit的jvm MarkWord 就是32bit
64bit的Jvm MarkWord 就是64bit
Java对象头 默认存储:对象的HashCode,分代年龄,锁标记位
构成:线程Id,Epoch 分代年龄, 是否偏向锁,锁标记位
设计理念:每次进入和释放同步块 都要获取锁,只有竞争才会释放锁,不能主动释放锁。
目的:在没有竞争的前提下,只有一个线程使用的前提下,节省轻量级锁的资源开销
。
构成:指向栈中锁记录的指针、锁标记位
目的:减低重量级锁造成的损耗
设计理念: 线程1开辟一个栈帧空间(包含Lock、Record信息),并且将MarkWord的指针 复制到该栈帧空间并且取名为Displaced Mark Word
根据该指针就可以操作线程了。 ↓
若CAS(compare and Swap 比较并替换) 操作MarkWord
成功, 线程1 获取同步块中的锁。
若CAS 操作MarkWord
操作失败,表面有竞争,锁被别的线程获取到了,这个时候线程1 就采用自旋来等待获取锁 【自旋就是让线程执行一个无意义的循环,就是干等的意思】
【线程1的物理空间包含着战 争帧空间】
构成: 指向重量级锁的指针,锁标记位
设计理念:使用的是操作系统的互斥来实现的互斥,十分慢
综上可知,偏先锁是为了解决轻量级锁造成的不必要的损耗,轻量级是为了解决重量级锁造成的不必要的损耗。
锁使用的流程:先偏先锁— 升级–->轻量级锁—升级—->重量级锁
构成:对象的HashCode , 分代年龄,是否为偏向锁,锁标志位
设计理解:可重入就是说某个线程已经获得某个锁A,假设同步块1和同步块2都需要该锁,则该线程不需要释放锁A,就可以进入完同步块1以后可重入进入同步块2(不需要获取和释放锁A)
常用可重入锁:
synchronized
ReentrantLock:ReentrantLock 和 synchronized 不一样,需要手动释放锁,所以使用 ReentrantLock的时候一定要手动释放锁,并且加锁次数和释放次数要一样
关键代码
private AtomicInteger atomicI = new AtomicInteger(0);
private int i = 0;
/**
* 使用CAS实现线程安全计数器
*/
void safeCount () {
for (; ; ) {
int i = atomicI.get();
boolean suc = atomicI.compareAndSet(i, ++i);
if (suc) {
break;
}
}
}
关键对比代码
自旋CAS实现的基本思路是循环进行CAS操作直到成功,以下代码实现一个基于CAS线程安全的计数器方法safeCount和一个非安全的普通计数器count
package Chapters02;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 计数器
*/
public class Counter {
private AtomicInteger atomicI = new AtomicInteger(0);
private int i = 0;
public static void main (String[] args) {
final Counter cas = new Counter();
List <Thread> ts = new ArrayList <Thread>(600);
long start = System.currentTimeMillis();
for (int j = 0; j < 100; j++) {
Thread t = new Thread(new Runnable() {
@Override
public void run () {
for (int i = 0; i < 10000; i++) {
cas.count();
cas.safeCount();
}
}
});
ts.add(t);
}
for (Thread t : ts) {
t.start();
}
// 等待所有线程执行完成
for (Thread t : ts) {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(cas.i);
System.out.println(cas.atomicI.get());
System.out.println(System.currentTimeMillis() - start);
}
/**
* 使用CAS实现线程安全计数器
*/
void safeCount () {
for (; ; ) {
int i = atomicI.get();
boolean suc = atomicI.compareAndSet(i, ++i);
if (suc) {
break;
}
}
}
/**
* 非线程安全计数器
*/
void count () {
i++;
}
}
并发编程需要解决的两个关键问题:
? 线程之间如何通信以及线程之间如何同步(并发中的线程活动实体)
线程通信有两种方式:
共享内存
Java中所有,所有的实例域,静态域、数组元素都存在堆内存中,堆内存在线程之间共享,局部变量、方法定义参数和异常处理器参数不会再线程之间共享。
Java内存模型描述:
? Java线程之间的通信是通过Java内存模型(JMM)控制的,JMM决定一个线程对共享变量的写入 何时对另外一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:
线程之间的共享变量存储在主存中,每个线程都有自己的本地内存,本地内存存储了该线程以读、写共享变量的副本,本地内存的JMM概念是一个抽象概念,并不真实存在。
线程通信步骤:
1) 线程A把本地内存A中更新过的共享变量刷新要主内存中
2) 线程B到主内存中去读取线程A之前以及更新了的变量
原文:https://www.cnblogs.com/blogger-Li/p/14333179.html