多线程初阶(二)
创始人
2024-03-18 01:47:36

目录

前言:

synchronized

解析

可重入和不可重入问题

解析

Java中线程安全类

死锁问题

解析

解决死锁问题

解析

内存可见性

解析

volatile关键字

解析

wait,notify

解析

小结:


前言:

    针对上篇文章讲到的线程安全问题,我们需要保证一些指令的原子性,在代码中可以通过加锁实现。针对于加锁,这个是有一定的开销的,还有可能导致死锁问题。因此在加锁的时候要慎重考虑。

synchronized

    1)修饰普通方法是把锁加到当前引用对象上。

    2)修饰静态方法是把锁加到类对象上。

    3)修饰代码块,可以指定加到哪个对象上。

注意:

    如果两个线程针对同一个对象加锁,就会出现锁竞争/冲突,一个线程能够获得到锁,先到的线程先获得锁。另一个线程则需要阻塞等待,直到上一个线程解锁(方法执行完),这个线程就可以回到就绪队列,才能够获取到锁。

    这里加锁虽然在方法上修饰,但实际加锁都是加到对象上面的。只有两个线程针对同一个对象加锁,才会出现锁冲突。如果针对不同对象加锁,则都会获取到锁,不会产生阻塞等待。

class Cumsum {public int a = 0;synchronized public void add() {a++;}
}
public class ThreadDemo15 {public static void main(String[] args) {Cumsum cumsum = new Cumsum();Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {for(int i = 0; i < 5000; i++) {cumsum.add();}}});Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {for(int i = 0; i < 5000; i++) {cumsum.add();}}});t1.start();t2.start();try {t1.join();t2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(cumsum.a);}
}

解析

    上篇讲述到这段代码,不加锁运行结果是有bug的,加了锁之后就正确了。不加锁出bug的原因在上片文章有讲述到,就是典型的线程安全问题。那么为什么加了锁之后代码就是正确呢?

    首先这里有两个线程t1和t2,main线程会阻塞等待,这两个线程并发执行。如果第一个线程首先获取到锁,这个锁是加到cumsum对象上的,当第二个线程在尝试获取到这个对象的锁,就会产生阻塞等待(对象是同一个)。直到上一个线程释放了这个对象的锁,这个线程才可以获取这个锁成功。获取锁成功后读取到a的值肯定是save过的,即就是正确的数值。

可重入和不可重入问题

    如果一个线程在一个方法里尝试针对同一个对象加锁两次,第一次加锁成功后,第二次在尝试对这个方法加锁,就会产生阻塞等待。即就会阻塞在这个方法里,第一次加的锁没有办法释放,程序就会一直阻塞在这里,产生死锁问题。

    针对给一个对象加锁两次产生的死锁问题,在java里很可能会写出这样的代码,因此synchronized就设计为可重入锁。对于这样死锁现象,可重入锁就不会产生阻塞等待,就会放过它,即代码可以正常执行。对于这样原因产生死锁问题,即就是不可重入锁。

class Add {public static int a = 0;synchronized public  void add() {a++;}synchronized public void add2() {synchronized (this) {a++;}}
}
public class ThreadDemo11 {public static void main(String[] args) throws InterruptedException {Add add2 = new Add();Thread thread1 = new Thread(new Runnable() {@Overridepublic void run() {for(int i = 0; i < 5000; i++) {add2.add();}}});Thread thread2 = new Thread(new Runnable() {@Overridepublic void run() {for(int i = 0; i < 5000; i++) {add2.add2();}}});//执行一次a++需要,load,add,save(都内存数据到寄存器,寄存器值++, 寄存器值写回内存)//由于两个线程是并发执行的,这些指令会随机组合(抢占式执行,随意调度),就会产生线程安全问题(不同的顺序,结果就会产生差异)//第二个线程读取的值是在第一个线程保存后读取的1,就会加2次(线程安全)//两次读取的值都为0,则最终只加1. 在一次线程切换中,另一个线程可能会执行多次三步流程(线程不安全)thread1.start();thread2.start();thread1.join();thread2.join();System.out.println(Add.a);}
}

解析

    如果第一个线程先执行,会给add2对象加锁成功。这个时候第二个线程在尝试对于这个对象加锁就会产生阻塞等待。当第一个线程释放锁之后,第二个线程就会针对于这个对象加锁成功,执行代码块的时候又会针对这个对象加锁第二次,由于synchronized是可重入锁,即在这里不会产生死锁问题,即代码就会正常执行。

Java中线程安全类

    Vector  HashTable  ConcurrentHashMap  StringBuffer 这些集合类中都内置了synchronized锁,多线程中线程是安全的。String类由于不可修改性,即天然就是线程安全的。

    AyyayList  LinkedList  HashMap  TreeMap  HashSet  TreeSet  StringBuilder 这些集合类中在线程安全问题需要手动加锁。

死锁问题

    上面说的在一个线程里给一个对象加两次锁,如果锁是不可重入锁,那么就会产生死锁。如果线程1先获取到锁A,被调度走,线程2先获取到锁B,再尝试获取锁A,就会阻塞等待,线程1调度回来获取锁B,也会阻塞等待。这个时候两个线程都在等对方释放锁,程序就会卡着不动了,产生了死锁问题。多个线程多把锁,如果每个线程都获取到锁,并且都在等对方释放锁,那么每个线程都会卡着不动,产生死锁问题。

    死锁问题的核心就是循环等待,想要解决死锁问题,那么就需要打破这种循环等待。

public class ThreadDemo12 {public static void main(String[] args) {Object o1 = new Object();Object o2 = new Object();Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {synchronized (o1) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (o2) {System.out.println("aaaa");}}}});Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {synchronized (o2) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (o1) {System.out.println("bbbbb");}}}});t1.start();t2.start();}
}

解析

    线程1先个o1对象加锁,然后sleep(),线程2给o2加锁,然后sleep()。接下来线程1尝试获取o2的锁,线程2尝试获取o1的锁,即两个线程都会阻塞等待,产生死锁。

解决死锁问题

    给锁编号,约定获取锁的顺序,从小到大或者从大到小。任意线程在加锁的时候都遵循这样的规则,就可以打破循环等待的问题,那么死锁问题也就解决了。

public class ThreadDemo12 {public static void main(String[] args) {Object o1 = new Object();Object o2 = new Object();Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {synchronized (o1) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (o2) {System.out.println("aaaa");}}}});Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {synchronized (o1) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (o2) {System.out.println("bbbbb");}}}});t1.start();t2.start();}
}

 解析

    线程1先获取o1再获取o2的锁,线程2也遵循这样的规则。调整了获取锁的顺序后,可以清楚看见代码正常执行了。

内存可见性

    如果针对于同一个变量即读又写,那么就会涉及内存可见性问题。实质上是编译器优化导致的bug。

    对于一个变量的修改,首先需要读取内存的数据到寄存器中,然后在寄存器中修改这个变量,最终写回到内存中。编译器优化可能会认为这个变量是不可变的,即在每次读数据的时候,只读取寄存器中的值,而不是修改后内存中的数据。这就导致读的数据就是修改之前的。

    一个线程针对于一个变量进行修改操作,同时另一个线程针对这个变量读取操作。此时读取到的值不一定是修改后的值,这个线程没有感知到这个变量的变化。

class Counter {//不能修饰局部变量//局部变量在不同的线程里占用不同的栈空间,意味这就是不同的变量(每个线程都有自己的栈空间)public int flag = 0;
}
public class ThreadDemo16 {public static void main(String[] args) {Counter counter = new Counter();Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {while (counter.flag == 0) {}System.out.println("aaaa");}});Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {Scanner scanner = new Scanner(System.in);counter.flag = scanner.nextInt();}});t1.start();t2.start();}
}

解析

    当t2线程修改掉flag值为2时,t1线程在while中死循环。因为t1线程读取到的值是没有修改的flag。这就是编译器优化导致认为flag是不可变的,即每次都是读取寄存器中的值,而不是t2线程修改后内存中的值。 

volatile关键字

    解决内存可见性,使用volatile关键字。声明这个变量是可变的,即告诉编译器在每次读取数据时,需要读取内存中的数据,而不是寄存器中的数据。这个时候编译器就不会随便优化了。

class Counter {//不能修饰局部变量//局部变量在不同的线程里占用不同的栈空间,意味这就是不同的变量(每个线程都有自己的栈空间)volatile public int flag = 0;
}
public class ThreadDemo16 {public static void main(String[] args) {Counter counter = new Counter();Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {while (counter.flag == 0) {}System.out.println("aaaa");}});Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {Scanner scanner = new Scanner(System.in);counter.flag = scanner.nextInt();}});t1.start();t2.start();}
}

 解析

    当给flag加上volatile关键字声明是可变的之后,while循环后的语句顺利打印了,说明t1线程读取到了t2线程修改后的值。因为voiatile修饰后,就会认为这个变量是可变的,即每次都会同步内存中的数据,即也就解决了内存可见性问题。

wait,notify

    由于线程的抢占式执行,随机调度。wait可以让线程主动放弃cpu的调度,进入阻塞队列。让其他线程可以被调度,可以控制线程的调度时机。当使用wait主动放弃cpu的时候,需要其他线程通过notify来唤醒该线程,进入就绪队列。wait和notify都是Object类下的方法。

    wait主动放弃cpu调度的机制,首先会释放锁,然后线程阻塞等待。那么在释放锁的时候需要有锁,即需要先得到锁然后在释放锁,进入阻塞队列。为什么这样设定呢?释放锁之后其他线程可以给这个对象加锁,就不会导致这个对象一直被加锁。wait不加任何参数就是死等。某个线程调用wait方法,就会进入阻塞队列(无论是哪个对象),此时就处在WAITING状态。

    notify通知线程唤醒机制。再唤醒线程也需要获得锁才可以唤醒线程。即首先需要获取锁,然后调用notify方法,唤醒线程进入就绪队列。notify只能唤醒同一个对象调用wait所阻塞的线程,如果有多个线程都在阻塞,则随机唤醒一个。notifyAll可以全部唤醒,一起进入就绪队列。这里的notify唤醒wait不会有任何异常。

public class ThreadDemo17 {public static void main(String[] args) throws InterruptedException {Object object = new Object();Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {System.out.println("t1前");synchronized (object) {try {object.wait(); //不加任何参数就是死等} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("t1后");}});//notify只能唤醒同一个对象上的等待线程//如果和wait对象不一致,则不生效//多个线程wait的时候,notify随机唤醒一个,notifyAll全部唤醒,一起竞争锁Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {System.out.println("t2前");synchronized (object) {object.notify();}System.out.println("t2后");}});t1.start();Thread.sleep(500);t2.start();}
}

解析

    需要保证先启动t1线程通过synchronized先给object加上锁,然后通过wait方法释放锁,使该线程阻塞等待。当t2线程执行的时候,也是通过synchronized先给object加上锁,然后object对象调用notify方法通知t1线程,进入就绪队列。可以看见代码的执行顺序也是这样。这里wait和notify方法的调用对象需要一致,才能明确具体通知哪一个线程。

小结:

    与大家共勉歌德的名言:志向和热爱是伟大行为的双翼。 

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...