☕☕ 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多线程并发的内容实在太过庞大,都可以单独写一本书了,作为刚刚接触多线程的新手来说掌握以下三种上锁方法就够用了:
synchronized()
对象锁synchronized
同步方法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.使用同步方法
⭐解决线程安全问题的第二种方法—使用同步方法:
- 把访问了共享数据的代码抽取出来,放到一个方法中。
- 在方法上添加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()
:释放锁。
🎯使用步骤:
- 在成员位置创建一个ReentrantLock对象。
- 在可能会出现线程安全问题的代码前调用Lock接口中的lock()方法获取锁。
- 在可能会出现线程安全问题的代码后调用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练习生~🏃🏻♂️🏀
🔸🔹文末已至,咱们下篇再见🔹🔸
┊且将新火试新茶,诗酒趁年华┊望江南·超然台作-苏轼
版权归原作者 Mymel_晗 所有, 如有侵权,请联系我们删除。