1 进程与线程:
进程指正在运行的程序,进程拥有一个完整的、私有的基本运行资源集合。它有自己的内存空间。
为了便于进程之间的通信,大多数操作系统都都支持进程间通信(IPC). IPC通信包括管道、消息队列、信号量、共享存储、socket、streams.其中socket与streams支持在不同主机上的两个进程IPC。
线程有时也被称为轻量级的进程,进程与线程都提供了一个执行环境,但是创建一个线程比进程所需要的资源更少。
线程是在进程中运行的,线程共享进程的资源,包括内存与打开的文件。
简而言之:一个程序运行后至少有一个进程,一个进程可以包含多个线程。
线程的创建整体可以分为两种,可以在我的博客中找到:
1 基于Thread的方式创建线程,通过thread 的构造函数传递一个runnable接口,一种是通过lambada表达式传递一个runnable接口创建;或者实现runnable接口,传一个runnable接口的实现类。这种传递runnable接口或者runbale接口的实现类是没有返回值的。如果想要有返回值,需要传递callable接口,并通过futureTask获取返回值。
此外还可以通过继承thread类,重写run方法来定义一个线程的子类,通过创建线程的子类从而创建线程。
2 另一只创建线程的方式是通过线程池的方式创建,这需要向线程池的submit方法传递runnbale、Callable接口或者他们的实现类。
线程的启动与停止:
线程的启动可以调用start方法,停止虽然可以通过调用stop方法,但是这样不会对停止线程的状态做保存,是的线程中涉及的对象处于未知状态,
如果其它线程也会使用到该对象,则会出现无法预料的异常。比较常见的作法是在线程的run方法中通过while循环的bool条件进行判断。
线程的状态:
1 new 初始状态,线程被构建,但是还没有调用start()方法。
2 runnable 运行状态,java线程将操作系统中的就绪与运行两种状态统称为"运行中"
3 blocked 阻塞状态,表示线程处于阻塞
4 waitting 等待状态,进入该状态表示当前线程需要等待其它线程做出一些特定动作(通知或中断)
5 time_waiting超时等待状态,该状态不同于waiting,可以在指定的时间内返回
6 terminated 终止状态,表示当前线程已经执行完毕。
临界资源:一次仅允许一个进程使用的资源。各进程采取互斥的方式,实现共享的资源称作为临界资源。如打印机、磁带、消息队列、变量、数组、缓冲区等。当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞争条件。导致竞争条件发生的区域称为临界区。在临界区中使用恰当的同步就可以避免竞争,如使用synchronized或者加锁。允许多个线程同时执行的代码称为线程安全的代码,线程安全的代码不包含竞争的条件。
java内存模型:
java内存模型即java memory model 简称JMM。 JMM定义了虚拟机(jvm)在计算机内存中的工作方式。JVM是整个计算机虚拟模型,所以JMM是隶属于JVM的。
CAS锁:
乐观锁:不加锁,假设没有冲突就去完成某项工作,如果因为冲突失败就重试,直到成功为止。其实现方式比较典型得到是compare and swap(CAS)
CAS使用了三个基本操作数:内存地址v, 旧的预期值A, 要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址v当中的实际值相同时,才会将内存地址v对应的值修改为B。
CAS的缺点:
1 CPU开销比较大
在并发量比较高的情况下,如果许多线程失败后反复重试更新某一个变量,却又一直更新不成功,循环往复给CPU带来较大的压力。
2 不能保证代码块的原子性
CAS机制保证的是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证三个变量共同进行更新的原子性,就不得不使用synchronized了。
synchronized块:
java中的同步块用synchronized标记。"同步块在java中是同步在某个对象上。同步在一个对象上的同步块同时只能被一个线程进入并执行操作。所有等待进入该同步块的线程将被阻塞。直到该同步块中的线程退出"。
有四种不同的同步块:
1 实例方法 在方法前面加synchronized关键字,java实例方法同步是同步在拥有该方法的对象上。
2 静态方法 静态方法上加synchronized关键字,静态方法的同步是指同步在该方法所在的类对象上。因为在java虚拟机中一个类只能对应一个类对象,所以静态方法同步只能同时允许一个线程执行同一个类中的静态同步方法。
3 实例方法中的同步块
4 静态方法中的同步块
synchronized锁得到存储:
synchronized用的锁存储在java对象头,java对象头的长度如下:
长度 | 内容 | 说明 |
32/64bit | Mark World | 存储对象的hashCode或锁信息 |
32/64bit | Class Metedata Address | 存储到对象类型数据的指针 |
32/32bit | Array length | 数组的长度(如果当前对象是数组) |
Mark的存储结构:
锁状态 | 25bit | 4bit | 1bit是否是偏向锁 | 2bit锁标志位 |
无锁状态 | 对象的hashCode | 对象分代年龄 | 0 | 01 |
Mark 可能存储的结果:
偏向锁的获取流程:
1 查看Mark word中偏向锁的标识以及锁标志位,若“是否偏向锁”为1且“锁标志位”为01,则该锁为可偏向状态;
2 若为可偏向状态,则测试mark word中的线程id是否与当前线程相同,若相同,标识线程已经获得了锁。若不相同,则使用CAS尝试将Mark word中的线程id是否与当前线程相同,若相同,标识线程已经获得了锁。
若不相同,则使用CAS尝试将Mark word中线程ID设置为当前线程
3 如果cas修改线程ID失败失败,则说明存在竞争。当到达全局安全点时(在这个时间点,没有正在执行的代码)之前获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续执行同步代码。
轻量级锁:
轻量级锁不是用来替代传统的重量级锁的,而是在没有多线程竞争的情况下,使用轻量级锁减少性能的损耗。但是当多个线程同时竞争锁时,轻量级锁就会膨胀为重量级锁。
轻量级锁的加锁过程:
1 当线程执行代码进入同步代码块时,若Mark world为无锁状态,虚拟机现在当前线程的栈帧中建立一个名为Lock Record的空间,用于存储当前对象的Mark Word拷贝,官方称之为“displaced mark word”
2 复制对象头中的mark word 到所记录中。
3 复制成功后,虚拟机利用CAS操作就对象的Mark word更新为执行Lock Record的指针,并将Lock Record里的owner指针指向对象的Mark owner。
4 如果更新成功,则该线程拥有了这个锁,并将锁标志设为00, 表示处于轻量级锁状态。
5 如果更新失败,则说明有其它线程竞争锁,当前线程便通过自旋来获取锁。轻量级锁就会膨胀为重量级锁。 Mrak word中存储重量级锁(互斥锁)的指针,后面等待锁的线程也要进入阻塞状态。
关键字volatile
volatile是轻量级的synchronized,在多处理器环境下,可以保证共享变量的可见性,它不会引起线程上下文的切换与调度,正确地使用volatile比synchronized的使用成本更低。
可见性:是指线程间的可见性,一个线程修改的状态对另一个线程是可见的,使用volatile修饰的变量,就具有可见性。“volatile修饰的变量不允许线程内部缓存与重排序,直接修改内存,所以对其他线程是可见的”。但是volatile只能保证可见性,不能保证原子性。在java中,volatile、synchronized、和final实现可见性。
原子性:一个操作不要可在分割,它就是原子操作。非原子操作都会存在线程安全的问题,需要我们使用同步技术(synchronized)来操作的保证原子性。java的current包下提供了一些原子类:AtomicInteger、AtomicLong、AtomicReference. 在Java中synchronized和在lock、unlock中保证操作的原子性。
有序性:JAVA语言提供了volatile和synchronized两个关键字来保证线程之间的有序性。volatile是因为本身包含“禁止指令重排序”的语义;
synchronized是由“一个变量在同一时刻只允许一个线程对其进行lock操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。
当一个变量定义为volatile之后,将具备这种特性:
1 保证此变量对所有线程的可见性;当一个线程修改了这个变量的值之后,volatile保证新值立即同步到主内存,而不允许线程内部缓存,以及每次使用前都必须从主内存刷新。
2 禁止指令重排序优化,有volatile修饰的变量,赋值后又执行一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序不能把后面的指令重新排序到内存屏障之前的位置)。指令重排序是指CPU采用了将多条指令不按程序规定的顺序分开发送给各相应电路单元处理。
本地线程:
Java中的ThreadLocal类允许我们创建只能被同一个线程读写的变量。因此,如果一段代码含有一个ThreadLocal变量的引用,即时两个线程同时执行这段代码,他们也无法访到彼此的ThreadLocal变量。
java中的死锁:
死锁通常发生在多个线程同时但以不通的顺序请求同一组锁的时候,出现了循环等待的情况。
死锁的避免:
1 改变加锁顺序
2 设置加锁时限:超过设置的时间后先释放自己持有的锁,进行回退。过一段时间之后在进行重试饥饿与公平:
1如果一个线程因为CPU时间全部被其他线程抢走而得不到CPU运行时间,我们称这种状态为“饥饿”。 解决饥饿的方案我们称之为“公平性”
java中导致饥饿的原因:
1 高优先级的线程吞噬所有低优先级线程的CPU时间
2线程被永久阻塞在一个等待进入同步块的状态
3 线程等待一个本身(在其上调用wait)也处于永久等待完成的对象。如果多个线程在wait(方法上执行),而对其调用notify()不会保证哪一个线程会被唤醒,任何线程都有可能处于继续等待的状态。因此存在一个风险:某个线程一直得不到被唤醒的机会。
集合:
1 BlockingQueue 阻塞队列接口
boolean add(E e)方法,添加成功返回True;队列达到容量限制时,抛IllegalStateException;添加的元素为空时,抛NullPointerException;
如果是有界的,添加元素时使用offer比add更好。
boolean offer(E e)方法,容量未达阙值,插入成功,返回true;达到上限,插入失败,返回false;添加的元素为空时,抛NullPointerException。
void put(E e)方法:容量未达阙值,插入成功;达到上限,阻塞,直到有空闲位置可以插入;添加的元素为空时,抛NullPointerException。
boolean offer(E e, long timeout, TimeUnit unit)方法,如果插入成功,返回true; 如果达到容量限制,则等待,超过超时时间还没有空闲位置可插入,则抛InterruptedException。
E take() 队列中没有元素就阻塞;有元素就返回头部的元素。
E poll(long timeout, TimeUnit unit) 队列中有元素就返回头部的元素,没有就等待一定时间,超时后抛则抛InterruptedException。
boolean remove(Object o) 如果待移除的元素为Null,则抛NullPointerException。
BlockingQueue的实现类:
ArrayBlockingQueue:线程安全的、基于数组的、有界的、阻塞的、FIFO队列。此类基于ReentrantLock来实现线程安全。创建队列时需给定容量。
DelayQueue:队列中每个元素都有过期时间,并且队列是个优先级队列,当从队列获取元素时,只有过期元素才会出队列。
LinkedBlockingDeque:LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列。所谓双向队列指的你可以从队列的两端插入和移出元素。双端队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的
竞争。
LinkedBlockingQueue:是一个基于单向链表,范围任意(其实是有界的,可以设置大小,否则默认容量为Integer.MAX_VALUE)、FIFO阻塞队列。访问与移除操作都是在队列头部进行,添加操作是在队列尾部进行,并分别使用不同的锁进行保护。队列是否为空、是否已满是通过元素数量的计数器进行判断的,由于可以同时在队头、队尾并发进行访问、添加操作,这里使用了一个原子类AtomicLong来保证操作的原子性,计数器的容量范围为1到Integer.MAX_VALUE。由于使用putLock与takeLock两把锁,因此加锁时顺序非常重要:必须以固定的顺序进行加锁,再以加锁的相反顺序进行解锁。
LinkedTransferQueue:LinkedTransferQueue是一个由链表结构组成的无界阻塞TransferQueue队列。相对于其他阻塞队列
LinkedTransferQueue多了tryTransfer和transfer方法。transfer方法。如果当前有消费者正在等待接收元素(消费者使用take()方法或带时间限制的poll()
方法时),transfer方法可以把生产者传入的元素立刻transfer(传输)给消费者。如果没有消费者在等待接收元素,transfer方法会将元素存放在队列的tail节点,并等到该元素被消费者消费了才返
回。
tryTransfer方法。则是用来试探下生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回false。
PriorityBlockingQueue:带优先级的无界阻塞队列,初始大小为11,可分配的最大大小Integer.MAX_VALUE - 8;
SynchronousQueue:是一个没有数据缓冲的BlockingQueue,生产者线程对其的插入操作put必须等待消费者的移除操作take,反过来也一样。支持公平访问队列,默认情况下,线程采用非公平策略,如果使用公平策略,等待的线程采用先进先出的顺序访问队列。executors.newCachedThreadPool()就使用了SynchronousQueue,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲了60秒后会被回收。
其中,ArrayBlockingQueue和LinkedBlockingQueue最为常用。ArrayBlockingQueue内部是一个定长队列,所以,创建时,必须设置一个队列长度。
LinkedBlockingQueue内部是一个链表,创建时,可以不用设置队列长度,是个无界队列。两者的差别除了内部数据结构不同外,ArrayBlockingQueue读和写公用一把锁,读写不能同时进行。LinkedBlockingQueue采用独立的锁来分别控制读和写。所以,生产者和消费者能并行操作队列。从这个角度看,LinkedBlockingQueue在高并发下的处理性能更高。但是,LinkedBlockingQueue在添加和移除队列元素时,会生成一个额外的Node对象,大批量处理处理时,GC的压力更大,而ArrayBlockingQueue在读写队列时,不会产生额外的对象。
2 ConcurrentHashMap
HashMap给定的默认容量为 16,负载因子为 0.75。Map在使用过程中不断的往里面存放数据,当数量达到了16*0.75=12 就需要将当前16的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等
操作,所以非常消耗性能。因此通常建议能提前预估 HashMap 的大小最好,尽量的减少扩容带来的性能损耗。线程不安全的HashMap, 因为多线程环境下,使用 HashMap进行put操作会引起死循环,导致CPU利用率接近 100%,所以在并发情况下不能使用 HashMap.
效率低下的 HashTable 容器
HashTable 容器使用 syncronized来保证线程安全,但在线程竞争激烈的情况下 HashTable 的效率非常低下。因为当一个线程访问 HashTable 的同步方法时,其他线程访问 HashTable 的同步方法时,可能会进入阻塞或轮询状态。如线程 1 使用 put 进行添加元素,线程2不但不能使用 put 方法添加元素,并且也不能使用 get 方法来获取元素,所以竞争越激烈效率越低
CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
CopyOnWrite容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象。如果这些对象占用的内存比较大,那么这个时候很有可能造成频繁的Yong GC和Full GC。