线程的创建和多线程

1 Java中关于应用程序和进程相关的概念

在Java中,一个应用程序对应着一个JVM实例(也有地方称为JVM进程),一般来说名字默认为java.exe或者javaw.exe(windows下可以通过任务管理器查看)。
Java采用的是 单线程编程模型,即在 我们自己的程序中如果没有主动创建线程的话,只会创建一个线程,通常称为主线程。但是要注意,虽然只有一个线程来执行任务,不代表JVM中只有一个线程,JVM实例在创建的时候,同时会创建很多其他的线程(比如垃圾收集器线程)。
由于Java采用的是单线程编程模型,因此在进行UI编程时要注意将耗时的操作放在子线程中进行,以避免阻塞主线程(在UI编程时,主线程即UI线程,用来处理用户的交互事件)。


2 线程的特点

线程是进程的组成部分,一个进程可以拥有多个线程,而一个线程必须拥有一个父进程。线程可以拥有自己的堆栈,自己的程序计数器和自己的局部变量,但不能拥有系统资源。 它与父进程的其他线程共享该进程的所有资源。

  • 线程可以完成一定任务,可以和其它线程共享父进程的共享变量和部分环境,相互协作来完成任务
  • 线程是独立运行的,其不知道进程中是否还有其他线程存在
  • 线程的执行是抢占式的,也就是说,当前执行的线程随时可能被挂起,以便运行另一个线程
  • 一个线程可以创建或撤销另一个线程,一个在这里插入代码片进程中的多个线程可以并发执行

3 线程的创建

Java使用Thread类代表线程所有的线程对象都必须是Thread或者其子类的实例,每个线程的作用是完成一定任务,实际上是就是执行一段程序流(一段顺序执行的代码)

3.1 继承Thread类创建线程类
  • 定义Thread类的子类 并重写该类的Run方法 该run方法的方法体就代表了线程需要完成的任务
  • 创建Thread类的实例,即创建了线程对象
  • 调用线程的start方法来启动线程
class MyThread extends Thread{
    private static int num = 0;
    public MyThread(){
       num++;
    }
    public void run() {
       System.out.println("主动创建的第"+num+"个线程");
    }
}

通过start()方法去启动线程,注意,不是调用run()方法启动线程,run方法中只是定义需要执行的任务,如果调用run方法,即相当于在主线程中执行run方法,跟普通的方法调用没有任何区别此时并不会创建一个新的线程来执行定义的任务。为了分清start()方法调用和run()方法调用的区别,请看下面一个例子:

public class Test {
   public static void main(String[] args)  {
      System.out.println("主线程ID:"+Thread.currentThread().getId());
      MyThread thread1 = new MyThread("thread1");
      thread1.start();
      MyThread thread2 = new MyThread("thread2");
      thread2.run();
   }
}

class MyThread extends Thread{
   private String name;
   public MyThread(String name){
      this.name = name;
   }
   public void run() {
      System.out.println("name:"+name+" 子线程ID:"+Thread.currentThread().getId());
   }
}

运行结果

主线程ID:1
name:thread2 子线程ID:1
name:thread1 子线程ID:8

从输出结果可以得出以下结论:
1)thread1和thread2的线程ID不同,thread2和主线程ID相同,说明通过run方法调用并不会创建新的线程,而是在主线程中直接运行run方法,跟普通的方法调用没有任何区别;
2)虽然thread1的start方法调用在thread2的run方法前面调用,但是先输出的是thread2的run方法调用的相关信息,说明 新线程创建的过程不会阻塞主线程的后续执行

3.2 实现 Runnable接口

Runnable对象仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。而实际的线程的对象依旧是 Thread实例,只是线程实例负责执行其 target 的 run()方法

  • 定义Runnable接口的实现类,并重写它的Run方法,run方法同样是该线程的执行体
  • 创建Runnable实现类的实例,并将此实例作为Thread的target创建一个Thread对象,该Thread对象才是真正的线程对象
  • 调用start方法启动该线程
public class Test {
  public static void main(String[] args)  {
     System.out.println("主线程ID:"+Thread.currentThread().getId());
     MyRunnable runnable = new MyRunnable();
     Thread thread = new Thread(runnable);
     thread.start();
  }
}

class MyRunnable implements Runnable{
   public MyRunnable() {
   }
   public void run() {
      System.out.println("子线程ID:"+Thread.currentThread().getId());
   }
}

这种方式必须将Runnable作为Thread类的参数,然后通过Thread的start方法来创建一个新线程来执行该子任务。如果调用Runnable的run方法的话,是不会创建新线程的,这根普通的方法调用没有任何区别
事实上,查看Thread类的实现源代码会发现Thread类是实现了Runnable接口的

3.3 两种继承方式的区别

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

  • Thread是Runnable接口的子类,实现Runnable接口的方式解决了Java单继承的局限
  • Runnable接口实现多线程比继承Thread类更加能描述数据共享的概念
4 多线程有几种实现同步方法 ?

同步的实现方面有两种,分别是synchronized,wait与notify

  • wait():使一个线程处于等待状态,并且 释放所持有的对象的lock
  • sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉 InterruptedException 异常
  • notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是 由JVM确定唤醒哪个线程,而且不是按优先级
  • Allnotity():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是 让它们竞争
5 产生死锁的原因

产生死锁的四个必要条件:

  • 1 互斥条件:一个资源每次只能被一个进程使用
  • 2 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
  • 3 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺
  • 4 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
6 避免死锁

上面列出了死锁的四个必要条件,我们只要想办法破其中的任意一个或多个条件,就可以避免死锁发生,一般有以下几种方法

  • 1 按同一顺序访问对象
  • 2 避免事务中的用户交互
  • 3 保持事务简短并处于一个批处理中
  • 4 使用较低的隔离级别
  • 5 使用基于行版本控制的隔离级别
  • 6 使用绑定连接
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章