Java多线程(拓展)
不提供代码,希望大家动动手指敲敲代码!
三种实现方式:
-
Thread
-
Runnable
-
Callable
Thread
作为子线程的类需继承Thread类,重写run()方法。
创建继承了Thread类的对象,使用start()方法启动子线程。
主线程(main())与子线程同时运行:
结果:
Main:26
Thread:3
Main:27
Thread:4
Main:28
Thread:5
Thread:6
Main:29
Thread:7
Main:30
Thread:8
Main:31
Thread:9
Thread:10
Thread:11
Runnable(主要)
作为子线程的类需要实现Runnable接口,重写run()方法。
创建实现Runnable接口的对象,再创建Thread对象,需传入参数,类型为Runnable,再使用start()方法启动子线程。
主线程(main())与子线程同时运行:
结果:
runnableTest:64
runnableTest:65
runnableTest:66
Main:18
runnableTest:67
Main:19
runnableTest:68
runnableTest:69
runnableTest:70
并发问题
模拟演示抢票过程
线程3:获得第2张票
线程2:获得第1张票
线程1:获得第2张票
线程1:获得第3张票
线程2:获得第4张票
线程3:获得第5张票
线程2:获得第7张票
线程3:获得第8张票
线程1:获得第6张票
线程1:获得第10张票
线程3:获得第9张票
在获取票的时候:
当线程2刚获取第一张票,执行total操作后,此时total=2。
此时线程3和线程1同时执行获取操作,当时total=2,所以线程3和线程1都是total=2。
Callable(了解)
结果:
true
小结
继承Thread类:
- 子类继承Thread类具备多线程能力
- 启动线程:子类对象.start
- 不建议使用:避免OOP单继承局限性
实现Runnable接口:
- 实现接口Runnable具有多线程能力
- 启动线程:传入目标对象+Thread对象.start()
- 推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用(共享)
静态代理
Runnable使用的是静态代理
结果:
婚前准备
xx要结婚了
婚后结算
Java代理模式
lamda表达式
使用lamda表达式前提:是函数式接口
函数式接口:任何接口,如果只包含唯一一个抽象方法,那么它就是一个函数式接口。
对于函数式接口,我们可以通过lambda表达式来创建该接口的对象。
多线程五大状态
新建状态(new)
当用new操作符创建一个线程时, 例如new Thread®,线程还没有开始运行,此时线程处在新建状态。此时他和其他java对象一样,仅仅由Java虚拟机为其分配内存并初始化成员变量值。
就绪状态(Runnable)
-
一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的*start()方法。当线程对象调用start()*方法即启动了线程,*start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()*方法返回后,线程就处于就绪状态。
-
处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。因为在单CPU的计算机系统中,不可能同时运行多个线程,一个时刻仅有一个线程处于运行状态。因此此时可能有多个线程处于就绪状态。对多个处于就绪状态的线程是由Java运行时系统的线程调度程序*(*thread scheduler*)*来调度的。
运行状态(Running)
表示某线程对象被CUP调度器调度,执行线程体。就绪状态和运行状态可以相互切换,切换的原因依旧参照CUP调度器调度了哪一个线程。
阻塞状态(Blocked)
- 阻塞状态:正在运行的线程遇到某个特殊情况,如同步、等待I/O操作完成等。进入阻塞状态的线程让除CPU资源,并暂时暂停自己的执行。
- 等待状态:有时一个运行状态线程转变成等待线程,它会等待另一个线程来执行一个任务,一个等待状态的线程只有通过另一个线程通知它转到可运行状态,才能继续执行
- 计时等待状态:进入等待状态的线程如果没有其他线程对象来召唤它,那么该线程对象将一直等待下去,此时我们可考虑设计一个类似于闹钟一样的提示器,该提示器会在等待特定时间段之后唤醒该线程对象。
终止状态(Dead)
即死亡状态,表示线程终止。当线程成功执行完成或线程抛出未捕获的Exception或Error或调用线程的stop方法(易导致死锁,不推荐)
有两个原因会导致线程死亡:
- run方法正常退出而自然死亡。
- 一个未捕获的异常终止了run方法而使线程猝死。
为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用isAlive方法。如果是可运行或被阻塞,这个方法返回true; 如果线程仍旧是new状态且不是可运行的, 或者线程死亡了,则返回false。
操作线程的方法
线程休眠方法
sleep()–单位为毫秒
- 让当前线程暂缓执行,等到了预计时间后再恢复执行
- 线程休眠会立刻交出CPU,但是不会释放锁
- 状态切换:从运行态到阻塞态再到就绪态
sleep实现倒计时
线程让步
yield()方法
- 暂停执行当前线程对象,并执行其他线程
- Yield()方法会让当前线程交出CPU,同样不会释放锁,
- yield()方法无法控制具体交出CPU的时间,并且yield()方法
- 只能让拥有相同优先级的线程有获取CPU的机会
- 状态切换:从运行态返回就绪态
遇到线程1时,线程1让步。
线程插队
join() 方法
参数:毫秒单位,0为无限等待
- 等待该线程终止,如果在主线程中调用该方法,会让主
- 程休眠,让调用该方法的线程执行完毕后再恢复执行主线程.
- 会释放对象锁
- 其实:Join方法只是对Object提供的wait() 做了一层包装而已
线程停止的
三种方法
-
设置标记位停止线程,可以是线程正常退出(建议使用)
-
调用stop方法强制停止线程
该方法不安全已被Deprecated。
为什么不安全?因为stop会解除由线程获取的所有锁定,当在一个线程对象上调用stop()方法时,这个线程对 象所运行的线程就会立即停止
-
调用Thread类的interrupt() 方法
interrupt() 方法只是将线程状态置为中断状态而已,它不会中断一个正在运行的线程,此方法只是给线程传递一个中断信号,程序可根据此信号来判断是否需要终止。
当线程中使用了wait(),sleep()以及join()方法导致线程阻塞,则interrupt()会在线程中抛出InterruptException,在catch块中捕获该异常,然后退出,并且将线程的中断状态由true置为false。
当线程中没有wait, sleep, join,调用interrupt只是将线程状态置为interrupt=true
无wait(),sleep()以及join()方法:
最终结果:终断线程,线程状态从false-->true
有sleep()方法:
最终结果:没有终断线程,线程状态一直都是false
线程挂起与执行
不安全,过时
Suspend()用于挂起线程,Resume()用于继续执行已经挂起的线程。可以使用这两个方法进行线程的同步,和Start()方法有些类似的是:在调用Suspend方法后不会立即的停止,而是执行到一个安全点后挂起。
线程优先级
Java线程可以有优先级的设定,高优先级的线程比低优先级的线程有更高的几率得到执行
- 优先级可以用从1到10的范围指定。10表示最高优先级,1表示最低优先级,5是普通优先级(默认级)。
- 记住优先级最高的线程在执行时被给予优先。但是不能保证线程在启动时就进入运行状态。与在线程池中等待运行机会的线程相比,当前正在运行的线程可能总是拥有更高的优先级。
- 最终由调度程序决定哪一个线程被执行。
- 记住在线程开始方法被调用之前,线程的优先级应该被设定。
- 你可以使用常量,如MIN_PRIORITY,MAX_PRIORITY,NORM_PRIORITY来设定优先级。
public final void setPriority(int newPriority);//设置线程优先级
thread3线程先执行的几率比其他两个高,只能说是几率。
守护线程与普通线程
守护线程与普通线程的唯一区别是:当JVM中所有的线程都是守护线程的时候,JVM就可以退出了;如果还有一个或以上的非守护线程则不会退出。(以上针对正常退出,调用System.exit则必定会退出)
setDeamon(true):告诉JVM不需要等待它退出
守护线程在没有用户线程可服务时自动离开,在Java中比较特殊的线程是被称为守护(Daemon)线程的低级别线程。这个线程具有最低的优先级,用于为系统中的其它对象和线程提供服务。将一个用户线程设置为守护线程的方式是在线程对象创建之前调用线程对象的setDaemon方法。典型的守护线程例子是JVM中的系统资源自动回收线程,我们所熟悉的Java垃圾回收线程就是一个典型的守护线程,当我们的程序中不再有任何运行中的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是Java虚拟机上仅剩的线程时,Java虚拟机会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依赖于系统,与系统“同生共死”。那Java的守护线程是什么样子的呢。当JVM中所有的线程都是守护线程的时候,JVM就可以退出了;如果还有一个或以上的非守护线程则JVM不会退出。
Java有两种Thread:“守护线程Daemon”与“用户线程User”。
用户线程:Java虚拟机在它所有非守护线程已经离开后自动离开。
守护线程:守护线程则是用来服务用户线程的,如果没有其他用户线程在运行,那么就没有可服务对象,也就没有理由继续下去。
setDaemon(boolean on)方法可以方便的设置线程的Daemon模式,true为Daemon模式,false为User模式。setDaemon(boolean on)方法必须在线程启动之前调用,当线程正在运行时调用会产生异常。isDaemon方法将测试该线程是否为守护线程。值得一提的是,当你在一个守护线程中产生了其他线程,那么这些新产生的线程不用设置Daemon属性,都将是守护线程,用户线程同样。
线程同步
- 由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放所即可。存在以下问题:
- 一个线程持有锁会导致其他所有需要此锁的线程挂起
- 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延迟,引起性能问题
- 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题
三大不安全案列
买票
结果:
线程3拿到8
线程1拿到10
线程2拿到9
线程2拿到7
线程3拿到5
线程1拿到6
线程2拿到4
线程1拿到2
线程3拿到3
线程2拿到1
线程3拿到-1
线程1拿到0
出现-1,不安全
银行取钱
结果:
结婚基金余额为:0
女朋友手里的钱:100
结婚基金余额为:-50
你手里的钱:50
银行存储出现负数,不安全
ArrayList
9959
没有完整的数据,不安全
同步锁
- 由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法:synchronized方法和synchronized块
public synchronized void method(int args){}
- synchronized方法控制对”对象“的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行
缺陷:若将一个大的方法声明为synchronized将会影响效率
synchronized 同步方法,锁的是this
结果:
线程1拿到10
线程1拿到9
线程3拿到8
线程2拿到7
线程2拿到6
线程2拿到5
线程2拿到4
线程3拿到3
线程3拿到2
线程3拿到1
买票正常,安全。
同步块:synchronized(Obj){}
- Obj称之为:同步监视器
- Obj可以是任何对象,但是推荐使用共享源作为同步监视器
- 同步方法中无需指定同步监视器,因为同步方法的同步监视器就说this,就说这个对象本身,或者是class(反射中讲解)
- 同步监视器的执行过程
- 第一个线程访问,锁定同步监视器,执行其中的代码
- 第二个线程访问,发现同步监视器被锁定,无法访问
- 第一个线程访问完毕,解锁同步监视器
- 第二个线程访问,发现同步监视器没有锁,然后锁定并访问
结婚基金余额为:50
你手里的钱:50
女朋友钱不够,取不了
其他代码不变,取钱正常,安全
结果:
10000
添加数据正常,安全
JUC安全类型的集合
CopyOnWriteArrayList<>源码实际使用synchronized块实现线程安全
死锁
-
多线程以及多进程改善了系统资源的利用率并提高了系统 的处理能力。然而,并发执行也带来了新的问题——死锁。所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
-
所谓死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
一个简单的死锁类
t1先运行,这个时候flag==true,先锁定obj1,然后睡眠1秒钟
而t1在睡眠的时候,另一个线程t2启动,flag==false,先锁定obj2,然后也睡眠1秒钟
t1睡眠结束后需要锁定obj2才能继续执行,而此时obj2已被t2锁定
t2睡眠结束后需要锁定obj1才能继续执行,而此时obj1已被t1锁定
t1、t2相互等待,都需要得到对方锁定的资源才能继续执行,从而死锁。
结果:程序无法停止
线程1占用锁
线程2占用锁
Lock类
之前已经说道,JVM提供了synchronized关键字来实现对变量的同步访问以及用wait和notify来实现线程间通信。在jdk1.5以后,JAVA提供了Lock类来实现和synchronized一样的功能,并且还提供了Condition来显示线程间通信。
Lock类是Java类来提供的功能,丰富的api使得Lock类的同步功能比synchronized的同步更强大。
- Lock类实际上是一个接口,我们在实例化的时候实际上是实例化实现了该接口的类
Lock lock = new ReentrantLock();
- 用
lock.lock
来加锁,用lock.unlock
来释放锁。在两者中间放置需要同步处理的代码。
lock.lock
lock.unlock
结果:
线程2-->1
线程3-->2
线程1-->3
Lock类(拓展)
生产者消费者问题
生产者消费者问题是一个著名 的线程同步问题,该问题描述如下:有一个生产者在生产产品,这些产品将提供给若干个消费者去消费,为了使生产者和消费者能并发执行,在两者之间设置一个具有多个缓冲区的缓冲池,生产者将它生产的产品放入一个缓冲区中,消费者可以从缓存区中取走产品进行消费,显然生产者和消费者之间必须保持同步,即不允许消费者到一个空大缓冲区中取产品,也不允许生产者向一个已经放满产品的缓存区中再次投入产品。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GyqVtfWi-1596205510891)(D:\笔记\博客\images\java\多线程\834666-20171006132515411-1581950806.jpg)]
解决上述问题:
管程法
例子:
- 生产者:麦当劳做全鸡的厨师
- 消费者:用户
- 缓冲区:消费前台
结果:生产者最多存储2只,达到2只生产者线程就会沉睡。消费者消费了1或2只就或唤醒生产者线程,当消费者连续消费达到2只,消费者线程就会沉睡,等待生产者唤醒。
存储1只全鸡...
存储2只全鸡...
消费第2只全鸡...
消费第1只全鸡...
存储1只全鸡...
存储2只全鸡...
消费第2只全鸡...
存储2只全鸡...
消费第2只全鸡...
存储2只全鸡...
消费第2只全鸡...
存储2只全鸡...
消费第2只全鸡...
存储2只全鸡...
消费第2只全鸡...
省略...
信号灯法
例子:
- 生产者:演员
- 消费者:观众
- 缓冲区:TV
结果:
TV播放:快乐大本营播放中...
观众观看了:快乐大本营播放中...
TV播放:抖音记录美好生活...
观众观看了:抖音记录美好生活...
TV播放:快乐大本营播放中...
观众观看了:快乐大本营播放中...
TV播放:抖音记录美好生活...
观众观看了:抖音记录美好生活...
TV播放:快乐大本营播放中...
观众观看了:快乐大本营播放中...
线程池
- 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
- 思路:提前创建多个线程,放入线程池中,使用时直接获取,使用完放回线程池中。可以避免频繁创建销毁、实现重复利用。
- 好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中的线程,不需要每次都创建)
- 便于线程管理(…)
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程每页任务时最多保持多久时间才会终止