0


『Java练习生的自我修养』java-se进阶² • 并发与多线程

header

☕☕ Java进阶攻坚克难,持续更新,一网打尽IO、注解、多线程…等

java-se

进阶内容。


🍑前言:

多线程虽然提高了程序的执行效率,但随之而来的是线程安全问题:当多个线程访问或操作同一个资源时,就会产生意想不到的错误。

🍦比如执行下面的代码块:

publicclassDemo{publicstaticint x =0;publicstaticvoidmain(String[] args){newThread(()-> x++).start();newThread(()-> x++).start();System.out.println("x = "+ x);}}

线程安全

👀同时开启两个线程,每个线程都对同一个

x

进行自增操作,直观感觉输出结果是

x = 2

,然而实际的输出结果却是

x = 1

。这是由于自增这条代码不是原子性操作,简单理解就是两个线程同时读取了

x = 0

,在每个线程内部进行了一次自增操作,两个线程执行完

x

的值都是

1

,再将

1

写回内存,结果就相当于

x

只自增了一次,同我们的预期相反,这就是所谓的线程不安全。

输出


🍋并发时的线程安全

👉🏻再来看一个卖票的例子:售票站有

100

张票,开放三个窗口进行售票操作。用代码模拟就是有一个初始值为

100

的变量

ticket

,同时开启三个线程对

ticket

执行自减操作,直到

ticket

减到

0

为止。

publicclassTicketSalesimplementsRunnable{publicint ticket =100;@Overridepublicvoidrun(){while(true){if(ticket >0){try{//                    为了让结果出现的错误更明显,设置成10ms卖一张票Thread.sleep(10);}catch(InterruptedException e){
                    e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"卖出了第"+(101- ticket--)+"张票");}}}publicstaticvoidmain(String[] args){TicketSales ts =newTicketSales();newThread(ts).start();newThread(ts).start();newThread(ts).start();}}

👉🏻以上代码不对线程进行任何限制,买票的结果如下:

运行结果

👉🏻可以看到三个线程不仅会卖同一张票,甚至在最后还卖出了第101张本来不存在的票!

线程安全

线程安全

🎈为了解决线程安全问题,就必须对访问同一资源的线程做出一定限制,在Java中使用锁机制来实现这一点。


🍓Java中的锁机制

⛅⛅⛅
🔒锁机制是线程同步技术的一种。既然线程的并发执行可能会导致线程不安全,那么不妨将线程的并发执行改成按顺序执行,也就是对线程中可能访问同一资源的代码片段上锁,使其在一段时间内只允许一个线程处于运行状态,而其他线程必须等待得到锁的线程执行完毕,释放出锁以后才能继续执行,通过锁机制实现多线程的同步。
⛅⛅⛅

锁机制

Java多线程并发的内容实在太过庞大,都可以单独写一本书了,作为刚刚接触多线程的新手来说掌握以下三种上锁方法就够用了:

  1. synchronized()对象锁
  2. synchronized同步方法
  3. Lock

🐋1.使用锁对象

锁对象又叫对象锁、同步锁或者叫对象监视器。通过

synchronized(obj){代码段}

声明一个锁对象,使用

obj

对象作为锁,多个线程并发执行时,遇到

synchronized代码块

会一起争夺锁,谁抢到了谁就获得cpu执行权,执行代码块中的内容,其余线程此时进入阻塞状态;待得到锁的线程执行完代码段释放锁后,其余线程会继续争夺锁,谁抢到谁获得cpu执行权…

🍦下面我们通过JOL对象解析工具来看一下对象被当成锁的前后有什么变化:

importorg.openjdk.jol.info.ClassLayout;publicclassLockTest{publicstaticvoidmain(String[] args){//        使用Object对象作为锁Object o =newObject();//        在没声明锁时o对象的头部信息System.out.println(ClassLayout.parseInstance(o).toPrintable());synchronized(o){System.out.println("声明了一个对象锁后:");System.out.println(ClassLayout.parseInstance(o).toPrintable());}}}

👀观察对象的头部信息:

对象头部信息

可以发现

synchronized()

是如何将对象当成一把锁的:改变对象头部信息的数值标记。

⭐对象锁的特点:

  • 使用一个对象作为锁,锁对象可以任意,一般使用Object o就可以。
  • 访问同一资源的多线程必须使用同一个锁对象。
  • 作用:只让一个线程在同步代码块中执行。
  • synchronized声明的是一个重量级锁,或者叫悲观锁,只有得到锁线程才能运行,其余线程都被阻塞。

🍦通过锁对象改造卖票案例:

publicclassTicketSales_SolutionimplementsRunnable{publicint ticket =100;//    创建一个锁对象Object o =newObject();@Overridepublicvoidrun(){while(true){//            同步代码块synchronized(o){if(ticket >0){try{//                    为了让结果出现的错误更明显,设置成10ms卖一张票Thread.sleep(10);}catch(InterruptedException e){
                        e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"卖出了第"+(101- ticket--)+"张票");}}}}publicstaticvoidmain(String[] args){TicketSales_Solution ts =newTicketSales_Solution();newThread(ts).start();newThread(ts).start();newThread(ts).start();}}

👀通过锁对象

synchronized

同步代码块可以解决线程同步问题,卖票案例成功得到我们想要的效果。

运行结果

🐄2.使用同步方法

⭐解决线程安全问题的第二种方法—使用同步方法:

  1. 把访问了共享数据的代码抽取出来,放到一个方法中。
  2. 在方法上添加synchronized修饰符。

⭐定义方法的格式:

修饰符 synchronized 返回值类型 方法名 (参数列表) {
    可能会出现线程安全问题的代码(访问共享数据)
}

🍦通过同步方法改造卖票案例:

publicclassTicketSales_SolutionimplementsRunnable{publicint ticket =100;//    将卖票的代码抽取出来publicsynchronizedvoidpayTickets(){if(ticket >0){try{//                    为了让结果出现的错误更明显,设置成10ms卖一张票Thread.sleep(10);}catch(InterruptedException e){
                e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"卖出了第"+(101- ticket--)+"张票");}}@Overridepublicvoidrun(){while(true){payTickets();}}publicstaticvoidmain(String[] args){TicketSales_Solution ts =newTicketSales_Solution();newThread(ts).start();newThread(ts).start();newThread(ts).start();}}

👀同样可以得到想要的结果:

运行结果

【注1】 既然同步方法也使用了synchronized关键字,肯定也需要一个对象作为锁,那么问题来了,充当锁的对象是谁?

  • 👩🏻‍🏫答:同步方法的锁对象是实现类对象,即当前对象,也就是我们常说的this
  • 👩🏻‍🏫答:也就是说我们抽取出来的代码还有一个等价写法—使用锁对象:// 将卖票的代码抽取出来publicvoidpayTickets(){// 使用锁对象,与同步方法等价的写法synchronized(this){if(ticket >0){try{// 为了让结果出现的错误更明显,设置成10ms卖一张票Thread.sleep(10);}catch(InterruptedException e){ e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"卖出了第"+(101- ticket--)+"张票");}}}

【注2】 同步方法还可以在前面加上关键字

static

使其成为静态同步方法:

//  静态方法只能访问静态变量,这里注意要用static修饰publicstaticint ticket =100;//    将卖票的代码抽取出来,并声明为静态方法publicstaticsynchronizedvoidpayTickets(){if(ticket >0){try{//            为了让结果出现的错误更明显,设置成10ms卖一张票Thread.sleep(10);}catch(InterruptedException e){
            e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"卖出了第"+(101- ticket--)+"张票");}}

这里问题又来了,this是创建对象之后产生的,静态方法优先于对象,this不能当成对象锁,那么静态同步方法中谁来充当锁?

  • 👩🏻‍🏫答:静态方法的锁对象是本类的class属性。

🦮3.使用Lock锁

Lock

锁是JDK1.5之后的新增特性,相比于传统的

synchronized

锁,

Lock

锁同时提供了

lock()

unlock()

方法,使用起来更加灵活。

🎯解决线程安全问题的第三种方式—Lock锁:

  • Lock接口在java.util.concurrent.locks包下,其实现类为java.util.concurrent.locks.Reentrantlock
  • Lock实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作。
  • Lock接口中的方法: - void lock():获取锁。- void unlock():释放锁。

🎯使用步骤:

  1. 在成员位置创建一个ReentrantLock对象。
  2. 在可能会出现线程安全问题的代码前调用Lock接口中的lock()方法获取锁。
  3. 在可能会出现线程安全问题的代码后调用Lock接口中的lock()方法获取锁。

🍦使用Lock锁改造卖票案例:

importjava.util.concurrent.locks.Lock;importjava.util.concurrent.locks.ReentrantLock;publicclassTicketSales_SolutionimplementsRunnable{publicint ticket =100;//    在成员位置创建一个ReentrantLock对象Lock lock =newReentrantLock();@Overridepublicvoidrun(){while(true){//            在可能会出现线程安全问题的代码前调用Lock接口中的lock()方法获取锁
            lock.lock();if(ticket >0){try{//                    为了让结果出现的错误更明显,设置成10ms卖一张票Thread.sleep(10);}catch(InterruptedException e){
                    e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"卖出了第"+(101- ticket--)+"张票");}//            在可能会出现线程安全问题的代码后调用Lock接口中的lock()方法获取锁
            lock.unlock();}}publicstaticvoidmain(String[] args){TicketSales_Solution ts =newTicketSales_Solution();newThread(ts).start();newThread(ts).start();newThread(ts).start();}}

👀依然可以的到我们想要的结果:

运行结果

💙🧡💙🧡💙🧡💙🧡💙🧡💙

🤍💬下篇预告:线程的等待与唤醒🤍

💛💚💛💚💛💚💛💚💛💚💛

🍍🍍🍍
创作不易,如果觉得本文对你有所帮助,欢迎点赞、关注、收藏。🙇🏻‍♀️
🍉🍉🍉
@作者:Mymel_晗,计算机专业练习时长两年半的Java练习生~🏃🏻‍♂️🏀

🔸🔹文末已至,咱们下篇再见🔹🔸

┊且将新火试新茶,诗酒趁年华┊望江南·超然台作-苏轼
footer


本文转载自: https://blog.csdn.net/m0_51338272/article/details/124523535
版权归原作者 Mymel_晗 所有, 如有侵权,请联系我们删除。

“『Java练习生的自我修养』java-se进阶² • 并发与多线程”的评论:

还没有评论