黑马程序员——JAVA学习笔记六(多线程)

时间:2023-02-20 10:36:01
1,    什么是多线程?一个程序可以执行多个任务,每一个任务称为一个线程,运行多个线程的程序称为多线程程序。
进程:正在进行中的程序(直译)。
线程:进程中一个负责程序执行的控制单元(执行路径)。
 
多线程的好处:解决了多部分代码同时运行的问题。
多线程的弊端:线程太多,会导致效率的降低。
其实,多个应用程序同时执行都是CPU在做着快速的切换完成的。这个切换是随机的。CPU的切换是需要花费时间的,从而导致了效率的降低
2 ,    创建线程方式:
 创建线程方式一:继承Thread类
1、定义一个类继承Thread类。
2、覆盖Thread类中的run方法。
3、直接创建Thread的子类对象创建线程。
4、调用start方法开启线程并调用线程的任务run方法执行。
 
创建线程的目的就是为了开启一条执行路径,去运行指定的代码和其他代码实现同时运行,而运行的指定代码就是这个执行路径的任务。
jvm创建的主线程的任务都定义在了主函数中。
而自定义的线程,它的任务在哪儿呢?
Thread类用于描述线程,线程是需要任务的。所以Thread类也有对任务的描述。这个任务就是通过Thread类中的run方法来体现。也就是说,run方法就是封装自定义线程运行任务的函数,run方法中定义的就是线程要运行的任务代码。
开启线程是为了运行指定代码,所以只有继承Thread类,并复写run方法,将运行的代码定义在run方法中即可。
创建线程方式二:实现Runnble接口。
1、定义类实现Runnable接口。
2、覆盖接口中的run方法,将线程的任务代码封装到run方法中。
3、通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递。
     为什么?因为线程的任务都封装在Runnable接口子类对象的run方法中。
     所以要在线程对象创建时就必须明确要运行的任务。
4、调用线程对象的start方法创建新线程。
 
实现Runnable接口的好处:
1、将线程的任务从线程的子类中分离出来,进行了单独的封装,按照面向对象的思想将任务封装成对象。
2,避免了Java单继承的局限性。所以,创建线程的第二种方式较为常用。
 
Thread类、Runnable接口内部源码关系模拟代码:
class Thread{
      private Runnable r ;
      Thread(){
      }
      Thread(Runnable r){
             this.r = r;
      }
 
       public void run(){
             if(r !=null)
                   r.run();
      }
       public void start(){
            run();
      }
}
 
如果需要有很多任务,要为每个任务创建一个独立的线程所付出的代价太大了,所以应该考虑线程池。
线程状态:有6种,可以调用getState得到线程的状态。
 
黑马程序员——JAVA学习笔记六(多线程)
黑马程序员——JAVA学习笔记六(多线程)
cpu执行资格:可以被cpu处理,在处理队列排队。
cpu的执行权:正在被cpu处理。
一旦调用start方法,线程处于可运行状态,可能正在运行也可能没有运行,取决于操作系统给线程提供运行的时间。
抢占式调度:操作系统给每个任务一个时间片来执行任务。时间片用完后,操作系统剥夺该线程的运行权,并把机会给另外一个线程,操作系统会考虑线程的优先级(MIN_PRIORITY MAX_PRIORITY, NORM_PRIORITY)。
协作式调度:一个线程只有在调用yield方法,或者被阻塞或等待时,线程才失去控制权。
若线程数多于处理器数,还是会按照时间片方式。
 
 
3,    中断线程,当线程的run方法执行方法体中最后一条语句时后,并经由执行return语句返回时,或者出现了在方法中没有捕获的异常时,线程将终止。在java早期版本中,可以一个stop方法,但是现在已经被弃用。
有一种可以强制线程终止的方法,线程的中断状态将被置位。每个线程都有一个boolean标志,每个线程都应该不时地检查这个标志,以判断线程是否被中断。
while(!Thead.currentThread().isInterrupted()&&more work to do) {
do more work
}
如果线程被阻塞,就无法检测中断状态,所以会抛出一个InterruptedException异常的地方。
public void run() {
    try {
        ...
        while(more work to do) {
                do more work
                Thread.sleep(delay);
        }
    }
catch( InterruptException e) {
    //thread was interrupted during sleep
}
finaly {
    cleanup if required
}
   //exiting the run method terminates the thread
}
 
interrupted是类方法,将会清除中断状态。另一方面,isInterrupted是实例方法,不会清除中断状态。
 
4,    线程同步,在大多数实际的多线程应用中,两个或者两个以上的线程需要共享对统一数据的存取。这样会导致数据不一致。这种情况叫做竞争条件。如何防止代码块受并发访问的干扰。JAVA提供了synchronized关键字达到这一目的,并且在java5中引入了ReentrantLock类。
 
同步函数和同步代码块的区别:
1、同步函数的锁是固定的this。
2、同步代码块的锁是任意的对象。
建议使用同步代码块。
 
由于同步函数的锁是固定的this,同步代码块的锁是任意的对象,那么如果同步函数和同步代码块都使用this作为锁,就可以实现同步
静态的同步函数使用的锁是该函数所属字节码文件对象,可以用getClass方法获取,也可以用当前类名.class表示。

等待/唤醒机制涉及的方法:
①wait():让线程处于冻结状态,被wait的线程会被存储到线程池中。
②notify():唤醒线程池中的一个线程(任何一个都有可能)。
③notifyAll():唤醒线程池中的所有线程。

P.S.
1、这些方法都必须定义在同步中,因为这些方法是用于操作线程状态的方法。
2、必须要明确到底操作的是哪个锁上的线程!
3、wait和sleep区别?
①wait可以指定时间也可以不指定。sleep必须指定时间。
②在同步中时,对CPU的执行权和锁的处理不同。
wait:释放执行权,释放锁。
sleep:释放执行权,不释放锁。

为什么操作线程的方法wait、notify、notifyAll定义在了object类中,因为这些方法是监视器的方法,监视器其实就是锁。
锁可以是任意的对象,任意的对象调用的方式一定在object类中。
 
ReentrantLock 基本结构如下:
mylock.lock();
try {
critial section;
}
finally {
mylock.lock();
锁是可重入的,所保持一个计数来跟踪对lock方法的嵌套调用。线程的每一次调用都要使用相同的锁的方法。要留心临界区的代码,不要因为异常而抛出异常跳出了临界区,使对象受损。
条件对象:通常线程进入临界区,却发现在某一条件满足之后它才能执行。要使用一个条件对象来管理那些已经获得了一个锁却不能做有用工作的线程。称作条件变量。
一个所对象可以有多个相关条件对象。用newCondition方法获取一个条件对象。
 
Lock接口:出现替代了同步代码块或者同步函数,将同步的隐式操作变成显示锁操作。同时更为灵活,可以一个锁上加上多组监视器。
lock():获取锁。
unlock():释放锁,为了防止异常出现,导致锁无法被关闭,所以锁的关闭动作要放在finally中。

Condition接口:出现替代了Object中的wait、notify、notifyAll方法。将这些监视器方法单独进行了封装,变成Condition监视器对象,可以任意锁进行组合。
Condition接口中的await方法对应于Object中的wait方法。
Condition接口中的signal方法对应于Object中的notify方法。
Condition接口中的signalAll方法对应于Object中的notifyAll方法。