Java语言学习(十二):多线程

原创
2018/07/19 17:14
阅读数 102

    Java中给多线程编程提供了内置的支持,多线程是多任务的一种特别形式,它使用了更小的资源开销。这里需要知道两个术语及其关系:进程和线程。

    进程:进程是系统进行资源分配和调度的一个独立单位。一个进程包括由操作系统分配的内存空间,包含一个或多个线程。

    线程:线程是进程的一个实体,是CPU调度和分派的基本单位。它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

    (一)线程的生命周期

    线程是一个动态执行的过程,从产生到死亡,这个过程称为线程的生命周期。线程的状态有:新建状态、就绪状态、运行状态、阻塞状态、死亡状态,如下图所示,注意整个执行过程的实现:

  • 新建状态(New):当线程对象对创建后,即进入了新建状态;
  • 就绪状态(Runnable):当调用线程对象的start()方法,线程即进入就绪状态,等待CPU调度执行;
  • 运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态;
  • 阻塞状态(Blocked):处于运行状态中的线程由于某种原因,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态;
  • 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期;

    (二):线程的创建

    Java提供了三种创建线程的方法:

  • 实现Runnable接口
  • 继承Thread类
  • 通过Callable和Future创建; 

    在开发中,前两种是常用的线程创建方式,下面来简单说下:

     (1)通过实现Runnable接口创建线程

public class RunnableDemo implements Runnable{
    private Thread t;
    private String threadName;

    //构造方法
    public RunnableDemo(String name) {
        this.threadName = name;
        System.out.println("创建线程:"+threadName);
    }
    //重写run()方法
    @Override
	public void run() {
        System.out.println("运行线程:"+threadName);
        try {
            for(int i=4;i>0;i--){
                System.out.println("Thread: "+threadName+","+i);
                Thread.sleep(50);
            }
        }catch (Exception e) {
             System.out.println("Thread "+threadName+" 阻塞");
        }
        System.out.println("Thread "+threadName+" 终止"); 
    }
    //调用方法(为了输出信息,可以忽略)
    public void start(){
        System.out.println("启动线程:"+threadName);
        if(t == null){
           t = new Thread(this,threadName);
           t.start(); 
        }  
    }
}

    测试类:

public class RunnableTest {
    public static void main(String[] args) {
        RunnableDemo r1 = new RunnableDemo("T1");
        r1.start();
        RunnableDemo r2 = new RunnableDemo("T2");
        r2.start();
    }   
}

    输出为:

创建线程:T1
启动线程:T1
创建线程:T2
启动线程:T2
运行线程:T1
Thread: T1
运行线程:T2
Thread: T2,4
Thread: T2,3
Thread: T1,3
Thread: T2,2
Thread: T1,2
Thread: T2,1
Thread: T1,1
Thread T2 终止
Thread T1 终止

    从上面的实例可以看出,通过实现Runnable接口创建线程的几个要点:

  • 构造方法来创建线程对象,有参或无参看自己需要;
  • 重写run()方法,这里写入自己需要实现的代码;
  • 启动start()方法,这里可以直接调用;
  • run()方法是线程的入口点,必须通过调用start()方法才能执行;

    (2)通过继承Thread类创建线程

    它本质上也是实现了 Runnable 接口的一个实例,所以这里就不贴出代码了,可以按照上面的实例,更改class为继承即可,如下:

public class ThreadDemo extends Thread{}

    Thread类的常用且重要的方法有:

  • public void start():使该线程开始执行;Java虚拟机调用该线程的 run 方法,对象调用;
  • public void run():对象调用;
  • public static void sleep(long millisec):在指定的毫秒数内让当前正在执行的线程休眠,静态方法,直接调用;

     注意:Java虚拟机允许应用程序并发的运行多个执行线程,利用多线程编程可以编写高效的程序,但线程太多,CPU 花费在上下文的切换的时间将多于执行程序的时间,执行效率反而降低,所以,线程并不是创建的越多越好好,一般来说小到1个,大到10左右基本就够用了。

    当然,关于线程的其他知识,如优先级、休眠、终止等,这里就不做介绍了。

    (三)synchronized关键字

    Java提供了很多方式和工具来帮助简化多线程的开发,如同步方法,即有synchronized关键字修饰的方法,这和Java的内置锁有关。每个Java对象都有一个内置锁,若方法用synchronized关键字声明,则内置锁会保护整个方法,即在调用该方法前,需要获得内置锁,否则就处于阻塞状态。一个简单的同步方法声明如下:

public synchronized void save(){}

    synchronized关键字也可以修饰静态方法,此时若调用该静态方法,则会锁住整个类。下面通过实例来说明下具体的使用:

    同步线程类:

public class SyncThread implements Runnable {
   //定义计数变量并在构造函数中初始化
   private static int count;
   public SyncThread(){
	   count = 0;
   }
   @Override
   public synchronized void run() {
       for(int i=0;i<5;i++){
          //打印当前count值并进行累加操作,可分开写
          System.out.println(Thread.currentThread().getName() +":"+ (count++));
          try {
             Thread.sleep(100);
          }catch (InterruptedException e) {
             e.printStackTrace();
          }
       }
   }
   public int getCount(){
		return count;
   }  
}

    测试类:

public class SyncTest {
    public static void main(String[] args) {
         SyncThread sThread = new SyncThread();
         //创建线程对象的同时初始化该线程的名称
         Thread t1 = new Thread(sThread,"SThread1");
         Thread t2 = new Thread(sThread,"SThread2");
         t1.start();
		 t2.start();
    }
}

    输出为:

SThread1:0
SThread1:1
SThread1:2
SThread1:3
SThread1:4
SThread2:5
SThread2:6
SThread2:7
SThread2:8
SThread2:9

    从上面可以看出:一个线程访问一个对象中的synchronized同步方法时,其他试图访问该对象的线程将被阻塞。当然,大家可以去掉synchronized关键字,看看会有什么不同。这里必须要注意:是访问同一个对象的不同方法,如上面的对象sThread,若是不同的对象,则不受阻塞。这里不做介绍了,大家可以参考:Java中synchronized的用法,好好理解下。

    (四)volatile关键字

    相比较synchronized而言,volatile关键字是Java提供的一种轻量级的同步机制,为域变量的访问提供了一种免锁机制,使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,因此每次使用该域就要重新计算,而不是使用寄存器中的值。

    如果读操作的次数要远远超过写操作,与锁相比,volatile 变量通常能够减少同步的性能开销。简单的定义如下:

private volatile int count = 0;

    volatile不具备原子特性,也不能用来修饰final类型的变量。要使volatile修饰的变量提供理想的线程安全,必须满足两个条件:

  • 对变量的写操作不依赖于当前值;
  • 变量没有包含在具有其他变量的不变式中;

    这里不做详述了,但需要注意一点:避免volatile修饰的变量用于复合操作,如 num++,这个复合操作包括三步(读取->加1->赋值),所以,在多线程的环境下,有可能线程会对过期的num进行++操作,重新写入到主存中,而导致出现num的结果不合预期的情况。

    线程间还可以实现通信,这里不做介绍。

    Java中的对象使用new操作符创建,若创建大量短生命周期的对象,则性能低下。所以才有了池的技术,如数据库连接有连接池,线程则有线程池。

    使用线程池创建对象的时间是0毫秒,说明其高效性。大家感兴趣的可自行查看、了解该块的知识点。

    好了,以上概括的就是多线程的基本知识点了,希望帮到大家。

展开阅读全文
打赏
0
8 收藏
分享
加载中
更多评论
打赏
0 评论
8 收藏
0
分享
返回顶部
顶部