JAVA多线程基础学习:基础知识

我们知道多线程是Java编程中重要的一块内容,也是面试重点覆盖区域,所以学好多线程对我们来说极其重要,下面跟我一起开启本次的学习之旅吧。

一、线程基本概念

1 线程:进程中负责程序执行的执行单元(执行路径)
线程本身依靠程序进行运行
线程是程序中的顺序控制流,只能使用分配给程序的资源和环境
2 进程:执行中的程序
一个进程至少包含一个线程
3 单线程:程序中只存在一个线程,实际上主方法就是一个主线程
4 多线程:在一个程序中运行多个任务
目的是更好地使用CPU资源

用多线程只有一个目的,那就是更好的利用cpu的资源,因为所有的多线程代码都可以用单线程来实现。说这个话其实只有一半对,因为反应“多角色”的程序代码,最起码每个角色要给他一个线程吧,否则连实际场景都无法模拟,当然也没法说能用单线程来实现:比如最常见的“生产者,消费者模型”。
很多人都对其中的一些概念不够明确,如同步、并发等等,让我们先建立一个数据字典,以免产生误会。
多线程:指的是这个程序(一个进程)运行时产生了不止一个线程。
并行与并发:
并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。
并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力。

二、线程的状态

线程状态:

image.png

说明:
线程共包括以下5种状态。
1. 新建状态(New) : 线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
2. 就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。

3. 运行状态(Running) : 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。

**4. 阻塞状态(Blocked) **: 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(01) 等待阻塞 – 通过调用线程的wait()方法,让线程等待某工作的完成。
(02) 同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
(03) 其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5. 死亡状态(Dead) : 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

这5种状态涉及到的内容包括Object类, Thread和synchronized关键字。这些内容我们会在后面的章节中逐个进行学习。
Object类,定义了wait(), notify(), notifyAll()等休眠/唤醒函数。
Thread类,定义了一些列的线程操作函数。例如,sleep()休眠函数, interrupt()中断函数, getName()获取线程名称等。
synchronized,是关键字;它区分为synchronized代码块和synchronized方法。synchronized的作用是让线程获取对象的同步锁
在后面详细介绍wait(),notify()等方法时,我们会分析为什么“wait(), notify()等方法要定义在Object类,而不是Thread类中”。

注:sleep和wait的区别:【考点】

  • sleepThread类的方法,waitObject类中定义的方法.
  • Thread.sleep不会导致锁行为的改变, 如果当前线程是拥有锁的, 那么Thread.sleep不会让线程释放锁.
  • Thread.sleepObject.wait都会暂停当前的线程. OS会将执行时间分配给其它线程. 区别是, 调用wait后, 需要别的线程执行notify/notifyAll才能够重新获得CPU执行时间.

三、线程的创建

线程的创建方式为:

1.继承Thread

java.lang包中定义, 继承Thread类必须重写run()方法

1.  class  MyThread  extends  Thread{

2.  private  static  int num =  0;

4.  public  MyThread(){

5.  num++;

6.  }

8.  @Override

9.  public  void run()  {

10.  System.out.println("主动创建的第"+num+"个线程");

11.  }

12.  }

创建好了自己的线程类之后,就可以创建线程对象了,然后通过start()方法去启动线程。注意,不是调用run()方法启动线程,run方法中只是定义需要执行的任务,如果调用run方法,即相当于在主线程中执行run方法,跟普通的方法调用没有任何区别,此时并不会创建一个新的线程来执行定义的任务。

1.  public  class  Test  {

2.  public  static  void main(String[] args)  {

3.  MyThread thread =  new  MyThread();

4.  thread.start();

5.  }

6.  }

7.  class  MyThread  extends  Thread{

8.  private  static  int num =  0;

9.  public  MyThread(){

10.  num++;

11.  }

12.  @Override

13.  public  void run()  {

14.  System.out.println("主动创建的第"+num+"个线程");

15.  }

16.  }

在上面代码中,通过调用start()方法,就会创建一个新的线程了。为了分清start()方法调用和run()方法调用的区别,请看下面一个例子:

1.  public  class  Test  {

2.  public  static  void main(String[] args)  {

3.  System.out.println("主线程ID:"+Thread.currentThread().getId());

4.  MyThread thread1 =  new  MyThread("thread1");

5.  thread1.start();

6.  MyThread thread2 =  new  MyThread("thread2");

7.  thread2.run();

8.  }

9.  }

11.  class  MyThread  extends  Thread{

12.  private  String name;

14.  public  MyThread(String name){

15.  this.name = name;

16.  }

18.  @Override

19.  public  void run()  {

20.  System.out.println("name:"+name+" 子线程ID:"+Thread.currentThread().getId());

21.  }

22.  }

运行结果:

image.png

从输出结果可以得出以下结论:

1)thread1和thread2的线程ID不同,thread2和主线程ID相同,说明通过run方法调用并不会创建新的线程,而是在主线程中直接运行run方法,跟普通的方法调用没有任何区别;

2)虽然thread1的start方法调用在thread2的run方法前面调用,但是先输出的是thread2的run方法调用的相关信息,说明新线程创建的过程不会阻塞主线程的后续执行。

** 2.实现Runnable接口**

在Java中创建线程除了继承Thread类之外,还可以通过实现Runnable接口来实现类似的功能。实现Runnable接口必须重写其run方法。

1.  public  class  Test  {

2.  public  static  void main(String[] args)  {

3.  System.out.println("主线程ID:"+Thread.currentThread().getId());

4.  **MyRunnable runnable** **=  new  MyRunnable();** 
5.  **Thread thread =  new** **Thread(runnable);** 
6.  **thread.start();**

7.  }

8.  }

9.  class  MyRunnable  implements  Runnable{

10.  public  MyRunnable()  {

11.  }

13.  @Override

14.  public  void run()  {

15.  System.out.println("子线程ID:"+Thread.currentThread().getId());

16.  }

17.  }

Runnable的中文意思是“任务”,顾名思义,通过实现Runnable接口,我们定义了一个子任务,然后将子任务交由Thread去执行。注意,这种方式必须将Runnable作为Thread类的参数,然后通过Thread的start方法来创建一个新线程来执行该子任务。如果调用Runnable的run方法的话,是不会创建新线程的,这根普通的方法调用没有任何区别。
事实上,查看Thread类的实现源代码会发现Thread类是实现了Runnable接口的。引出静态代理模式:

线程体(也就是我们要执行的具体任务)实现了Runnable接口和run方法。同时Thread类也实现了Runnable接口。此时,线程体就相当于目标角色,Thread就相当于代理角色。当程序调用了Thread的start()方法后,Thread的run()方法会在某个特定的时候被调用。thread.run()方法:

1.  public  void run()  {

2.  if  (target !=  null)  {

3.  target.run();

4.  }

5.  }

说明:target是一个Runnable对象。run()就是直接调用Thread线程的Runnable成员的run()方法,并不会新建一个线程。

在Java中,这2种方式都可以用来创建线程去执行子任务,具体选择哪一种方式要看自己的需求。直接继承Thread类的话,可能比实现Runnable接口看起来更加简洁,但是由于Java只允许单继承,所以如果自定义类需要继承其他类,则只能选择实现Runnable接口。

3.使用ExecutorService、Callable、Future实现有返回结果的多线程

上面我们发现都是没有办法获取到线程执行的返回结果的,为了解决这个问题,于是出现了Callable;并发里面会继续学到,这里暂时先知道一下有这种方法即可。
ExecutorService、Callable、Future这个对象实际上都是属于Executor框架中的功能类。想要详细了解Executor框架的可以访问,这里面对该框架做了很详细的解释。返回结果的线程是在JDK1.5中引入的新特征,确实很实用,有了这种特征我就不需要再为了得到返回值而大费周折了,而且即便实现了也可能漏洞百出。

可返回值的任务必须实现Callable接口,类似的,无返回值的任务必须Runnable接口。执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了,再结合线程池接口ExecutorService就可以实现传说中有返回结果的多线程了。下面提供了一个完整的有返回结果的多线程测试例子。代码如下:

1.  /**

2.  * 有返回值的线程

3.  */

4.  @SuppressWarnings("unchecked")

5.  public  class  Test  {

6.  public  static  void main(String[] args)  throws  ExecutionException,

7.  InterruptedException  {

8.  System.out.println("----程序开始运行----");

9.  Date date1 =  new  Date();  

11.  int taskSize =  5;

12.  // 创建一个线程池

13.  ExecutorService pool =  Executors.newFixedThreadPool(taskSize);

14.  // 创建多个有返回值的任务

15.  List list =  new  ArrayList();

16.  for  (int i =  0; i < taskSize; i++)  {

17.  Callable c =  new  MyCallable(i +  " ");

18.  // 执行任务并获取Future对象

19.  Future f = pool.submit(c);

20.  // System.out.println(">>>" + f.get().toString());

21.  list.add(f);

22.  }

23.  // 关闭线程池

24.  pool.shutdown();  

26.  // 获取所有并发任务的运行结果

27.  for  (Future f : list)  {

28.  // 从Future对象上获取任务的返回值,并输出到控制台

29.  System.out.println(">>>"  + f.get().toString());

30.  }  

32.  Date date2 =  new  Date();

33.  System.out.println("----程序结束运行----,程序运行时间【"

34.  +  (date2.getTime()  - date1.getTime())  +  "毫秒】");

35.  }

36.  }  

38.  class  MyCallable  implements  Callable  {

39.  private  String taskNum;  

41.  MyCallable(String taskNum)  {

42.  this.taskNum = taskNum;

43.  }  

45.  public  Object call()  throws  Exception  {

46.  System.out.println(">>>"  + taskNum +  "任务启动");

47.  Date dateTmp1 =  new  Date();

48.  Thread.sleep(1000);

49.  Date dateTmp2 =  new  Date();

50.  long time = dateTmp2.getTime()  - dateTmp1.getTime();

51.  System.out.println(">>>"  + taskNum +  "任务终止");

52.  return taskNum +  "任务返回运行结果,当前任务时间【"  + time +  "毫秒】";

53.  }

54.  }

代码说明:
上述代码中Executors类,提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。
public static ExecutorService newFixedThreadPool(int nThreads)
创建固定数目线程的线程池。

public static ExecutorService newCachedThreadPool()
创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。

public static ExecutorService newSingleThreadExecutor()
创建一个单线程化的Executor。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。

ExecutoreService提供了submit()方法,传递一个Callable,或Runnable,返回Future。如果Executor后台线程池还没有完成Callable的计算,这调用返回Future对象的get()方法,会阻塞直到计算完成。

四、线程的信息

对于单核CPU来说(对于多核CPU,此处就理解为一个核),CPU在一个时刻只能运行一个线程,当在运行一个线程的过程中转去运行另外一个线程,这个叫做线程上下文切换(对于进程也是类似)。
由于可能当前线程的任务并没有执行完毕,所以在切换时需要保存线程的运行状态,以便下次重新切换回来时能够继续切换之前的状态运行。举个简单的例子:比如一个线程A正在读取一个文件的内容,正读到文件的一半,此时需要暂停线程A,转去执行线程B,当再次切换回来执行线程A的时候,我们不希望线程A又从文件的开头来读取。
因此需要记录线程A的运行状态,那么会记录哪些数据呢?因为下次恢复时需要知道在这之前当前线程已经执行到哪条指令了,所以需要记录程序计数器的值,另外比如说线程正在进行某个计算的时候被挂起了,那么下次继续执行的时候需要知道之前挂起时变量的值时多少,因此需要记录CPU寄存器的状态。所以一般来说,线程上下文切换过程中会记录程序计数器、CPU寄存器状态等数据。
说简单点的:对于线程的上下文切换实际上就是 存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。
虽然多线程可以使得任务执行的效率得到提升,但是由于在线程切换时同样会带来一定的开销代价,并且多个线程会导致系统资源占用的增加,所以在进行多线程编程时要注意这些因素。在此我向大家推荐一个架构学习交流圈。交流学习指导伪鑫:1253431195(里面有大量的面试题及答案)里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

1.线程的常用方法

image.png

image.png

1)currentThread()方法

currentThread()方法可以返回代码段正在被哪个线程调用的信息

1.  public  class  Run1{

2.  public  static  void main(String[] args){

3.  System.out.println(Thread.currentThread().getName());

4.  }

5.  }

6.  ```

8.  ### sleep()方法

9.  方法sleep()的作用是在指定的毫秒数内让当前“正在执行的线程”休眠(暂停执行)。这个“正在执行的线程”是指this.currentThread()返回的线程。

11.  sleep方法有两个重载版本:

12.  ```java

13.  sleep(long millis)  //参数为毫秒

14.  sleep(long millis,int nanoseconds)  //第一参数为毫秒,第二个参数为纳秒

sleep相当于让线程睡眠,交出CPU,让CPU去执行其他的任务。
但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。看下面这个例子就清楚了:

1.  public  class  Test  {

3.  private  int i =  10;

4.  private  Object  object  =  new  Object();

6.  public  static  void main(String[] args)  throws  IOException  {

7.  Test test =  new  Test();

8.  MyThread thread1 = test.new  MyThread();

9.  MyThread thread2 = test.new  MyThread();

10.  thread1.start();

11.  thread2.start();

12.  }  

14.  class  MyThread  extends  Thread{

15.  @Override

16.  public  void run()  {

17.  synchronized  (object)  {

18.  i++;

19.  System.out.println("i:"+i);

20.  try  {

21.  System.out.println("线程"+Thread.currentThread().getName()+"进入睡眠状态");

22.  Thread.currentThread().sleep(10000);

23.  }  catch  (InterruptedException e)  {

24.  // TODO: handle exception

25.  }

26.  System.out.println("线程"+Thread.currentThread().getName()+"睡眠结束");

27.  i++;

28.  System.out.println("i:"+i);

29.  }

30.  }

31.  }

32.  }

image.png

上面输出结果可以看出,当Thread-0进入睡眠状态之后,Thread-1并没有去执行具体的任务。只有当Thread-0执行完之后,此时Thread-0释放了对象锁,Thread-1才开始执行。

注意,如果调用了sleep方法,必须捕获InterruptedException异常或者将该异常向上层抛出。当线程睡眠时间满后,不一定会立即得到执行,因为此时可能CPU正在执行其他的任务。所以说调用sleep方法相当于让线程进入阻塞状态。

2.yield()方法

yield()的作用是让步。它能让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权;但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是当前线程又进入到“运行状态”继续运行!

1.  // YieldTest.java的源码

2.  class  ThreadA  extends  Thread{

3.  public  ThreadA(String name){

4.  super(name);

5.  }

6.  public  synchronized  void run(){

7.  for(int i=0; i <10; i++){

8.  System.out.printf("%s [%d]:%d\n",  this.getName(),  this.getPriority(), i);

9.  // i整除4时,调用yield

10.  if  (i%4  ==  0)

11.  Thread.yield();

12.  }

13.  }

14.  }  

16.  public  class  YieldTest{

17.  public  static  void main(String[] args){

18.  ThreadA t1 =  new  ThreadA("t1");

19.  ThreadA t2 =  new  ThreadA("t2");

20.  t1.start();

21.  t2.start();

22.  }

23.  }

结果:

1.  t1 [5]:0

2.  t2 [5]:0

3.  t1 [5]:1

4.  t1 [5]:2

5.  t1 [5]:3

6.  t1 [5]:4

7.  t1 [5]:5

8.  t1 [5]:6

9.  t1 [5]:7

10.  t1 [5]:8

11.  t1 [5]:9

12.  t2 [5]:1

13.  t2 [5]:2

14.  t2 [5]:3

15.  t2 [5]:4

16.  t2 [5]:5

17.  t2 [5]:6

18.  t2 [5]:7

19.  t2 [5]:8

20.  t2 [5]:9

“线程t1”在能被4整数的时候,并没有切换到“线程t2”。这表明,yield()虽然可以让线程由“运行状态”进入到“就绪状态”;但是,它不一定会让其它线程获取CPU执行权(即,其它线程进入到“运行状态”),即使这个“其它线程”与当前调用yield()的线程具有相同的优先级。

注意:

我们知道,wait()的作用是让当前线程由“运行状态”进入“等待(阻塞)状态”的同时,也会释放同步锁。而yield()的作用是让步,它也会让当前线程离开“运行状态”。它们的区别是:
(01) wait()是让线程由“运行状态”进入到“等待(阻塞)状态”,而不yield()是让线程由“运行状态”进入到“就绪状态”。
(02) wait()是会线程释放它所持有对象的同步锁,而yield()方法不会释放锁。

1.  // YieldLockTest.java 的源码

2.  public  class  YieldLockTest{  

4.  private  static  Object obj =  new  Object();

6.  public  static  void main(String[] args){

7.  ThreadA t1 =  new  ThreadA("t1");

8.  ThreadA t2 =  new  ThreadA("t2");

9.  t1.start();

10.  t2.start();

11.  }  

13.  static  class  ThreadA  extends  Thread{

14.  public  ThreadA(String name){

15.  super(name);

16.  }

17.  public  void run(){

18.  // 获取obj对象的同步锁

19.  synchronized  (obj)  {

20.  for(int i=0; i <10; i++){

21.  System.out.printf("%s [%d]:%d\n",  this.getName(),  this.getPriority(), i);

22.  // i整除4时,调用yield

23.  if  (i%4  ==  0)

24.  Thread.yield();

25.  }

26.  }

27.  }

28.  }

29.  }

结果:

1.  t1 [5]:0

2.  t1 [5]:1

3.  t1 [5]:2

4.  t1 [5]:3

5.  t1 [5]:4

6.  t1 [5]:5

7.  t1 [5]:6

8.  t1 [5]:7

9.  t1 [5]:8

10.  t1 [5]:9

11.  t2 [5]:0

12.  t2 [5]:1

13.  t2 [5]:2

14.  t2 [5]:3

15.  t2 [5]:4

16.  t2 [5]:5

17.  t2 [5]:6

18.  t2 [5]:7

19.  t2 [5]:8

20.  t2 [5]:9

结果说明
主线程main中启动了两个线程t1和t2。t1和t2在run()会引用同一个对象的同步锁,即synchronized(obj)。在t1运行过程中,虽然它会调用Thread.yield();但是,t2是不会获取cpu执行权的。因为,t1并没有释放“obj所持有的同步锁”!

3.start()方法

start()用来启动一个线程,当调用start方法后,系统才会开启一个新的线程来执行用户定义的子任务,在这个过程中,会为相应的线程分配需要的资源。

4.run()方法

run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。

5.getId()

getId()的作用是取得线程的唯一标识

1.  public  class  Test  {

2.  public  static  void main(String[] args)  {

3.  Thread t=  Thread.currentThread();

4.  System.out.println(t.getName()+" "+t.getId());

5.  }

6.  }

1.  #### isAlive()方法 

3.  方法isAlive()的功能是判断当前线程是否处于活动状态

4.  代码:

5.  ```java

6.  public class MyThread  extends Thread{

7.  @Override

8.  public void run() {

9.  System.out.println("run="+this.isAlive());

10.  }

11.  }

12.  public class RunTest {

13.  public static void main(String[] args) throws InterruptedException {

14.  MyThread myThread=new MyThread();

15.  System.out.println("begin =="+myThread.isAlive());

16.  myThread.start();

17.  System.out.println("end =="+myThread.isAlive());

18.  }

19.  }

程序运行结果:

1.  begin  ==false

2.  run=true

3.  end  ==false

方法isAlive()的作用是测试线程是否偶处于活动状态。什么是活动状态呢?活动状态就是线程已经启动且尚未终止。线程处于正在运行或准备开始运行的状态,就认为线程是“存活”的。
有个需要注意的地方

System.out.println(“end ==”+myThread.isAlive());

虽然上面的实例中打印的值是true,但此值是不确定的。打印true值是因为myThread线程还未执行完毕,所以输出true。如果代码改成下面这样,加了个sleep休眠:

1.  public  static  void main(String[] args)  throws  InterruptedException  {

2.  MyThread myThread=new  MyThread();

3.  System.out.println("begin =="+myThread.isAlive());

4.  myThread.start();

5.  Thread.sleep(1000);

6.  System.out.println("end =="+myThread.isAlive());

7.  }

则上述代码运行的结果输出为false,因为mythread对象已经在1秒之内执行完毕。

6.join()方法

join() 的作用:让“主线程”等待“子线程”结束之后才能继续运行。这句话可能有点晦涩,我们还是通过例子去理解:

1.  // 主线程

2.  public  class  Father  extends  Thread  {

3.  public  void run()  {

4.  Son s =  new  Son();

5.  s.start();

6.  s.join();

7.  ...

8.  }

9.  }

10.  // 子线程

11.  public  class  Son  extends  Thread  {

12.  public  void run()  {

13.  ...

14.  }

15.  }

说明
上面的有两个类Father(主线程类)和Son(子线程类)。因为Son是在Father中创建并启动的,所以,Father是主线程类,Son是子线程类。
在Father主线程中,通过new Son()新建“子线程s”。接着通过s.start()启动“子线程s”,并且调用s.join()。在调用s.join()之后,Father主线程会一直等待,直到“子线程s”运行完毕;在“子线程s”运行完毕之后,Father主线程才能接着运行。 这也就是我们所说的“join()的作用,是让主线程会等待子线程结束之后才能继续运行”!

实例:

1.  // JoinTest.java的源码

2.  public  class  JoinTest{  

4.  public  static  void main(String[] args){

5.  try  {

6.  ThreadA t1 =  new  ThreadA("t1");  // 新建“线程t1”

8.  t1.start();  // 启动“线程t1”

9.  t1.join();  // 将“线程t1”加入到“主线程main”中,并且“主线程main()会等待它的完成”

10.  System.out.printf("%s finish\n",  Thread.currentThread().getName());

11.  }  catch  (InterruptedException e)  {

12.  e.printStackTrace();

13.  }

14.  }  

16.  static  class  ThreadA  extends  Thread{

18.  public  ThreadA(String name){

19.  super(name);

20.  }

21.  public  void run(){

22.  System.out.printf("%s start\n",  this.getName());  

24.  // 延时操作

25.  for(int i=0; i <1000000; i++)

26.  ;

28.  System.out.printf("%s finish\n",  this.getName());

29.  }

30.  }

31.  }

运行结果

1.  t1 start

2.  t1 finish

3.  main finish

结果说明
运行流程如图 
(01) 在“主线程main”中通过 new ThreadA(“t1”) 新建“线程t1”。 接着,通过 t1.start() 启动“线程t1”,并执行t1.join()。
(02) 执行t1.join()之后,“主线程main”会进入“阻塞状态”等待t1运行结束。“子线程t1”结束之后,会唤醒“主线程main”,“主线程”重新获取cpu执行权,继续运行。

image.png

7.getName和setName

用来得到或者设置线程名称。

8.getPriority和setPriority

用来获取和设置线程优先级

9.setDaemon和isDaemon

用来设置线程是否成为守护线程和判断线程是否是守护线程。

守护线程和用户线程的区别在于:守护线程依赖于创建它的线程,而用户线程则不依赖。举个简单的例子:如果在main线程中创建了一个守护线程,当main方法运行完毕之后,守护线程也会随着消亡。而用户线程则不会,用户线程会一直运行直到其运行完毕。在JVM中,像垃圾收集器线程就是守护线程。

在上面已经说到了Thread类中的大部分方法,那么Thread类中的方法调用到底会引起线程状态发生怎样的变化呢?下面一幅图就是在上面的图上进行改进而来的:

image.png

2.停止线程

停止线程是在多线程开发时很重要的技术点,掌握此技术可以对线程的停止进行有效的处理。
停止一个线程可以使用Thread.stop()方法,但最好不用它。该方法是不安全的,已被弃用。
在Java中有以下3种方法可以终止正在运行的线程:

  • 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
  • 使用stop方法强行终止线程,但是不推荐使用这个方法,因为stop和suspend及resume一样,都是作废过期的方法,使用他们可能产生不可预料的结果。
  • 使用interrupt方法中断线程,但这个不会终止一个正在运行的线程,还需要加入一个判断才可以完成线程的停止。(可参考:http://www.cnblogs.com/skywang12345/p/3479949.html)

3.线程的优先级

在操作系统中,线程可以划分优先级,优先级较高的线程得到的CPU资源较多,也就是CPU优先执行优先级较高的线程对象中的任务。
设置线程优先级有助于帮“线程规划器”确定在下一次选择哪一个线程来优先执行。
设置线程的优先级使用setPriority()方法,此方法在JDK的源码如下:

1.  public  final  void setPriority(int newPriority)  {

2.  ThreadGroup g;

3.  checkAccess();

4.  if  (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY)  {

5.  throw  new  IllegalArgumentException();

6.  }

7.  if((g = getThreadGroup())  !=  null)  {

8.  if  (newPriority > g.getMaxPriority())  {

9.  newPriority = g.getMaxPriority();

10.  }

11.  setPriority0(priority = newPriority);

12.  }

13.  }

在Java中,线程的优先级分为1~10这10个等级,如果小于1或大于10,则JDK抛出异常throw new IllegalArgumentException()。
JDK中使用3个常量来预置定义优先级的值,代码如下:
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;

线程优先级特性:

  • 继承性
    比如A线程启动B线程,则B线程的优先级与A是一样的。
  • 规则性
    高优先级的线程总是大部分先执行完,但不代表高优先级线程全部先执行完。
  • 随机性
    优先级较高的线程不一定每一次都先执行完。

4.守护线程

在Java线程中有两种线程,一种是User Thread(用户线程),另一种是Daemon Thread(守护线程)。
Daemon的作用是为其他线程的运行提供服务,比如说GC线程。其实User Thread线程和Daemon Thread守护线程本质上来说去没啥区别的,唯一的区别之处就在虚拟机的离开:如果User Thread全部撤离,那么Daemon Thread也就没啥线程好服务的了,所以虚拟机也就退出了。

守护线程并非虚拟机内部可以提供,用户也可以自行的设定守护线程,方法:public final void setDaemon(boolean on) ;但是有几点需要注意:

  • thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。 (备注:这点与守护进程有着明显的区别,守护进程是创建后,让进程摆脱原会话的控制+让进程摆脱原进程组的控制+让进程摆脱原控制终端的控制;所以说寄托于虚拟机的语言机制跟系统级语言有着本质上面的区别)

  • 在Daemon线程中产生的新线程也是Daemon的。 (这一点又是有着本质的区别了:守护进程fork()出来的子进程不再是守护进程,尽管它把父进程的进程相关信息复制过去了,但是子进程的进程的父进程不是init进程,所谓的守护进程本质上说就是“父进程挂掉,init收养,然后文件0,1,2都是/dev/null,当前目录到/”)

  • 不是所有的应用都可以分配给Daemon线程来进行服务,比如读写操作或者计算逻辑。因为在Daemon Thread还没来的及进行操作时,虚拟机可能已经退出了。

5.sleep

sleep() 的作用是让当前线程休眠,即当前线程会从“运行状态”进入到“休眠(阻塞)状态”。sleep()会指定休眠时间,线程休眠的时间会大于/等于该休眠时间;在线程重新被唤醒时,它会由“阻塞状态”变成“就绪状态”,从而等待cpu的调度执行。

1.  // SleepTest.java的源码

2.  class  ThreadA  extends  Thread{

3.  public  ThreadA(String name){

4.  super(name);

5.  }

6.  public  synchronized  void run()  {

7.  try  {

8.  for(int i=0; i <10; i++){

9.  System.out.printf("%s: %d\n",  this.getName(), i);

10.  // i能被4整除时,休眠100毫秒

11.  if  (i%4  ==  0)

12.  Thread.sleep(100);

13.  }

14.  }  catch  (InterruptedException e)  {

15.  e.printStackTrace();

16.  }

17.  }

18.  }  

20.  public  class  SleepTest{

21.  public  static  void main(String[] args){

22.  ThreadA t1 =  new  ThreadA("t1");

23.  t1.start();

24.  }

25.  }

运行结果

1.  t1:  0

2.  t1:  1

3.  t1:  2

4.  t1:  3

5.  t1:  4

6.  t1:  5

7.  t1:  6

8.  t1:  7

9.  t1:  8

10.  t1:  9

结果说明
程序比较简单,在主线程main中启动线程t1。t1启动之后,当t1中的计算i能被4整除时,t1会通过Thread.sleep(100)休眠100毫秒。

我们知道,wait()的作用是让当前线程由“运行状态”进入“等待(阻塞)状态”的同时,也会释放同步锁。而sleep()的作用是也是让当前线程由“运行状态”进入到“休眠(阻塞)状态”。
但是,wait()会释放对象的同步锁,而sleep()则不会释放锁。

6.interrupt

interrupt()的作用是中断本线程。

本线程中断自己是被允许的;其它线程调用本线程的interrupt()方法时,会通过checkAccess()检查权限。这有可能抛出SecurityException异常。

如果本线程是处于阻塞状态:调用线程的wait(), wait(long)或wait(long, int)会让它进入等待(阻塞)状态,或者调用线程的join(), join(long), join(long, int), sleep(long), sleep(long, int)也会让它进入阻塞状态。若线程在阻塞状态时,调用了它的interrupt()方法,那么它的“中断状态”会被清除并且会收到一个InterruptedException异常。例如,线程通过wait()进入阻塞状态,此时通过interrupt()中断该线程;调用interrupt()会立即将线程的中断标记设为“true”,但是由于线程处于阻塞状态,所以该“中断标记”会立即被清除为“false”,同时,会产生一个InterruptedException的异常。

如果线程被阻塞在一个Selector选择器中,那么通过interrupt()中断它时;线程的中断标记会被设置为true,并且它会立即从选择操作中返回。

如果不属于前面所说的情况,那么通过interrupt()中断线程时,它的中断标记会被设置为“true”。

中断一个“已终止的线程”不会产生任何操作。

7.wait(), notify(), notifyAll()

在Object.java中,定义了wait(), notify()和notifyAll()等接口。wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。而notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程;notify()是唤醒单个线程,而notifyAll()是唤醒所有的线程。

Object类中关于等待/唤醒的API详细信息如下:
**notify()        **-- 唤醒在此对象监视器上等待的单个线程。
**notifyAll() **  – 唤醒在此对象监视器上等待的所有线程。
**wait()      **-- 让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法”,当前线程被唤醒(进入“就绪状态”)。
**wait(long timeout)   **-- 让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量”,当前线程被唤醒(进入“就绪状态”)。
**wait(long timeout, int nanos)  **-- 让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量”,当前线程被唤醒(进入“就绪状态”)。

更详细参考:http://www.cnblogs.com/skywang12345/p/3479224.html

五、线程的同步

同步目的:并发 多个线程访问同一份资源 确保资源安全 -->线程安全

1.同步代码块在代码块上加上”synchronized”关键字,则此代码块就称为同步代码块

2.同步代码块格式

1.  synchronized(同步对象){

2.  需要同步的代码块;

3.  }

3.同步方法
除了代码块可以同步,方法也是可以同步的

4.方法同步格式

synchronized void 方法名称(){}

死锁: 过多的同步容易造成死锁

注:synchronized后续会单独来学习

六、生产者和消费者模式

生产/消费者问题是个非常典型的多线程问题,涉及到的对象包括“生产者”、“消费者”、“仓库”和“产品”。他们之间的关系如下:
(01) 生产者仅仅在仓储未满时候生产,仓满则停止生产。
(02) 消费者仅仅在仓储有产品时候才能消费,仓空则等待。
(03) 当消费者发现仓储没产品可消费时候会通知生产者生产。
(04) 生产者在生产出可消费产品时候,应该通知等待的消费者去消费。

简单实例:

1.  /**

2.  一个场景,共同的资源

3.  生产者消费者模式 信号灯法

4.  wait() :等待,释放锁   sleep 不释放锁

5.  notify()/notifyAll():唤醒

6.  与 synchronized

7.  * @author Administrator

8.  *

9.  */

10.  public  class  Movie  {

11.  private  String pic ;

12.  //信号灯

13.  //flag -->T 生产生产,消费者等待 ,生产完成后通知消费

14.  //flag -->F 消费者消费 生产者等待, 消费完成后通知生产

15.  private  boolean flag =true;

16.  /**

17.  * 播放

18.  * @param pic

19.  */

20.  public  synchronized  void play(String pic){

21.  if(!flag){  //生产者等待

22.  try  {

23.  this.wait();

24.  }  catch  (InterruptedException e)  {

25.  e.printStackTrace();

26.  }

27.  }

28.  //开始生产

29.  try  {

30.  Thread.sleep(500);

31.  }  catch  (InterruptedException e)  {

32.  e.printStackTrace();

33.  }

34.  System.out.println("生产了:"+pic);

35.  //生产完毕

36.  this.pic =pic;

37.  //通知消费

38.  this.notify();

39.  //生产者停下

40.  this.flag =false;

41.  }

43.  public  synchronized  void watch(){

44.  if(flag){  //消费者等待

45.  try  {

46.  this.wait();

47.  }  catch  (InterruptedException e)  {

48.  e.printStackTrace();

49.  }

50.  }

51.  //开始消费

52.  try  {

53.  Thread.sleep(200);

54.  }  catch  (InterruptedException e)  {

55.  e.printStackTrace();

56.  }

57.  System.out.println("消费了"+pic);

58.  //消费完毕

59.  //通知生产

60.  this.notifyAll();

61.  //消费停止

62.  this.flag=true;

63.  }

64.  }

1.  /**

2.  * 生产者

3.  * @author Administrator

4.  *

5.  */

6.  public  class  Player  implements  Runnable  {

7.  private  Movie m ;

9.  public  Player(Movie m)  {

10.  super();

11.  this.m = m;

12.  }

14.  @Override

15.  public  void run()  {

16.  for(int i=0;i<20;i++){

17.  if(0==i%2){

18.  m.play("左青龙");

19.  }else{

20.  m.play("右白虎");

21.  }

22.  }

23.  }

25.  }

1.  public  class  Watcher  implements  Runnable  {

2.  private  Movie m ;

4.  public  Watcher(Movie m)  {

5.  super();

6.  this.m = m;

7.  }

9.  @Override

10.  public  void run()  {

11.  for(int i=0;i<20;i++){

12.  m.watch();

13.  }

14.  }

16.  }

1.  public  class  App  {

2.  public  static  void main(String[] args)  {

3.  //共同的资源

4.  Movie m =  new  Movie();

6.  //多线程

7.  Player p =  new  Player(m);

8.  Watcher w =  new  Watcher(m);

10.  new  Thread(p).start();

11.  new  Thread(w).start();

12.  }

13.  }

上面采用的是信号灯法,即标志位来控制,那么下面这个实例则是通过仓库来控制:

1.  public  class  TestProduce  {

2.  public  static  void main(String[] args)  {

3.  SyncStack sStack =  new  SyncStack();

4.  Shengchan sc =  new  Shengchan(sStack);

5.  Xiaofei xf =  new  Xiaofei(sStack);

6.  sc.start();

7.  xf.start();

8.  }

9.  }

11.  class  Mantou  {

12.  int id;

13.  Mantou(int id){

14.  this.id=id;

15.  }

16.  }

18.  class  SyncStack{

19.  int index=0;

20.  Mantou[] ms =  new  Mantou[10];

22.  public  synchronized  void push(Mantou m){

23.  while(index==ms.length){

24.  try  {

25.  this.wait();

26.  //wait后,线程会将持有的锁释放。sleep是即使睡着也持有互斥锁。

27.  }  catch  (InterruptedException e)  {

28.  e.printStackTrace();

29.  }

30.  }

31.  this.notify();  //唤醒在当前对象等待池中等待的第一个线程。notifyAll叫醒所有在当前对象等待池中等待的所有线程。

32.  //如果不唤醒的话。以后这两个线程都会进入等待线程,没有人唤醒。

33.  ms[index]=m;

34.  index++;

35.  }

36.  public  synchronized  Mantou pop(){

37.  while(index==0){

38.  try  {

39.  this.wait();

40.  }  catch  (InterruptedException e)  {

41.  e.printStackTrace();

42.  }

43.  }

44.  this.notify();

45.  index--;

46.  return ms[index];

47.  }

48.  }

50.  class  Shengchan  extends  Thread{

51.  SyncStack ss =  null;

53.  public  Shengchan(SyncStack ss)  {

54.  this.ss=ss;

55.  }

56.  @Override

57.  public  void run()  {

58.  for  (int i =  0; i <  20; i++)  {

59.  System.out.println("造馒头:"+i);

60.  Mantou m =  new  Mantou(i);

61.  ss.push(m);

62.  }

63.  }

64.  }

66.  class  Xiaofei  extends  Thread{

67.  SyncStack ss =  null;

69.  public  Xiaofei(SyncStack ss)  {

70.  this.ss=ss;

71.  }

72.  @Override

73.  public  void run()  {

74.  for  (int i =  0; i <  20; i++)  {

75.  Mantou m = ss.pop();

76.  System.out.println("吃馒头:"+i);

78.  }

79.  }

80.  }

七、面试题

线程和进程有什么区别?
答:一个进程是一个独立(self contained)的运行环境,它可以被看作一个程序或者一个应用。而线程是在进程中执行的一个任务。线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。别把它和栈内存搞混,每个线程都拥有单独的栈内存用来存储本地数据。在此我向大家推荐一个架构学习交流圈。交流学习指导伪鑫:1253431195(里面有大量的面试题及答案)里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

如何在Java中实现线程?
答:
创建线程有两种方式:
一、继承 Thread 类,扩展线程。
二、实现 Runnable 接口。

启动一个线程是调用run()还是start()方法?
答:启动一个线程是调用start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM 调度并执行,这并不意味着线程就会立即运行。run()方法是线程启动后要进行回调(callback)的方法。

Thread类的sleep()方法和对象的wait()方法都可以让线程暂停执行,它们有什么区别?
答:sleep()方法(休眠)是线程类(Thread)的静态方法,调用此方法会让当前线程暂停执行指定的时间,将执行机会(CPU)让给其他线程,但是对象的锁依然保持,因此休眠时间结束后会自动恢复(线程回到就绪状态,请参考第66题中的线程状态转换图)。wait()是Object类的方法,调用对象的wait()方法导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),只有调用对象的notify()方法(或notifyAll()方法)时才能唤醒等待池中的线程进入等锁池(lock pool),如果线程重新获得对象的锁就可以进入就绪状态。

线程的sleep()方法和yield()方法有什么区别?
答:
① sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
② 线程执行sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态;
③ sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常;
④ sleep()方法比yield()方法(跟操作系统CPU调度相关)具有更好的可移植性。

请说出与线程同步以及线程调度相关的方法。
答:

wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常;
notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;
notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;

八、总结

重点:1)线程的创建   2)线程的终止方法   3)线程的同步    4)生产者消费

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值