我的后端学习大纲
我的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张票,属性不共享@Overridepublic void run() {while(true){show();}}//卖票的完整方法//在同步方法中,锁就是thisprivate 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();@Overridepublic 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.实例化ReentrantLockprivate ReentrantLock lock = new ReentrantLock();@Overridepublic 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方法中重写存钱的逻辑@Overridepublic 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(){@Overridepublic 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() {@Overridepublic 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.尽量
避免嵌套同步