day-3-1-3

死锁问题

多线程以及多进程改善了系统资源的利用率并提高了系统的处理能力。然而,并发执行也带来了新的问题,即死锁。所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。

特殊的概念

饥饿指的线程无法访问到它需要的资源而不能继续执行时,引发饥饿最常见资源就是CPU时钟周期。虽然在Thread API中由指定线程优先级的机制,但是只能作为操作系统进行线程调度的一个参考,换句话说就是操作系统在进行线程调度是平台无关的,会尽可能提供公平的、活跃性良好的调度,那么即使在程序中指定了线程的优先级,也有可能在操作系统进行调度的时候映射到了同一个优先级。通常情况下,不要去修改线程的优先级,一旦修改程序的行为就会与平台相关,并且会导致饥饿问题的产生。在程序中使用的Thread.yield或者Thread.sleep表明该程序试图客服优先级调整问题,让优先级更低的线程拥有被CPU调度的机会。

活锁指的是线程不断重复执行相同的操作,但每次操作的结果都是失败的。尽管这个问题不会阻塞线程,但是程序也无法继续执行。活锁通常发生在处理事务消息的应用程序中,如果不能成功处理这个事务那么事务将回滚整个操作。解决活锁的办法是在每次重复执行的时候引入随机机制,这样由于出现的可能性不同使得程序可以继续执行其他的任务。

Java中活锁和死锁有什么区别?

活锁和死锁类似,不同之处在于处于活锁的线程或进程的状态是不断改变的,活锁可以认为是一种特殊的饥饿。一个现实的活锁例子是两个人在狭小的走廊碰到,两个人都试着避让对方好让彼此通过,但是因为避让的方向都一样导致最后谁都不能通过走廊。简单的说就是,活锁和死锁的主要区别是前者进程的状态可以改变但是却不能继续执行。

怎么检测一个线程是否拥有锁?

在java.lang.Thread中有一个方法叫holdsLock(),它返回true如果当且仅当当前线程拥有某个具体对象的锁。

死锁产生的必要条件

产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生。

  • 互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。

  • 不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。

  • 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。

  • 循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, …, pn},其中Pi等 待的资源被P(i+1)占有(i=0, 1, …, n-1),Pn等待的资源被P0占有。

如何避免死锁

加锁顺序(线程按照一定的顺序加锁,避免嵌套封锁)

加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)

Java中如何获取到线程dump文件

死循环、死锁、阻塞、页面打开慢等问题,打线程dump是最好的解决问题的途径。所谓线程dump也就是线程堆栈,获取到线程堆栈有两步:

  • 获取到线程的pid,可以通过使用jps命令,在Linux环境下还可以使用ps -ef | grep java

  • 打印线程堆栈,可以通过使用jstack pid命令,在Linux环境下还可以使用kill -3 pid

Thread类提供了一个getStackTrace()方法也可以用于获取线程堆栈。这是一个实例方法,因此此方法是和具体线程实例绑定的,每次获取获取到的是具体某个线程当前运行的堆栈

死锁检测
Jconsole

Jconsole是JDK自带的图形化界面工具,使用JDK的工具JConsole,可以通过打开cmd然后输入jconsole打开

线程相关的模型

  • 启动一个os的用户线程,然后实际的任何操作都直接对应该用户线程,这就是1:1,这样做之后,调度就由os负责,jvm就不管了,hotspot等主流jvm基本上都是这种做法

  • 启动一个虚拟线程,然后执行的时候,交给os上的一个用户线程去执行,这样做的好处就是jvm可以自己实现调度,而且可以控制虚拟线程的大小,这就是n:m或者1:m,看具体的实现,而如果将线程虚拟化之后,调度就可以由jvm来实现了,做成1:m还是n:m完全看jvm调度的实现,这就是协程,go其实是这种做法,早期的solaris上的hotspot也是这种做法,后来改了,改成选择1的做法,这样在不同os上hotspot的实现就统一了

volatile类型修饰符

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;
    }
}
两个重要的实现模型 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是能回到零态的。

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。

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的无锁操作保证了共享变量的线程安全性和原子性。

线程池和并行处理

线程是稀缺资源,如果被无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,合理的
使用线程池对线程进行统一分配、调优和监控

有以下好处:
  • 降低资源消耗;
  • 2高响应速度;
  • 提高线程的可管理性。

Java1.5中引入的Executor框架把任务的提交和执行进行解耦,只需要定义好任务,然后提交给线
程池,而不用关心该任务是如何执行、被哪个线程执行,以及什么时候执行。

  • 线程池可以节约系统资源,包括线程、内存资源等,这样可以避免创建过多的线程导致线程资源匮乏、系统频繁进行上下文切换以及内存溢出等问题,因为线程池中的每一个线程可能会轮询地执行多个任务
  • 线程池可以节省重新创建线程的时间,进而提高响应速度。

ExecutorService是Java中对线程池定义的一个接口,它java.util.concurrent包中。Java API对ExecutorService接口的实现有两个(ThreadPoolExecutor和ScheduledThreadPoolExecutor),所以这两个即是Java线程池具体实现类。除此之外,ExecutorService还继承了Executor接口(注意区分Executor接口和Executors工厂类),这个接口只有一个execute()方法

Executors只是一个工厂类,它所有的方法返回的都是ThreadPoolExecutor、ScheduledThreadPoolExecutor这两个类的实例

Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一
个执行线程的工具。真正的线程池接口是ExecutorService。

线程池的作用:
  • 线程池作用就是限制系统中执行线程的数量
  • 根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程排队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池中有等待的工作线程,就可以开始运行了;否则进入等待队列。
为什么要用线程池:
  • 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务
  • 可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值