今日学习总结如下:
死锁问题
多线程以及多进程改善了系统资源的利用率并提高了系统的处理能力。然而,并发执行也带来了新的问题,即死锁。所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
特殊的概念
饥饿指的线程无法访问到它需要的资源而不能继续执行时,引发饥饿最常见资源就是CPU时钟周期。虽然在Thread API中由指定线程优先级的机制,但是只能作为操作系统进行线程调度的一个参考,换句话说就是操作系统在进行线程调度是平台无关的,会尽可能提供公平的、活跃性良好的调度,那么即使在程序中指定了线程的优先级,也有可能在操作系统进行调度的时候映射到了同一个优先级。通常情况下,不要去修改线程的优先级,一旦修改程序的行为就会与平台相关,并且会导致饥饿问题的产生。在程序中使用的Thread.yield或者Thread.sleep表明该程序试图客服优先级调整问题,让优先级更低的线程拥有被CPU调度的机会。
活锁指的是线程不断重复执行相同的操作,但每次操作的结果都是失败的。尽管这个问题不会阻塞线程,但是程序也无法继续执行。活锁通常发生在处理事务消息的应用程序中,如果不能成功处理这个事务那么事务将回滚整个操作。解决活锁的办法是在每次重复执行的时候引入随机机制,这样由于出现的可能性不同使得程序可以继续执行其他的任务。
Java中活锁和死锁有什么区别?
活锁和死锁类似,不同之处在于处于活锁的线程或进程的状态是不断改变的,活锁可以认为是一种特殊的饥饿。一个现实的活锁例子是两个人在狭小的走廊碰到,两个人都试着避让对方好让彼此通过,但是因为避让的方向都一样导致最后谁都不能通过走廊。简单的说就是,活锁和死锁的主要区别是前者进程的状态可以改变但是却不能继续执行。
怎么检测一个线程是否拥有锁?
在java.lang.Thread中有一个方法叫holdsLock(),它返回true如果当且仅当当前线程拥有某个具体对象的锁。
重点:锁产生的必要条件
产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生。
1)互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
2)不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。
3)请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
4)循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, ..., pn},其中Pi等 待的资源被P(i+1)占有(i=0,1, ..., n-1),Pn等待的资源被P0占有。
如何避免死锁
加锁顺序(线程按照一定的顺序加锁,避免嵌套封锁)
加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
Java中如何获取到线程dump文件
死循环、死锁、阻塞、页面打开慢等问题,打线程dump是最好的解决问题的途径。所谓线程dump也就是线程堆栈,获取到线程堆栈有两步:
1)获取到线程的pid,可以通过使用jps命令,在Linux环境下还可以使用ps -ef | grep java
2)打印线程堆栈,可以通过使用jstack pid命令,在Linux环境下还可以使用kill -3 pid
Thread类提供了一个getStackTrace()方法也可以用于获取线程堆栈。这是一个实例方法,因此此方法是和具体线程实例绑定的,每次获取获取到的是具体某个线程当前运行的堆栈
死锁检测Jconsole
- Jconsole
- Jconsole是JDK自带的图形化界面工具,使用JDK的工具JConsole,可以通过打开cmd然后输入jconsole打开
- jconsole就会给我们检测出该线程中造成死锁的线程,点击选中即可查看详情
线程相关的模型
1)启动一个os的用户线程,然后实际的任何操作都直接对应该用户线程,这就是1:1,这样做之后,调度就由os负责,jvm就不管了,hotspot等主流jvm基本上都是这种做法
2)启动一个虚拟线程,然后执行的时候,交给os上的一个用户线程去执行,这样做的好处就是,jvm可以自己实现调度,而且可以控制虚拟线程的大小,这就是n:m或者1:m,看具体的实现,而如果将线程虚拟化之后,调度就可以由jvm来实现了,做成1:m还是n:m完全看jvm调度的实现,这就是协程,go其实是这种做法,早期的solaris上的hotspot也是这种做法,后来改了,改成选择1的做法,这样在不同os上hotspot的实现就统一了
volatile类型修饰符
工具需要自行下载安装
通过 hsdis 可以查看 Java 编译后的机器指令
> HSDIS是一个Sun官方推荐的HotSpot虚拟机编译代码的反汇编插件,包含在HotSpot虚拟机的源码之中,但没有提供编译后的程序。在Project Kenai的网站也可以下载到单独的源码
使用 jitwatch 工具,可以帮助分析该日志
> HotSpot JIT编译器的日志分析器和可视化工具
volatile用于修改属性,保证多线程访问属性时的可见性、顺序性,但是并不保证原子性
保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)--原理
禁止进行指令重排序。(实现有序性)
volatile 只能保证对单次读/写的原子性。i++这种操作不能保证原子性volatile 变量的内存可见性是基于内存屏障Memory Barrier实现。内存屏障,又称内存栅栏,是一个CPU指令。
在程序运行时,为了提高执行性能,编译器和处理器会对指令进行重排序,JMM为了保证在不同的编译器和CPU上
有相同的结果,通过插入特定类型的内存屏障来禁止特定类型的编译器重排序和处理器重排序,插入一条内存屏障会告诉编译器和CPU:不管什么指令都不能和这条Memory Barrier指令重排序。
volatile的应用场景
只有在状态真正独立于程序内其他内容时才能使用 volatile
典型案例:双重检查
忽略volatile关键字程序也可以很好的运行,只不过代码的稳定性不是100%,说不定在未来的某个时刻,隐藏的 bug 就出来了
public class Singleton {
private volatile static Singleton instance;
private Singleton(){}
public static Singleton getInstance() {
if (instance == null) {
syschronized(Singleton.class) {
if (instance == null) instance = new Singleton();
}
}
return instance;
}
}
推荐饿汉模式加载优雅写法 Initialization on Demand Holder(IODH)---实际上实现了延迟的效果
public class Singleton {
static class SingletonHolder {
static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
}
枚举类型的写法
public enum Singleton{
INSTNCE;
public void pp(){}
}
两个重要的实现模型 AQS和CAS
AQS模型
AbstractQueuedSynchronizer抽象队列同步器:AQS是JDK下提供的一套用于实现基于FIFO等待队列的阻塞锁和相关的同步器的一个同步框架。AQS维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)
抽象队列同步器中包括两个部分:临时资源和一个FIFO的CLH阻塞队列.
AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
内部使用AQS的例子:以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到
state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync; 锁的具体实现,依赖于同步器实现锁机制
内部类:
abstract static class Sync extends AbstractQueuedSynchronizer 同步器实际上就是AQS的一个子实现
static final class NonfairSync extends Sync非公平同步器的实现类
static final class FairSync extends Sync 公平同步器的实现类
CAS模型
Java5中引入了AutomicInteger、AutomicLong、AutomicReference等特殊的原子性变量类,它们提供的如compareAndSet、incrementAndSet和getAndIncrement等方法都使用了CAS操作。都是由硬件指令来保证的原子方法。
CAS即比较并交换。是解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS操作包含三个操作数——内存位置V、预期原值A和新值B。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。
比较和交换(Compare And Swap)是用于实现多线程同步的原子指令。 它将内存位置的内容与给定值进行比较,只有在相同的情况下,将该内存位置的内容修改为新的给定值。这是作为单个原子操作完成的。
原子性保证新值基于最新信息计算; 如果该值在同一时间被另一个线程更新,则写入将失败。操作结果必须说明是否进行替换; 这可以通过一个简单的布尔响应(这个变体通常称为比较和设置),或通过返回从内存位置读取的值来完成
CAS 的特性:
- 通过调用JNI的代码实现
- 非阻塞算法
- 非独占锁
CAS 存在的问题:
- ABA
- 循环时间长开销大
- 只能保证一个共享变量的原子操作
典型案例:原子量
所谓的原子量即操作变量的操作是原子的,该操作不可再分,因此是线程安全的。
为何要使用原子变量呢,
原因是多个线程对单个变量操作也会引起一些问题。在Java5之前,可以通过volatile
、synchronized关键字来解决并发访问的安全问题,但这样太麻烦。
Java5后专门提供了用来进行单变量多线程并发安全访问的工具包
java.util.concurrent.atomic,其中的类也很简单。例如
AtomicLong aLong=new AtomicLong(10000); //原子量,每个线程都可以自由操作
具体实现:
Java中的Unsafe类提供了类似C++手动管理内存的能力。Unsafe类对普通程序员来说是危险的,一般应用开发者不会用到这个类。getIntVolatile方法用于在对象指定偏移地址处volatile读取一个int。putIntVolatile方法用于在对象指定偏移地址处volatile写入一个int。
原子量实现的计数器
//自定义计数器
public class AtomicCounter {
private static AtomicCounter instance = new AtomicCounter();
private AtomicCounter() {
}
public static AtomicCounter getIntance() {
return instance;
}
// 存储的数据,同时利用CAS提供线程安全的特性
private AtomicInteger counter = new AtomicInteger();
public int getCounter() {
return counter.get();
}
public int increase() {
// 内部使用死循环for(;;)调用compareAndSet(current, next)
return counter.incrementAndGet();
// return counter.getAndIncrement();
}
public int increase(int i) {
// 内部使用死循环for(;;)调用compareAndSet(current, next)
return counter.addAndGet(i);
// return counter.getAndAdd(i);
}
public int decrease() {
// 内部使用死循环调用compareAndSet(current, next)
return counter.decrementAndGet();
// return counter.getAndDecrement();
}
public int decrease(int i) {
// 内部使用死循环调用compareAndSet(current, next)
return counter.addAndGet(-i);
// return counter.getAndAdd(-i);
}
public static void main(String[] args) throws Exception {
final AtomicCounter ac = AtomicCounter.getIntance();
// 可缓存线程池
ExecutorService service = Executors.newCachedThreadPool();
Set<Integer> set = new CopyOnWriteArraySet<>();
for (int i = 0; i < 1000; i++) {
service.execute(new Runnable() {
public void run() {
int rr=ac.increase();
set.add(rr);
System.out.println(Thread.currentThread() + "::" + rr);
}
});
}
Thread.currentThread().sleep(2000);
System.out.println(set.size());
service.shutdown();
}
}
CAS模型的问题
1、ABA问题
> 解决方法:JAVA中提供了AtomicStampedReference/AtomicMarkableReference来处理会发生
ABA问题的场景,主要是在对象中额外再增加一个标记来标识对象是否有过变更。
2、CAS应用场景隐含竞争是短暂的,否则不断的自旋尝试会过度消耗CPU
> 解决方法加入超时设置
3、CAS只能保证一个共享变量的原子操作,解决方法是使用锁或者合并多个变量
> AtomicReference提供了以无锁方式访问共享资源的能力
总结:
AtomicInteger、AtomicBoolean、AtomicLong、AtomicReference这些原子类型,它们无一例外
都采用了基于volatile 关键字 +CAS 算法无锁的操作方式来确保共享数据在多线程操作下的线程安
全性。
- volatile关键字保证了线程间的可见性,当某线程操作了被volatile关键字修饰的变量,其他线
程可以立即看到该共享变量的变化。
- CAS算法,即对比交换算法,是由UNSAFE提供的,实质上是通过操作CPU指令来得到保证的。CAS算法提供了一种快速失败的方式,当某线程修改已经被改变的数据时会快速失败。
- 当CAS算法对共享数据操作失败时,因为有自旋算法的加持,对共享数据的更新终究会得到计算。总之,原子类型用自旋+CAS的无锁操作保证了共享变量的线程安全性和原子性。