安慕希点关注做任务是真的吗?


一、 基本概念:程序、进程、线程

  • 是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
  • 是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。——生命周期
    • 如:运行中的QQ,运行中的MP3播放器
    • 程序是静态的,进程是动态的
    • 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
  • 进程可进一步细化为线程,是一个程序内部的一条执行路径。
    • 若一个进程同一时间并行执行多个线程,就是支持多线程的
    • 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小。
    • 一个进程中的多个线程共享相同的内存单元/内存地址空间–>它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患
  • 单核CPU和多核CPU的理解
    • 单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。例如:虽然有多车道,但是收费站只有一个工作人员在收费,只有收了费
      才能通过,那么CPU就好比收费人员。如果有某个人不想交钱,那么收费人员可以
      把他“挂起”(晾着他,等他想通了,准备好了钱,再去收费)。但是因为CPU时
      间单元特别短,因此感觉不出来。
    • 如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核的)
    • 垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。
    • 并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
    • 并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。

二、 线程的创建和使用

    • 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常
      把run()方法的主体称为线程体
    • 通过该Thread对象的start()方法来启动这个线程,而非直接调用run()
创建线程并指定线程实例名
指定创建线程的目标对象,它实现了Runnable接中的run方法

线程的创建方式一:继承Thread类

  1. 定义子类继承Thread类。
  2. 子类中重写Thread类中的run方法
  3. 创建Thread子类对象,即创建了线程对象。
  4. 调用线程对象start方法(注意有两个作用):启动线程;调用run方法。

 
 
 
 
 

其实run方法是Runnable接口中的抽象方法(Runnable接口中只有一个run方法),只不过我们在Thread类中重写了run()方法。
而采用继承Thread的方式时,我们有一步操作是重写了run()方法,那么由继承的知识可以知道,当调用了父类重写的方法时,实际执行的是子类重写后的方法,当启动多线程时(调用start方法),JVM会调用run()方法,那么调用的就是我们重写后的run()方法,而run()方法里面就是我们想让线程做的事情。

  • 如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式。
  • run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定。
  • 想要启动多线程,必须调用start方法。
  • 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上的异常“IllegalThreadStateException”。

线程的创建方式二:实现Runnable接口

  1. 定义子类,实现Runnable接口。
  2. 子类中重写Runnable接口中的run方法。
  3. 通过Thread类含参构造器创建线程对象。
  4. 将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
  5. 调用Thread类的start方法:开启线程,调用Runnable子类接口中的run方法。

 
 
 
 
 

采用实现Runnable的底层原理分析
对于上述程序,我们点进去看一下底层的执行原理

我们发现构造器的参数是Runnable类型,而我们传递的参数实现了Runnable接口,此时发生了多态。
我们在查看Init方法,如下

我们只关注其中的重要一行代码:如下
它把我们传递的参数赋给了Thread类中的target,下面查看run()方法
此处run方法中调用了我们传递过来的实现Runnable类的对象的run方法,当我们调用Thread对象的start()方法时(start()方法作用,创建一个线程,执行run()方法),也就执行了我们在实现类重写的run()方法。

线程的创建方式三:使用Callable接口

对比第一种和第二种创建线程的方式发现,无论第一种继承Thread类的方式还是第二种实现Runnable接口的方式。都需要有一个run方法,但是这个run方法有不足:
1、没有返回值 2、不能抛出异常
基于以上两个不足,在JDK1.5以后出现了第三种创建线程的方式:实现Callable接口:实现Callable接口好处:1、有返回值 2、抛出异常 3、支持泛型操作 缺点:创建线程比较麻烦。

  1. 创建一个实现Callable的实现类。
  2. 实现call方法,将此线程需要执行的操作声明在call()中
  3. 创建Callable接口实现类的对象
  4. 将此Callable接口实现类的对象作为参数传递到FutureTask构造器中

 
 
 
 
 
 

我们知道启动一个线程必须调用Thread类中的start()方法,上述程序我们创建了一个实现Callable接口的实例对象,并把它丢入到Thread中的构造器中,但程序报错了,说明我们Thread中的构造器没有参数是Callable的:
但是我们注意到存在Runnable类型参数的构造器:
所以我们可以传入一个实现了Runnable接口的实现类对象,但是Callable接口不是Runnable接口的实现类,所以我们就像借助一个FutureTask类,FutureTask类的声明如下

线程的创建方式四:使用线程池

【1】线程池使用到的是阻塞队列
线程的生命周期:出生----》死亡
新生(假设用时3s)----》就绪(2s)-----》运行(1)-----》死亡(3s)

使用线程池目的:将运行之前的时间节省,将运行之后的时间节省—》线程池:减少创建和消亡的时间。


执行线程5时,队列以及满了,就会创建新线程执行该任务。创建的新线程是“pool-1-thread-2",它执行完并不是立即销毁,如果3ms内有任务,他就会和核心线程
"pool-1-thread-1"分摊执行剩下的任务,如果3ms内没任务,该新线程消亡。试着在运行上述程序,查看“pool-1-thread-2"确实分摊任务了。


报错了,因为线程队列以及满了,此时的新任务要创建新的线程来执行,但是目前已经有两个线程了,我们初始也设置了最大线程数为2,所以此时就拒绝执行了。

在java5以前,开发者必须手动实现自己的线程池;从Java5开始,Java内建支持线程池。Java5新增了一个Executors工厂类来实现线程池,该工厂类包含如下几个静态工厂方法来创建线程池。

创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中
创建一个可重用的、具有固定线程数的线程池。
创建一个只有单线程的线程池,它相当于调用newFixedThreadPool()方法时传入参数为1.
创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务。corePoolSize指池中所保存的线程数,即使线程是空闲的也被保存在线程池内。
创建只有一个线程的线程池,它可以在指定延迟后执行线程任务。

上面5个方法中的前3个方法返回一个ExecutorService对象,该对象代表一个线程池,它可以执行Runnable对象或Callable对象所代表的线程;而后两个方法返回一个ScheduledExecutorService线程池,它是ExecutorService的子类,它可以在指定延迟后执行线程任务。
JDK1.5之后提供了内置线程池

 
 
 
 

最开始没有核心线程,来一个任务,新建一个线程来执行这个任务,当这个任务执行完以后,这个线程继续执行其他的任务,所以在结果中可以看到线程大量重复的


运行结果:只有3个线程在执行任务


 
 
 

1、通过继承Thread类的方法来创建线程时,多个线程之间无法共享线程类的实例变量。除非加static变成类变量。或者把继承Thread的子类对象作为参数传递给Thread对象(其实相当于实现了Runnable接口,具体看下文演示)。
2、采用Runnable接口的方式创建的多个线程可以共享线程类的实例属性。这是因为在这种方式下,程序所创建的Runnable对象只是线程的target,而多个线程可以共享一个target,所以多个线程可以共享同一个线程类(实际上应该是线程的target类对象)的实例属性。

1、继承Thread类的方式来演示抢票操作,不能实现数据的共享


 

运行结果如下:如果不加static抢票就会发生错误(每个窗口买票都会从100号开始卖)。tickets是实例属性,因为程序每次创建线程对象时都需要创建一个Window对象,所以“窗口1“线程、”窗口2“线程不能共享属性,解决就是加static,让其成为类属性。
把继承Thread的子类对象作为参数传递给Thread对象也可以实现数据共享(其实相当于实现了Runnable接口,具体看下文演示)。



因为MyThreadr继承了Thread类,Thread类又实现了Runnable接口,相当于MyThreader间接的实现了Runnable接口,所以可以把MyThread的对象传递给声明为Runnable类型的参数,此时发生了多态。虽然我们new了好几个线程对象,但其实操作的都是同一个MyThread的子类对象,当然可以共享tickets属性。

创建线程的三种方式对比
通过继承Thread类或实现Runnable、Callable接口都可以实现多线程,不过实现Runnable接口与实现Callable接口的方式基本相同,只是Callable接口里定义的方法有返回值,可以声明抛出异常而已。因此可以实现Runnable接口和实现Callable接口归为一种方式。这种方式与继承Thread方式之间的主要差别如下

  • 线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
  • 在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将cpu、代码和数据分开,形成清晰的模型,较好体现了面向对象的思想。
  • 劣势是:编程稍稍复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法。
    采用继承Thread类的方式创建多线程—
  • 因为线程类以及继承了Thread类,所以不能再继承其他父类。
  • 优势是:编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
    鉴于分析,因此一般推荐采用实现Runnable接口、Callable接口的方式来创建多线程。
  • 每个线程执行时都具有一定的优先级,优先级高的线程获得较多的执行机会,而优先级低的线程则获得较少的执行机会。
  • 每个线程默认的优先级都与创建它的父线程的优先级相同,默认情况下,main线程具有普通优先级,由main线程创建的子线程也具有普通优先级。

**注意:**优先级高的线程获得更多的执行机会,并不是一定先执行。

启动线程,并执行对象的run()方法
线程在被调度时执行的操作
返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类
释放当前cpu的执行权,让其它的线程去获取,但该方法只会给优先级相同,或优先级更高的线程执行机会。它可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程进入就绪状态。yield只是让当前线程暂停一下,让系统的线程调度器重新调度一次,完全可能的情况是:当某个线程调用了yield方法暂停之后,线程调度器又将其调度出来重新执行。。
在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。
让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前线程是阻塞状态。
已过时。当执行方法时,强制结束当前线程。

重点是这句话:yield()不会将线程转入阻塞转态,它只是强制当前线程进入就绪状态。因此完全有可能某个线程调用yield()方法暂停之后,立即再次获得处理器资源被执行。

要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:

  • 新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。
  • 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源。
  • 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线程的操作和功能。
  • 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态。
  • 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束。

出现问题(继承和实现都会出现下面问题):
1、出现了两张10号票或者3张10号票。
上面的代码出现问题:出现了重票,错票----》线程安全引起的问题
原因:多个线程,在争抢资源的过程中,导致共享的资源出现问题。一个线程还没执行完,另一个线程就参与进来了,开始争抢。

解决方式:当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,其他线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。–Java 中通过加**“锁”(同步机制、同步监视器)**

方式一:使用同步代码块

下面我们对通过继承Thread实现的抢票代码执行同样的操作。

说明并没有解决线程的安全问题,要想保证安全,锁必须只有一个,而上述程序我们锁用this的话指的是t1、t2、t3(this含义是哪个对象调用了该方法this就是哪个对象)。我们在synchronized中做个改变如下:用类的字节码,因为一个类只有一份。


运行如上代码发现没有出现问题。

总结1:认识同步监视器(锁子)—synchronized(同步监视器){ …}

  1. 必须是引用数据类型,不能是基本数据类型

  2. 也可以创建一个专门的同 步监视器,没有任何业务含义

  3. 一般使用共享资源做同步监视器即可

  4. 在同步代码块中不能改变同步监视器对象的引用

  5. 尽量不要String和包装类Integer做同步监视器

  6. 建议使用final修饰同步监视器

总结2:同步代码块的执行过程
1) 第一个线程来到同步代码块,发现同步监视器open状态,需要close,然后执行其中的代码
2)第一个线程执行过程中 ,发生了线程切换(阻塞 就绪),第一个线程失去了cpu,但是没有开锁open
3)第二个线程获取了Cpu,来到了同步代码块,发现同步监视器close状态,无法执行其中的代码,第二个线程也进入阻塞状态
4)第一个线程再次获取cpu,接着执行后续的代码;同步代码块执行完毕,释放锁open
5)第二个线程也再次获取cpu,来到了同步代码块,发现同步监视器open状态,拿到锁并且上锁,由阻塞状态进入就绪状态,再进入运行状态,重复第一个线程的处理过程(加锁)
强调:同步代码块中能发生Cpu的切换吗?能!!! 但是后续的被执行的线程也无法执行同步代码块(因为锁close)

1)多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块,其他线程无法访问其中的任何一个代码块

2)多个代码块使用了同一个同步监视器(锁),锁住了一个代码块的同时,也锁住了所有使用该锁的所有代码块,但是没有锁住使用其他同步监视器的代码块,其他线程有机会访问其他同步监视器的代码块。

1、同步方法解决实现Runnable接口买票的方式

使用同样的方式对继承Thread的方式进行修改
并没有解决问题,因为synchronized锁住的是this,而对于继承方式来说,this代表的是不同的窗口对象。解决方法是在同步方法加static让其在内存中只有一份,那么它的同步监视器就是当前类名.class

  1. 不要将run()定义为同步方法
  2. 非静态同步方法的同步监视器是this 静态同步方法的同步监视器是 类名.class 字节码信息对象
  3. 同步代码块的效率要高于同步方法 原因:同步方法是将线程挡在了方法的外部,而同步代码块锁将线程挡在了代码块的外部,但是却是方法的内部
  4. 同步方法的锁是this,一旦锁住一个方法,就锁住了所有的同步方法;同步代码块只是锁住使用该同步监视器的代码块,而没有锁住使用其他监视器的代码块
    对同步方法和同步代码块解决线程安全的总结:
    多线程在争抢资源,就要实现线程的同步(就要进行加锁,并且这个锁必须是共享的,必须是唯一的。咱们的锁一般都是引用数据类型的。

JDK1.5后新增新一代的线程同步方式:Lock锁
与采用synchronized相比,lock可提供多种锁方案,更灵活
synchronized是Java中的关键字,这个关键字的识别是靠JVM来识别完成的呀。是虚拟机级别的。
但是Lock锁是API级别的,提供了相应的接口和对应的实现类,这个方式更灵活,表现出来的性能优于之前的方式。

* 解决线程安全问题的方式三:Lock锁---JDK5.0新增 * 相同:二者都可以解决线程安全问题 * 不同:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器 * Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock()* 2.优先使用顺序: * Lock 同步代码块(已经进入了方法体,分配了相应资源) 同步方法(在方法体之外) * 面试题:如何解决线程安全问题?有几种方式

1.Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁
3.使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

Lock----同步代码块(已经进入了方法体,分配了相应资源)----同步方法(在方法体之外)

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。

原因分析:本程序中使用的是相同的监视器。比如cpu先执行如下这个线程,我们称这个线程为线程1
线程1拿到str1这把锁,进入到同步代码块中,此时线程睡了10毫秒,那么此时cpu可能切换到下面的线程,我们成为线程2
线程2的同步代码块需要str2这把锁,而此时这把锁还没被占用,所以此时线程2继续执行。当执行到如下的代码处时:
它需要str1这把锁,而这把锁str1正在使用,所以该线程就在这阻塞等着str1这把锁释放。比如此时cpu又切换到线程1,执行到如下代码之处


它需要str2这把锁,而这把锁正被线程2使用,所以他等着线程2释放str2这把锁,就这样线程1等着线程2释放str2锁,线程2等着线程1释放str1这把锁,两个线程就阻塞在这了。

解决死锁方法:减少同步资源的定义,避免嵌套同步

1、线程安全,效率低;线程不安全,效率高
2、可能造成死锁现象。

线程通信例子1:使用两个线程打印1-100。线程1,线程2 交替打印

线程通信例子2:生产者消费者问题
实现要求:生成者和消费者要交替输出


1、生产者和消费者没有交替输出(我们的要求是要交替输出)
----》原因没有加同步

方式一:使用同步代码块

方式二:使用同步方法只需在Product中写就行
分别在生产者类和消费者类把run方法中的代码剪切到一个同步方法中可不行,因为此时的同步监视器是this(即当前类的对象),这样的话生产者和消费者的同步方法中用的就不是同一个监视器了,没有解决线程安全。

线程安全问题解决,但是先执行生产者,在执行消费者还没实现,而实现该功能就要用到线程之间的通信了。
分解3:实现线程之间通信
就如我们在饭店吃饭一样,当我们付完款之后,服务员会给我们一个号,如果饭做好了服务员叫号,我们就去端饭,如果没叫到我们的号我们就等着;类比,如果生成者生产好了产品把灯泡置为红色,生产者停下来,通知消费者进行消费,否则的话,生产者就生产;生产者如果看到是绿色说明还没做好饭,那么消费者就等着,让生产者进行生产,否则的话消费者消费掉商品,并通知生产者进行生产。

只需要更改product中的方法即可

一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个
一旦执行此方法,就会唤醒所有被wait的线程。

由以上知识知道生产者和消费者轮流持有锁。谁持有了这个锁,另一个线程就进入到等待池中。比如说生产者已经生产出商品了,那么我就不需要在生产了,就进入到等待池中进行等待;如果是消费者的话,现在想进行消费,但发现没有商品的话,我还在同一个等待池中进行等待。所以我们发现,生产者消费者等待时都在同一个等待池中进行等待。比如当前等待池中有一个生产者P1、消费者c1,那么这个等待池要么p1用,要么c1用,此时没啥关系。如果现在等待池中有多个生产者、消费者。比如现在存在4个线程分别是生产者:p1、p2和消费者c1和c2。目前p1持有这锁了,那么p2 c1 c2都在同一个线程池中等待,然后p1释放锁了,要通知等待池中的线程持有这个锁,但是p1通知的时候线程池里的生产者p1和消费者c1和c2都有资格拿到这个锁。如我本应该唤醒的是一个消费者,确有可能唤醒的是生产者c1和c2,那么这个情况就不友好了。我们上面的刚好是一个生产者一个消费者,所以他们没有问题,当有多个生产者、消费者就有问题了。
解决:就是生产者p1、p2一个等待池,c1、c2一个等待池,比如p1持有这个锁进行生产,生产完之后,它应该通知c1、c2这个等待池。所以我们应该解决把生产者和消费者分别放置子两个不同的池子中,lock锁可以解决该问题。结构如图。
lock锁解决上述问题代码如下:

product类代码修改如下:


 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

它的更强大的地方在于:能够更加精细的控制多线程的休眠与唤醒。对于同一个锁,我们可以创建多个Condition,在不同的情况下使用不同的Condition
一个Condition包含一个等待队列。一个Lock可以产生多个Condition,所以可以有多个等待队列。
在Object的监视器模型上,一个对象拥有一个同步队列和等待队列, 而Lock(同步器)拥有一个同步队列(锁池)和多个等待队列。
造成当前线程在接到信号或被中断之前一直处于等待状态。
与此 Condition 相关的锁以原子方式释放,并且出于线程调度的目的,将禁用当前线程,且在发生以下四种情况之一 以前,当前线程将一直处于休眠状态:
· 其他某个线程调用此 Condition 的 signal() 方法,并且碰巧将当前线程选为被唤醒的线程;或者
· 其他某个线程中断当前线程,且支持中断线程的挂起;或者
在所有情况下,在此方法可以返回当前线程之前,都必须重新获取与此条件有关的锁。在线程返回时,可以保证它保持此锁。
如果所有的线程都在等待此条件,则选择其中的一个唤醒。在从 await 返回之前,该线程必须重新获取锁。
如果所有的线程都在等待此条件,则唤醒所有线程。在从 await 返回之前,每个线程都必须重新获取锁。

我要回帖

更多关于 安慕希广告策略 的文章

 

随机推荐