马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
x
目录
1.观察count++
原因总结
2.办理方案-synchronized关键字
(1)synchronized的特性
(2)怎样精确使用
语法格式
3.死锁
(1)造成死锁的情况
(2)死锁的四个必要条件
4.Java标准库中的线程安全类
5.volatile关键字
(1)内存可见性标题
原因
办理方案
(2)不办理原子性标题
6.wait和notify
(1)wait()
(2)notify()
(3)线程饿死标题
7.wait和sleep的对比(口试题)
1.观察count++
我们观察以下代码:
- public class Demo20 {
- private static int count = 0;
- public static void main(String[] args) throws InterruptedException {
- Thread t1 = new Thread(() ->{
- for (int i = 0; i < 50000; i++) {
- count++;
- }
- });
- Thread t2 = new Thread(() ->{
- for (int i = 0; i < 50000; i++) {
- count++;
- }
- });
- t1.start();
- t2.start();
- t1.join();
- t2.join();
- System.out.println(count);
- }
- }
复制代码 他的逻辑是将count在差别的线程下进行五万次++操作,抱负的结果是100000,但由于是并发实验,结果并不能到达预期,每次的结果都不雷同,因为多个线程并发实验,引起的bug
这样的bug称为“"线程安全标题"或者叫做"线程不安全"
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是 线程安全的。
我们从cpu的视角来观察count++操作,它是由3个指令的:
- 把内存中的数据读取到cpu寄存器里 load
- 把cpu寄存器里的数据+1 add
- 把寄存器的值写回内存 save
由于CPU是随即调度,抢占式先行所以在调度线程的时间不知道什么时间会切换线程
指令是cpu实验的最基本单位,要调度,至少把当前实验完,不会实验一半调度走,所以当针对一条指令的时间就不会出现安全性标题;但是由于count++是三个指令,大概会出现cpu 实验了此中的1个指令或者2个指令或者3个指令调度走的情况,这样就会出现线程安全标题产生bug。
无bug的情况:
有bug的情况(出现了覆盖的状态):
原因总结
- 线程在操作系统中是随即调度,抢占式实验的(根本原因)
- 多个线程同时修改同一个变量
- 修改操作不是“原子”的
- 内存可见性标题
- 指令重排序标题
原子性:原子是不可分割的最小单位,cpu视角不可分割的最小单位就是一条指令,cpu在进行调度切换线程的时间势必会确保实验完一条指令才能调度走再实验下一条下令,所以像count++, +=,-=之类的操作都不具备原子性 赋值操作a=b是具备原子性的
2.办理方案-synchronized关键字
针对原因一我们无法干预,操作系统内核,负责的工作,咱们作为应用层的程序员,无法干预
针对原因二取决于现实的需求.有的场景能这么改,有的场景不能这么改取决于现实的需求
在Java中这个方案不算很普适的方案.
针对原因三我们重点进行探讨,该操作不是原子的那怎么可以变成原子的呢
进行加锁操作,想象一个上厕所的场景,你对门进行了加锁,这样别人就不能进来,只有当你上完厕所出来才算解锁
留意:此处的加锁操作并非是将count++操作变成原子的,也没有干预到线程的调度,只是通过这种加锁的方式来保证一个线程在实验count++操作的过程中其他线程的count++不能插队进来
(1)synchronized的特性
- 互斥:synchronized会起到互斥效果,某个线程实验到某个对象的synchronized中时,其他线程如果也实验 到同一个对象synchronized就会壅闭等候.
进入synchronized修饰的代码块,相称于加锁
退出synchronized修饰的代码块,相称于解锁
synchronized用的锁是存在Java对象头里的。
- 可重入:针对一个线程一把锁.这个线程针对这把锁,一连加锁两次这种情况理论上应该死锁,但由于该特性不会造成死锁
在可重入锁的内部,包含了"线程持有者"和"计数器"两个信息.
如果某个线程加锁的时间,发现锁已经被人占用,但是恰恰占用的正是自己,那么仍旧可以继续获取到锁,并让计数器自增.
解锁的时间计数器递减为0的时间,才真正开释锁.(才能被别的线程获取到)
(2)怎样精确使用
synchronized不是函数而是关键字,括号内也不是参数,而是用来指定一个锁对象(可以指定任何对象),通过锁对象来进行后续的判定
{}里面的代码,就是要打包到一起的代码~~
{}还可以放任意的其他代码,包括调用别的方法等正当的java代码
进入代码块就会进行加锁,出代码块就会进行解锁
- public class Demo21 {
- private static int count = 0;
- private static Object locker = new Object();
- public static void main(String[] args) throws InterruptedException {
- Thread t1 = new Thread(() ->{
- for (int i = 0; i < 50000; i++) {
- synchronized (locker){
- count++;
- }
- }
- });
- Thread t2 = new Thread(() ->{
- for (int i = 0; i < 50000; i++) {
- synchronized (locker){
- count++;
- }
- }
- });
- t1.start();
- t2.start();
- t1.join();
- t2.join();
- System.out.println(count);
- }
复制代码 代码表明:t1,t2针对同一个对象locker进行加锁,t1先进行加锁,实验代码块中的代码,此时t2进行等候,t1实验完毕后,t2进行加锁再实验该线程下的代码
(这两者的++操作,不会穿插实验了,也就不会相互覆盖掉对方的结果了)
本质上是把随机的并发实验过程,强制变成了串行,从而办理了刚才的线程安全标题
上述操作能够精确实验的原因是,两个线程都加锁了,并且针对的是同一个对象加锁了
以下两种情况就不能精确实验
- 只有一个线程加锁
- public class Demo21 {
- private static int count = 0;
- private static Object locker1 = new Object();
- private static Object locker2 = new Object();
- public static void main(String[] args) throws InterruptedException {
- Thread t1 = new Thread(() ->{
- for (int i = 0; i < 50000; i++) {
- synchronized (locker1){
- count++;
- }
- }
- });
- Thread t2 = new Thread(() ->{
- for (int i = 0; i < 50000; i++) {
- synchronized (locker2){
- count++;
- }
- }
- });
-
- t1.start();
- t2.start();
- t1.join();
- t2.join();
- System.out.println(count);
- }
复制代码
- 多线程针对差别的对象加锁
- private static int count = 0;
- private static Object locker1 = new Object();
- private static Object locker2 = new Object();
- public static void main(String[] args) throws InterruptedException {
- Thread t1 = new Thread(() ->{
- for (int i = 0; i < 50000; i++) {
- synchronized (locker1){
- count++;
- }
- }
- });
- Thread t2 = new Thread(() ->{
- for (int i = 0; i < 50000; i++) {
- synchronized (locker2){
- count++;
- }
- }
- });
- t1.start();
- t2.start();
- t1.join();
- t2.join();
- System.out.println(count);
复制代码 以上情况为两个线程针对同一个对象加锁,当第一个线程解锁之后就会实验第二个线程进行加锁;如果是三个线程针对同一个对象加锁,当某个线程先加上锁,另外两个线程开始壅闭等候,此时这两个线程谁先拿到锁是无法预期的,但不存在线程安全标题
多个线程针对同一个对象加锁(大于2)
- public class Demo21 {
- private static int count = 0;
- private static Object locker1 = new Object();
- private static Object locker2 = new Object();
- public static void main(String[] args) throws InterruptedException {
- Thread t1 = new Thread(() ->{
- for (int i = 0; i < 50000; i++) {
- synchronized (locker1){
- count++;
- }
- }
- });
- Thread t2 = new Thread(() ->{
- for (int i = 0; i < 50000; i++) {
- synchronized (locker2){
- count++;
- }
- }
- });
- Thread t3 = new Thread(() ->{
- for (int i = 0; i < 50000; i++) {
- synchronized (locker2){
- count++;
- }
- }
- });
- t1.start();
- t2.start();
- t3.start();
- t1.join();
- t2.join();
- t3.join();
- System.out.println(count);
- }
复制代码
锁对象的作用:用来区分多个线程是否针对“同一个对象”加锁,
是同一个就会发生“壅闭”(锁竞争/锁冲突)
不是同一个对象就不会发生壅闭,两个线程仍旧是随即调度的并发实验
留意事项:
synchronized关键字本质上比join的串行实验,效率照旧要高的
join的串行化是针对线程与线程之间,而synchronized关键字是针对线程中的一小部分逻辑进行加锁来实现串行化
语法格式
修饰类对象
在编写Java代码,自己是.java文件,通过javac编译成.class文件,jvm运行的时间把.class文件加载到内存中进而形成对应的类对象
一个 java进程中一个类的类对象只有唯一一个
- private static int count = 0;
- public static void main(String[] args) throws InterruptedException {
- Thread t1 = new Thread(() ->{
- for (int i = 0; i < 50000; i++) {
- synchronized (Demo21.class){
- count++;
- }
- }
- });
- Thread t2 = new Thread(() ->{
- for (int i = 0; i < 50000; i++) {
- synchronized (Demo21.class){
- count++;
- }
- }
- });
- t1.start();
- t2.start();
- t1.join();
- t2.join();
- System.out.println(count);
- }
复制代码
修饰普通方法
- class Counter {
- public int count = 0;
- public synchronized void add(){
- count++;
- }
- }
- class Counter {
- public int count = 0;
- public void add(){
- synchronized (this) {
- count++;
- }
- }
- }
- public class Demo23 {
- public static void main(String[] args) throws InterruptedException {
- Counter counter = new Counter1();
-
- Thread t1 = new Thread(() -> {
- for (int i = 0; i < 50000; i++) {
- counter1.add();
- }
- });
- Thread t2 = new Thread(() -> {
- for (int i = 0; i < 50000; i++) {
- counter1.add();
- }
- });
- t1.start();
- t2.start();
- t1.join();
- t2.join();
- System.out.println(Counter.count);
- }
- }
复制代码
修饰静态方法
- class Counter {
- public static int count = 0;
- public synchronized static void add(){
- count++;
- }
-
- }
- public class Demo22 {
- public static int count = 0;
- public static void main(String[] args) throws InterruptedException {
- Thread t1 = new Thread(() ->{
- for (int i = 0; i < 50000; i++) {
- synchronized (Counter.class){
- count++;
- }
- }
- });
- Thread t2 = new Thread(() ->{
- for (int i = 0; i < 50000; i++) {
- synchronized (Counter.class){
- count++;
- }
- }
- });
- t1.start();
- t2.start();
- t1.join();
- t2.join();
- System.out.println(count);
- }
- }
复制代码
3.死锁
(1)造成死锁的情况
- 一个线程一把锁.这个线程针对这把锁,一连加锁两次 这个情况在代码实例中,并没有真的出现死锁,synchronized针对这个情况做了特殊处理synchronized是“可重入锁”
针对上述一个线程一连加锁两次的情况做了特殊处理,只有第一次加锁生效,之后的加锁不会生效直接放行
- class Counter1{
- public static int count = 0;
- public void add(){
- synchronized (this) {
- synchronized (this) {
- count++;
- }
- }
- }
- }
复制代码 那可重入锁是怎样判定是否用加锁的情况呢?
- 两个线程两把锁
t1获取锁A,t2获取锁B
t1获取锁B,t2获取锁A- public class Demo24 {
- private static Object locker1 = new Object();
- private static Object locker2 = new Object();
- public static void main(String[] args) throws InterruptedException {
- Thread t1 = new Thread(() ->{
- synchronized (locker1){
- System.out.println("t1加锁成功locker1");
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- synchronized (locker2){
- System.out.println("t1加锁成功locker2");
- }
- }
- });
- Thread t2 = new Thread(() ->{
- synchronized (locker2){
- System.out.println("t2加锁成功locker2");
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- synchronized (locker1){
- System.out.println("t2加锁成功locker1");
- }
- }
- });
- t1.start();
- t2.start();
- }
- }
复制代码
- N个线程M把锁
经典哲学家标题:有5个哲学家坐在一起吃饭,但只有5根筷子,哲学家就只做两件事变,一件事变为思索,另一件事变就是吃饭,当此中一个哲学家要吃饭时,就会拿起左右两边的筷子,那么此时如果左右相邻的哲学家也想吃饭时,就需要等候正在吃饭的哲学家吃完饭,放下筷子,才能继续吃,在通常情况下,整个系统可以很好的运转,但是当5个哲学家同时拿起左边的筷子时,就会出现死锁标题
(2)死锁的四个必要条件
- 锁是互斥的(锁的基本特性)
- 锁是不可被抢占的,线程1拿到锁A后,如果线程1不主动开释A,线程2就不能把锁A抢过来(锁的基本特性)
以上两点对于synchronized这样的锁,互斥和不可抢占都是基本特性,我们无法进行干预
- 请求和保持。线程1拿到锁A后,不开释A的条件下去拿锁B(代码结构)
避免出现锁的嵌套即可办理
- 循环等候/环路等候/循环依靠 多个线程获取锁的过程存在循环等候(代码结构)
给锁加编号,约定加锁顺序
如果在获取多把锁的时间,不要构成循环等候,就可以了~一~
假设代码按照请求和保持的方式,获取到N个锁,怎样避免出现循环等候呢??一个简单有效的办法:给锁编号,1,2,3....N
约定所有的线程在加锁的时间,都必须按照肯定的顺序来加锁.(比如,必须先针对编号小的锁,加锁,后针对编号大的锁加锁)
- public class Demo24 {
- private static Object locker1 = new Object();
- private static Object locker2 = new Object();
- public static void main(String[] args) throws InterruptedException {
- Thread t1 = new Thread(() ->{
- synchronized (locker1){
- System.out.println("t1加锁成功locker1");
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- synchronized (locker2){
- System.out.println("t1加锁成功locker2");
- }
- }
- });
- Thread t2 = new Thread(() ->{
- synchronized (locker1){
- System.out.println("t2加锁成功locker1");
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- synchronized (locker2){
- System.out.println("t2加锁成功locker2");
- }
- }
- });
- t1.start();
- t2.start();
- }
- }
复制代码
4.Java标准库中的线程安全类
Java标准库中很多都是线程不安全的.这些类大概会涉及到多线程修改共享数据,又没有任何加锁措施.
ArrayList;LinkedList;HashMap;TreeMap;HashSet;TreeSet;StringBuilder
但是还有一些是线程安全的.使用了一些锁机制来控制.
Vector (不保举使用)
HashTable(不保举使用)
ConcurrentHashMap
StringBuffer
5.volatile关键字
(1)内存可见性标题
观察以下代码
- public class Demo25 {
- private static int n = 0;
- public static void main(String[] args) throws InterruptedException {
- Thread t1= new Thread(() ->{
- while (n == 0){
- //
- }
- });
- Thread t2= new Thread(() ->{
- Scanner scanner = new Scanner(System.in);
- System.out.println("请输入一个整数:");
- n = scanner.nextInt();
- });
- t1.start();
- Thread.sleep(2000);
- t2.start();
- }
- }
复制代码
原因
怎样进行优化导致出现内存可见性标题标?
此时JVM实验这个代码时发现每次循环过程中(1)操作的开销非常大,而且每次实验(1)操作它的结果都是一样的,并且JVM根本没意识到用户大概将来会修改n,于是JVM就做了一个大胆的操作直接将(1)操作给优化掉了,每次循环不会去读取内存中的数据,而是直接读取寄存器/cache中的数据(缓存的结果)
当JVM做出上述决定后此时意味着,循环的开销大幅度的低落,但是当用户修改n的时间返现内存中的n已经改变了,但是t1线程每次循环不会真的读内存,并没有感知到n的改变,也就是说对于线程t1来说n的改变是“不可见的”,这样就引起了内存可见性的标题
内存可见性标题本质上是编译器/JVM对代码进行优化出现的bug,如果代码是单线程,优化后的代码非常精确,但在多线程中大概会出现误判,这就导致了内存可见性的标题
办理方案
办理方案一:
在t1线程中添加sleep等候
和读内存相比,sleep相比之下就更慢了,足以等到你scanner输入之后t2线程修改后t1感知
- public class Demo25 {
- private static int n = 0;
- public static void main(String[] args) throws InterruptedException {
- Thread t1= new Thread(() ->{
- while (n == 0){
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- System.out.println("t1线程结束");
- });
- Thread t2= new Thread(() ->{
- Scanner scanner = new Scanner(System.in);
- System.out.println("请输入一个整数:");
- n = scanner.nextInt();
- });
- t1.start();
- t2.start();
- }
- }
复制代码 办理方案二:
添加volatile关键字,该关键字用来修饰一个变量,用来提示编译器这个变量是“易变”的,优化的条件是变量是频仍读取的,而且结果是固定的,此时编译器就会克制上述优化,以此来确保灭磁都实验从内存中从新读取数据
引入该变量后,编译器生成该代码时,就会给这个变量的读取操作附近生成一些特殊指令,称为“内存屏蔽”,后续JVM实验到此处时就不会进行优化
- public class Demo25 {
- private static volatile int n = 0;
- public static void main(String[] args) throws InterruptedException {
- Thread t1= new Thread(() ->{
- while (n == 0){
- //
- }
- System.out.println("t1线程结束");
- });
- Thread t2= new Thread(() ->{
- Scanner scanner = new Scanner(System.in);
- System.out.println("请输入一个整数:");
- n = scanner.nextInt();
- });
- t1.start();
- t2.start();
- }
- }
复制代码 (2)不办理原子性标题
- public class Demo26 {
- private static volatile int count = 0;
- public static void main(String[] args) throws InterruptedException {
- Thread t1 = new Thread(() -> {
- for (int i = 0; i < 50000; i++) {
- count++;
- }
- });
- Thread t2 = new Thread(() -> {
- for (int i = 0; i < 50000; i++) {
- count++;
- }
- });
- t1.start();
- t2.start();
- t1.join();
- t2.join();
- System.out.println(count);
- }
- }
复制代码
6.wait和notify
线程在操作系统上的调度是随机的,多个线程需要控制线程直接某个逻辑的先后顺序,此时就可以让后实验的逻辑使用wait,先实验的线程,完成某些逻辑之后,通过notify唤醒对方的wait
(1)wait()
作用:
使当前实验代码的线程进行等候(将线程放到等候队列中)
开释当前锁
满足肯定条件时被唤醒,重新尝试获取这个锁
竣事等候的条件:
其他线程调用该对象的notify方法.
wait等候时间超时(wait方法提供⼀个带有timeout参数的版本,来指定等候时间).
其他线程调用该等候线程的interrupted方法,导致wait抛出InterruptedException 异常.
- public class Demo27 {
- public static void main(String[] args) throws InterruptedException {
- Object object = new Object();
- System.out.println("wait之前");
- object.wait();
- System.out.println("wait之后");
- }
- }
复制代码
非法的监视器状态异常,这里的意思是在调用wait方法时,当前锁的状态是不精确的;很显着此处我们都没加锁又何谈解锁呢?wait方法会针对对象先进行解锁所以要使用synchronized关键字来上锁
加上锁之后由于没有notify解锁,所以会一直等候
wait在实验时会将进行解锁,壅闭等候(目标是为了收到关照)同时实验,这两个操作方法内部已经做好了
(2)notify()
notify方法是唤醒等候的线程.
- notify()也要在同步方法或同步块中调用,该方法是用来关照那些大概等候该对象的对象锁的别的线程,对其发出关照notify,并使它们重新获取该对象的对象锁
- 如果有多个线程等候,则有线程调度器随机挑选出一个呈 wait状态的线程。(并没有"先来后到")
- 在notify()方法后,当火线程不会立刻开释该对象锁,要等到实验notify()方法的线程将程序实验完,也就是退出同步代码块之后才会开释对象锁。
- public class Demo27 {
- private static Object locker1 = new Object();
- public static void main(String[] args) {
- Thread t1 = new Thread(() ->{
- System.out.println("wait之前");
- synchronized (locker1) {
- try {
- locker1.wait();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- System.out.println("wait之后");
- });
- Thread t2 = new Thread(() ->{
- System.out.println("notify之前");
- synchronized (locker1) {
- locker1.notify();
- }
- System.out.println("notify之后");
- });
- t1.start();
- t2.start();
- }
复制代码
(3)线程饿死标题
定义︰线程饿死是指一个或多个线程由于某种原因无法获取所需的资源或实验机会,导致它们无法继续正常实验,从而被壅闭在某个状态,不能完成其使命。这种情况通常是由于资源竞争或优先级设置不当导致的。
举例说明,第一个人(t1线程)去取钱并上了锁,但机器里没钱,第一个人可以先出来,可以反反复复收支,这就导致其他人只能干等着,无法获取到锁,此时就会产生线程饿死的情况
办理方案:
让第一个人拿到锁的同时进行判定,判定当前能否实验取钱的操作,能则正常实验,不能则主动开释锁,并且进行“壅闭等候”(调用wait实现),此时线程就就不会在后续参与锁的竞争,一直壅闭到取钱的条件具备,此时再由别的线程关照唤醒(notify实现)唤醒这个线程
7.wait和sleep的对比(口试题)
一个是用于线程之间的通信的,一个是让线程壅闭一段时间,
唯—的雷同点就是都可以让线程放弃实验一段时间.
1. wait需要搭配synchronized使用. sleep 不需要.
2. wait是Object的方法sleep是Thread的静态方法.
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! 更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |