☕☕ 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
只自增了一次,同我们的预期相反,这就是所谓的线程不安全。
🍋并发时的线程安全
👉🏻再来看一个卖票的例子:售票站有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();
}
}
👉🏻以上代码不对线程进行任何限制,买票的结果如下:
👉🏻可以看到三个线程不仅会卖同一张票,甚至在最后还卖出了第101张本来不存在的票!
🎈为了解决线程安全问题,就必须对访问同一资源的线程做出一定限制,在Java中使用锁机制来实现这一点。
🍓Java中的锁机制
⛅⛅⛅
🔒锁机制是线程同步技术的一种。既然线程的并发执行可能会导致线程不安全,那么不妨将线程的并发执行改成按顺序执行,也就是对线程中可能访问同一资源的代码片段上锁,使其在一段时间内只允许一个线程处于运行状态,而其他线程必须等待得到锁的线程执行完毕,释放出锁以后才能继续执行,通过锁机制实现多线程的同步。
⛅⛅⛅
Java多线程并发的内容实在太过庞大,都可以单独写一本书了,作为刚刚接触多线程的新手来说掌握以下三种上锁方法就够用了:
synchronized()
对象锁synchronized
同步方法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());
}
}
}
👀观察对象的头部信息:
可以发现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
同步代码块可以解决线程同步问题,卖票案例成功得到我们想要的效果。
🐄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();
}
}
👀同样可以得到想要的结果:
【注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()
:释放锁。
🎯使用步骤:
- 在成员位置创建一个ReentrantLock对象。
- 在可能会出现线程安全问题的代码前调用Lock接口中的lock()方法获取锁。
- 在可能会出现线程安全问题的代码后调用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();
}
}
👀依然可以的到我们想要的结果:
💙🧡💙🧡💙🧡💙🧡💙🧡💙
🤍💬下篇预告:线程的等待与唤醒🤍
💛💚💛💚💛💚💛💚💛💚💛
🍍🍍🍍
创作不易,如果觉得本文对你有所帮助,欢迎点赞、关注、收藏。🙇🏻♀️
🍉🍉🍉
@作者:Mymel_晗,计算机专业练习时长两年半的Java练习生~🏃🏻♂️🏀
🔸🔹文末已至,咱们下篇再见🔹🔸
┊且将新火试新茶,诗酒趁年华┊
望江南·超然台作-苏轼