讲讲并发


有一阵子没有更新博文了,这段时间接触的知识比较杂,内容不足以写成一篇像样的文章,不过只是因为这样的原因而迟迟不更新的话太不像样了,所以我决定以对目前所学知识的理解写一篇大概的框架,随着理解的深入再不断补充

并发的一些概念

首先我们需要理清一些概念问题:
  • 线程与进程的区别
   1、一个进程同时执行多个任务,通常,每一个任务相当于一个线程
   2、每个进程拥有自己的一整套变量,而线程则共享数据(共享数据是不安全的,这就需要同步机制解决这个问题)

  • 并行和并发区别
   1、并行是指两者同时执行一件事,比如赛跑,两个人都在不停的往前跑;
   2、并发是指资源有限的情况下,两者交替轮流使用资源,比如一段路(单核CPU资源)  同时只能过一个人,A走一段后,让给B,B用完继续给A ,交替使用,目的是提高效率
单台拥有多个CPU的计算机往往给人一种进程与CPU一一对应的错觉,事实上并发执行的进程数目并不是由CPU数目制约的,操作系统将CPU的时间片分配给每一个进程,给人以并行处理的感觉(在进程数目小于处理器数目时的确是并行处理的)

多线程的作用

在我们的程序中或多或少会出现一些耗时的任务,
在传统的单线程程序中(只有主线程)我们不得不等待这些恼人的任务结束后才能继续执行其他操作,也就是说在此期间我们无法与程序进行交互,这无疑是致命的。

举个栗子,现在需要我们设计一个音乐播放器,
有播放和取消两个按钮,
想象在不使用多线程的情况下执行这个程序会发生什么,

正如预料的那样,当我们点击播放按钮后这个程序的确是在播放音乐,但在此同时我们丧失了对这个程序的控制权,在音乐播放的这段时间内我们无法对这个程序进行操作

我们可以通过运行一个线程中的关键代码来保持用户对程序的控制权从而解决这个问题


创建一个线程

有两个方法可以定义一个线程

#方法一

   1、实现Runnable接口

        class xxx implements Runnable {
    
            @Override
            public void run() {
        
            }
        }

        由于Runnable是一个函数式接口,可以用lambda表达式建立一个实例
        Runnable r =() ->{task code};

   2由Runnable创建一个Thread对象:

        Thread t = new Thread(r);

   3、启动线程:

        t.start();

#方法二

   1、构建一个Thread类的子类定义一个线程

        class xxx extends Thread {
           pubilc void run(){}
        }

      2、创建实例启动线程:

            new xxx().start();

//此方法并不推荐,因为我们应该将要并行运行的任务与运行机制解耦和

//同时我们要注意不要调用run方法,直接调用run方法,只会执行一个线程中的任务,而不会启动新线程



中断线程


  • 线程因如下两个原因之一而被终止:
  1. 当线程的run方法执行方法体中最后一条语句后
  2. 在run方法中出现了未捕获的异常


#如何中断线程

在早期的Java版本中,有一个stop方法可以终止线程。但是这个方法已经被弃用了(原因之后会提到)

因为线程终止原因的绝对性,看起来我们没有办法强制终止线程,

然而,interrupt方法可以用来请求终止线程,
当对一个线程调用 interrupt 方法时,线程的中断状态将置位,

可以使用interrupted和isInterrupted方法检测当前线程中断是否已置位,置位将返回true,则返回false。

不过这两个方法还是有区别的,使用 interrupted 方法会有一个副作用,返回检测状态后,该线程的中断状态将复位,而 isInterrupted 方法则不会有任何副作用。


#使用interrupt注意事项


  • 如果线程被阻塞,我们对其调用interrupt方法会产生一个 InterruptedException 异常,这会中断砠塞调用,就是说中断状态并不会置位,并且会清除该线程的阻塞状态(此时状态为RUNNABLE)
(线程在阻塞状态对其调用 interrupted 或者 isInterrrupted 方法并不会产生异常)


  • 如果一个线程在中断状态被置位时调用sleep方法(或其他可中断方法)程序将会抛出一个 InterruptedException 异常,这会清除其中断状态,同时也不会执行sleep方法(此时状态为RUNNABLE)

#例子

下面是一个比较通用的run方法的方法体(本例使用了lamubda表达式)

Runnable runnable = () ->{
    
    while(!Thread.currentThread().isInterrupted() && more work to do){
        
        try {
            do some work
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            cleanup,if required
        }
    }

};

如果线程每次工作迭代之后都调用了sleep方法(或者其他的可中断方法)isInterrupted方法没有必要也没有用处,在这种情况下推荐使用如下方法体,注意这个例子while循环置于try语句块中,这代表只要出现异常就会跳出循环

Runnable runnable = () ->{

    try {
        while (more work to do){
            do some work
            Thread.currentThread().sleep(delay);
        }
    } catch(InterruptedException e){
        //something to do
    } finally {
        cleanup,if required
    }

};



#interruptedException异常注意事项

注意不要将interruptedException异常抑制在低层次上,下面有两种合理的选择:


  • 在catch子句中调用Thread.currentThread.interrupt()来设置中断状态,这样一来调用者可以对其进行检测
  • 更好的选择就是将异常向上抛出,可以使调用者根据自身的代码处理这个异




线程的状态

线程可以有如下6种状态:
  • NEW(新创建)
  • Runnable(可运行)
  • Blocked(被阻塞)
  • Waiting(等待)
  • Timed waiting(计时等待)
  • Terminated(被终止)
我们可以对一个线程调用其getState方法得到这一线程的状态,它将返回一个 Thread.State 类型的对象


线程属性

  • 优先级
默认情况下一个线程继承其父类的优先级,我们可以用setPriority方法设定一个线程的优先级,共有1-10十个等级,
或者设定成
MIN_PRIORITY(Thread类中定义为1)、
NORM_PRIORITY(Thread类中定义为5)、
MAX_PRIORITY(Thread类中定义为10)
值得注意的是,线程优先级是高度依赖于系统的,根据系统的不同,
Java线程的优先级映射在系统上优先级个数也会有差异
我们在编写程序时不应该将程序功能的正确性依赖于优先级(可能会导致低优先级的线程完全饿死)
  • 守护线程
对一个线程调用setDaemon(true)来将其设定成守护线程。
守护线程的唯一作用是为其他线程提供服务,当程序只剩下守护线程时,程序将自动退出
注意:守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至是在一个操作的中间发生中断。
  • 未捕获异常处理器
....(这部分鸽一段,很麻烦的...)

线程同步的相关概念

之前我们提到了线程之间共享数据虽然高效,但是并不安全,在讲不安全的原因之前,
按照惯例我们需要需要理解一些概念:
1.原子性
什么是原子性呢?原子是指在化学反应中不可再分的基本微粒(虽然使用物理手段可以分割),
我们的重点应该落于不可分割这个属性上,意味着这一步操作无法再拆分,意味着此操作完成前
不会被其他任何任务所打断
让我们来看一个例子,i++这条自增指令我们应该再熟悉不过了,看上去它是一个原子性操作
可是事实的确如此吗?
实际上,这条指令并不是原子性的,在JVM中它可以拆分为三个指令
内存到寄存器【工作内存】(取值)
寄存器自增(i+1)
写回内存(i=新值)
现在大家可以思考一下那什么才是原子性的操作呢,敏锐的同学其实已经感觉到了,
没错上面第3条指令就是一个原子性的操作,简单的说就是赋值操作具有原子性
2.可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
在JVM中每个线程都有自己的线程栈,当一个线程执行需要数据时,可以分为具体的三个步骤:
①会到内存中将需要的数据复制到自己的线程栈中,
②然后对线程栈中的副本进行操作,
③再操作完成后再将数据写回到内存中。
//可见性问题
如果某一线程读取了一个变量i(执行①),并在线程栈中对其进行了+1操作,但是这一操作并没有被及时的写回到内存中,这时其他线程在访问变量i时观察到的值仍是原值。(之后会讲到如何解决这个问题)
     3.有序性
有序性即程序执行的顺序按照代码的先后顺序执行。我们写代码会有一个先后的顺序,但是那仅仅是我们看到的顺序,但是当编译器编译时会进行指令重排,于是代码的执行顺序有可能和我们想的不一样。例如:
int i = 0;            //语句1  
boolean flag = false; //语句2
i = 1;                //语句3  
flag = true;          //语句4
  • 1
  • 2
  • 3
  • 4
简单分析这段代码可以发现语句3和语句4的执行顺序对程序并没有影响,而语句2和语句4顺序颠倒的话会产生向前引用的问题,这时编译器会限制语句的执行顺序来保证程序的正确性。
在单线程中,改变指令的顺序可能不会产生不良后果,但是在多线程中就不一定了:

//线程1:
context = loadContext();   //语句1
inited = true;             //语句2

//线程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
由于语句1和语句2没有数据依赖性,所以编译器可能会将两条指令重新排序,如果先执行语句2,这时线程1被阻塞,然后线程2的while循环条件不满足(这时线程2将继续执行,这与程序原意不相符),接着往下执行,但是由于context没有赋值,于是会产生错误。

竞争条件(race condition)

什么是竞争条件?

ReentrantLock类

有两种机制可以有效解决竞争条件的问题,现在介绍第一种,我们可以显式的定义一个锁对象(实质上是一个可重入的互斥锁),
用ReentrantLock保护代码块的基本结构如下:

class XXX {
    private final ReentrantLock lock = new ReentrantLock();

    // 其他变量的定义

    public void method() { 
     lock.lock();  // 当试图获得锁时,如果锁已经被别的线程占有,那么该线程会一直被阻塞,直到获得锁
     try {
       // 处理数据
     } finally {
       lock.unlock(); //释放锁
     }
   }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
这个结构确保任何时刻只有一个线程进入临界区,一旦一个线程获得了锁,其他线程调用lock时,它们将会被阻塞(状态为WAITING)直到第一个线程释放锁。解锁条件放在finally里面是最合理的,无论try语句块中发生了什么,锁对象最终都会被释放。
ReentrantLock类对象实质上是一个可重入互斥锁
可重入意味着一个线程可以重复的获得已持有的锁(重复使用lock方法)
锁保持着一个持有计数,线程每一次调用lock方法都需要unlock来释放锁,调用unlock方法会使锁的持有计数减1(调用lock方法会使持有计数加1),当锁的持有计数将至0的时候,线程释放锁。

条件对象

通常我们在编程时,线程进入了临界区,却又需要在某一条件满足之后它才能执行,大家的第 一反应或许是使用 if 来处理这个问题,不妨考虑下面一种情况

Comments

Popular posts from this blog

抓包工具Wireshark下载及安装教程

HTTP协议特性

Java中Synchronized的用法