4.线程安全问题(重点:⭐⭐⭐⭐⭐)
4.1为什么这个是重点?
后在开发中,我们的项目都是运行在服务器当中,而服务器已经将线程的定义,线程对象的创建,线程的启动等,都 已经实现了。这些代码我们都不需要编写。
最重要的是:你要知道,你编写的程序需要放到一个多线程的环境下运行,你更需要关注的是这些数据再多线程并发的环境下是否是安全的。
4.2.什么情况下数据在多线程并发的情况下,存在线程安全问题
三个条件:
- 条件1:多线程并发。
- 条件2:有共享数据。
- 条件3:共享数据有修改的行为。
4.3.怎么解决线程安全问题呢?
当多线程并发的情况下,有共享数据,并且这个数据还会被修改,此时就存在线程安全问题,怎么解决这个问题?
线程同步机制
- 线程排队执行(不能并发)。
- 用排队执行解决线程安全问题。
- 这种机制被称为:线程同步机制
怎末解决线程安全问题呀?
- 使用“线程同步机制”
线程同步就是线程排队了,县城排队了就会牺牲一部分效率,没办法,数据安全第一位,只有数据安全了,我们才可以谈效率。数据不安全,没有效率的事儿。
4.4.线程同步,两个专业术语
1、异步编程模型:
- 线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,谁也不需要等谁,这种编程模型叫做:异步编程模型。其实就是:多线程并发(效率更高。)
- 异步就是并发。
2、同步编程模型:
- 线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行结束,或者或在t2线程执行的时候,必须等待t1线程执行结束,两个线程之间发生了等待关系,这就是同步编程模型。效率较低。线程排队执行。
- 同步就是排队。
4.5模拟线程安全问题。
4.5.1编写程序模拟两个线程同时对同一个账户进行取款操作。
账户类
package com.newXianCheng.ThreadSafe; /** * @Description: 银行账户类 * @auther:Li Ya Hui * @Time:2021年5月14日上午9:25:19 */ public class Account { //账号 private String actno; //余额 private double balance; //无参 public Account() { } //有参 public Account(String actno, double balance) { this.actno = actno; this.balance = balance; } //方法 public String getActno() { return actno; } public void setActno(String actno) { this.actno = actno; } public double getBalance() { return balance; } public void setBalance(double balance) { this.balance = balance; } //取款 public void withdarw(double money) { //取款之前的余额 double before = this.getBalance(); //取款之后的余额 double after = before-money; //模拟网络延迟 一定出问题 try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } //更新余额 this.setBalance(after); } }
取款的线程
package com.newXianCheng.ThreadSafe; /** * @Description: 取款的线程 * @auther:Li Ya Hui * @Time:2021年5月14日上午10:54:38 */ public class AccountThread extends Thread{ //两个线程必须共享同一个账户对象。 private Account act ; //通过构造方法传递过来账户对象 public AccountThread(Account act) { super(); this.act = act; } @Override public void run() { //run方法的执行表示取款操作 double money = 5000; //取款 //多线程并发执行这个方法 act.withdarw(money); System.out.println("账户"+act.getActno()+"取款成功,余额"+act.getBalance()); } }
取款的测试类
package com.newXianCheng.ThreadSafe; /** * @Description: 测试类 测试取款操作 * @auther:Li Ya Hui * @Time:2021年5月14日上午10:20:50 */ public class Test { public static void main(String[] args) { Account act = new Account("act-001",20000); AccountThread t1 = new AccountThread(act); AccountThread t2 = new AccountThread(act); //设置name t1.setName("t1"); t2.setName("t2"); //启动线程取款 t1.start(); t2.start(); } }
输出结果:出现问题
账户act-001取款成功,余额15000.0账户act-001取款成功,余额15000.0
4.5.2.同步代码块synchronized
线程同步机制的语法是:
- synchronized后面小括号中传的这个“数据”是相当关键的。
- 这个数据必须是多线程共享的数据。才能达到多线程排队。
- ()中写什么?
- 需要看你想让那些线程同步。
- 假设t1,t2,t3,t4,t5 5个线程
- 你只希望t1,t2,t3排队,t4,t5不需要排队,怎么办?
- 你一定要在()中写一个t1,t2,t3共享的对象。而这个对象对于t4,t5来说,不是共享的。
synchronized(){ //线程同步代码块}
这里共享的对象是:账户对象。
账户对象是共享的,那么this就是账户对象!
不一定是this,这里只要是多线程共享的那个对象就行
在Java语言中,任何一个对象都有‘一把锁’,其实这把锁就是标记。(只是把它叫做锁)
100个对象,100把锁
以下代码的执行原理:
- 假设t1和t2线程并发,开始执行以下代码的时候,肯定有一个先,一个后。
- 假设t1先执行了,遇到了synchronized,这个时候自动找”后面共享对象“的对象锁,找到之后,并占有这把锁,然后执行同步代码块结束,这把锁才会释放。
- 假设t1已经占有这把锁,此时t2也遇到synchronized关键字,也会去占有后面共享对象的这把锁,结果这把锁被t1占有,t2只能在同步代码块外面等待t1的结束,直到t1把同步代码块执行结束了,t1会归还这把锁,此时,t2终于等到这把锁,然后t2占有这把锁之后,进入同步代码块执行程序。
- 这样就达到了线程排队执行
- 这里需要注意的:这个共享对象一定要选好了。这个共享对象一定是你需要排队执行的线程对象所共享的
//取款 public void withdarw(double money) { //以下几行代码必须是线程排队的,不能并发。 //一个线程把这里的代码全部执行结束之后,另一个线程才能进来 /* *线程同步机制的语法是: synchronized() { //线程同步代码块 } */ synchronized (this) { double before = this.getBalance(); double after = before-money; //模拟网络延迟 一定出问题 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //更新余额 this.setBalance(after); } }
4.5.2.1对synchronized的理解
4.5.2.2.Java中有三大变量(线程安全问题)?
- 实例变量:在堆中。
- 静态变量:在方法区中。
- 局部变量:在栈中。
以上三大变量中:
局部变量永远都不会存在线程安全问题。
因为局部变量不共享。(一个线程一个栈)
局部变量在栈中。所以局部变量永远都不会共享。
实例变量在堆中,堆只有一个。
静态变量在方法区中,方法去只有一个。
堆和方法都是多线程共享的,所以可能存在线程安全问题。
局部变量+常量:不会有线程安全问题。
成员变量:可能会有线程安全问题。
4.5.2.3在实例方法上可以使用synchronized吗?可以的
- synchronized出现在实例方法上,一定锁的是this。没得挑。只能是this。不能是其他的对象了。所以这种方式不灵活。
- 另外还有一个缺点:synchronized出现在实例方法上,表示整个方法体都需要同步,可能会无故扩大同步的范围,导致程序员的执行效率降低。所以这种方式不常用。
- synchronized使用在实例方法上有什么优点? 代码写得少了,节鉴了。
- 如果共享的对象就是this,并且需要同步的代码块是整个方法体, 建议使用这种方式。
4.2.4.如果使用局部变量的话:
建议使用:StringBuilder。
因为局部变量不存在线程安全问题。选择stringBuilder.
StringBuffer效率比较低。
- ArratList是非线程安全的。
- Vector是线程安全的。
- HashMap HashSet是非线程安全的。
- Hashtable是线程安全的。
4.5.3.synchronized总结
synchronized有两种写法:
- 第一种:同步代码块 灵活
synchronized(线程共享对象){ 同步代码块;}
- 第二种:在实例方法上使用synchronized
表示共享对象一定是this
并且同步代码块是整个方法体。
- 第三种:在静态方法上使用synchronized 表示找类锁。
类锁永远只有1把。
就创建了100个对象,那类锁也只有一把。
- 对象锁:1个对象1把锁,100个对象100把锁。
- 类锁:100个对象,也可能只是1把类锁。
4.5.4.synchronized面试题
package com.newXianCheng.ThreadSafe3; /** * * @Description: synchronized 面试题: 第二个线程是否运行的方法是否会 * @auther:Li Ya Hui * @Time:2021年5月16日上午10:46:12 */ //测试类 public class Exam { public static void main(String[] args) { System.out.println(~-12); System.out.println(~12); MyClass mc = new MyClass(); MyThread t1 = new MyThread(mc); MyThread t2 = new MyThread(mc); t1.setName("t1"); t2.setName("t2"); t1.start(); try { Thread.sleep(1000);//保证t1线程先执行 } catch (InterruptedException e) { e.printStackTrace(); } t2.start(); } } //线程类 class MyThread extends Thread { private MyClass mClass ; public MyThread(MyClass myClass) { this.mClass = myClass; } public void run() { if(Thread.currentThread().getName().equals("t1")) { mClass.doSome(); }else if(Thread.currentThread().getName().equals("t2")) { mClass.doOther(); } } } //我的任务类 class MyClass { public synchronized void doSome() { System.out.println("doSome begin"); try { Thread.sleep(1000*3); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("doSome over"); } public void doOther() { System.out.println("doOther begin"); System.out.println("doOther over"); } }
- 判断t2线程是否会等待t1线程结束
- 结果证明,当一个方法锁住当前对象时,有线程去运行时,其他没有锁的方法在别的线程去运行时并不会等待前一个线程
4.5.5.死锁演示
package com.newXianCheng.ThreadSafe3.DeadLock; /** * @Description: * 死锁代码要会写 * 一般面试官要求你写。 * 只有会写的,才会在以后的开发中注意这个事儿。 * 因为死锁很难调试 * @auther:Li Ya Hui * @Time:2021年5月16日下午6:55:47 */ public class DeadLock { public static void main(String args[]) { Object o1 = new Object(); Object o2 = new Object(); Thread t1 = new MyThread1(o1, o2); Thread t2 = new MyThread2(o1, o2); t1.start(); t2.start(); } } /** * @Description: 死锁演示 * @auther:Li Ya Hui * @Time:2021年5月16日下午6:58:42 */ class MyThread1 extends Thread{ Object o1 ; Object o2 ; public MyThread1(Object o1 , Object o2){ this.o1 = o1 ; this.o2 = o2; } public void run() { synchronized (o2) { try { Thread.sleep(1); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } synchronized (o1) { System.out.println("1"); } } } } class MyThread2 extends Thread{ Object o1 ; Object o2 ; public MyThread2(Object o1 , Object o2){ this.o1 = o1 ; this.o2 = o2; } public void run() { synchronized (o1) { try { Thread.sleep(1); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } synchronized (o2) { System.out.println("2"); } } } }
4.5.6.开发中怎么解决线程安全问题
聊一聊,我们以后开发中应该怎么解决线程安全问题?
- 是一上来就选择线程同步吗?synchronized
- 不是,synchronized会让程序的执行效率降低,用户体验不好。
- 系统的用户吞吐量降低。用户体验极差。在不得已的情况下在选择线程同步机制。
第一种方案:尽量使用局部变量代替“实例变量和静态变量”。
第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。(一个线程对应1个对象,100个线程对应100个对象,对象不共享,就没有数据安全问题了。)
第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候就只能synchronized了。线程同步机制。
5.线程剩余内容(难度:⭐⭐⭐)
5.1.守护线程
Java语言中线程分为两大类:
- 一类是:用户线程
- 一类是:守护线程(后台线程)
- 其中具有代表性的是:垃圾回收线程(守护线程)。
守护线程的特点:
- 一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束。
注意:主线程main方法是一个用户线程
守护线程用在什么地方呢?
- 每天00:00的时候系统数据自动备份。
- 这个需要使用到定时器,并且我们可以将定时器设置为守护线程。
- 一直在那里看着,每到00:00的时候就备份一次。所有的用户线程如果结束了,守护线程自动退出,没有必要进行数据备份了。
设置守护线程的语法
- Thread.setDaemon(true);
package com.newXianCheng.ThreadShouHU; /** * @Description: 守护线程 * @auther:Li Ya Hui * @Time:2021年5月16日下午9:29:31 */ public class Test01 { public static void main(String[] args) { Thread t2 = new bakDatathread(); t2.start(); Thread t = new bakDatathread(); t.setName("备份数据的线程"); //设置守护线程 //启动线程之前,将线程设置为守护线程 t.setDaemon(true); t.start(); //主线程 : 主线程是用户线程 for (int i = 0; i < 2; i++) { System.out.println(Thread.currentThread().getName()+"-->"+i); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } class bakDatathread extends Thread{ public void run() { int i = 0; //即使是死循环,但由于该线程是守护者,当用户线程结束后,守护线程自动终止。 while(true) { System.out.println(Thread.currentThread().getName()+" -- "+(++i)); try { Thread.sleep(1999); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
需要注意的一些知识点:
- 守护线程–也称“服务线程”,在没有用户线程可服务时会自动离开。
- 守护线程就是运行在系统后台的线程,如果JVM中只有守护线程,则JVM退出。
- Main主线程结束了(Non-daemon thread), 如果此时正在运行的其他threads是daemon threads , JVM会使得这个threads停止 , JVM也停下 , 如果此时正在运行的其他threads有Non-daemon threads,那么必须等所有的Non daemon线程结束了,JVM才会停下来。
- 必须等所有的Non-daemon线程都运行结束了,只剩下daemon的时候,JVM才会停下来,注意Main主程序是Non-daemon线程.
- 线程划分为用户线程和后台(daemon)进程,setDaemon将线程设置为后台进程
- 典型的守护线程例子是JVM中的系统资源自动回收线程, 我们所熟悉的Java垃圾回收线程就是一个典型的守护线程。
- setDarmon()方法在start()方法之前。
- setDaemon方法把java的线程设置为守护线程,此方法的调用必须在线程启动之前执行。
5.2.定时器
定时器的作用
- 间隔特定的时间,执行特定的程序。
- 每周要进行银行账户的总帐操作。
- 每天要进行数据的备份操作。
- 在实际开发中,每隔多久执行一段特定的程序,这种需求是很常见的,那么在Java中其实可以采用很多中方式实现:
- 可以使用sleep方法,睡眠,设置睡眠时间,每到这个时间点醒来,执行任务。这种方式是最原始的定时器。(比较low)
- 在Java的类库中已经写好了一个定时器:java.util.Timer,可以直接拿来用。不过,这种方式在目前的开发中很少用,因为现在有很多高级框架都是支持定时任务的。
- 在实际的开发当中,目前使用较多的是spring框架中提供的springTask框架,这个框架只要进行简单的配置,就可以完成定时器的任务。
5.3.实现线程的第三种方式:FutureTask方式,实现Callable接口。(JDK8新特性)
- 这种方式实现的线程可以获取线程的返回值。
- 之前讲解的那两种方式是无法获取线程的返回值的,因为run方法返回void。
思考:
- 系统委派一个线程去执行一个任务,该线程执行完任务之后,可能会有一个执行结果,我们怎么能拿到这个执行结果呢?
- 使用第三种方式:实现Callable接口方式。
语法
package com.newXianCheng.CallableTest; /** * @Description: 实现线程的第三种方式: * 实现Callable接口 * 这种方式的优点:可以获取到线程的执行结果。 * 这种方式的缺点:效率比较低,在获取t线程的时候,当前线程受阻塞,效率较低。 * @auther:Li Ya Hui * @Time:2021年5月19日上午11:13:56 */ import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class CallableTest01 { public static void main(String[] args) throws InterruptedException, ExecutionException { //第一步,创建一个“未来任务类”对象。 //参数非常重要:效率比较低,在获取t线程的时候,当前线程受阻塞,效率较低。 FutureTask task = new FutureTask(new Callable() { @Override public Object call() throws Exception { //线程执行一个任务 , 执行之后可能会有一个执行结果 //模拟执行 System.out.println("call method begin"); Thread.sleep(1000*3); System.out.println("call method end"); int a=100; int b=100; return a+b; // (自动装箱 integer) } }); //创建线程对象 Thread t =new Thread(task); //启动线程 t.start(); //现在是main方法,主线程中 //在主线程中,怎么获取t线程的返回结果? System.out.println(task.get()); //main方法这里的程序要想执行必须等待get方法的结束 //而get()方法可能需要很久。因为get方法是为了那另一个线程的执行结果 //另一个线程执行是需要时间的。 } }
5.3.1.关于分支栈开启Callable会不会造成main栈堵塞的问题
- 回答:不会,c1线程的get方法只会对t1线程造成堵塞,不会对main方法造成影响
5.4.关于Object类中的wait方法和notify方法。(生产者和消费者模式!)
wait和notify方法不是线程对象的方法,是Java中任何一个Java对象都有的方法,因为这两个方式是object类中自带的。
- wait方法和notify方法不是通过线程对象调用的,
- 不是这样的:t.wait(),也不是这样的:t.notify()…不对。
wait方法的作用?
- Object o = new Object();
- o.wait();
- 表示:
- 让正在o对象上活动的线程进入等待状态,无限期待等待,直到唤醒为止
- o.wait;方法的调用,会让“当前线程(正在o对象上活动的线程)”进入等待状态。
notify()方法作用?
- Object o = new Object();
- o.notify();
- 表示:
- 唤醒正在o对象上等待的线程。
- 还有一个notifyAll()方法:
- 这个方法是唤醒o对象上处于等待的所有线程。
重点:
- o.wait()方法会让正在o对象上活动的当前线程进入等待状态,并且释放之前占有的o对象的锁。
- o.notify()方法只会通知,不会释放之前占有的o对象的锁。
生产者和消费者 是为了专门解决某个特定需求的。
- 一个线程负责生产 、 一个线程负责消费
- 最终要达到生产和消费必须均衡。
- 例如:
- 生产满了,就不能再继续生产了,必须让消费线程进行消费。
- 消费完了,就不能再消费了,必须让生产线程进行生产。
- 仓库是多线程共享,所以需要考虑仓库的线程安全问题。
- 仓库对象最终调用wait()和notify()方法。
- wait()和notify()建立在synchronized线程同步的基础之上。
- 代码
package com.newXianCheng.producer; import java.util.ArrayList; import java.util.List; /** * @Description: * 1.使用wait方法和notify方法实现“生产者和消费者模式” * 2.什么是“生产者和消费者模式”? * 生产者负责生产,消费线程负责消费。 * 生产线程和消费线程要达到均衡。 * 这是一种特殊的业务需求,在这种特殊的情况下需要使用wait方法和notify方法。 * 3.wait和notify方法不是线程对象的方法,是普通Java对象都有的方法。 * 4.wait方法和nitify方法建立在线程同步的基础之上。因为多线程要同时操作一个仓库,有线程安全问题。 * 5.wait方法作用:o.wait()让正在o对象上活动的线程t进入等待状态,并且释放掉t线程之前占有的o对象的锁。 * 6.notify方法作用:o.notify()让正在o对象上等待的线程唤醒,只是通知,不会释放o对象上之前占有的锁 * 7.模拟这样一个需求: * 仓库我们采用List集合。 * List集合中假设只能存储1个元素。 * 1个元素就表示仓库满了。 * 如果List中元素个数是0,就表示仓库空了。 * 保证List中永远都是最多存储1个元素。 * * @auther:Li Ya Hui * @Time:2021年5月19日下午8:30:34 */ public class Test { public static void main(String[] args) { //创建仓库对象 List list = new ArrayList(); //创建两个线程对象 //生产者线程 Thread t1 = new Thread(new Producer(list)); //消费者线程 Thread t2 = new Thread(new Consumer (list)); t1.setName("生产者线程"); t2.setName("消费者线程"); t1.start(); t2.start(); } } //生产线程 class Producer implements Runnable{ //仓库 private List list ; public Producer(List list) { this.list = list ; } @Override public void run() { //一直生产(使用死循环模拟一直生产) while (true) { //给仓库list加锁 synchronized (list) { if (list.size()>0) { //当前线程进入等待状态,并且释放List集合的锁 try { list.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //程序能够执行到这里说明仓库是空的,可以生产 Object obj = new Object(); list.add(obj); System.out.println(Thread.currentThread().getName() + "--->" + obj); //唤醒消费者,进行消费 list.notify(); } } } } //消费线程 class Consumer implements Runnable{ //仓库 private List list ; public Consumer(List list) { this.list = list ; } @Override public void run() { //一直消费 while (true) { synchronized (list) { if (list.size() == 0) { try { //仓库空了,消费者线程等待, 释放掉List集合的锁 list.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } //程序执行到此处说明仓库中有数据,进行消费。 Object obj = list.remove(0); System.out.println(Thread.currentThread().getName() + "--->" + obj); //唤醒生产者,进行生产 list.notify(); } } } }