1 走进Synchronized
1.1 线程同步机制
在前面的多线程篇的学习中,我们可以尝试对售票活动进行模拟,将每个售票窗口看成一个进程。但是,前面由于没有学习过线程的同步,有可能会出现超卖的问题。比如只剩最后一张票,但是,两个窗口此时都在同时卖,就会由于数据更新不及时,导致多卖出票。
🐱何为线程同步机制?
在多线程编程中,一些敏感数据不允许被多个线程同时访问, 此时就需要使用同步访问技术,保证数据在任何时刻,最多有一个线程被访问,以保证数据的完整性。
线程同步,即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作。
1.2 同步的具体方法–synchronized
1️⃣ 同步代码块:
synchronized(对象){//得到对象的锁,才能操作同步代码 //需要被同步的代码 }
2️⃣ 同步方法:
public synchronized void method(参数列表){ //需要被同步的代码 }
1.3 使用线程同步解决售票问题
在下面的案例中,我们模拟售票问题,通过synchronized解决超卖问题。
package syn; /** * @author 兴趣使然黄小黄 * @version 1.0 * 售票问题 */ public class SellTicket { public static void main(String[] args) { SellTicketToCustomer sellTicketToCustomer = new SellTicketToCustomer(); //模拟三个售票窗口 new Thread(sellTicketToCustomer).start(); new Thread(sellTicketToCustomer).start(); new Thread(sellTicketToCustomer).start(); } } class SellTicketToCustomer implements Runnable{ /** * 票数 */ private int ticketNum = 100; /** * 控制run中的while,用于以通知的形式终止线程 */ private boolean loop = true; @Override public void run() { while (loop){ sellTicket(); } } /** * 售票的方法 */ public synchronized void sellTicket(){ //同步方法, 在统一时刻, 只能有一个线程来执行sellTicket方法 if (ticketNum <= 0){ System.out.println("售票结束..."); loop = false; return; } try { Thread.sleep(50); }catch (InterruptedException e){ e.getStackTrace(); } System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票, " + "剩余票数 = " + (--ticketNum)); } }
🐰 说明:
在上述例子中,sellTicket方法被设置为同步方法,在同一时刻,只允许一个线程进入,解决了超卖的情况。运行结果如下:
2 互斥锁
2.1 基本介绍
Java语言引入了对象互斥锁的概念,来保证共享数据操作的完整性;
每个对象都对应于一个可称为“互斥锁”的标记,这个标记 用来保证在任一时刻,只能有一个线程访问该对象;
关键字synchronized用来与对象的互斥锁联系。当某个对象用synchronized修饰时,表示该对象在任一时刻只能由一个线程访问;
同步具有局限性,会导致程序的执行效率降低;
非静态同步方法的锁可以是this,也可以是其他对象(要求为同一对象);
静态的同步方法的锁为当前类本身,类名.class。
2.2 使用互斥锁解决售票问题
售票问题我们既可以使用同步方法,也可以使用对象锁,给代码块上锁。在之前的案例中,我们演示的就是使用了同步方法:
public synchronized void sellTicket()就是一个同步方法,这时锁在 this 对象上
这里,我们尝试 使用对象锁解决, 解决代码如下:
其实也不一定必须使用this,只要对象为同一个就行,比如下面的代码中,在该线程类中,初始化了一个对象,加锁代码块用的也是同一对象,因此,结果一致:
/** * @author 兴趣使然黄小黄 * @version 1.0 * 售票问题 */ public class SellTicket { public static void main(String[] args) { SellTicketToCustomer sellTicketToCustomer = new SellTicketToCustomer(); //模拟三个售票窗口 new Thread(sellTicketToCustomer).start(); new Thread(sellTicketToCustomer).start(); new Thread(sellTicketToCustomer).start(); } } class SellTicketToCustomer implements Runnable{ /** * 票数 */ private int ticketNum = 100; /** * 控制run中的while,用于以通知的形式终止线程 */ private boolean loop = true; /** * 用于加锁 */ Object obj = new Object(); @Override public void run() { while (loop){ sellTicket(); } } /** * 售票的方法 */ public void sellTicket(){ synchronized (obj) { if (ticketNum <= 0) { System.out.println("售票结束..."); loop = false; return; } try { Thread.sleep(50); } catch (InterruptedException e) { e.getStackTrace(); } System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票, " + "剩余票数 = " + (--ticketNum)); } } }
互斥锁小结一下:
同步方法如果没有static修饰,默认锁对象为this;
如果同步方法有static修饰,默认锁对象为 当前类.class;
注意多个线程的锁对象为同一个即可。
3 线程死锁
3.1 基本介绍
多个线程都占用了对方的锁资源,但是不肯相让,就导致了死锁。 在实际编程开发中,一定要避免死锁的发生。
🐦 举个例子:
话说,在某一药店门口… …
👮 你好,请您佩戴口罩!
👦我没有口罩!
👮 你好,没有口罩是不能入内的哦~
👦我就是没有口罩才来买口罩啊!
👮 没有佩戴口罩是不能进入的呢…
👦… …
👮 … …
在上述例子中,我们可以简单的把口罩看作锁,把保安放人与客户通过分别看成一个线程。此时,保安和客户都占用口罩这一锁资源,互不相让,就导致了死锁的情况。
3.2 案例演示
/** * @author 兴趣使然黄小黄 * @version 1.0 * 模拟线程死锁 */ public class DeadLockTest { public static void main(String[] args) { DeadDemo deadDemo1 = new DeadDemo(true); DeadDemo deadDemo2 = new DeadDemo(false); Thread thread1 = new Thread(deadDemo1); Thread thread2 = new Thread(deadDemo2); thread1.start(); thread2.start(); } } /** * 线程 */ class DeadDemo implements Runnable{ static Object o1 = new Object(); static Object o2 = new Object(); boolean flag; public DeadDemo(boolean flag){ this.flag = flag; } @Override public void run() { if (flag){ synchronized (o1){ System.out.println(Thread.currentThread().getName() + "进入1"); synchronized (o2){ System.out.println(Thread.currentThread().getName() + "进入2"); } } }else { synchronized (o2){ System.out.println(Thread.currentThread().getName() + "进入3"); synchronized (o1){ System.out.println(Thread.currentThread().getName() + "进入4"); } } } } }
🐰 说明:
thread0与thread1两个线程运行时,thread0首先拿到o1锁,而后请求o2锁,这时,thread1已经拿到了o2锁,请求o1锁,由于各自请求的锁都被其他线程占用,就导致了死锁的情况, 运行结果就像卡住了一样,不再动弹。
4 释放锁
4.1 释放锁的情况
当前线程的同步方法、同步代码块执行结束;
当前线程在同步代码块、同步方法中遇到break、return;
当前线程在同步代码块、同步方法中出现了未处理的Error或者Exception,导致异常结束;
当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。
4.2 不会释放锁的情况
线程执行同步代码块或者同步方法时,程序调用了Thread.sleep()、Thread.yield()方法,暂停了当前线程的执行,不会释放锁;
线程执行同步代码块时,其他线程调用了该线程的suspend()方法将线程挂起,该线程不会释放锁。