多线程及线程的同步与锁死

本文深入探讨Java多线程的实现方式,包括继承Thread、实现Runnable或Callable接口,以及使用线程池。讲解了用户线程与守护线程的区别,多线程的常用方法如sleep、yield和join,以及synchronized关键字的底层实现机制。同时,分析了线程同步与等待队列的工作原理,以及synchronized与ReentrantLock的异同。

Java中的多线程实现方式:

  • 继承Thread
  • 实现Runnable、Callable
  • 线程池(推荐)

Java中线程分为俩类:

  1. 用户线程:用户创建的默认都是用户线程,包括主线程。可以使用setDeamn()方法将用户线程置为守护线程。
  2. 守护线程:只有当当前的JVM进程中最后一个用户线程终止,守护线程会随着JVM一起终止。
    GC就是一个典型的守护线程

多线程的常用方法

  • sleep()线程休眠,状态由运行----->>阻塞,不会释放锁,立即交出CPU
  • yield()线程让步运行----->>就绪(唯一一个),不会释放锁,交出CPU时间不确定,由系统调度,会让拥有相同优先及的线程获取CPU的机会
  • join()当前线程等待另一线程执行完毕后,在恢复执行,状态由运行----->>阻塞(谁调用,谁阻塞)会释放锁
  • wait与notify(等待唤醒机制):要想用这俩,必须在同步方法或同步代码块中使用(及要有synchronized)
    二者都会释放锁
    Wait :运行----->>阻塞
    notify:阻塞----->>就绪

线程状态可用下图来简单描述
在这里插入图片描述
线程的同步与等待队列
Object及其子类都有俩个队列:

  1. 同步队列:获取该对象锁失败的线程进入同步队列(简单说一下锁,为了安全性,会给某个对象或代码块“上锁”,当线程去使用这个对象或代码块时,先获"进入"的线程会把这个对象“锁住”,此时其他线程就无法使用该对象,只有等他用完后,释放锁,其他对象才可以继续去"抢"这个锁,而没“抢”到的,就是获取失败的,就会进入同步队列)
  2. 等待队列:调用wait()的线程进入等待队列(等待被notify,被notify后会进入到同步队列的末尾)

同步

需要考虑,保护的对象是谁 锁是谁
同步是为实现线程安全,线程安全的三个特性
(1)原子性:一组操作要么同时发生,要么一个都不发生
基本数据类型的读写操作都属于原子性操作

(2)可见性:某一线程对于变量的修改对于其他线程而言是立即可见的
synchronized(lock)、volatile、final这三个可保证可见性。

(3)有序性:在单线程场景下,代码执行顺序就是代码书写顺序。
多线程场景下,所有代码都是乱序的。
synchronized关键字

使用synchronized解决同步问题
同步代码块
synchronized(对象)
-任意类的对象
-类.class

同步方法
-修饰类成员方法 锁的是当前对象this
-修饰类方法(static) 锁的是当前类的反射对象

synchronized底层实现:对象Monitor机制

任意Object及其子类对象内部在JVM中都附加Monitor.获取一个对象的锁,
实际上就是获取该对象Monitor(计数器).

当一个线程尝试获取对象Monitor时,
I.若此时Monitor值为0,该对象未被任何线程获取,当前线程获取Monitor,将持有线程
置为当前线程,Monitor值+1;
II.若此时Monitor值不为0,此时该Monitor已被线程持有

  • a.若当前线程恰好是持有线程,Monitor值再次+1,当前线程继续进入同步块(锁的可重入)
  • b.若持有线程不是当前线程,当前线程进入同步队列等待Monitor值减为0

加锁:monitorenter + 1
解锁:monitorexit - 1

任意时刻只有当Monitor值为0表示无锁状态。

synchronized的优化
1.CAS:(Compare And Swap) 无锁保证线程安全

CAS(O,V,N)包含以下三个值:
V:主内存存放的实际变量值
O:当前线程认为的变量值
N:希望将变量替换的值
添加概念:
JVM内存模型(JMM):并发程序
描述共享变量(类成员变量、静态变量)如何存储
工作内存:变量在线程中的操作(读写)必须在工作内存中进行,
工作内存中保存了所有变量的副本

主内存:所有变量必须在主内存中存储
主内存 ticket = 0;

线程1 ticket = 0; cas(1,1,0) O==V,认为此时没有线程修改主内存的值,0 -> 主内存
线程2 ticket = 0; cas(1,0,0) O != V,
认为此时已经有别的线程修改了主内存的值,修改失败,返回主内存的最新值
线程3 ticket = 1;

当O == V时,认为此时没有线程修改变量值,成功将N值替换回主内存
当O != V时,此时已有线程修改变量值,替换失败,返回主内存的最新值再次重试。

ABA问题:
添加版本号
num = 0;
线程1 cas(0,0,1) 1-> 主内存 num.1

线程2 cas(1,1,0) 0 -> 主内存 num.2
线程3 cas(0,0,5) 5 -> 主内存 num.0

偏向锁 -> 轻量级锁 -> 重量级锁(JDK1.6之前 synchronized就是重量级锁)

重量级锁(悲观锁):获取Monitor的失败的线程进入同步队列,状态置为阻塞态

偏向锁(乐观锁):
认为只有一个线程在来回进入同步块,
直接将加锁与解锁的过程都省略,每次进入同步块之前只是判断一下同步块线程是否是当前线程

轻量级锁:
不同时刻有不同的线程进入同步块
每次线程在进入同步块时都需要加锁与解锁

重量级锁:
同一时刻有不同线程在进入同步块随着竞争的不断升级,锁也会不断升级,锁不会降级。

自适应自旋:重量级锁的优化
获取锁失败的线程不会立即阻塞,而是在CPU空跑一段无用代码,若在此时间段成功获取锁,
则下次再获取锁失败时,空跑时间适当延长;否则下次空跑时间缩短。

锁粗化;将多次连续的加减锁过程粗化为一次大的加锁与解锁过程,减少无用的加减锁过程,提高效率.
**锁消除:**当变量为线程私有变量时,将原先方法上的synchronized消除掉。

死锁
产生死锁的四个条件:
1.互斥:资源x在任意一个时刻只能被一个线程持有
2.占有且等待:线程1占有资源x的同时等待资源y,并不释放x
3.不可抢占:资源x一旦被线程1占有后,其他线程不能抢占x
4.循环等待:线程1持有x,等待y;线程2持有y,等待x;
死锁的产生原因:以上四个条件同时满足

synchronized如何解锁死锁?
Synchronized产生死锁基本无法解决。因此产生了Lock体系(JDK1.5)

a.使用格式
try {
// 同步代码块
// 显式加锁
lock.lock();
}catch (Exception e) {

}finally {
// 显式解锁
lock.unlock();
}

b.常用方法
lock() : 加锁,语义与synchronized完全一致
unlock() : 解锁

void lockInterruptibly() throws InterruptedException:响应中断加锁
boolean tryLock():非阻塞式获取锁.获取锁成功返回true,进入同步块;获取锁失败返回false,线程继续执行其他代码
boolean tryLock(long time, TimeUnit unit) throws InterruptedException: 支持超时

synchronized与ReentrantLock的关系与区别:
1.都属于独占锁(任意一个时刻,只有一个线程能获取到资源)的实现
【添加共享锁的概念:任意一个时刻,有多个线程可以获取资源。
读写锁:
读锁:共享锁,读读异步,意义:写线程开始工作时,会阻塞所有读线程,读写要搭配使用
写锁:独占锁,读写互斥,写写互斥
读写锁的应用:线程安全的缓存(缓存其实就是map)
Hashmap + ReentrantReadWriteLock = 高效的线程安全的缓存】
2.都支持可重入锁
区别:
1.synchronized是关键字,jvm层面实现
reentrantlock是Java语言层面的“管程”
2.reentrantlock具备一些synchronized不具备的功能:响应中断,非阻塞式获取锁,支持超时获取锁
还支持公平锁,支持获取多个等待队列(condition)。
所谓公平锁就是等待时间最长的线程最先获取到锁

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值