[001-02-001]. 第07-03节:理解线程的安全问题
我的后端学习大纲
我的Java学习大纲
当多个线程
共享一份数据的
时候,不同的线程对数据进行操作,就可能会导致线程安全问题,比如卖票过程中出现了错票和重复
票的问题:
1、卖票问题分析:
1.1.理想状态:
1.2.问题状态:
问题原因
:当多条语句
在操作同一个
线程共享的数据时,一个线程对多条语句只执行了一部分
,还没有执行完,另一个线程参与进来执行。导致共享数据的错误
private int tick = 100;
public void run(){
while(true){
if(tick>0){
try{
//这里可能就多个线程进入,进行了各种操作。。。。导致线程安全
Thread.sleep(10);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+“售出车票,tick号为: "+tick--);
}
}
}
1.3.改善后状态:
- 对多条操作共享数据的语句,只能
让一个线程都执行完
,在执行过程中,其他线程不可以参与执行
2、线程安全问题的解决
2.1.解决线程安全办法:
- 对多条操作共享数据的语句,
只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行
,在java中,我们通过同步机制
,来解决线程安全问题
2.2.实现方式:
a.方式一:同步代码块:
- 1.操作
共享数据
的代码,就是需要被同步的代码;所谓的共享数据就是多个线程共同操作的变量
。比如卖票中的ticket数量就是变量
- 2.
同步监视器
就是锁
的意思;任何一个类对象
都可以充当锁
,一定要保证多个线程要共用同一把锁
,即用的是同一个对象;- 在
实现Runnable接口
创建的多线程的方式中,可以考虑使用this
充当同步监视器; - 在
继承Thread类
创建多继承的方式中,要慎用this
,看看this代表的是不是一样的意义。可以考虑当前类
- 在
synchronized(同步监视器){
//需要被同步的代码(操作共享数据的那部分代码就是需要被同步的代码)
}
- 3.
继承方式
中问题的改进:
- 5.
接口实现方式
中程序的改进 :
- 6.同步监视器的要求:即为锁的要求:
多个线程必须要用同一把锁
- 7.同步方式解决了线程的安全问题,但是降低了效率,在操作同步代码的时候,相当于一个单线程
b.方式二:同步方法
- 1.
如果操作共享数据的代码完整的声明在了一个完整的方法中,我们不妨就考虑此方法声明式同步的
解决实现方式的线程问题:
class ImporeTicket2 implements Runnable{
private int ticket = 10;//每个对象都有100张票,属性不共享
@Override
public void run() {
while(true){
show();
}
}
//卖票的完整方法
//在同步方法中,锁就是this
private synchronized void show(){
if (ticket >0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号是::" + ticket);
ticket--;
}
}
}
package com.atguigu.java;
/**
* @author: jianqun
* @email: 1033586391@qq.com
* @creat: 2022-04-09-13:50
* @Description:
*/
public class ThreadTest007 {
public static void main(String[] args) {
ImporeTicket2 impporeTicket = new ImporeTicket2();
Thread thread1 = new Thread(impporeTicket);
Thread thread2 = new Thread(impporeTicket);
Thread thread3 = new Thread(impporeTicket);
thread1.setName("窗口1");
thread2.setName("窗口2");
thread3.setName("窗口3");
thread1.start();
thread2.start();
thread3.start();
}
}
- 3.解决继承方式的线程问题:这里的锁是当前类,即
Bwindow2.class
class Bwindow2 extends Thread{
private static int ticket = 10;
private static Object obj = new Object();
@Override
public void run() {
while(true){
show2();
}
}
//private synchronized void show2(){}这个更改是错误的,代表的是不同对象的不同方法,要定义成静态的方法才可以
private static synchronized void show2(){//这里的锁是当前类
if (ticket >0){
System.out.println(Thread.currentThread().getName() + ":卖票,票号是::" + ticket);
ticket--;
}
}
}
package com.atguigu.java;
/**
* @author: jianqun
* @email: 1033586391@qq.com
* @creat: 2022-04-09-9:09
* @Description: 卖票问题
*/
public class ThreadTest008 {
public static void main(String[] args) {
Bwindow bwindow1 = new Bwindow();
Bwindow bwindow2 = new Bwindow();
Bwindow bwindow3 = new Bwindow();
bwindow1.setName("窗口1");
bwindow2.setName("窗口2");
bwindow3.setName("窗口3");
bwindow1.start();
bwindow2.start();
bwindow3.start();
}
}
- 3.同步方法的总结:
同步方法仍然是涉及到了同步监视器,但是不需要我们自己手动显示声明
- 非静态同步方法:
锁是:this
- 静态的同步方法:
锁是当前类的本身
c.方式三:线程同步方式-Lock锁方式 来解决线程安全问题
1.Lock(锁)
- 从JDK 5开始,Java提供了更强大的线程同步机制,通过
显式定义同步锁对象来实现同步
。同步锁使用Lock对象
充当 java.util.concurrent.locks.Lock接口
是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。ReentrantLock 类
实现了 Lock ,它拥有与synchronized 相同的并发性和内存语义
,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁
2.语法:
- 注意:如果
同步代码有异常,要将unlock()写入finally语句块
//Lock(锁)
class A{
private final ReentrantLock lock = new ReenTrantLock();
public void m(){
lock.lock();
try{
//保证线程安全的代码;
}finally{
lock.unlock();
}
}
}
3.卖票问题代码演示
package com.atguigu.java;
/**
* @author: jianqun
* @email: 1033586391@qq.com
* @creat: 2022-04-09-15:57
* @Description:
*/
import java.util.concurrent.locks.ReentrantLock;
class Window implements Runnable{
private int ticket = 10;
//1.实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true){
try{
//2.调用锁定方法lock()
lock.lock();
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
ticket--;
}else{
break;
}
}finally {
//3.调用解锁方法:unlock()
lock.unlock();
}
}
}
}
public class ThreadTest011 {
public static void main(String[] args) {
Window w = new Window();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
3、使用同步机制将单例模式中的懒汉式改写为线程安全的
package com.atguigu.java;
public class ThreadTest009 {
}
class Bank{
private Bank(){}
private static Bank instance = null;
public static Bank getInstance(){
//方式一:效率稍差
// 当有多个线程的时候,因为一上来就是锁,多个线程都在排队,然后当拿到锁之后,进入判断instance是否是null,
// 当不是null的时候,就直接返回了。这样一堆线程都在排队,但是拿到锁的时候,就又啥都不干,这样就浪费了时间,降低了效率
//synchronized (Bank.class) {
// if(instance == null){
// instance = new Bank();
// }
// return instance;
// }
//方式二:效率更高
if(instance == null){
synchronized (Bank.class) {
if(instance == null){
instance = new Bank();
}
}
}
return instance;
}
}
4、面试题目:
4.1.synchronized 与 Lock的异同
?
a.相同:
- 二者都可以解决线程安全问题
b.不同:
synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器
Lock需要手动的启动同步(lock())
,同时结束同步也需要手动的实现(unlock())
4.2.synchronized 与 Lock 的对比:
- Lock是
显式锁(手动开启和关闭锁,别忘记关闭锁)
,synchronized是隐式锁,出了作用域自动释放
- Lock
只有代码块锁,synchronized有代码块锁和方法锁
- 使
用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
4.3.优先使用顺序:
Lock --> 同步代码块(已经进入了方法体,分配了相应资源)--> 同步方法(在方法体之外)
5、线程的安全练习:
银行有一个账户。有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额
分析:是一个多线程问题(两个储户
),通过判断是否出现共享数据来判断是否出现线程安全问题。当有共享数据(账户余额是共享数据
)的时候,就使用三种方式来实现线程同步机制
//账户类
class Account{
private double balance;
public Account(double balance) {
this.balance = balance;
}
//存钱:
// 在未控制多线程同步的时候分析:
// 当乙用户存钱的时候,执行了 balance+=amt,然后未及时打印输出余额,
// 然后呢甲用户又存了钱,然后甲又进入了睡眠状态
// 过了一会乙和甲都睡醒了,但是都是存了1000元,可以打印的是存了2000元,这样就出现了线程安全的问题。
// 以下进行了线程安全控制:
public synchronized void deposits(double amt){
if (amt > 0){
balance+=amt;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "存储成功,余额:"+ balance);
}
}
}
//储户类存钱,可以看成是两个线程
class Customer extends Thread{
private Account acct;//定义一个Account类型的属性
//提供一个有参构造器,用于对属性的实例化
public Customer(Account acct){
this.acct = acct;
}
//run方法中重写存钱的逻辑
@Override
public void run() {
for (int i = 0; i < 3; i++) {
//存钱的方法
acct.deposits(1000);
}
}
}
package com.atguigu.java;
public class ThreadTest12 {
public static void main(String[] args) {
//对Account中double类型的balance属性进行初始化
Account account = new Account(0);
//对Customer中的Account类型的属性acc进行实例化,通过实例化出来两个用户
Customer customer1 = new Customer(account);
Customer customer2 = new Customer(account);
customer1.setName("张三");
customer2.setName("李四");
customer1.start();
customer2.start();
}
}
5、线程同步中的死锁问题:
5.1.死锁定义:
- 1.不同的线程分别
占用对方需要的同步资源不放弃
,都在等待对方放弃自己需要的同步资源
,就形成了线程的死锁 - 2.出现死锁后,
不会出现异常,不会出现提示
,只是所有的线程都处于阻塞状态,无法继续
5.2.举例分析死锁
package com.atguigu.java;
/**
* @author: jianqun
* @email: 1033586391@qq.com
* @creat: 2022-04-09-14:39
* @Description:
*/
public class ThreadTest010 {
public static void main(String[] args) {
StringBuffer stringBuffer1 = new StringBuffer();
StringBuffer stringBuffer2 = new StringBuffer();
//继承方式创建匿名方式实现多线程的同步
new Thread(){
@Override
public void run() {
synchronized (stringBuffer1){
stringBuffer1.append("a");
stringBuffer2.append("1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (stringBuffer2){
stringBuffer1.append("b");
stringBuffer2.append("2");
System.out.println(stringBuffer1);
System.out.println(stringBuffer2);
}
}
}
}.start();
//接口实现方式创建匿名内部类实现线程同步
new Thread(new Runnable() {
@Override
public void run() {
synchronized (stringBuffer2){
stringBuffer1.append("c");
stringBuffer2.append("3");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (stringBuffer1){
stringBuffer1.append("d");
stringBuffer2.append("4");
System.out.println(stringBuffer1);
System.out.println(stringBuffer2);
}
}
}
}){}.start();
}
}
5.3.死锁解决方法
- 1.专门的算法、原则
- 2.尽量减少同步资源的定义
- 3.尽量
避免嵌套同步