Java多线程(五)

volatile

volatile关键字的作用
volatile关键字保证了线程间的可见性和禁止指令重排。volatile提供happens-before的保证,确保一个线程的修改对其他线程是可见的。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当其它线程需要读取时,它会去内存中读取新值。

从实践角度而言,volatile的一个重要作用就是和CAS结合,保证了原子性,详细参考java.util.concurrent.atomic包下的类,比如AtomicInteger
volatile常用于多线程环境下的单次操作(单次读或者单次写)。

可以创建volatile数组吗
可以,可以创建volatile类型数组,不过只是一个指向数组的引用,而不是整个数组。
如果改变引用指向的数组,将会受到volatile保护,但是,如果多线程同时改变数组的元素,volatile标识符就不能起到之前的保护作用。

volatile变量和atomic变量有什么不同
volatile变量可以确保先行关系,即写操作会发生在后续的读操作之前,但它并不能保证原子性。如果用volatile修饰count变量,那么count++就不是原子性的。

AtomicInteger类提供的atomic方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加一。其它数据类型和引用变量也可以进行相似操作。

volatile能使得一个非原子操作变成原子操作吗?
volatile关键字的主要作用是使变量在多线程间可见,但无法保证原子性,对于多个线程访问同一个实例变量需要加锁进行同步。
虽然volatile只能保证可见性不能保证原子性,但用volatile修饰long和double可以保证其操作原子性

从Oracle Java Spec里面可以看到
1.对于64的long和double,如果没有被volatile修饰,那么对其操作可以不是原子的。在操作时,可以分两步,每次对32位操作
2.如果使用volatile修饰double和long,那么其读写都是原子操作
3.对于64位的引用地址的读写,都是原子操作
4.在实现JVM时,可以自由选择是否把读写long和double作为原子操作
5.推荐JVM实现为原子操作

volatile修饰符的有过什么实践
单例模式
是否Lazy初始化:是
是否多线程安全:是
实现难度:较复杂

synchronized和volatile的区别
synchronized表示只有一个线程可以获取作用对象的锁,执行代码,其他线程阻塞
volatile表示变量在CPU的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性,禁止指令重排

区别:
1.volatile是变量修饰符;synchronized可以修饰类、方法、变量
2.volatile仅能实现变量修改的可见性,不能保证原子性;而synchronized可以保证变量的修改可见性和原子性
3.volatile不会造成线程阻塞,synchronized可能造成线程阻塞
4.volatile标记的变量不会被编译器优化,synchronized标记的变量可以被编译器优化
5.volatile关键字是线程同步的轻量级实现,volatile性能比synchronized关键字好,但volatile只能用于变量,而synchronized关键字可以修饰方法和代码块
synchronized关键字在SE1.6后进行了包括减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际synchronized关键字使用的场景更多。

final
什么是不可变对象,它对写并发应用有什么帮助
不可变对象(Immutable Objects)即对象一旦被创建,它的状态就不能(对象的数据,对象的属性值)改变,反之为可变对象。

不可变对象的类即为不可变类,类库中包含许多不可变类,如:String、基本类型的包装类、BigInteger和BigDecimal等

不可变对象条件
1.它的状态不能在创建后再被修改
2.所有域都是final类型;且被正确创建(创建期间没有发生this引用的溢出)

作用:
不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。

Lock体系
Lock与AQS
Lock接口(Lock interface)是什么,对比同步的优势
Lock接口比同步方法和同步块提供了更具扩展性的锁操作。允许更灵活的结构,可以具有完全不同的性质,并且支持多个相关类的条件对象

优势:
1.可以使锁更公平
2.可以使线程在等待锁的时候响应中断
3.可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
4.可以在不同的范围,以不同的顺序获取和释放锁

Lock是synchronized的扩展版,Lock提供了无条件的、可轮询的(tryLock方法)、定时的(tryLock带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition方法)锁操作。Lock的实现类基本都支持非公平锁和公平锁,synchronized只支持非公平锁,大部分情况下,非公平锁是高效的选择。

乐观锁和悲观锁如何实现,实现方式有哪些

悲观锁:
假设最坏情况,每次获取数据时都认为会被修改,所以在每次获取数据时都会上锁,这样其他线程想要获取数据就会阻塞直到它获取锁。传统关系型数据库里面就用到了这种锁机制,如行锁,表锁等,读锁,写锁等,都是在操作之前先上锁。synchronized关键字的实现也是悲观锁。

乐观锁:
每次获取数据时都认为不会被修改,所以不会上锁,但在更新时会判断在此期间是否有其他线程更新数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,可以提高吞吐量,数据库提供的write_condition机制,就是提供的乐观锁。在java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

乐观锁的实现方式:
1.使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试策略

2.Compare and Swap即CAS方式:当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会挂起,而是被告知这次竞争失败,并可以再次尝试,CAS操作中包括三个操作数-需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置V和预期原值A相匹配,那么处理器就会自动将该位置的值更新为新值B。否则处理器不做任何操作。

什么是CAS
CAS是compare and swap的缩写,是比较交换

CAS是一种基于锁的操作,而且是乐观锁。锁分为乐观锁和悲观锁,悲观锁是将资源锁住,等一个之前获得锁的线程释放之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能比悲观锁有很大提高

CAS操作包含三个操作数-内存位置(V)、预期原值(A)和新值(B),如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS是通过无限循环来获取数据,如果在第一轮循环中,a线程获取地址里面的值被b修改了,那么a线程需要自旋,到下次循环才有可能执行。java.util.concurrent.atomic包下的类大多使用CAS操作实现(AtomicInteger、AtomicBoolean、AtomicLong)

例:
线程T1和线程T2同时更新内存中的同一变量1为2,CAS步骤:
1.T1和T2同时访问同一变量1,且都会将主内存的值1拷贝到自己的工作内存空间,此时T1和T2的预期值都为1
2.当T1和T2同时使用CAS更新同一个变量时,只有一个线程可以更新变量的值,假设此时T1在竞争中能去更新变量,T2线程失败
3.T1线程此时发现内存值为1,预期值为1,符合CAS要求,更新变量的值为2,然后写到内存中,T1释放锁
4.T2获取锁,此时内存值为2,与预期原值1不一致,操作失败,想修改的值不再是原来的值1
5.T2失败后重新执行动作,内存值为2,预期值为2,将新值改为2

CAS会产生的问题
1.ABA问题
有两个线程A和B,需要修改的内存值为1,
1.线程A从内存位置中取出内存值1,线程A的预期值就为1。
2.线程B从内存位置中取出内存值1,线程B的预期值就为1。
3.线程B进行修改操作,判断内存值和预期值一致,修改内存值为2。
4.线程B又将内存位置的值变为1
5.线程A进行CAS操作成功,判断内存值和预期值为1,虽然线程A操作成功,但此时内存值变化了两次,可能存在潜藏的问题

2.循环时间长,开销大
对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized

3.只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,可以使用循环CAS方式保证原子操作,但对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就需要使用锁

防止死锁的方法:
1.尽量使用tryLock(long timeout,TimeUnit unit)的方法(ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时可以退出,防止死锁。
2.尽量使用java.util.concurrent并发类代替自己手写类
3.尽量降低锁的使用粒度,尽量不要几个功能使用同一把锁
4.尽量减少同步的代码块

死锁和活锁的区别
死锁:两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,则无法推进下去
活锁:任务或者执行者没有被阻塞,由于某些条件未满足,导致一直重复尝试。

主要区别:
处于活锁的实体是不断地改变状态,而处于死锁的实体表现为等待,活锁可能自行解开,死锁则不能

死锁与飢饿的区别
飢饿:一个或多个线程因为种种原因无法获得所需资源,导致一直无法执行的状态

导致飢饿的原因
1.高优先级线程吞噬所有低优先级线程的CPU时间
2.线程被永久阻塞在一个等待进入同步块的状态,其它线程总是能在它之前持续地对该同步块进行访问
3.线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的wait方法),因为其它线程总是被持续地获得唤醒

多线程锁的升级原理
锁有四种状态,级别从高到低:无状态锁、偏向锁、轻量级锁和重量级锁状态,这几个状态会随着竞争情况逐渐升级,锁可以升级但不能降级。

AQS
AQS全称AbstractQueuedSynchronizer(抽象队列同步器)这个类在java.util.concurrent.locks包下面
AQS是一个用来构建锁和同步器的框架,使用AQS能简单且有效地构造出应用广泛的大量同步器,比如ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue,FutureTask等都是基于AQS,我们也能利用AQS非常轻松地构造出符合我们自己需求的同步器。

AQS原理分析
核心思想:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将被请求的共享资源设置为锁状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即,将暂时获取不到锁的线程加入到队列中。

CLH队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列的实例,仅存在节点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个节点(Node)来实现锁的分配

AQS使用一个int成员变量表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对值的修改

private volatile int state;

状态信息通过protected类型的getState,setState,compareAndSetState进行操作
返回同步状态的当前值

protected final int getState(){
	return state;
}

设置同步状态的值

protected final void setState(int newState){
	state= newState;
}

原子(CAS操作)地将同步状态值设置为给定值update,如果当前同步状态值等于预期原值

protected final boolean compareAndSetState(int expect,int update){
	return unsafe.compareAndSwapInt(this,stateOffset,expect,update);
}

AQS对资源的共享方式
AQS定义了两种资源共享方式
1.Exclusive(独占):
只有一个线程可以执行,如ReentrantLock。分为公平锁和非公平锁:
公平锁:
按照线程在队列中的排队顺序,先到者先得到锁
非公平锁:
当线程要获取锁时,无视队列顺序直接抢锁,哪个线程抢到是哪个线程的
2.Share(共享):
多个线程可同时执行,如Semaphore/CountDownLatch、CyclicBarrier、ReadWriteLock

ReentrantReadWriteLock可以看成是组合式,ReentrantReadWriteLock读写锁允许多个线程同时对某一资源进行读写。

不同的自定义同步器争用共享资源的方式不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护
如获取资源失败入队/唤醒出队,AQS顶层已经实现好

AQS底层使用模板方法模式
同步器的设计基于模板方法模式,自定义同步器一般方法(模板方法模式经典应用):
1.使用者继承AbstractQueuedSynchronizer并重写指定方法。(重写方法是对于共享资源state的获取和释放)
2.将AQS组合在自定义同步组件的实现中,并调用模板方法,模板方法会调用使用者重写的方法

模板方法和实现接口的方式有很大区别。

AQS使用了模板方法模式,自定义同步器时需要重写以下几个AQS提供的模板方法:
1.isHeldExclusively()//该线程是否正在独占资源,只有用到condition才需要实现它。
2.tryAcquier(int)//独占方式。尝试获取资源,成功返回true,失败返回false
3.tryRelease(int)//独占方式。尝试释放资源,成功返回true,失败返回false
4.tryAcquireShared(int)//共享方式。尝试获取资源,负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源
5.tryReleaseShared(int)//共享方式。尝试获取资源,成功返回true,失败返回false

默认情况下,每个方法都抛出UnsupportedOperationException。这些方法的实现必须是内部线程安全,并且应该简短而不是阻塞。
AQS类中的其它方法都是final,所以无法被其它类使用,只有以上几个方法可以被其它类使用。

ReentrantLock:state初始化为0,表示未锁定状态。
线程A lock()时,调用tryAcquire()独占该锁并将state+1。
其它线程调用tryAcquire()时失败,直到线程A unlock()到state=0(释放锁)为止,其它线程才有机会获取该锁。
释放锁之前,线程A可以重复获取此锁(state会累加),这就是可重入锁,获取多少次就需要释放多少次,才能保证state状态回到0。

CountDownLatch:任务分为N个子线程执行,state初始化为N,与线程个数一致。N个子线程并行执行,每个子线程执行完后countDown()一次,state会CAS减1。等所有子线程都执行完后(state=0),会unpark()主调用线程,然后主调用线程会从await()函数返回,继续后面动作。

一般情况,自定义同步器要么是独占方法,要么是共享方式,它们只需要实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock

ReentrantLock(可重入锁)实现原理与公平锁和非公平锁区别

synchronized隐式支持重入性,synchronized通过获取自增,释放自减的方式实现重入。

重入性的实现原理
支持重入性,需要解决两个问题:
1.在线程获取锁时,如果已经获取锁的线程是当前线程则直接再次获取成功
2.由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算完全释放成功

ReentrantLock支持两种锁:公平锁和非公平锁:
如果一个锁是公平的,那么锁的获取顺序就应该符合请求上的绝对时间顺序,满足FIFO

读写锁ReentrantReadWriteLock源码分析
ReadWriteLock是什么
ReentrantLock的局限性:
使用ReentrantLock可能本身是为了防止线程A在写数据,线程B在读数据时造成数据的不一致。但如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但还是加锁了,降低了程序的性能。因此,诞生了读写锁ReadWriteLock

ReadWriteLock是一个读写锁接口,读写锁是用来提升并发程序性能的锁分离技术,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写性能。

读写锁的三个重要特性:
1.公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量非公平优于公平
2.重进入:读锁和写锁都支持线程冲进入
3.锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁可以降级为读锁

LockSupport
ConcurrentHashMap是一个线程安全且高效的HashMap的实现,其中利用了锁分段的思想提高了并发度。

如何实现线程安全
JDK1.6
1.segment继承了ReentrantLock充当锁的角色,为每一个segment提供了线程安全的保证。
2.segment维护了哈希散列表的若干个桶,每个桶由HashEntry构成链表。

JDK1.8
ConcurrentHashMap放弃原有的锁分段,采用CAS+synchronized来保证并发安全性

并发容器的实现

同步容器:通过synchronized来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。比如Vector、HashTable以及Collections.synchronizedSet,synchronizedList等方法返回的容器。可以通过查看Vector,HashTable等同步容器的实现,这些容器实现线程安全的方式是将它们的状态封装起来,并在需要同步的方法上加上关键字synchronized。

并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性。
在ConcurrentHashMap中采用粒度更细的加锁机制,称为分段锁,在这种锁机制下,允许任意数量的读线程并发地访问map,并且执行读操作的线程和写操作的线程也可以并发地访问map,同时允许一定数量的写操作线程并发地修改map,所以它可以在并发环境下实现更高的吞吐量。

并行和并发
并行:对于多处理器而言,多个程序在同一时刻发生。
并发:对单处理器而言,多个程序在同一时间段发生。

并发中的互斥和同步
互斥:进程间相互排斥使用临界资源,比如写操作
同步:不是排斥关系而是依赖关系,前一个进程的输出是后一个进程的输入,当第一个进程没有结束时,第二个进程必须等待,相互协同完成;具有同步关系的一组进程并发时发送的消息称为消息或者事件。

同步和异步
异步:线程B在等待线程A的结果时可以继续做自己的事情,之间通过消息和事件来通知对方,提高了程序运行效率。异步是最终目的,多线程是实现的一种方法。

同步集合和并发集合的区别
同步集合和并发集合都为多线程和并发提供了合适的线程安全集合,并发集合的可扩展性更高;同步集合在多线程并发时会导致争用,阻碍系统的扩展性。并发集合ConcurrentHashMap不仅提供了线程安全还使用锁分离和内部分区提高了可扩展性。

SynchronizedMap和ConcurrentHashMap的区别
SynchronizedMap:
一次锁住整张表来保证线程安全,所以每次只有一个线程访问map

ConcurrentHashMap:
使用分段锁来保证多线程下的性能
ConcurrentHashMap一次锁住一个桶,默认将hash表分为16个桶,如get、put和remove等常用操作只锁住当前需要用到的桶。这样,原本只能一个线程进入,现在可以同时16个写线程执行,提高了并发性能。

ConcurrentHashMap使用了特殊的迭代方式,当iterator被创建后集合再发生改变不再抛出ConcurrentModificationException。取而代之的是在改变时new新的数据从而不影响原有的数据,iterator完成后再将指针替换为新的数据,这样iterator线程可以使用原数据,而写线程也可以并发地完成改变。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值