十 多线程(重)
10.1 线程和进程
进程:
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。——百度百科;
可以简单理解为正在进行的程序。
线程:
线程是程序中一个单一的顺序控制流程。进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指运行中的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程。——百度百科; 简单理解:进程中一个负责程序执行的控制单元,程序的执行路径。
一个进程中至少有一个线程。每个线程都有自己的任务。开启多个线程是为了同时运行多个代码;
注:这里的同时运行,并不是真正意义上的同时;而是cpu非常快的运行速度达成的“同时”,因为cup是对多个应用程序切换运行的,并且是随机的。
优点:解决了多部分同时运行的问题;
缺点:线程过多时,执行效率降低。
Jvm启动时就启动了多线程,至少分析出两个线程:执行main()的线程,负责垃圾回收器的线程。jvm创建的主线程的任务都定义在了主函数中。
10.2 Thread
Java中创建线程方式一:继承Thread类。
步骤:
1,定义一个类继承Thread类。
2,覆盖Thread类中的run方法。
3,直接创建Thread的子类对象创建线程。
4,调用start方法开启线程并调用线程的任务run方法执行。
创建线程的目的是为了开启一条执行路径,去运行指定的代码和其他代码实现同时运行。而运行的指定代码就是这个执行路径的任务。
自定义的线程它的任务在哪儿呢?
Thread类用于描述线程,线程是需要任务的。所以Thread类也对任务的描述。这个任务就通过Thread类中的run方法来体现。也就是说,run方法就是封装自定义线程运行任务的函数。run方法中定义就是线程要运行的任务代码。
开启线程是为了运行指定代码,所以只有继承Thread类,并复写run方法。
将运行的代码定义在run方法中即可。
发生异常的线程不影响其他线程的运行。
在未设置优先级前,各线程随机运行。
线程的状态图:
10.3 Runnable
Java创建线程的另一种方式。当一个类有了父类,又想具有线程,怎么办呢?针对这种需求, 出现了Runnable接口。可以通过实现Runnable接口来完成。
步骤:
1 定义一个类声明实现接口Runnable
2 实现run方法 ,将任务代码封装到run方法中。
3 创建类对象,将对象引用传入Thread 的构造器中,创建线程。
4 启动线程start()
很多时候,我们需要的仅仅是Thread中的run()方法,如果我们使用继承来创建线程,子类就具有了很多我们不需要的方法。因此,一般情况下,都是使用第二种线程创建方式,即实现Runnable接口。并且Runnable恰恰实现了对线程任务的进行对象封装。
优点:1 将线程任务从子类中分离出来,单独封装;按照面向对象的思想。将任务封装成了对象。2 避免了java单继承的局限性。
10.4 经典买票
代码1:
private int count = 100;//票数
public void run() {
sale();
}
public void sale(){
while(count>0){
System.out.println( Thread.currentThread().getName() + "已买出:"+(200-count)+ "张票" + "-----"+ "剩余票数:" + count--);
} System.out.println( "票已卖完!");
}
main(){
SellWindow s1 = new SellWindow();
Thread t1 = new Thread(s1);
Thread t3 = new Thread(s1);
t1.start();
t3.start();
}
运行上述代码发现100的票被卖了两次;那是因为线程是随机执行的,两个线程之间没有通信,互相独立造成对共享数据票数的重复操作。
造成线程安全问题的原因:1 有多个线程操作共享数据 2 操作共享数据的线程代码有多条。
解决思路:将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候,其他线程不能参与操作。必须等到当前线程把代码执行完毕后,其他线程才能参与运算。
如何将代码封装起来呢?使用synchronized关键字!
“synchronized”关键词的作用是,确保在某个时刻只有一个线程被允许执行特定的代码块。
格式:
synchronized(对象){
同步代码;
}
synchronized(obj){
sale();
}
这就是同步代码块,使用同步代码块可以解决上述问题。
括号中的对象可以理解为一种标志,当有线程进入时,该标记值改变可以理解为由“开”到“关”,使其他线程无法进入。当前线程退出后,标志由“关”变会“开”,这样拥有该任务的多个线程才能竞争进入,反复如此。一般这个对象称为对象锁。
同步解决了线程安全的问题,但降低了效率。同步外的线程都会判断为同步锁。
同步的前提:必须是多个线程使用同一个锁。
synchronized也可以作为修饰符修饰函数,public synchronized void xxx(){},这样的函数称为同步函数。如: public synchronized void sale(){} 。但同步函数是对整个函数的全局同步。不适用于仅仅为了同步函数中的部分代码就将函数声明为同步的。适合于将需要同步的代码封装成同步函数。
注:同步函数使用的锁是this。同步代码块的锁是任意的对象。一般建议使用同步代码块。静态函数的同步锁是当前的字节码对象,this.getClass()
同步使用的技巧:找出真正需要同步的代码,是关键!
死锁: 是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。————百度百科
Java中一般同步的嵌套会产生死锁,出现两个线程互相等待对方做出某个动作的情形。
10.5 线程间的通信
线程拥有相同资源,执行不同的任务。并且有一定的执行顺序,那么线程之间就需要通信。
通信方法:
wait():让线程进入冻结状态,被wait的线程会被存储到线程池中。
notify():唤醒线程池中的一个线程(任意的,随机的)。
notifyAll():唤醒线程池中的所有线程。
我们发现Object类中有wait()、notify()、notifyAll()这些用来操作线程的方法。为什么这些方法会放在Object中呢?
1 这些方法存在与同步 2 使用这些方法时必须要标识所属的同步锁 3锁可以是任意的,所以任意对象的调用方法一定定义在Object中,所有对象都继承Object,所有对象都有这些方法。
注:这些方法都是final的,即它们都是不能被重写的,不能通过子类覆写去改变它们的行为。
这些方法必须定义在同步中,因为这些方法都是用来控制线程状态的方法,必须明确操作的是那个锁上的线程。即用锁对象来调用这些方法。
sleep和wait的最大区别:sleep:释放执行权,不释放锁 wait: 释放执行权,释放锁。
停止线程: Thread 提供了停止线程的方法。stop(),但是已过时,千万别用!
当run方法结束,线程结束。一般的多线程任务都是用来执行循环的,所以控制循环的结束就能控制线程结束。而控制循环的结束一般用定义标记来完成。如
布尔型flag等。
但当线程处于冻结状态是无法读取,标记的,那线程如何结束呢?
使用interrupt()方法。该方法可以将冻结状态的线程恢复到运行状态,让线程具备cpu执行资格,当时该方法是强制动作,会发生中断异常。
setDaemon()方法:将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。 该方法必须在启动线程前调用。 守护线程可以理解为后台线程,前台线程需要手动结束,后台线程会随着前台线程的结束而自动结束。
join()方法:等待某个线程终结,临时加入一个线程运算时可以使用jion方法。
线程的优先级:1——10 MIN_PRIORITY = 1;NORM_PRIORITY = 5;MAX_PRIORITY=10 ,线程的优先级越大,获得cpu执行权的几率越高。
线程组:ThreadGroup 线程组表示一个线程的集合。此外,线程组也可以包含其他线程组。线程组构成一棵树,在树中,除了初始线程组外,每个线程组都有一个父线程组。
yield()方法:暂停当前正在执行的线程对象,并执行其他线程。