PSJay Blog

#FIXME, seriously

Java并发总结(一):线程基础

| Comments

最近复习Java并发,写点东西总结总结。好记性不如烂博客。

并发

什么是并发?

与顺序编程不同,并发使得程序在同一时刻可以执行多个操作(宏观)。

为什么需要并发?

通常是为了提高程序的运行速度或者改善程序的设计。

线程

Java对并发编程提供了语言级别的支持。Java通过线程来实现并发编程。一个线程通常完成某个特定的任务,一个进程可以拥有多个线程,当这些线程一起执行的时候,就实现了并发。与操作系统中的进程相似,每个线程看起来好像拥有自己的CPU,但是其底层是通过切分CPU时间来实现的。与进程不同的是,线程并不是相互独立的,它们通常要相互合作来完成一些任务。

实现线程

你可以通过继承Thread类来实现自己的线程,关键的部分是,依靠重写run()方法来完成线程的工作。先写一个简单的线程,它的任务就是在控制台中依次打印出0,1,2,3,4。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SimpleThread extends Thread{
  @Override
  public void run() {
      for(int i = 0; i < 5 ;i++) {
          System.out.println(i);
          try {
              sleep(20); //让线程每打印一个数字之后休息20毫秒
          } catch (InterruptedException e) {
              System.out.println("Throw InterruptedException!");
          }
      }
  }
}

之后,我们启动两个这样的线程,让它们分别完成自己的任务。

1
2
3
4
5
6
7
8
public class ThreadTest {
  public static void main(String[] args) {
      SimpleThread simpleThread1 = new SimpleThread();
      SimpleThread simpleThread2 = new SimpleThread();
      simpleThread1.start();
      simpleThread2.start();
  }
}

执行结果如下:

0 0 1 1 2 2 3 3 4 4

结果似乎并不是像单线程程序那样,分先后两次分别打印出0,1,2,3,4,而是两个线程似乎同时在执行一样,都分别打印出自己的0,1,2,3,4,这便是“并行”。在调用一个Thread对象的start()方法之后,JVM就会启动一个新的线程。

直接继承Thread类并不是一个好的方法。因为有时候我们自己的线程类可能需要继承别的类,而Java并不支持多重继承。这个时候Runnable接口就有了用处(实际上Thread类本身也实现了Runnable接口)。我们要做的就是实现Runnable接口,并且重写run()方法。最后将一个实现了Runnable接口的类的实例交给Thread类的构造函数,就可以实例化出一个可用的线程了。

1
2
3
4
5
6
7
8
9
10
11
class MyTask implements Runnable {
        @override
        public void run() {
               //需要进行的操作
        }
}

//像下面这样来使用
MyTask mt = new MyTask();
Thread t = new Thread(mt);
t.start();

实际上,在很多时候,我们可以将一个线程看做一个任务,这个线程存在的价值就是为了完成某个任务。

休眠

正如第一个例子那样,我们可以让一个线程暂时休息一会儿。Thread类有一个sleep静态方法,你可以将一个long类型的数据当做参数传进去,单位是毫秒,表示线程将会休眠的时间。第一个例子中之所以调用了sleep方法,是因为打印5个数字的时间太短,如果不休眠的话可能看不到“并发”的效果,因为CPU的时间片还来不及转换,线程的任务就已经完成了。

让步

Thread类还有一个名为yield()的静态方法。这个方法的作用是为了建议当前正在运行的线程做个让步,让出CPU时间给别的线程来运行。程序中可能会有一个线程在某个时刻已经完成了一大部分的任务,并且这个时候让别的线程来运行比较合理。这样的情况下,就可以调用yield()方法进行让步。不过,调用这个方法并不能保证一定会起作用,毕竟它只是建议性的。所以,不应该用这个方法来控制程序的执行流程。

串入(join)

当一个线程t1在另一个线程t2上调用t1.join()方法的时候,线程t2将等待线程t1运行结束之后再开始运行。正如下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class ThreadTest {
  public static void main(String[] args) {
      SimpleThread simpleThread = new SimpleThread();

      Thread t = new Thread(simpleThread);

      t.start();

  }
}
public class SimpleThread implements Runnable{
  @Override
  public void run() {
      Thread tempThread = new Thread() {
                              @Override
                              public void run() {
                                  for(int i = 10; i < 15 ;i++) {
                                      System.out.println(i);
                                  }
                              }
                          };
      
      tempThread.start();
      
      try {
          tempThread.join();        //tempThread串入
      } catch (InterruptedException e) {
          e.printStackTrace();
      }
      
      for(int i = 0; i < 5; i++) {
          System.out.println(i);
      }
  }
}

输出结果为:

10
11
12
13
14
0
1
2
3
4

优先级

我们可以给一个线程设定一个优先级。线程调度器在做调度工作的时候,优先级越高的线程越可能得到先运行的机会。Thread类的setPriority方法和getPriority方法分别用来设置线程的优先级和获取线程的优先级。由于线程调度器根据优先级的大小来调度线程的效果在各种不同的JVM上差别很大,所以在绝大多数情况下,我们不应该依靠设定优先级来完成我们的工作,保持默认的优先级是一条很好的建议。

守护线程(deamon thread)

通常,程序中有一些线程的工作并不是不可或缺的,它只是用来协助其他线程来工作。这样的线程叫做守护线程或者后台线程。当进程中的所有非守护线程结束时,守护线程也就终止了,就算它还没有完全完成自己的任务。我们可以在一个线程启动之前调用setDaemon()方法来将这个线程设定成守护线程。

Comments