当前位置:   article > 正文

java中的线程安全

java中的线程安全

目录

一、分析线程安全

1.通过实现Runnable接口

2.通过继承Thread类

3.继承Thread类创建线程与实现Runnable接口创建线程的区别

4.线程状态

二、死锁

1.概述:

2.锁嵌套

三、生产者和消费者的线程安全

四、线程池

概述:

应用:

Callable接口

同步、异步在多线程中的体现


一、分析线程安全

在多线程中,出现了共享数据,需要加锁处理;锁的注意事项: 1.同一个锁对象 2.锁的范围

实现任务案例:买票系统,5个窗口共卖1000张票;观察卖票过程

1.通过实现Runnable接口

  1. //例如: 窗口1正则卖第1000张票;
  2. // 窗口5正则卖第999张票;
  3. // 窗口3正则卖第998张票;
  4. // ...
  5. // 窗口3正则卖第1张票;
  6. //分析:创建5个窗口-5个线程 票数-共享数据(安全隐患)
  7. //方式1:实现Runnable方式
  8. //问题1:出现部分重票问题
  9. //原因:共享数据有篡改数据--有隐患;
  10. //解决方案:加锁 锁住共享数据操作; 需要考虑锁的范围
  11. //问题2:出现负数
  12. //原因:临界点判断出现了问题
  13. //处理方案: 在锁里继续判断;大于0才买票
  14. //优化升级:买完票应该要有退出的提示,且提示应该是完整的
  15. //使用while(true)循环,表示只有一个出口退出即可
  16. //锁的分类:同步代码块 同步方法
  17. class Task implements Runnable{
  18. private int ticket = 1000; //1000张票-共享数据
  19. @Override
  20. public void run() {
  21. while(true) {
  22. /*
  23. //同步代码块方式
  24. synchronized (this) { //"lock" 静态对象
  25. if(ticket>0) {
  26. //Thread.currentThread().getName():实现任务获取的线程名
  27. System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票");
  28. ticket--;
  29. }else {
  30. System.out.println(Thread.currentThread().getName()+"已经卖完了");
  31. break;
  32. }
  33. }*/
  34. if(save()) { //同步方法调用
  35. break;
  36. }
  37. }
  38. }
  39. //同步方法: 如何确保同一个锁对象?谁调的方法就是哪个对象--this(同一个锁对象)
  40. private synchronized boolean save() {
  41. if(ticket>0) {
  42. //Thread.currentThread().getName():实现任务获取的线程名
  43. System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票");
  44. ticket--;
  45. return false; //返回false表示还可继续循环卖票
  46. }else {
  47. System.out.println(Thread.currentThread().getName()+"已经卖完了");
  48. return true; //结束卖票
  49. }
  50. }
  51. }
  52. public class Test1 {
  53. public static void main(String[] args) {
  54. Task task = new Task();
  55. for(int i=1;i<=5;i++) {
  56. new Thread(task,"窗口"+i).start();
  57. }
  58. }
  59. }

2.通过继承Thread类

继承案例:

  1. //案例:买票系统,5个窗口共卖1000张票;观察卖票过程:
  2. //方式2:继承Thread方式
  3. //问题1:每new线程对象,都有一份独立的属性--卖了5000张
  4. //处理方案:将属性变为共享数据 属性+static--静态属性值维护一份
  5. //问题2:会出现部分重票问题----原因是this不是同一个锁对象
  6. //解决方案:锁对象变为字符串常量
  7. class MyThread extends Thread{
  8. private static int ticket = 1000; //1000张票-共享数据
  9. public MyThread(String name) {
  10. super(name);
  11. }
  12. @Override
  13. public void run() {
  14. while(true) {
  15. //同步代码块方式
  16. synchronized ("lock") {
  17. if(ticket>0) {
  18. System.out.println(getName()+"正在卖第"+ticket+"张票");
  19. ticket--;
  20. }else {
  21. System.out.println(getName()+"已经卖完了");
  22. break;
  23. }
  24. }
  25. }
  26. }
  27. }
  28. public class Test2 {
  29. public static void main(String[] args) {
  30. for(int i=1;i<=5;i++) {
  31. new MyThread("窗口00"+i).start();
  32. }
  33. }
  34. }

3.继承Thread类创建线程与实现Runnable接口创建线程的区别

实现任务 VS 继承Thread

实现任务:

里面的属性是共享数据;同步锁可使用this;获取线程名需使用Thread.currentThread().getName()

继承Thread:

属性+static才是共享数据;同步锁不能使用this;获取线程名直接使用getName()

4.线程状态

线程状态---阻塞

基本状态---启动状态 等待状态----sleep,join

阻塞状态:在执行的现在中加锁,进入阻塞状态;其他线程只能等待; 等待释放锁资源后,其他就绪状态线程才能继续执行

二、死锁

1.概述:

加了锁之后,有一个线程进到锁里面没有出来,导致锁资源没有得到释放,其他线程一直等待锁资源的释放,这样就导致了死锁的产生

同步代码块和同步方法都是自动释放锁资源,所以不容易出现死锁;如果需要演示死锁案例;在同步锁中需要进行锁嵌套;

注意:此处只是为了测试死锁,才进行的锁嵌套; 以后使用时尽量避免锁嵌套(避免死锁)

2.锁嵌套

代码解释:当一个线程执行到锁A后,另一个线程进入锁B,导致进入到锁A中的线程一直等待锁B资源的释放,而锁B一直等待锁A资源的释放,A里面有B想要的,B里面有A想要的,但是彼此都是倔脾气谁都不肯低头,谁都等对方道歉还回来资源,导致这是一个死局,这就是死锁产生,所以我们要避免使用锁嵌套,因为容易产生死锁

死锁案例:

  1. /死锁案例:两个线程,各自执行自身的代码
  2. class MyThread extends Thread{
  3. private boolean a; //标记判断
  4. public MyThread(boolean a) {
  5. this.a = a;
  6. }
  7. @Override
  8. public void run() {
  9. if(a) {
  10. synchronized ("A") { //线程1
  11. System.out.println("进入线程1的A锁");
  12. synchronized ("B") {
  13. System.out.println("进入线程1的B锁");
  14. }
  15. }
  16. }else {
  17. synchronized ("B") { //线程2
  18. System.out.println("进入线程2的B锁");
  19. synchronized ("A") {
  20. System.out.println("进入线程2的A锁");
  21. }
  22. }
  23. }
  24. }
  25. }
  26. public class Test1 {
  27. public static void main(String[] args) {
  28. new MyThread(true).start(); //线程1
  29. new MyThread(false).start(); //线程2
  30. }
  31. }

三、生产者和消费者的线程安全

生产者和消费者的线程安全模型,代表类很多种应用中的场景; 例如:生活中的生产产品入库;消费产品出库;

生产者和消费者案例:

//案例:生产者消费者线程案例,生产者一直负责生产,消费者一直负责消费
//分析:生产者线程和消费者线程都操作同一个库存(共享数据)
//问题:
//1.会出现还没有等到生产,就已经消费了的情况  
//2.数据混乱:生产者线程数量+1后,直接打印出了消费数据

//处理方案:
//数据混乱--需要加锁来解决
//没有生产提前消费的问题:线程等待的功能,判断件数为0,一直等待,无限期等待,直到发指令
//wait()--等待;  notify--唤醒(唤醒等待)

main方法:

  1. public class Test1 {
  2. public static void main(String[] args) {
  3. Store store = new Store();
  4. //生产者和消费者都操作同一个库存
  5. new Producter(store).start();
  6. new Customer(store).start();
  7. }
  8. }

生产者:

  1. public class Producter extends Thread { //生产者线程
  2. private Store store;
  3. public Producter(Store store) { //从外面传入库存对象
  4. this.store = store;
  5. }
  6. @Override
  7. public void run() {
  8. while(true) {//循环生产
  9. try {
  10. store.push(); //生产者负责入库
  11. } catch (Exception e) {
  12. e.printStackTrace();
  13. }
  14. }
  15. }
  16. }

消费者:

  1. public class Customer extends Thread { //消费者线程
  2. private Store store; //消费者操作库存
  3. public Customer(Store store) {
  4. this.store = store;
  5. }
  6. @Override
  7. public void run() {
  8. while(true) { //循环消费
  9. try {
  10. store.pop(); //消费者出库
  11. } catch (InterruptedException e) {
  12. e.printStackTrace();
  13. }
  14. }
  15. }
  16. }

仓库:仓满和仓空由库存决定--在库存中判断

  1. public class Store {
  2. private int count; //库存数量的标记--共享数据,库存对象只有一个,那么成员也只有一个
  3. public void push() throws InterruptedException {//负责生产的功能
  4. synchronized (this) { //this可以表示同一个锁对象
  5. if(count>=20) { //仓满则停止生产
  6. this.wait(); //等待;释放锁资源
  7. }
  8. count++;
  9. System.out.println("已经生产了第"+count+"件货");
  10. this.notify(); //唤醒,唤醒等待的线程(唤醒对方)
  11. }
  12. }
  13. public void pop() throws InterruptedException { //负责消费的功能
  14. synchronized (this) {
  15. if(count<=0) {//仓空停止消费
  16. this.wait(); //等待;释放锁资源
  17. }
  18. System.out.println("已经消费了第"+count+"件货");
  19. count--;
  20. this.notify(); //唤醒,唤醒生产者
  21. }
  22. }
  23. }

四、线程池

概述:

线程池:就是装线程的容器,预先在容器中创建指定个数的线程对象;当用户需要时,直接俄从容器中获取;用完了,再回收到线程池中(用完了放回去

之前创建线程的方式:创建线程对象后,执行完毕则销毁线程对象;如果程序中需要频繁创建线程时,会非常影响性能及消耗内存的资源(用完了就丢

好处:减少了创建和销毁线程的数目,提升了性能及减少了资源的消耗

线程池的复用机制:线程对象使用完后,重新再回到线程池;可以交给其他用户继续使用

内部实现:创建集合,集合中都是存线程对象;有用户使用,则添加回集合中

应用:

代码解释:指定线程池中两个线程,然后开启了三个任务,两个线程先执行两个任务,谁先执行完就去执行第三个任务

  1. //线程池:
  2. class Task implements Runnable{
  3. @Override
  4. public void run() {
  5. for(int i=1;i<=10;i++) {
  6. System.out.println(Thread.currentThread().getName()+"-->"+i);
  7. }
  8. }
  9. }
  10. public class Test1 {
  11. public static void main(String[] args) {
  12. //在线程池中创建单个线程对象,通过单个线程对象去处理多个任务
  13. //ExecutorService es = Executors.newSingleThreadExecutor();
  14. //带缓冲区的线程池:有多少个任务,则会产生多少个线程对象的处理
  15. //ExecutorService es = Executors.newCachedThreadPool();
  16. //带复用机制的线程池:指定线程池个数,进行复用
  17. ExecutorService es = Executors.newFixedThreadPool(2);//(常用)
  18. es.submit(new Task()); //任务接口Runnable
  19. es.submit(new Task());
  20. es.submit(new Task()); //有三个任务,则谁先执行完,再执行第3个--复用
  21. es.shutdown(); //线程池的终止
  22. }
  23. }

Callable接口

在线程池的执行中,有两个接口表示执行线程任务的接口;Runnable和Callable接口

区别:Callable接口可以带返回值;线程的执行结果需要返回时,可以选择Callable

线程池的执行,通过Runnable接口、 Callable接口来 创建任务 ,而不是继承Thread类

案例:

  1. //需求:使用两个线程并发的执行1~50,51~100的和;并汇总结果
  2. //线程池中有Callable接口,可以带返回结果
  3. public class Test2 {
  4. public static void main(String[] args) throws InterruptedException, ExecutionException {
  5. ExecutorService es = Executors.newFixedThreadPool(2);
  6. Future<Integer> fu1 = es.submit(new Callable<Integer>() {
  7. @Override
  8. public Integer call() throws Exception { //线程1
  9. int sum = 0;
  10. for(int i=1;i<=50;i++) {
  11. sum += i;
  12. }
  13. return sum;
  14. }
  15. });
  16. Future<Integer> fu2 = es.submit(new Callable<Integer>() {
  17. @Override
  18. public Integer call() throws Exception { //线程2
  19. int sum = 0;
  20. for(int i=51;i<=100;i++) {
  21. sum += i;
  22. }
  23. return sum;
  24. }
  25. });
  26. int sum = fu1.get()+fu2.get(); //get是阻塞方法--和join类似
  27. System.out.println("和为:"+sum);
  28. es.shutdown();
  29. }
  30. }

同步、异步在多线程中的体现

问题:什么是异步?,什么是同步?,在多线程中如何体现的?

异步:多个线程并发执行--随机互抢资源

同步:加了同步锁,只运行一个线程执行

上述案例中,fu1和fu2线程的执行就是异步的;get方法是同步的,等待多线程处理完,才进行汇总; 加锁也是属于同步里面的。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小惠珠哦/article/detail/900219
推荐阅读
相关标签
  

闽ICP备14008679号