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

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

header

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


🍑前言:

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

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

public class Demo {    public static int x = 0;    public static void main(String[] args) { new Thread(() -> x++).start(); new Thread(() -> x++).start(); System.out.println("x = " + x);    }}

线程安全

👀同时开启两个线程,每个线程都对同一个x进行自增操作,直观感觉输出结果是x = 2,然而实际的输出结果却是x = 1。这是由于自增这条代码不是原子性操作,简单理解就是两个线程同时读取了x = 0,在每个线程内部进行了一次自增操作,两个线程执行完x的值都是1,再将1写回内存,结果就相当于x只自增了一次,同我们的预期相反,这就是所谓的线程不安全。

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


🍋并发时的线程安全

👉🏻再来看一个卖票的例子:售票站有100张票,开放三个窗口进行售票操作。用代码模拟就是有一个初始值为100的变量ticket,同时开启三个线程对ticket执行自减操作,直到ticket减到0为止。

public class TicketSales implements Runnable{    public int ticket = 100;    @Override    public void run() { while (true) {     if (ticket > 0) {  try {//      为了让结果出现的错误更明显,设置成10ms卖一张票      Thread.sleep(10);  } catch (InterruptedException e) {      e.printStackTrace();  }  System.out.println(Thread.currentThread().getName() + "卖出了第" + (101 - ticket--) + "张票");     } }    }    public static void main(String[] args) { TicketSales ts = new TicketSales(); new Thread(ts).start(); new Thread(ts).start(); new Thread(ts).start();    }}

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

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

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

线程安全

线程安全

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


🍓Java中的锁机制

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

锁机制

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

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

🐋1.使用锁对象

锁对象又叫对象锁、同步锁或者叫对象监视器。通过synchronized(obj){代码段}声明一个锁对象,使用obj对象作为锁,多个线程并发执行时,遇到synchronized代码块会一起争夺锁,谁抢到了谁就获得cpu执行权,执行代码块中的内容,其余线程此时进入阻塞状态;待得到锁的线程执行完代码段释放锁后,其余线程会继续争夺锁,谁抢到谁获得cpu执行权…

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

import org.openjdk.jol.info.ClassLayout;public class LockTest {    public static void main(String[] args) {// 使用Object对象作为锁 Object o = new Object();// 在没声明锁时o对象的头部信息 System.out.println(ClassLayout.parseInstance(o).toPrintable()); synchronized (o) {     System.out.println("声明了一个对象锁后:");     System.out.println(ClassLayout.parseInstance(o).toPrintable()); }    }}

👀观察对象的头部信息:

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

可以发现synchronized()是如何将对象当成一把锁的:改变对象头部信息的数值标记。

⭐对象锁的特点:

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

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

public class TicketSales_Solution implements Runnable{    public int ticket = 100;//    创建一个锁对象    Object o = new Object();    @Override    public void run() { 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--) + "张票");  }     } }    }    public static void main(String[] args) { TicketSales_Solution ts = new TicketSales_Solution(); new Thread(ts).start(); new Thread(ts).start(); new Thread(ts).start();    }}

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

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

🐄2.使用同步方法

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

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

⭐定义方法的格式:

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

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

public class TicketSales_Solution implements Runnable {    public int ticket = 100;//    将卖票的代码抽取出来    public synchronized void payTickets() { if (ticket > 0) {     try {//      为了让结果出现的错误更明显,设置成10ms卖一张票  Thread.sleep(10);     } catch (InterruptedException e) {  e.printStackTrace();     }     System.out.println(Thread.currentThread().getName() + "卖出了第" + (101 - ticket--) + "张票"); }    }    @Override    public void run() { while (true) {     payTickets(); }    }    public static void main(String[] args) { TicketSales_Solution ts = new TicketSales_Solution(); new Thread(ts).start(); new Thread(ts).start(); new Thread(ts).start();    }}

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

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

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

  • 👩🏻‍🏫答:同步方法的锁对象是实现类对象,即当前对象,也就是我们常说的this

  • 👩🏻‍🏫答:也就是说我们抽取出来的代码还有一个等价写法—使用锁对象:

    //    将卖票的代码抽取出来public void payTickets() {//使用锁对象,与同步方法等价的写法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修饰public static int ticket = 100;//    将卖票的代码抽取出来,并声明为静态方法public static synchronized void payTickets() {    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锁改造卖票案例:

import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class TicketSales_Solution implements Runnable {    public int ticket = 100;//    在成员位置创建一个ReentrantLock对象    Lock lock = new ReentrantLock();    @Override    public void run() { 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(); }    }    public static void main(String[] args) { TicketSales_Solution ts = new TicketSales_Solution(); new Thread(ts).start(); new Thread(ts).start(); new Thread(ts).start();    }}

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

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

💙🧡💙🧡💙🧡💙🧡💙🧡💙 🤍💬下篇预告:线程的等待与唤醒🤍 💛💚💛💚💛💚💛💚💛💚💛

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

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


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

footer