Java多线程:多线程基础知识

一、线程安全性

定义:多个线程之间的操作无论采用何种执行时序或交替方式,都要保证不变性条件不被破坏
    “共享”:变量可以由多个线程同时访问;
    “可变”:变量的值在其生命周期内可以发生改变
如果当多个线程访问同一个可变的状态变量时,没有使用合适的同步,那么程序将会出现错误。有三种方式可以修复该问题:
  • 不在线程之间共享该变量。
  • 将状态变量修改为不可变的变量。
  • 在访问状态变量时使用同步。
 
 
竞态条件:
    最常见的类型就是“先检查后执行”操作,即通过一个可能失效的观测接口来决定下一步的动作。
 

二、线程启动    

  •     继承Thread类,重写里面的run方法,用start方法启动线程
  •     实现Runnable接口,实现里面的run方法,用new Thread(Runnable target).start()方法来启动

start()和run()

    线程的启动并不是简单的调用了run方法,而是由一个线程调度器来分别调用所有线程的run方法,普通的run方法如果没有执行完是不会返回的,也就是会一直执行下去,这样run方法下面的方法就不可能会执行了,可是线程里的run方法却不一样,它只有一定的CPU时间,执行过后就给别的线程了,这样反复的把CPU的时间切来切去,因为切换的速度很快,所以我们就感觉是很多线程在同时运行一样
    (01) mythread.run()是在“主线程main”中调用的,该run()方法直接运行在“主线程main”上。
    (02) mythread.start()会启动“线程mythread”,“线程mythread”启动之后,会调用run()方法;此时的run()方法是运行在“线程mythread”上。
 

三、线程同步   

  • synchronized关键字
内置锁(互斥锁):
    在java中,每一个对象有且仅有一个同步锁。这也意味着,同步锁是依赖于对象而存在。线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁。同一时间最多只有一个线程持有该锁,其他线程必须等待或者阻塞,直到该线程释放锁。
    如果持有锁的时间过长,将会带来性能问题。
 
    实例锁 -- 锁在某一个实例对象上。如果该类是单例,那么该锁也具有全局锁的概念。
                   实例锁对应的就是synchronized关键字。
    全局锁 -- 该锁针对的是类,无论实例多少个对象,那么线程都共享该锁。
                   全局锁对应的就是static synchronized(或者是锁在该类的class或者classloader对象上)。
  • volatile变量
    volatile相当于synchronized的弱实现,也就是说volatile实现了类似synchronized的语义,却又没有锁机制。它确保对volatile字段的更新以可预见的方式告知其他的线程。
    volatile包含以下语义:
      (1)Java 存储模型不会对valatile指令的操作进行重排序:这个保证对volatile变量的操作时按照指令的出现顺序执行的。
      (2)volatile变量不会被缓存在寄存器中(只有拥有线程可见)或者其他对CPU不可见的地方,每次总是从主存中读取volatile变量的结果。也就是说对于volatile变量的修改,其它线程总是可见的,并且不是使用自己线程栈内部的变量。也就是在happens-before法则中,对一个valatile变量的写操作后,其后的任何读操作理解可见此写操作的结果。
    尽管volatile变量的特性不错,但是volatile并不能保证线程安全的,也就是说volatile字段的操作不是原子性的,volatile变量只能保证可见性(一个线程修改后其它线程能够理解看到此变化后的结果),要想保证原子性,目前为止只能加锁!
 

四、线程协作

线程等待:

等待的原因可能是如下几种情况:
     (1)sleep() 的作用是让当前线程休眠,即当前线程会从“运行状态”进入到“休眠(阻塞)状态”。sleep()会指定休眠时间,线程休眠的时间会大于/等于该休眠时间;在线程重新被唤醒时,它会由“阻塞状态”变成“就绪状态”,从而等待cpu的调度执行。
     (2)通过调用join()方法使线程挂起,使自己等待另一个线程的结果,直到另一个线程执行完毕为止。
     (3)通过调用wait()方法使线程挂起,直到线程得到了notify()和notifyAll()消息,线程才会进入“可执行”状态。
     (4)yield()的作用是让步。它能让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权;但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是当前线程又进入到“运行状态”继续运行!
 
注意:
  (01) wait()是让线程由“运行状态”进入到“等待(阻塞)状态”,而yield()是让线程由“运行状态”进入到“就绪状态”。
  (02) wait()是会让线程释放它所持有对象的同步锁,而yield()方法不会释放锁。
  (03) sleep()使当前线程暂停执行一段时间,从而让其他线程有机会继续执行,但它并不释放对象锁
 
    wait():   
      “当前线程”在调用wait()时,必须拥有该对象的同步锁。该线程调用wait()之后,会释放该锁;然后一直等待直到“其它线程”调用对象的同步锁的notify()或notifyAll()方法。然后,该线程继续等待直到它重新获取“该对象的同步锁”,然后就可以接着运行wait()后面的代码。wait()的作用是让“当前线程”等待,而“当前线程”是指正在cpu上运行的线程!因此调用wait()方法必须在同步块或者同步方法中进行(synchronized块或者synchronized方法)
  wait的东西一定要notify吗?不一定,
    notify():
      notify() 执行该方法的线程唤醒在对象的等待池中等待的一个线程,JVM从对象的等待池中随机选择一个线程,把它转到对象的锁池中。使线程由阻塞队列进入就绪状态。
    notifyAll():
      当前的线程已经放弃对资源的占有,通知所有的等待线程从wait()方法后的语句开始运行。
这里要注意一点:notify()和notifyAll()方法只是唤醒等待该对象的锁的线程,并不决定哪个线程能够获取到锁.
  举个简单的例子:
  假如有三个线程Thread1、Thread2和Thread3都在等待对象objectA的锁,此时Thread4拥有对象objectA的锁,当在Thread4中调用objectA.notify()方法之后,Thread1、Thread2和Thread3只有一个能被唤醒。注意,被唤醒不等于立刻就获取了objectA的锁,假若在Thread4中调用objectA.notifyAll()方法,则Thread1、Thread2和Thread3三个线程都会被唤醒,至于哪个线程接下来能够获取到objectA的锁就具体依赖于操作系统的调度了。
  上面尤其要注意一点,一个线程被唤醒不代表立即获取了对象的锁,只有等调用完notify()或者notifyAll()并退出synchronized块,释放对象锁后,其余线程才可获得锁执行。
 
首先,wait()和notify(),notifyAll()是Object类的方法,sleep()和yield()是Thread类的方法。
  当然由于Thread类继承了Object类,所以Thread也可以调用前面三个方法
  由于每个对象都拥有锁,所以让当前线程等待某个对象的锁,当然应该通过这个对象来操作了。而不是用当前线程来操作,因为当前线程可能会等待多个对象的锁,如果通过线程来操作,就非常复杂了。
 
(1).常用的wait方法有wait()和wait(long timeout):
    void wait() 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。 
    void wait(long timeout) 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量前,导致当前线程等待。
    wait()后,线程会释放掉它所占有的“锁标志”,从而使线程所在对象中的其它synchronized数据可被别的线程使用。
    wait()和notify()因为会对对象的“锁标志”进行操作,所以它们必须在synchronized函数或synchronized  block中进行调用。如果在non-synchronized函数或non-synchronized block中进行调用,虽然能编译通过,但在运 行时会发生IllegalMonitorStateException的异常。
 
(2).Thread.sleep(long millis),必须带有一个时间参数。
    sleep(long)使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;
    sleep(long)可使优先级低的线程得到执行的机会,当然也可以让同优先级和高优先级的线程有执行的机会;
    sleep(long)是不会释放锁标志的。
 
(3).yield()没有参数。
    sleep 方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出CPU占有权,但让出的时间是不可设定的。
    yield()也不会释放锁标志。
 
    实际上,yield()方法对应了如下操作: 先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,否则继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程。
 
    sleep方法允许较低优先级的线程获得运行机会,但yield()方法执行时,当前线程仍处在可运行状态,所以不可能让出较低优先级的线程些时获得CPU占有权。 在一个运行系统中,如果较高优先级的线程没有调用 sleep 方法,又没有受到 I/O阻塞,那么较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。
 
    yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。所以yield()只能使同优先级的线程有执行的机会。
 

sleep和yield区别:

  1、sleep()方法会给其他线程运行的机会,而不考虑其他线程的优先级,因此会给较低线程一个运行的机会;yield()方法只会给相同优先级或者更高优先级的线程一个运行的机会。 
  2、当线程执行了sleep(long millis)方法后,将转到阻塞状态,参数millis指定睡眠时间;当线程执行了yield()方法后,将转到就绪状态。 
  3、sleep()方法声明抛出InterruptedException异常,而yield()方法没有声明抛出任何异常 
 

为什么notify(), wait()等函数定义在Object中,而不是Thread中?    

  Object中的wait(), notify()等函数,和synchronized一样,会对“对象的同步锁”进行操作。
wait()会使“当前线程”等待,因为线程进入等待状态,所以线程应该释放它锁持有的“同步锁”,否则其它线程获取不到该“同步锁”而无法运行!那么:notify()是依据什么唤醒等待线程的?或者说,wait()等待线程和notify()之间是通过什么关联起来的?答案是:依据“对象的同步锁”。
  负责唤醒等待线程的那个线程(我们称为“唤醒线程”),它只有在获取“该对象的同步锁”(这里的同步锁必须和等待线程的同步锁是同一个),并且调用notify()或notifyAll()方法之后,才能唤醒等待线程。虽然,等待线程被唤醒;但是,它不能立刻执行,因为唤醒线程还持有“该对象的同步锁”。必须等到唤醒线程释放了“对象的同步锁”之后,等待线程才能获取到“对象的同步锁”进而继续运行。
  总之,notify(), wait()依赖于“同步锁”,而“同步锁”是对象锁持有,并且每个对象有且仅有一个!这就是为什么notify(), wait()等函数定义在Object类,而不是Thread类中的原因。

 

Condition

Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition,Java阻塞队列实际上是使用了Condition来模拟线程间协作。
  • Condition是个接口,基本的方法就是await()和signal()方法;
  • Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition() 
  •  调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用
  Conditon中的await()对应Object的wait();
  Condition中的signal()对应Object的notify();
  Condition中的signalAll()对应Object的notifyAll()。
 

五、线程中断  

    Java的线程调度不提供抢占式中断,而采用协作式的中断。其实,协作式的中断,原理很简单,就是轮询某个表示中断的标记。
        
    通常,我们通过“标记”方式终止处于“运行状态”的线程。其中,包括“中断标记”和“额外添加标记”。

(01) 通过“中断标记”终止线程

  形式如下:
@Override
publicvoid run() {
    while (!isInterrupted()) {
        // 执行任务...    }
}

 

说明:isInterrupted()是判断线程的中断标记是不是为true。当线程处于运行状态,并且我们需要终止它时;可以调用线程的interrupt()方法,使用线程的中断标记为true,即isInterrupted()会返回true。此时,就会退出while循环。
注意:interrupt()并不会终止处于“运行状态”的线程!它会将线程的中断标记设为true。

(02) 通过“额外添加标记”。

  形式如下:
复制代码
private volatile boolean flag = true;
protected void stopTask() {
    flag = false;
}

@Override
public void run() {
    while (flag) {
        // 执行任务...    }
}

 

复制代码
说明:线程中有一个flag标记,它的默认值是true;并且我们提供stopTask()来设置flag标记。当我们需要终止该线程时,调用该线程的stopTask()方法就可以让线程退出while循环。
注意:将flag定义为volatile类型,是为了保证flag的可见性。即其它线程通过stopTask()修改了flag之后,本线程能看到修改后的flag的值。
    通常,我们通过“中断”方式终止处于“阻塞状态”的线程。
当线程由于被调用了sleep(), wait(), join()等方法而进入阻塞状态;若此时调用线程的interrupt()将线程的中断标记设为true。由于处于阻塞状态,中断标记会被清除,同时产生一个InterruptedException异常。将InterruptedException放在适当的为止就能终止线程
@Override
public void run() {
    try {
        while (true) {
            // 执行任务...        }
    } catch (InterruptedException ie) {  
        // 由于产生InterruptedException异常,退出while(true)循环,线程终止!    }
}

 

注意:对InterruptedException的捕获务一般放在while(true)循环体的外面,这样,在产生异常时就退出了while(true)循环。
 
 

参考资料:

   梁飞:Java并发编程常识.pptx
 

posted @ 2017-09-27 16:17 ^_TONY_^ 阅读(...) 评论(...) 编辑 收藏