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

什么是线程?

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

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

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

什么是并发?

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

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

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

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

什么线程安全?

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

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

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不能再移除。

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资源一定获得锁,而是有了争夺的权利。

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函数后的操作,例如:

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。

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

(0)

相关推荐

  • Java多线程和并发基础面试题(问答形式)

    本文帮助大家掌握Java多线程基础知识来对应日后碰到的问题,具体内容如下 一.Java多线程面试问题 1. 进程和线程之间有什么不同? 一个进程是一个独立(self contained)的运行环境,它可以被看作一个程序或者一个应用.而线程是在进程中执行的一个任务.Java运行环境是一个包含了不同的类和程序的单一进程.线程可以被称为轻量级进程.线程需要较少的资源来创建和驻留在进程中,并且可以共享进程中的资源. 2. 多线程编程的好处是什么? 在多线程程序中,多个线程被并发的执行以提高程序的效率,C

  • JAVA多线程与并发学习总结分析

    1.计算机系统使用高速缓存来作为内存与处理器之间的缓冲,将运算需要用到的数据复制到缓存中,让计算能快速进行:当运算结束后再从缓存同步回内存之中,这样处理器就无需等待缓慢的内存读写了. 缓存一致性:多处理器系统中,因为共享同一主内存,当多个处理器的运算任务都设计到同一块内存区域时,将可能导致各自的缓存数据不一致的情况,则同步回主内存时需要遵循一些协议. 乱序执行优化:为了使得处理器内部的运算单位能尽量被充分利用. 2.JAVA内存模型目标是定义程序中各个变量的访问规则.(包括实例字段.静态字段和构

  • 浅谈Java线程并发知识点

    发布:一个对象是使它能够被当前范围之外的代码所引用: 常见形式:将对象的的引用存储到公共静态域:非私有方法中返回引用:发布内部类实例,包含引用. 逃逸:在对象尚未准备好时就将其发布. 不要让this引用在构造函数中逸出.例,在构造函数中启动线程,线程会包含对象的引用. 同步容器:对容器的所有状态进行穿行访问,Vector.Hashtable,Cllections.synchronizedMap|List 并发容器:ConcurrentHashMap,CopyOnWriteArrayList,Co

  • Java线程的基本概念

    在之前的章节中,我们都是假设程序中只有一条执行流,程序从main方法的第一条语句逐条执行直到结束.从本节开始,我们讨论并发,在程序中创建线程来启动多条执行流,并发和线程是一个复杂的话题,本节,我们先来讨论Java中线程的一些基本概念. 创建线程 线程表示一条单独的执行流,它有自己的程序执行计数器,有自己的栈.下面,我们通过创建线程来对线程建立一个直观感受,在Java中创建线程有两种方式,一种是继承Thread,另外一种是实现Runnable接口,我们先来看第一种. 继承Thread Java中j

  • 详谈java线程与线程、进程与进程间通信

    线程与线程间通信 一.基本概念以及线程与进程之间的区别联系: 关于进程和线程,首先从定义上理解就有所不同 1.进程是什么? 是具有一定独立功能的程序.它是系统进行资源分配和调度的一个独立单位,重点在系统调度和单独的单位,也就是说进程是可以独 立运行的一段程序. 2.线程又是什么? 线程进程的一个实体,是CPU调度和分派的基本单位,他是比进程更小的能独立运行的基本单位,线程自己基本上不拥有系统资源. 在运行时,只是暂用一些计数器.寄存器和栈 . 他们之间的关系 1.一个线程只能属于一个进程,而一个

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

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

  • 基于java中集合的概念(详解)

    1.集合是储存对象的,长度可变,可以封装不同的对象 2.迭代器: 其实就是取出元素的方式(只能判断,取出,移除,无法增加) 就是把取出方式定义在集合内部,这样取出方式就可以直接访问集合内部的元素,那么取出方式就被定义成了内部类. 二每一个容器的数据结构不同,所以取出的动作细节也不一样.但是都有共性内容判断和取出,那么可以将共性提取,这些内部类都符合一个规则Iterator Iterator it = list.iterator(); while(it.hasNext()){ System.out

  • Go语言并发编程基础上下文概念详解

    目录 前言 1 Go 中的 Context 2 Context 接口 3 Context Tree 4 创建上下文 4.1 上下文创建函数 4.2 Context 使用规范 4.3 Context 使用场景 5 总结 前言 相信大家以前在做阅读理解的时候,一定有从老师那里学一个技巧或者从参考答案看个:结合上下文.根据上下文我们能够找到有助于解题的相关信息,也能更加了解段落的思想. 在开发过程中,也有这个上下文(Context)的概念,而且上下文也必不可少,缺少上下文,就不能获取完整的程序信息.那

  • java并发编程_线程池的使用方法(详解)

    一.任务和执行策略之间的隐性耦合 Executor可以将任务的提交和任务的执行策略解耦 只有任务是同类型的且执行时间差别不大,才能发挥最大性能,否则,如将一些耗时长的任务和耗时短的任务放在一个线程池,除非线程池很大,否则会造成死锁等问题 1.线程饥饿死锁 类似于:将两个任务提交给一个单线程池,且两个任务之间相互依赖,一个任务等待另一个任务,则会发生死锁:表现为池不够 定义:某个任务必须等待池中其他任务的运行结果,有可能发生饥饿死锁 2.线程池大小 注意:线程池的大小还受其他的限制,如其他资源池:

  • 基于java 线程的几种状态(详解)

    线程可以有六种状态: 1.New(新创建) 2.Runnable(可运行)(运行) 3.Blocked(被阻塞) 4.Waiting(等待) 5.Timed waiting(计时等待) 6.Terminated(被终止) 新创建线程: 当用new操作符创建一个新线程时,如new Thread(r),该线程还没有开始运行,它的当前状态为new,在线程运行之前还有一些基础工作要做. 可运行线程: 一旦线程调用start方法,线程处于runnable状态.在这个状态下的线程可能正在运行也可能没有运行(

  • 基于ScheduledExecutorService的两种方法(详解)

    开发中,往往遇到另起线程执行其他代码的情况,用java定时任务接口ScheduledExecutorService来实现. ScheduledExecutorService是基于线程池设计的定时任务类,每个调度任务都会分配到线程池中的一个线程去执行,也就是说,任务是并发执行,互不影响. 注意,只有当调度任务来的时候,ScheduledExecutorService才会真正启动一个线程,其余时间ScheduledExecutorService都是处于轮询任务的状态. 1.scheduleAtFix

  • 基于NIO的Netty网络框架(详解)

    Netty是一个高性能.异步事件驱动的NIO框架,它提供了对TCP.UDP和文件传输的支持,Netty的所有IO操作都是异步非阻塞的,通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果. Netty的优点有: a.功能丰富,内置了多种数据编解码功能.支持多种网络协议. b.高性能,通过与其它主流NIO网络框架对比,它的综合性能最佳. c.可扩展性好,可通过它提供的ChannelHandler组件对网络通信方面进行灵活扩展. d.易用性,API使用简单.

  • java并发编程之cas详解

    CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术.简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值.这听起来可能有一点复杂但是实际上你理解之后发现很简单,接下来,让我们跟深入的了解一下这项技术. CAS的使用场景 在程序和算法中一个经常出现的模式就是"check and act"模式.先检查后操作模式发生在代码中首先检查一个变量的值,然后再基于这个值做一些操作.下面是一个

  • Java内存模型之happens-before概念详解

    简介 happens-before是JMM的核心概念.理解happens-before是了解JMM的关键. 1.设计意图 JMM的设计需要考虑两个方面,分别是程序员角度和编译器.处理器角度: 程序员角度,希望内存模型易于理解.易于编程.希望是一个强内存模型. 编译器和处理器角度,希望减少对它们的束缚,以至于编译器和处理器可以做更多的性能优化.希望是一个弱内存模型. ​因此JSR-133专家组设计JMM的核心目标就两个: 为程序员提供足够强的内存模型对编译器和处理器的限制尽可能少 ​下面通过一段代

  • G1垃圾回收器在并发场景调优详解

    目录 序言 G1概览 1.最大堆大小 2.Region大小 3.获取默认值 三种GC模式 1.新生代回收 2.混合回收 3.Full GC 默认参数 1.堆内存 2.新生代内存回收 3.混合回收 垃圾在堆中流转 1.对象如何进入老年代 (1)大对象直接到老年代 (2)动态年龄判断 2.高并发加速进入老年代 调优步骤 1.设置垃圾回收器 2.设置堆大小 3.元空间设置 4.GC停顿时间 5.新生代大小 调优实践 1.频繁的YGC 2.频繁的Mixed GC (1)大对象 (2)元空间 3.Full

随机推荐