网站建设的硬件平台,电商运营有前途吗,微信网页手机登录入口官网,wordpress手机商城多线程#xff08;四#xff09;
在开始讲之前#xff0c;我们先来回顾回顾前三篇所讲过的内容~ 线程的概念 并发编程#xff0c;多进程#xff0c;比较重#xff0c;频繁创建销毁#xff0c;开销大 Thread的使用 创建线程 继承Thread实现Runnable继承Thread#xff…多线程四
在开始讲之前我们先来回顾回顾前三篇所讲过的内容~ 线程的概念 并发编程多进程比较重频繁创建销毁开销大 Thread的使用 创建线程 继承Thread实现Runnable继承Thread匿名内部类实现Runnable匿名内部类使用lambda Thread中的重要性启动线程start终止线程isInterrupted() interrupt()本质上是让线程快点执行完入口方法等待线程join a.join()让调用这个方法的线程等待a线程的结束获取线程引用休眠线程 线程状态方便快速判定当前程序执行的情况 NEWTERMINATEDRUNNABLETIMED_WAITINGWAITINGBLOCKED 线程安全 演示线程不安全的例子两个线程自增5w次 原因 操作系统对于线程的调度是随机的多个线程同时修改同一个量修改操作不是原子性的内存可见性指令重排序 解决加锁 synchronized synchronized修饰的是一个代码块 同时指定一个锁对象 进入代码块的时候对该对象进行加锁 出了代码块的时候对该对象进行解锁 锁对象 锁对象到底用哪个对象是无所谓的对象是谁不重要重要的是两线程加锁的对象是否是同一个对象 这里的意义/规则有且只有一个 当两个线程同时尝试对一个对象加锁此时就会出现“锁冲突”/“锁竞争”一旦竞争出现一个线程能够拿到锁继续执行代码一个线程拿不到锁就只能阻塞等待等待前一个线程释放锁之后他才有机会拿到锁继续执行~ 这样的规则本质上就是把“并发执行” “串行执行”这样就不会出现“穿插”的情况了。 文章目录 多线程四synchronized 关键字互斥刷新内存可重入 死锁死锁的成因 synchronized 关键字
互斥
续上文最后synchronized除了修饰代码块之外还可以修饰一个实例方法或者一个静态方法
class Counter{public int count;synchronized public void increase(){count;}public void increase2(){synchronized (this) {count;}}synchronized public static void increase3(){}public static void increase4(){synchronized (Counter.class){}}
}
// synchtonized 使用方法
public class Demo14 {public static void main(String[] args) throws InterruptedException {Counter counter new Counter();Thread t1 new Thread(()-{for (int i 0; i 50000; i) {counter.increase();}});Thread t2 new Thread(()-{for (int i 0; i 50000; i) {counter.increase();}});t1.start();t2.start();t1.join();t2.join();System.out.println(counter.count);}
}synchronized用的锁是存在Java对象头里的。
何为对象头呢
Java的一个对象对应的内存空间中除了你自己定义的一些属性之外还有一些自带的属性 在对象头中其中就会有属性表示当前对象是否已经加锁了 刷新内存
synchronized的工作过程 获得互斥锁 从主内存拷贝变量的最新副本到工作的内存 执行代码 将更改后的共享变量的值刷新到主内存 释放互斥锁
但是目前刷新内存这一块知识各种说法都有目前也难以通过实例验证pass~ 可重入
synchronized重要的特性可重入的
所谓的可重入锁指的就是一个线程连续针对一把锁加锁两次不会出现死锁。满足这个需求就是“可重入锁”反之就是“不可重入锁”。
下面见图 上述的现象很明显就是一个bug,但是我们在日常开发中又难以避免出现上述的代码~例如下面这样的案例
public class Demo15 {private static Object locker new Object();public static void func1(){synchronized (locker){func2();}}public static void func2(){func3();}public static void func3(){func4();}public static void func4(){synchronized (locker){}}public static void main(String[] args) {}
}要解决死锁问题我们可以将synchronized设计成可重入锁就可以有效解决上述的死锁问题~
就是让锁记录一下是哪个线程给它锁住的后续再加锁的时候如果加锁线程就是持有锁的线程就直接加锁成功~
用一个例子来理解
你向一个哥们表白我爱你成功了他接受你了也就是你对他加锁成功了同时他也会记得你就是她的男朋友~
过了几天你又对他说宝贝我爱你这时候的那个哥们当然也不会拒绝反而会更加基情~
不过要是换成别人结果肯定就是不一样的排除绿你的情况~ 这里提出个问题
synchronizedlocker{synchronizedlocker{........................}②
}①在上述代码中synchronized是可重入锁没有因为第二次加锁而死锁但是当代码执行到 }②此时锁是否应该释放
**不能**因为如果释放了锁很可能就会导致②和①之间的一些代码逻辑无法执行也就起不到锁保护代码的作用了~
进一步如果上述的锁有n层释放时机该怎么判断
无论此处有多少层都是要在最外层才能释放锁~~ 引用计数 锁对象中不光要记录谁拿到了锁还要记录锁被加了几次 每加锁一次计数器就1. 每解锁一次计数器就·1. 出了最后一个大括号恰好就是减成0了才真正释放锁 死锁
那么上面我们讲解了死锁的一种情况一个线程针对一把锁加锁两次。
接下来下面我们继续介绍死锁的情况~ 一个线程针对一把锁加锁两次如果是不可重入锁就会死锁~ synchronized不会出现但是隔壁C的std::mutex就是不可重入锁就会出现死锁 两个线程t1、t2两把锁A、B此时无论是不是不可重入锁都会死锁 举个例子钥匙锁车里车钥匙锁家里~ t1获取锁At2获取锁Bt1尝试获取B,t2尝试获取A 实例代码 // 死锁
public class Demo16 {private static Object locker1 new Object();private static Object locker2 new Object();//此处的sleep很重要要确保 t1 和 t2 都分别拿到一把锁之后再进行后续动作public static void main(String[] args) {Thread t1 new Thread(()-{synchronized (locker1){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker2){System.out.println(t1 加锁成功);}}});Thread t2 new Thread(()-{synchronized (locker2){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker1){System.out.println(t2 加锁成功);}}});t1.start();t2.start();}
}死锁现象出现 我们可以在jconsole.exe中看看线程情况~ 同时也要注意死锁代码中 两个synchronized是嵌套关系不是并列关系. 嵌套关系说明是在占用一把锁的前提下获取另一把锁.则是可能出现死锁 并列关系则是先释放前面的锁再获取下一把锁.不会死锁的 N个线程M把锁相当于2的扩充 此时这个情况更加容易出现死锁了。 下面给出一个经典例子哲学家就餐问题 死锁是属于比较严重的bug会直接导致线程卡住也就无法执行后续的工作了~ 那么我们应该怎么避免死锁
死锁的成因
那么首先我们要了解死锁的成因 互斥使用。锁的基本特性 当线程持有一把锁之后另一个线程也想获取到锁那么就需要阻塞等待、 不可抢占。锁的基本特性 当锁已经被 线程 1 拿到之后线程 2 只能等 线程 1 主动释放不可以强行抢过来 请求保持。代码结构 一个线程尝试获取多把锁。先拿到 锁1 之后再尝试获取 锁2 获取的时候 锁1 不会被释放 这种也就是典型的吃着碗里的看着锅里的 public class Demo16 {private static Object locker1 new Object();private static Object locker2 new Object();//此处的sleep很重要要确保 t1 和 t2 都分别拿到一把锁之后再进行后续动作public static void main(String[] args) {Thread t1 new Thread(()-{synchronized (locker1){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker2){System.out.println(t1 加锁成功);}}});Thread t2 new Thread(()-{synchronized (locker2){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker1){System.out.println(t2 加锁成功);}}});t1.start();t2.start();}
}循环等待 / 环路等待代码结构 等待的依赖关系形成环了~ 也即是上面那个例子钥匙锁车里车钥匙锁家里
实际上要想出现死锁也不是个容易事情 因为得把上面4条都占了. (不幸的是1和2是锁本身的特性只要代码中把3和4占了死锁就容易出现了)
所以说解决死锁核心就是破坏上述必要条件死锁就形成不了~
针对上述的四种成因1和 2是破坏不了的因为synchronized自带特性我们是无法干预 滴~
对于3来说就是调整代码结构避免编写“锁嵌套”逻辑
对于4来说可以约定加锁的顺序就可以避免循环等待 所以针对上面的哲学家就餐问题我们可以采取针对锁进行编号
比如说约定加多一把锁的时候先加编号小的锁后加编号大的锁所有线程都要遵守这个规则 这样的话循环等待就会被解除死锁也不会出现了~
回到上述我们讲的synchronized关键字 在使用规则上并不复杂只要抓住一个原则两个线程针对同一个对象加锁就会产生锁竞争. 但是在底层原理上synchronized还有不少值得讨论的地方.接下来会展开讲讲~ 至此多线程四讲解到这接下来会持续更新敬请期待~