线程安全、线程同步(同步代码块、同步方法、同步锁)

news/2024/11/24 3:30:51/

一. 线程安全

1.1 线程安全问题是什么,发生的原因

  • 多个线程同时修改同一共享资源的时候,会出现线程安全问题。
  • 读数据是绝对不会出现线程安全问题的,它一定是因为同时在修改。
  • 一旦线程同步了,就是解决了安全问题了。
  • CPU负责调度线程执行的,它是控制中心。

  1. 线程安全问题出现的原因?

  • 存在多线程并发
  • 同时访问并存在修改同一共享资源

1.2 线程安全问题案例模拟

package com.gch.d3_thread_safe;/**定义账户类*/
public class Account {private double money; // 账户的余额public Account(double money) {this.money = money;}public Account() {}public double getMoney() {return money;}public void setMoney(double money) {this.money = money;}/*** 取钱功能* @param money:取钱的金额*/public void drawMoney(double money){// 1.先获取是谁来取钱,线程的名字设置的是人名String name = Thread.currentThread().getName();// 2.判单账户的余额 >= 取钱的金额if(this.money >= money){// 可以取钱了System.out.println(name + "取钱成功,取出" + money + "元!");
//            setMoney(getMoney() - money);// 更新余额this.money -= money;System.out.println(name + "取钱后共享账户剩余:" + this.money);}else{System.out.println(name + "来取钱,账户余额不足");}}
}
package com.gch.d3_thread_safe;/**取钱的线程类*/
public class DrawThread extends Thread {// 接收处理的账户对象private Account acc;/*** 有参构造器* @param acc:接共享的账户对象* @param name:线程名*/public DrawThread(Account acc,String name) {super(name);this.acc = acc;}public DrawThread() {}public Account getAcc() {return acc;}public void setAcc(Account acc) {this.acc = acc;}@Overridepublic void run() {// 小明、小红:取钱acc.drawMoney(100000);}
}
package com.gch.d3_thread_safe;/**需求:模拟取钱案例*/
public class ThreadDemo {public static void main(String[] args) throws InterruptedException {// 1.定义账户类,创建一个账户对象代表2个人共享的账户对象Account acc = new Account(100000);// 2.定义线程类,创建两个线程对象,代表小明和小红同时进来了// 直接new对象这叫匿名对象new DrawThread(acc,"小明").start();
//        DrawThread.sleep(30);new DrawThread(acc,"小红").start();}
}

 1. 线程安全问题发生的原因是什么?

  • 多个线程同时访问同一共享资源且存在修改该资源。

 线程安全问题模拟案例二:卖票

  • 案例需求

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

package com.gch.d3_thread_safe;public class MyThread extends Thread {/*** 调用父类的有参构造器* @param name:线程名*/public MyThread(String name){super(name);}public static int ticket = 1; // 1 ~ 100@Overridepublic void run() {while(true){if(ticket > 100){// 卖完了break;}else{try {Thread.sleep(100);} catch (Exception e) {e.printStackTrace();}System.out.println(getName() + "正在卖第" + ticket + "张票!");ticket++;}}}
}
package com.gch.d3_thread_safe;public class ThreadDemo2 {public static void main(String[] args) {
//        需求:某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,
//        请设计一个程序模拟该电影院卖票// 1.创建线程对象Thread t1 = new MyThread("窗口1");Thread t2 = new MyThread("窗口2");Thread t3 = new MyThread("窗口3");// 2.开启线程t1.start();t2.start();t3.start();}
}
  •  出现重复票的根本原因是:线程在执行的时候,它是具有随机性的,CPU的执行权有可能随时会被其他的线程给抢走,还没来得及去打印,CPU的执行权就被其他的线程给抢走了。
  • 线程在执行的时候,它是具有随机性的,CPU的执行权随时有可能会被其他的线程给抢走!

 

 二. 线程同步

2.1 同步思想概述

线程同步

  • 为了解决线程安全问题。

1. 取钱案例出现问题的原因?

  • 多个线程同时执行,发现账户都是够钱的。

2. 如何才能保证线程安全呢?

  • 让多个线程实现先后依次访问共享资源,这样就解决了安全问题。
  • 把操作共享数据的代码给锁起来!

线程同步的核心思想

  • 加锁,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来。其他线程就算抢夺到了CPU的执行权,它也得在外面等着,它进不来!让所有的线程在核心代码当中能轮流执行!
  • 注意:synchronized的锁对象,它一定要是唯一的!如果锁对象不唯一,导致一个线程进一个锁,那么这个锁就没有意义了!

线程同步解决安全问题的思想是什么?

  • 加锁:让多个线程实现先后依次访问共享资源,这样就解决了安全问题。

 

2.2 方式一:同步代码块:

                                       利用同步代码块把操作共享数据的代码给锁起来,让同步代码块里面的代码是轮流去执行的!

锁对象的两个特点:

  • 特点1:锁默认打开,有一个线程进去了,锁自动关闭
  • 特点2:里面的代码全部执行完毕,线程出来,锁自动打开

 锁对象用任意唯一的对象好不好呢?

  • 不好,会影响其他无关线程的执行。

锁对象的规范要求

  • 规范上:建议使用共享资源作为锁对象。
  • 对于实例方法建议使用this作为锁对象。
  • 对于静态方法建议使用字节码(类名.class)对象作为锁对象。
package com.gch.d5_thread_synchronized_code;/**定义账户类*/
public class Account {private double money; // 账户的余额public Account(double money) {this.money = money;}public Account() {}public double getMoney() {return money;}public void setMoney(double money) {this.money = money;}/*** 取钱功能* @param money:取钱的金额*/public void drawMoney(double money){// 1.先获取是谁来取钱,线程的名字设置的是人名String name = Thread.currentThread().getName();// 同步代码块// 小明,小红  唯一的同步锁对象// 规范上,建议使用共享资源作为锁对象   this = acc 共享账户// 对于实例方法建议使用this作为锁对象synchronized (this) {  //  acc.drawMoney(100000);// 2.判单账户的余额 >= 取钱的金额if(this.money >= money){// 可以取钱了System.out.println(name + "取钱成功,取出" + money + "元!");//            setMoney(getMoney() - money);// 更新余额this.money -= money;System.out.println(name + "取钱后共享账户剩余:" + this.money);}else{System.out.println(name + "来取钱,账户余额不足");}}}
}
package com.gch.d5_thread_synchronized_code;
/**取钱的线程类*/
public class DrawThread extends Thread {// 接收处理的账户对象private Account acc;/*** 有参构造器* @param acc:接共享的账户对象* @param name:线程名*/public DrawThread(Account acc, String name) {super(name);this.acc = acc;}public DrawThread() {}public Account getAcc() {return acc;}public void setAcc(Account acc) {this.acc = acc;}@Overridepublic void run() {// 小明、小红:取钱acc.drawMoney(100000);}
}
package com.gch.d5_thread_synchronized_code;/**需求:模拟取钱案例*/
public class ThreadSafeDemo {public static void main(String[] args) throws InterruptedException {// 测试线程安全问题// 1.定义账户类,创建一个账户对象代表2个人共享的账户对象Account acc = new Account(100000);// 2.定义线程类,创建两个线程对象,代表小明和小红同时进来了// 直接new对象这叫匿名对象new DrawThread(acc,"小明").start();
//        DrawThread.sleep(30);new DrawThread(acc,"小红").start();}
}

 

  1. 同步代码块是如何实现线程安全的?
  • 对出现问题的核心代码使用synchronized进行加锁
  • 每次只能一个进程占锁进行访问
  1. 同步代码块的同步锁对象有什么要求?
  • 对于实例方法建议使用this作为锁对象
  • 对于静态方法建议使用(当前类的字节码文件对象)字节码(类名.class)对象作为锁对象,字节码文件对象一定是唯一的!

 卖票案例加同步代码块!

 注意:不要把synchronized放在死循环的外面,这样导致一个线程进来以后一直是当前线程在卖票,直到这个线程窗口卖完票才退出,导致其他窗口没有机会!

package com.gch.d3_thread_safe;public class MyThread extends Thread {/*** 调用父类的有参构造器* @param name:线程名*/public MyThread(String name){super(name);}// 表示这个类所有的对象,都共享ticket数据public static int ticket = 1; // 1 ~ 100// 锁对象,一定要是唯一的
//    public static Object obj = new Object();@Overridepublic void run() {while(true){// 同步代码块  锁对象用当前类的字节码文件,当前类的字节码文件对象一定是唯一的!synchronized (MyThread.class) {if(ticket > 100){// 卖完了break;}else{try {Thread.sleep(100);} catch (Exception e) {e.printStackTrace();}System.out.println(getName() + "正在卖第" + ticket + "张票!");ticket++;}}}}
}
package com.gch.d3_thread_safe;public class ThreadDemo2 {public static void main(String[] args) {
//        需求:某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,
//        请设计一个程序模拟该电影院卖票// 1.创建线程对象Thread t1 = new MyThread("窗口1");Thread t2 = new MyThread("窗口2");Thread t3 = new MyThread("窗口3");// 2.开启线程t1.start();t2.start();t3.start();}
}

 

2.3 方式二:同步方法

同步方法

  • 作用:把出现线程安全问题的核心方法上锁
  • 原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。

同步方法的两个特点:

  • 特点1:同步方法是锁住方法里面所有的代码
  • 特点2:同步方法的锁对象不能自己指定,是Java已经规定好的。如果当前方法是非静态的,那么锁对象就是this,也就是当前方法的调用者;如果当前方法是静态的,那么锁对象是当前类的字节码文件对象

同步方法底层原理

  • 同步方法其实底层也是有隐式锁对象的(只是我们看不到而已),只是锁的范围是整个方法代码块。
  • 如果方法是实例方法:同步方法默认使用this作为锁对象。但是代码要高度面向对象!
  • 如果方法是静态方法:同步方法默认使用类名.class作为锁对象。

同步代码块好还是同步方法好一点儿?

  • 同步代码块锁的范围更小,锁的范围小,性能更好一点儿,而同步方法锁的范围更大。
  • 比如上厕所,一个是在坑位门口锁,一个是在厕所门口锁
  • 但是在实际开发中,同步方法比同步代码块用的更多一点,因为同步方法它的可读性好,写法方便。
  • 官方(JDK)的源码也在大量使用同步方法,比如HashTable。
package com.gch.d6_thread_synchronized_method;/**定义账户类*/
public class Account {private double money; // 账户的余额public Account(double money) {this.money = money;}public Account() {}public double getMoney() {return money;}public void setMoney(double money) {this.money = money;}/*** 取钱功能* @param money:取钱的金额*/public synchronized void drawMoney(double money){// 1.先获取是谁来取钱,线程的名字设置的是人名String name = Thread.currentThread().getName();// 2.判单账户的余额 >= 取钱的金额if(this.money >= money){// 可以取钱了System.out.println(name + "取钱成功,取出" + money + "元!");//            setMoney(getMoney() - money);// 更新余额this.money -= money;System.out.println(name + "取钱后共享账户剩余:" + this.money);}else{System.out.println(name + "来取钱,账户余额不足");}}}
package com.gch.d6_thread_synchronized_method;/**取钱的线程类*/
public class DrawThread extends Thread {// 接收处理的账户对象private Account acc;/*** 有参构造器* @param acc:接共享的账户对象* @param name:线程名*/public DrawThread(Account acc, String name) {super(name);this.acc = acc;}public DrawThread() {}public Account getAcc() {return acc;}public void setAcc(Account acc) {this.acc = acc;}@Overridepublic void run() {// 小明、小红:取钱acc.drawMoney(100000);}
}
package com.gch.d6_thread_synchronized_method;/**需求:模拟取钱案例*/
public class ThreadSafeDemo {public static void main(String[] args) throws InterruptedException {// 测试线程安全问题// 1.定义账户类,创建一个账户对象代表2个人共享的账户对象Account acc = new Account(100000);// 2.定义线程类,创建两个线程对象,代表小明和小红同时进来了// 直接new对象这叫匿名对象new DrawThread(acc,"小明").start();
//        DrawThread.sleep(30);new DrawThread(acc,"小红").start();}
}

  1.  同步方法是如何保证线程安全的?
  • 对出现问题的核心方法使用synchronized修饰
  • 每次只能一个线程占锁进入访问
  1. 同步方法的同步锁对象的原理?
  • 同步方法的底层是有隐式锁对象的,只是锁的范围是整个方法代码块!
  • 对于实例方法默认使用this作为锁对象。
  • 对于静态方法默认使用当前类的字节码文件,类名.class对象作为锁对象​​​

 同步方法案例二:卖票

  • 不要去写同步方法,先写同步代码块,然后再把同步代码块里面的代码,去抽取成方法,这就OK了!
package com.gch.d3_thread_safe_2;
/**线程任务类*/
public class MyRunnable implements Runnable {// MyRunnable对象只创建一次,因此变量票数面前无需加staticint ticket = 0; // 0 ~ 99@Overridepublic void run() {// 1.循环while(true){// 2.同步方法if (method()) break;}}// 锁对象:thisprivate synchronized boolean method() {// 3.判断共享数据是否到了末尾,如果到了末尾if(ticket == 100){return true;}else{// 4.判断共享数据是否到了末尾,如果没有到末尾try {Thread.sleep(100);} catch (Exception e) {e.printStackTrace();}ticket++;System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票!" );}return false;}
}
package com.gch.d3_thread_safe_2;public class ThreadDemo {public static void main(String[] args) {/*案例需求:某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票*/// 1.创建线程任务对象Runnable target = new MyRunnable();// 2.创建线程对象Thread t1 = new Thread(target,"窗口1");Thread t2 = new Thread(target,"窗口2");Thread t3 = new Thread(target,"窗口3");// 3.启动线程t1.start();t2.start();t3.start();}
}

 补充知识:StringBuilder和StringBuffer的区别

  1. StringBuilder和StringBuffer的API一模一样。
  2. StringBuilder是线程不安全的,StringBuffer是线程安全的。
  3. StringBuffer的源码里面方法都是同步方法,加了synchronized修饰。
  4. 使用场景区分:
  • 如果你的代码是单线程的,不需要考虑多线程当中数据安全的抢矿,你就用StringBuilder就可以了。
  • 如果说你是多线程环境下需要考虑数据安全,那么就可以选择StringBuffer。

 2.4 方式三:Lock锁

  • 有了Lock锁我们就可以手动的上锁,还有手动的释放锁了!

package com.gch.d7_thread_synchronized_lock;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;/**定义账户类*/public class Account {private double money; // 账户的余额// 加了final的变量只能被初始化一次!// final修饰后:锁对象是唯一和不可替换的,非常专业private final Lock lock = new ReentrantLock(); // 实例成员变量,每创建一个账户对象就创建一个锁对象public Account(double money) {this.money = money;}public Account() {}public double getMoney() {return money;}public void setMoney(double money) {this.money = money;}/*** 取钱功能* @param money:取钱的金额*/public void drawMoney(double money){
//        lock = null; 直接报错,因为锁对象被final修饰,是唯一的不可替换的// 1.先获取是谁来取钱,线程的名字设置的是人名String name = Thread.currentThread().getName();// 2.判单账户的余额 >= 取钱的金额lock.lock(); // 上锁try {if(this.money >= money){// 可以取钱了System.out.println(name + "取钱成功,取出" + money + "元!");//            setMoney(getMoney() - money);// 更新余额this.money -= money;System.out.println(name + "取钱后共享账户剩余:" + this.money);}else{System.out.println(name + "来取钱,账户余额不足");}} finally { // 加了finally,解锁更加安全,即使出了bug也会解锁lock.unlock(); // 解锁}}
}
package com.gch.d7_thread_synchronized_lock;/**取钱的线程类*/
public class DrawThread extends Thread {// 接收处理的账户对象private Account acc;/*** 有参构造器* @param acc:接共享的账户对象* @param name:线程名*/public DrawThread(Account acc, String name) {super(name);this.acc = acc;}public DrawThread() {}public Account getAcc() {return acc;}public void setAcc(Account acc) {this.acc = acc;}@Overridepublic void run() {// 小明、小红:取钱acc.drawMoney(100000);}
}
package com.gch.d7_thread_synchronized_lock;/**需求:模拟取钱案例*/
public class ThreadSafeDemo {public static void main(String[] args) throws InterruptedException {// 测试线程安全问题// 1.定义账户类,创建一个账户对象代表2个人共享的账户对象Account acc = new Account(100000);// 2.定义线程类,创建两个线程对象,代表小明和小红同时进来了// 直接new对象这叫匿名对象new DrawThread(acc,"小明").start();
//        DrawThread.sleep(30);new DrawThread(acc,"小红").start();}
}

Lock锁案例二:卖票

package com.gch.d3_thread_safe_3;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class MyThread extends Thread {/*** 调用父类Thread的有参构造器* @param name:线程名*/public MyThread(String name){super(name);}// 表示本类 / 当前类的所有对象,都共享ticket数据public static int ticket = 0; // 0 ~ 99// 定义Lock锁,锁对象必须是唯一和不可替换的// 静态成员变量,本类 / 当前类的所有对象共享一把锁// 如果定义成实例成员变量,那么每创建一个线程对象就创建一把锁,窗口1,窗口2,窗口3各自都有各自的锁,各自卖各自的private static final Lock lock = new ReentrantLock();@Overridepublic void run() {// 1.循环while(true){// 2.上锁lock.lock();try {// 3.判断if(ticket == 100){break; // 如果ticket == 100,将会直接跳出循环,这导致的结果就是没有释放锁!!!程序运行就会出现bug}else{// 4.判断try {Thread.sleep(100);} catch (Exception e) {e.printStackTrace();}ticket++;System.out.println(getName() + "正在卖第" + ticket + "张票!");}} finally {// 4.解锁lock.unlock();}}}
}
package com.gch.d3_thread_safe_3;public class ThreadDemo {public static void main(String[] args) {/*案例需求:某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票用JDK5的Lock实现*/// 1.创建线程对象Thread t1 = new MyThread("窗口1");Thread t2 = new MyThread("窗口2");Thread t3 = new MyThread("窗口3");// 2.启动线程t1.start();t2.start();t3.start();}
}

 


http://www.ppmy.cn/news/36714.html

相关文章

AD360自助式密码管理

通常情况下,数字企业的 IT 管理员发现自己淹没在与密码相关的票证或与身份相关的问题的海洋中。通过为最终用户部署自助服务解决方案,可以轻松对此进行排序,同时还可以节省组织在解决密码相关问题上花费的时间和金钱。 AD360有助于通过自助密…

平替笔和ipad笔差别大吗?平价好用的电容笔品牌

苹果原装的Pencil,作为一款具有强大功能的电容笔,其的售价处于在昂贵的价位上,若是丢失甚至损坏,对我们来说,都是一种莫大的打击。另外,对于大部分用户来说,他们也没有太多的钱来买这种昂贵的电…

ChatGPT引发的人机交互发展历程与升级思考

ChatGPT自从去年12月火爆以来一直热度不减,最近正好研读了科技之巅,书中详细阐述了人机交互、人工智能、算力算法等技术的发展历史,本文主要围绕ChatGPT引发的人机交互的思考。 在讨论人机交互之前,首先需要说明的一点&#xff0…

Nsmf_PDUSession 服务--更新 SM 上下文服务操作【TS 29.502】

以下场景会触发更新SM上下文服务操作 PDU会话修改(参见 3GPP TS 23.502 [3]的第 4.3.3节);UE或网络请求的 PDU会话释放(参见 3GPP TS 23.502 [3]的第 4.3.4.2节和第 4.3.4.3节);UE通过其他接入请求建立了 M…

React项目路由实战

react中路由中文文档 http://www.reactrouter.cn/docs/api#routes-%E5%92%8C-route 安装&#xff1a; cnpm i --save-dev react-router-dom 配置路由 配置具体的路由表 <Router> {/* 配置路由表 */} <Routes> <Route path"/admin" element{<Ad…

MAC系统分区的目录结构

不同于windows的多根逻辑存储结构&#xff0c;MAC系统目录结构是按照unix系统规范来的。 举个例子&#xff1a;windows是分为c, d, e, f, g盘等等的。而你进入mac是看不见这些的&#xff0c;他其实相当于所有文件储存在一个盘下面&#xff0c;而这个盘分为很多个文件夹&#x…

蓝桥杯之贪心

蓝桥杯之贪心1055.股票买卖II104.货仓选址AcWing112.雷达设备1235.付账问题1055.股票买卖II 将股票价格的变动抽象成折线图&#xff0c;将每个上升阶段累加起来&#xff08;每次的累加并不一定代表真实的一次买卖交易&#xff0c;比如两段连续上升的折线&#xff0c;只进行了一…

指针的用法和注意事项(二)

指针及其大小、用法 指针的定义: 指针是一种变量类型&#xff0c;其值为另一个变量的地址&#xff0c;即内存位置的直接地址。就像其他变量或常量一样&#xff0c;必须在使用指针存储其他变量地址之前&#xff0c;对其进行声明。在 64 位计算机中&#xff0c;指针占 8 个字节空…