目录
十二、线程安全3显式锁Lock---reentrantlock
一、多线程
一个软件可以播放音乐,接收用户输入,2件事,或多件事,就通过多线程解决。
一个软件可能有多个进程,每一个进程可能有多个线程
1.线程与进程
进程 :
是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间 (有自己的堆,栈不共享)
也可以理解为应用程序。
线程:
是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行. 一个进程最少
有一个线程 。也可以理解为应用程序里的执行路径。
线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分
成若干个线程
2.线程调度:
让多条执行路径均分,更合理的交替执行。
分时调度
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
电脑的应用程序看似同时执行,实际是交替的。例:把1秒分为很多部分,在这一秒交替执行,变化很快,人感知不到。
抢占式调度
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),
Java使用抢占式调度,CPU会随机抛出分好的时间偏,由优先级最高的开始抢占。
抢占式调度。
CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核新而言,某个时刻,
只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是 在同一时
刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的 使
用率更高。
2.同步与异步
同步:排队执行 , 效率低但是安全. (并不是同时的意思)
异步:同时执行 , 效率高但是数据不安全.
即:
有100个人进屋子
同步:只有一个入口需要排对进去排队,进去后一个一个找位置坐下(安全)
异步:有很多个入口,同时进去,进去后杂乱无章的找位置坐(不安全)
同步,异步指的就是一个应用程序的进程,是一条线程执行,还是多条线程执行。
3.并发与并行
完成一个目标,多个线程做事情,各做各的。
在同步,异步,上存在抢占时间偏的过程
同步:需要排队去做,丢失时间偏后,下个线程会继续做。此时数据是接着的。
异步,不需要排队同时做,其中一个线程丢失时间偏,其他的线程还在执行。
对于丢失时间偏的线程,数据仍然是变化的。
异步的时候:
A、B、C、3个线程 并行发生,在一段时间内B丢失时间偏,又抢到时间偏回来,
在B抢到时间偏这个时间段
B和A、C是并发
A、C是并行,
实际情况下并发,并行是都会存在的,因为抢占调度不确定。
两个线程并发执行,在执行完之前,由于会丢失时间偏,再次开始时,就成了并发。
而同步不会出现,并发,并行,因为始终是一个线程,后面的线程在排队等待。
所以:线程安全问题也可以理解为异步并发的安全问题。
并发:指两个或多个事件在同一个时间段内发生(不是同时)。
并行:指两个或多个事件在同一时刻发生(同时发生)。
二、创建线程的3种方式
1.Class Thread(线程)
用于实现线程的类,线程是程序中执行的线程。 Java虚拟机允许应用程序同时运行多个执行线程。
构造方法:
Thread() | 分配新的 Thread对象。 |
Thread(Runnable target) | 分配新的 Thread对象。 |
Thread(Runnable target, String name) | 分配新的 Thread对象。 |
Thread(String name) | 分配新的 Thread对象。 |
方法:
String | getName() | 返回此线程的名称。 |
void | start() | 导致此线程开始执行; Java虚拟机调用此线程的run方法。 |
long | getId() | 返回此Thread的标识符 | |
int | 返回此线程的优先级。 | ||
void | setPriority(int newPriority) | 更改此线程的优先级。 | |
void | setDaemon(boolean on) | 将此线程标记为 daemon线程或用户线程。 |
守护线程属于守护用户线程的线程
一个进程若一个线程都没有执行就是停止
用户线程自己控制自己的死亡
若用户线程都没有了守护线程也就没有了,依附用户线程
静态方法
static void | sleep(long millis) | 导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数,具体取决于系统计时器和调度程序的精度和准确性。 |
static void | sleep(long millis, int nanos) | 导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数加上指定的纳秒数,具体取决于系统定时器和调度程序的精度和准确性。 |
字段
static int | 线程可以拥有的最大优先级。 | |
static int | 线程可以拥有的最低优先级。 | |
static int | 分配给线程的默认优先级。 |
如何开启新线程?
三种方法:
一、thread
我们想开启新的执行线程需要继承Thread,重写一个方法run()
创建线程对象,调用start()方法
两个线程异步并发执行,谁先走完不一定为什么?
主线程和子线程去抢占了CPU分配的时间,抢占式调度。
每个线程都有自己的栈空间,共用一份堆空间。
子线程里运行的方法都在子线程里运行,
由一个线程所执行的方法,这个方法就会执行在这个线程里面
有局限性,java为单继承
也可以用匿名内部类的方式创建线程:
2.Interface Runnable(线程任务)
1Runnable接口应有一个自己实例(表示线程任务)接口的实例类必须定义一个名为run的无参数的方法。
2借助线程执行的类Thread,开启线程,但是不用在写一个类去继承Thread,只用实现Runnable接口。
具体操作:
1:实现Runnable接口并重写run()方法
2.创建线程,并把线程任务传进去,并开启线程。
总结:
用线程任务Runnable相比线程Thread好处:
- 通过创建任务,然后给线程分配任务。更适合给多个线程执行相同任务的情况。更方便。
- 可以避免单继承所带来的的局限性。Java只有单继承,允许多实现。更灵活。
- 任务与线程是分类的,提高了程序的健壮性。
- 后续学习的线程池技术,只接收Runnable类型的任务,不接收Thread类型的线程。
也可以使用匿名内部类方式创建。
3.线程的特殊创建方式:带返回值的线程Callable
3.1Runnable 与 Callable
接口定义:
//Callable接口:
public interface Callable<V> {
V call() throws Exception; ----抽象方法
}
//Runnable接口:
public interface Runnable {
public abstract void run(); ---抽象方法
}
3.2如何使用Callable接口3步:
1. 编写类实现Callable接口 , 实现call方法
class XXX implements Callable<T> {
@Override
public <T> call() throws Exception {
return T;
}
}
2. 创建FutureTask对象 , 并传入第一步编写的Callable类对象
FutureTask<Integer> future = new FutureTask<>(callable);
3. 通过Thread,启动线程
new Thread(future).start();
3.3FutureTask
获取返回值:
V | get() | 如果需要等待计算完成,然后检索其结果。 |
V | get(long timeout, TimeUnit unit) | 如果需要,最多等待计算完成的给定时间,然后检索其结果(如果可用)。 |
Main线程和FutureTask线程,如果FutureTask线程调用get方法main线程会等待Future计算完成返回结果
1:实现callable,并重写call()方法
返回值:
2:main线程:
创建callable接口实例对象
创建FutureTask对象 , 并传入第一步编写的Callable类对象
通过Thread,启动线程
FutureTask调用了get方法,让main线程的一直在等待FutureTask线程执行完拿到返回值在执行。
IsDone方法判断FutureTask是否执行完
Cancel方法取消等待
3.4Runnable 与 Callable的相同点
都是接口
都可以编写多线程程序
都采用Thread.start()启动线程
3.5Runnable 与 Callable的不同点
Runnable没有返回值;Callable可以返回执行结果
Callable接口的call()允许抛出异常;Runnable的run()不能抛出
3.6Callable获取返回值
Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执 行,如果不调用不会阻塞。
三、设置和获取线程名称
1通过SetName()设置和getName()方法获取
2Thread类的静态方法correntThread();获取当前正在运行的线程
3用Thread的构造方法设置,也可以用线程对象调用Set方法
如果不设置线程名称:“锄禾日当午”,有自己默认的名称
从0开始增加,但不一定按顺序输出
四:线程休眠Sleep
每间隔1s输出一次
五、线程阻塞
不只是线程休眠!
线程任务里有100段代码,其中有一部分在读取文件,耗费1s钟,这一秒也就是阻塞的。
读完文件才会继续执行。----------------------所有比较消耗时间的操作
例如:接收用户输入,读取文件。
六、线程中断interrupt()
一个线程是独立的执行路径,是否应该结束,应该由其自身决定,会使用很多资源,也涉及很多资源需要释放。
如果由外部决定,会产生过多的垃圾占用资源。
如何使用?
1. 通过给线程对象打标记。
2.在一些特殊操作下,会检查自身是否携带标记,如果携带就触发异常,new一个异常抛出来被catch块捕捉
3.try、catch,捕捉到标记
线程对象调用interrupt()方法,打上标记。
sleep()方法,会检查自身是否携带标记,如果携带就触发异常,new一个异常抛出来被catch块捕捉
打标记只会进入catch块而已,可以选择死亡或不死亡。
只是告诉了应该死亡了,在catch块里决定是否死亡。
怎么死亡。在实现Rannable后重写的run()方法直接结束这个方法
后续在catch里需要释放资源
七、守护线程setDeamon()
必须在启动线程之前调用此方法。
True表示标记为守护线程,设置之后,主线程main,结束,守护线程也就跟着结束。
八、线程生命周期
九、 线程安全与不安全问题
线程任务子线程:
主线程:开启三个子线程任务
运行结果:
即不安全,异步并发,出现了负数
A抢到时间偏,判断为1走到休眠的过程丢失时间偏
B抢到时间偏,c也抢到,B丢失时间偏,c继续,这时候已经过了while的判断
B、C同时丢失时间偏,A拿到继续执行,得到0。
B,C后续依次抢到时间偏,就出现-1,-2.----判断的和使用的不一样
而且在设置循环时如果判断条件,在一些特定情况下还有可能出现死循环
十、线程安全1:同步代码块
怎么让一段代码排队,同步(不是同时的意思)执行
Syn chro nized(锁对象){
}
锁对象:java中任何对象都可以往这里传,相当于一个锁
首先创建了一个锁对象,
多个线程对同样的一个锁对象有使用权,但是runnable方法里进行判断时一次只能进一个,在同时抢到时间偏时,谁先使用就拿走了这个对象,谁先进入判断。
后面的线程就没有对象,等着任务执行完,再去看谁抢到时间偏,拿到锁。
共用堆内存。
1.Object为锁对象,在run()方法外面创建,并锁住了if语句,在进行if语句时都需要先过锁。
2.Run()方法才是任务启动的开始
创建3个线程执行同一个线程任务,即只有一个锁对象
总结:
通过观察Ticket类:
1.可以发现锁对象是run()方法外创建,所以创建的是同一个锁对象,
2.3个新线程Thread,都是执行同一个线程任务(就算是同一个任务,每new一个对象,都是独立的一个任务,并不是同一个任务,new对象就是新的内存空间)
2.3个线程启动,都在使用同一个锁对象
4.Synchronized是锁住可一段代码块,可以很细腻的操作部分。以代码为单位操作。
所以达到排队的目的。
反例:
1.如果不在run()方法外面创建锁对象而是在run()方法里面,就相当于每个线程都拥有自己的锁对象去进去而不是同一个锁对象,都可以进自己的Synchronized,就达不到锁的目的。
2.如果执行的任务不一样,也体现不出锁的概念,那是一个新的任务,会有自己新的锁对象和锁的代码块。
1在run()方法里面创建锁对象,就相当于每个线程都拥有自己的锁对象去进去而不是
同一个锁对象
十一、线程安全2同步方法
Synchronized锁住方法
这是锁代码块的
把需要锁的代码块抽成一个方法(放在同一个类里):
这里方法没有加锁标识
这里加了锁标识
指synchronized这个标识的方法,需要排队执行
这里的锁对象在方法里有个this,
哪一个线程调用方法this指的是同一个,而且只有一个
方法抽出来,在同一个类下,其实也相对于所代码块需要的Object,这里只是用this代替了。
----------------------------------------------------------------------------------------------------------------------
线程任务由3个子线程分别执行同一个任务
-------------------------------------------------------
若不是同一个线程任务依旧不能同步执行。
3个线程执行各自的新线程任务,只是任务一样。
同步方法和同步代码块可以同时存在。
即:在一个类里this对象只有一个,不管后面有多少锁方法,都需要通过这个锁对象,排队。
十二、线程安全3显式锁Lock---reentrantlock
同步代码块和同步方法都属于隐式锁
显式锁Lock类,子类reentrantlock
如何使用?
1.自己创建锁对象,Lock l = new ReentrantLock()
2.自己锁lock()方法
3.自己放开unlock()方法
3个线程执行同一个线程任务。
锁住lock()方法
解锁:unLock()方法
显式锁更能体现,锁对象,锁住和放锁的过程。
显式锁和隐式锁区别
公平锁和非公平锁
公平锁:解锁后,在排队时谁先等谁先用
非公平锁:解锁后,大家一块抢
线程的死锁
A线程里任务被上锁了,谁先拿到谁使用
B线程里任务被上锁了,谁先拿到谁使用
但是
A线程的任务,需要用到B线程里的东西,他就会等B线程执行完
B线程的任务,需要用到A线程里的东西,他也会等A线程执行完
2个线程互相等,就形成了死锁。
只有在A线程启动执行完毕结束线程,B线程才开始启动执行线程就不会形成死锁。
怎么避免,在锁的时候让A线程不要使用到B线程里被锁的内容。
十三、多线程通信问题
1.Class Object类
方法:
void | wait() | 导致当前线程等待它被唤醒,通常是 通知或 中断 。 |
void | wait(long timeoutMillis) | 导致当前线程等待它被唤醒,通常是 通知或 中断 ,或者直到经过一定量的实时。 |
void | wait(long timeoutMillis, int nanos) | 导致当前线程等待它被唤醒,通常是 通知或 中断 ,或者直到经过一定量的实时。 |
Wait():
通过对象调用方法让线程等待。
wait(long timeoutMillis):
通过对象调用方法让线程等待多少毫秒
wait(long timeoutMillis, int nanos)
通过对象调用方法让线程等待多少毫秒,多少纳秒
void | notify() | 唤醒正在此对象监视器上等待的单个线程。 |
void | 唤醒等待此对象监视器的所有线程。 |
notify():通过对象随机唤醒等待的线程
notifyAll():通过对象唤醒等待所有线程
哪个对象叫他们睡的,就哪个对象叫他们醒过来
2.生产者和消费者问题:
一个线程在生产数据时另一个线程需要使用在等待wait(),
生产数据的线程完毕后,在唤醒等待的线程notifyAll()。
使用完后在唤醒生产数据的生产新的数据。
1.生产者
线程任务:生产50份粥和50份煎饼果子
生产完一份,唤醒等待的线程,自己在去休眠
2.消费者
线程任务:取走生产的食物,唤醒等待的厨师,自己睡
3:main方法
总结:
同步中A线程和B线程,虽然上锁但是,不是公平排队不是一个一个的排。
就会出现A线程做的一次数据,被B使用很多次。
或者A线程做了很多次,B线程值使用一次。
就会出现数据混乱的情况。
同步只能保证A线程执行时,其他线程不能执行。
丢失时间偏后,需要重新抢占时间偏。
A和B是非公平抢占时间偏的。
而wait()方法和notifyAll()方法,可以让线程有序的执行
我不唤醒你你就不执行,A线程在执行过程中丢失时间偏,B线程没有被唤醒,所以他是不能去抢占时间偏的。
这样可以保证A线程,丢失时间偏,仍然可以执行剩下的任务,在让自己睡去,唤醒B线程。
B线程完成完整的任务,在让自己睡去,唤醒A线程。
有序的保证每个线程,在没有完成完整任务时,其他线程不能破坏数据
十四、线程的六种状态:
线程状态。 线程可以处于以下状态之一:
NEW
尚未启动的线程处于此状态。
RUNNABLE
在Java虚拟机中执行的线程处于此状态。
BLOCKED
被阻塞等待监视器锁定的线程处于此状态。 排队时
WAITING
无限期等待另一个线程执行特定操作的线程处于此状态。
TIMED_WAITING
正在等待另一个线程执行最多指定等待时间的操作的线程处于此状态。
TERMINATED
已退出的线程处于此状态。
线程生命周期: