Java多线程​(三)线程安全:同步及锁
  93byl95AX42O 2023年12月24日 24 0

线程安全问题

考虑如下情景:

某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票

public class TicketSeller extends Thread{

 // 定义票的数量
 static int ticket = 0; // 取值范围: 0~99

 @Override
 public void run() {
 while (true){
 if (ticket < 100){
 try {
 // 创建延时效果
 Thread.sleep(100);
 } catch (InterruptedException e) {
 throw new RuntimeException(e);
 }
 ticket++;
 System.out.println(getName() + ":正在卖第" + ticket + "张票!");
 }else {
 break;
 }
 }
 }
}


public class TicketWindows {
 public static void main(String[] args) {
 // 创建线程对象
 TicketSeller ts1 = new TicketSeller();
 TicketSeller ts2 = new TicketSeller();
 TicketSeller ts3 = new TicketSeller();
 // 命名线程
 ts1.setName("窗口1");
 ts2.setName("窗口2");
 ts3.setName("窗口3");
 // 开启线程
 ts1.start();
 ts2.start();
 ts3.start();
 }
}

一次运行结果如下:

窗口1:正在卖第2张票!
窗口2:正在卖第2张票!
窗口3:正在卖第2张票!
窗口1:正在卖第3张票!
窗口2:正在卖第3张票!
窗口3:正在卖第3张票!
。。。。。。
窗口2:正在卖第96张票!
窗口1:正在卖第97张票!
窗口3:正在卖第98张票!
窗口2:正在卖第99张票!
窗口1:正在卖第100张票!
窗口3:正在卖第101张票!
窗口2:正在卖第102张票!

我们发现,这个程序并不能达到预期的效果,

①相同的票出现了多次

出现了超出范围的票

原因是线程执行有随机性。

同步代码块

解决的方式之一是使用同步代码块,即把操作共享数据的代码锁起来。格式如下:

synchronized (锁){
 操作共享数据的代码
}

特点1:锁默认打开,有一个线程进去了,锁自动关闭

特点2:里面的代码全部执行完毕,线程出来,锁自动打开

利用同步代码块改写后:

public class MyThreadSync extends Thread{
 // 表示这个类所有的对象都共享ticketCount数据
 static int ticketCount = 0; // 0-99
 // 锁对象,可以是任意的对象,但是必须要是唯一的
 static Object obj = new Object();

 @Override
 public void run() {
 while (true){
 // 同步代码块
 synchronized (obj){
 if (ticketCount < 100){
 try {
 Thread.sleep(50);
 } catch (InterruptedException e) {
 throw new RuntimeException(e);
 }
 ticketCount++;
 System.out.println(getName() + "正在卖第" + ticketCount + "张票!");
 }else {
 break;
 }
 }
 }
 }
}

这里要注意2点,第一是synchronized代码块要写在while(true)循环的里面,否则这100张票就只能由一个线程卖完为止。第二个注意点是,锁对象要是唯一,这里的对象是obj,需要使用static关键字修饰,在实际开发中一般写当前类的class对象名,如本例中可以写成MyThreadSync.class。


同步方法

同步方法,就是把synchronized关键字加到方法上。格式:

修饰符 synchronized 返回值类型 方法名(参数列表...){方法体...}

  • 特点1:同步方法是锁住方法里面所有的代码
  • 特点2:锁对象不能自己指定。如果当前方法是非静态的,那么锁对象就是this;如果是静态方法,那么锁对象就是当前类的字节码文件对象
public class MyThreadSyncFunc implements Runnable {

 // 表示电影票的数量
 // 注意这里与前面的继承Thread类的方式不一样
 // 这里的ticketCount不需要被static修饰
 // 因为实现Runnable接口的对象只会创建一个,
 // 然后作为参数传给Thread对象
 int ticketCount = 0;

 @Override
 public void run() {


// 4.判断共享数据是否到了末尾,如果没有到末尾


 //1.循环
 while (true) {
 //2.同步代码块(同步方法)
 if (method())
 break;
 }
 }

 private synchronized boolean method(){
 //3.判断共享数据是否到达末尾,则退出
 if (ticketCount == 100) {
 return true;
 } else {
 try {
 // 睡眠10毫秒
 Thread.sleep(10);
 } catch (InterruptedException e) {
 throw new RuntimeException(e);
 }
 ticketCount++;
 System.out.println(Thread.currentThread().getName() + "正在卖第" + ticketCount + "张票!");
 }
 return false;
 }
}

使用代码:

public class MyThreadSyncFuncTest {
 public static void main(String[] args) {
 MyThreadSyncFunc mtsf = new MyThreadSyncFunc();

 // 创建线程对象
 Thread t1 = new Thread(mtsf);
 Thread t2 = new Thread(mtsf);
 Thread t3 = new Thread(mtsf);

 // 线程命名
 t1.setName("窗口1");
 t2.setName("窗口2");
 t3.setName("窗口3");

 // 启动线程
 t1.start();
 t2.start();
 t3.start();
 }
}

补充知识点:StringBuilder与StringBuffer是两个相似的类,前者不是线程安全的,而StringBuffer是线程安全的。

Lock锁

虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock

Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作Lock中提供了获得锁和释放锁的方法:

  • void lock():获得锁
  • void unlock():释放锁

Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化ReentrantLock的构造方法

ReentrantLock():创建一个ReentrantLock的实例

手动上锁/解锁的线程实现类:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyThreadLock extends Thread {
 // 表示电影票的数量
 static int ticketCount = 0;
 // 锁,必须共享一个锁,所以要用static修饰
 static Lock lock = new ReentrantLock();

 @Override
 public void run() {
 // 1.循环
 while (true) {
 // 上锁
 lock.lock();
 try {
 if (ticketCount == 100) {
 break;
 } else {
 Thread.sleep(10);
 ticketCount++;
 System.out.println(Thread.currentThread().getName() + "在卖第" + ticketCount + "张票!");
 }
 } catch (InterruptedException e) {
 throw new RuntimeException(e);
 } finally {
 // 解锁
 lock.unlock();
 }
 }
 }
}

使用该线程:

public class MyThreadLockTest {
 public static void main(String[] args) {
 // 创建线程对象
 MyThreadLock mtl1 = new MyThreadLock();
 MyThreadLock mtl2 = new MyThreadLock();
 MyThreadLock mtl3 = new MyThreadLock();
 // 线程命名
 mtl1.setName("窗口1");
 mtl2.setName("窗口2");
 mtl3.setName("窗口3");
 // 线程启动
 mtl1.start();
 mtl2.start();
 mtl3.start();
 }
}

解锁的代码放在finally语句块中,表示无论如何都要执行解锁操作。


死锁

死锁是一种编程错误,要避免程序陷入死锁的局面从而让程序卡死。在编程实践中要避免锁嵌套。

多线程编程的一般套路

  1. 循环
  2. 同步代码块
  3. 判断共享数据是否到了末尾(到了末尾)
  4. 判断共享数据是否到了末尾(没有到末尾,执行核心逻辑)



【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年12月24日 0

暂无评论

93byl95AX42O