服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - Java教程 - 基于线程、并发的基本概念(详解)

基于线程、并发的基本概念(详解)

2020-11-02 17:43Java教程网 Java教程

下面小编就为大家带来一篇基于线程、并发的基本概念(详解)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧

什么是线程

提到“线程”总免不了要和“进程”做比较,而我认为在Java并发编程中混淆的不是“线程”和“进程”的区别,而是“任务(Task)”。进程是表示资源分配的基本单位。而线程则是进程中执行运算的最小单位,即执行处理机调度的基本单位。关于“线程”和“进程”的区别耳熟能详,说来说去就一句话:通常来讲一个程序有一个进程,而一个进程可以有多个线程。

但是“任务”是很容易忽略的一个概念。我们在实际编码中通常会看到这么一个包叫做xxx.xxx.task,包下是XxxTask等等以Task后缀名结尾的类。而XxxTask类通常都是实现Runnable接口或者Thread类。严格来说,“任务”和并发编程没多大关系,就算是单线程结构化顺序编程中,我们也可以定义一个Task类,在类中执行我们想要完成的一系列操作。“任务”我认为是我们人为定义的一个概念,既抽象又具体,抽象在它指由软件完成的一个活动,它可以是一个线程,也可以是多个线程共同达到某一目的的操作,具体在于它是我们认为指定实实在在的操作,例如:定时获取天气任务(定时任务),下线任务……关键就在于不要认为一个任务对应的就是一个线程,也许它是多个线程,甚至在这个任务中是一个线程池,这个线程池处理这个我们定义的操作。

我产生“线程”和“任务”的疑惑就是在《Thinking in Java》这本书的“并发”章节中它将线程直接定义为一个任务,在开篇标题就取名为“定义任务”,并且提到定义任务只需实现Runnable接口.而这个任务则是通过调用start来创建一改新的线程来执行.说来说去有点绕,其实也不必纠结于在书中时而提到线程,时而提到人任务.我认为就记住:任务是我们在编程时所赋这段代码的实际意义,而线程就关注它是否安全,是否需要安全,这就是后面要提到的线程安全问题.在像我一样产生疑惑时,不用在意它两者间的关系和提法。

什么是并发?

提到了并发,那又不得不和并行作比较。并发是指在一段时间内同时做多个事情,比如在1点-2点洗碗、洗衣服等。而并行是指在同一时刻做多个事情,比如1点我左手画圆右手画方。两个很重要的区别就是“一段时间”和“同一时刻”.在操作系统中就是:

1) 并发就是在单核处理中同时处理多个任务。(这里的同时指的是逻辑上的同时)

2) 并行就是在多核处理器中同时处理多个任务。(这里的同时指的就是物理上的同时)

初学编程基本上都是单线程结构化编程,或者说是根本就接触不到线程这个概念,反正程序照着自己实现的逻辑,程序一步一步按照我们的逻辑去实现并且得到希望输出的结果。但随着编程能力的提高,以及应用场景的复杂多变,我们不得不要面临多线程并发编程。而初学多线程并发编程时,常常出现一些预料之外的结果,这就是涉及到“线程安全”问题。

什么线程安全?

这是在多线程并发编程中需要引起足够重视的问题,如果你的线程不足够“安全”,程序就可能出现难以预料,以及难以复现的结果。《Java并发编程实战》提到对线程安全不好做一个定义,我的简单理解就是:线程安全就是指程序按照你的代码逻辑执行,并始终输出预定的结果。书中的给的定义:当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。具体有关线程安全的问题,例如原子性、可见性等等不在这里做详细阐述,适当的时候会进行详细介绍,简单说一点,想要这个线程安全,得在访问的时候给它上个锁,不让其他线程访问,当然这种说法不严谨,不过可以暂时这么理解。

以上是从基本概念理论出发来大致了解需要知道的一些概念,下面就针对JDK中有关线程的API来对多线程并发编程做一个了解。

?
1
2
3
4
5
6
java.lang.Object
  -public void notify()//唤醒这个对象监视器正在等待获取锁的一个线程
  -public void notifyAll()//唤醒这个对象监视器上正在等待获取锁的所有线程
  -public void wait()//导致当前线程等待另一个线程调用notify()或notifyAll()
  -public void wait(long timeout)// 导致当前线程等待另一个线程调用notify()或notifyAll(),或者达到timeout时间
  -public void wait(long timeout, int nanos)//与上个方法相同,只是将时间控制到了纳秒nanos

我们先用一个经典的例子——生产者消费者问题来说明上面的API是如何使用的。生产者消费者问题指的的是,生产者生产产品到仓库里,消费者从仓库中拿,仓库满时生产者不能继续生产,仓库为空时消费者不能继续消费。转化成程序语言也就是生产者是一个线程

1,消费者是线程

2,仓库是一个队列,线程1往队尾中新增,线程2从队首中移除,队列满时线程1不能再新增,队列空时线程2不能再移除。

?
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
package com.producerconsumer;
 
import java.util.Queue;
 
 
 
/**
 
 * 生产者
 
 * Created by yulinfeng on 2017/5/11.
 
 */
 
public class Producer implements Runnable{
 
  private final Queue<String> queue;
 
  private final int maxSize;
 
  public Producer(Queue<String> queue, int maxSize) {
 
    this.queue = queue;
 
    this.maxSize = maxSize;
 
  }
  public void run() {
    produce();
  }
 
  /**
 
   * 生产
 
   */
 
  private void produce() {
 
    try {
      while (true) {
        synchronized (queue) {
          if (queue.size() == maxSize) {
            System.out.println("生产者:仓库满了,等待消费者消费");
            queue.wait();
          }
          System.out.println("生产者:" + queue.add("product"));
          queue.notifyAll();
        }
      }
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}
 
 
 
package com.producerconsumer;
 
import java.util.Queue;
 
/**
 
 * 消费者
 
 * Created by yulinfeng on 2017/5/11.
 
 */
 
public class Consumer implements Runnable {
 
  private final Queue<String> queue;
  public Consumer(Queue<String> queue) {
    this.queue = queue;
  }
 
  public void run() {
    consume();
  }
 
 
 
  /**
 
   * 消费
 
   */
 
  private void consume() {
    synchronized (queue) {
      try {
        while (true) {
          if (queue.isEmpty()) {
            System.out.println("消费者:仓库空了,等待生产者生产");
            queue.wait();
          }
          System.out.println("消费者:" + queue.remove());
          queue.notifyAll();
        }
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
 
}
 
 
 
package com.producerconsumer;
 
import java.util.LinkedList;
import java.util.Queue;
 
/**
 
 * Created by yulinfeng on 2017/5/11.
 
 */
 
public class Main {
 
  public static void main(String[] args) {
 
    Queue<String> queue = new LinkedList<String>();
    int maxSize = 100;
    Thread producer = new Thread(new Producer(queue, maxSize));
    Thread consumer = new Thread(new Consumer(queue));
    producer.start();
    consumer.start();
 
  }
 
}

基于线程、并发的基本概念(详解) 

这个生产者消费者问题的实现,我采用线程不安全的LinkedList,使用内置锁synchronized来保证线程安全,在这里我们不讨论synchronized,主要谈notify()、notifyAll()和wait()。

在这里例子中,作为生产者,当队列满时调用了队列的wait()方法,表示等待,并且此时释放了锁。作为消费者此时获取到锁并且移除队首元素时调用了notifyAll()方法,此时生产者由wait等待状态转换为唤醒状态,但注意!此时仅仅是线程被唤醒了,有了争夺CPU资源的资格,并不代表下一步就一定是生产者生产,还有可能消费者继续争夺了CPU资源。一定记住是被唤醒了,有资格争夺CPU资源。notifyAll()表示的是唤醒所有等待的线程,所有等待的线程被唤醒过后,都有了争夺CPU资源的权利,至于是谁会获得这个锁,那不一定。而如果是使用notify(),那就代表唤醒所有等待线程中的一个,只是一个被唤醒具有了争夺CPU的权力,其他没被唤醒的线程继续等待。如果等待线程就只有一个那么notify()和notifyAll()就没区别,不止一个那区别就大了,一个是只唤醒其中一个,一个是唤醒所有。唤醒不是代表这个线程就一定获得CPU资源一定获得锁,而是有了争夺的权利。

?
1
2
3
4
5
java.lang.Thread
  -public void join()
  -public void sleep()
  -public static void yield()
  -……

针对Thread线程类,我们只说常见的几个不容易理解的方法,其余方法不在这里做详细阐述。

关于sleep()方法,可能很容易拿它和Object的wait方法作比较。两个方法很重要的一点就是sleep不会释放锁,而wait会释放锁。在上面的生产者消费者的生产或消费过程中添加一行Thread.sleep(5000),你将会发现执行到此处时,这个跟程序都会暂停执行5秒,不会有任何其他线程执行,因为它不会释放锁。

关于join()方法,JDK7的解释是等待线程结束(Waits for this thread to die)似乎还是不好理解,我们在main函数中启动两个线程,在启动完这两个线程后main函数再执行其他操作,但如果不加以限制,有可能main函数率先执行完需要的操作,但如果在main函数中加入join方法,则表示阻塞等待这两个线程执行结束后再执行main函数后的操作,例如:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.join;
 
 
public class Main {
 
  public static void main(String[] args) throws Exception{
    Thread t1 = new Thread(new Task(0));
    Thread t2 = new Thread(new Task(0));
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.print("main结束");
  }
}

上面个例子如果没有join方法,那么“main”结束这条输出语句可能就会先于t1、t2,加上在启动线程的调用方使用了线程的join方法,则调用方则会阻塞线程执行结束过后再执行剩余的方法。

关于Thread.yield()方法,本来这个线程处于执行状态,其他线程也想争夺这个资源,突然,这个线程不想执行了想和大家一起来重新夺取CPU资源。所以Thread.yield也称让步。从下一章开始就正式开始了解java.util.concurrent。

以上这篇基于线程、并发的基本概念(详解)就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持服务器之家。

延伸 · 阅读

精彩推荐