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

1、线程安全性

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

2、线程启动    

  •     继承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”上。
 

3、线程同步   

  • 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变量只能保证可见性(一个线程修改后其它线程可以理解看到此变化后的结果),要想保证原子性,目前为止只能加锁!
 

4、线程协做

线程等待:

等待的缘由多是以下几种状况:
     (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()方法只是唤醒等待该对象的锁的线程,并不决定哪一个线程可以获取到锁.
  举个简单的例子:
  假若有三个线程Thread一、Thread2和Thread3都在等待对象objectA的锁,此时Thread4拥有对象objectA的锁,当在Thread4中调用objectA.notify()方法以后,Thread一、Thread2和Thread3只有一个能被唤醒。注意,被唤醒不等于马上就获取了objectA的锁,倘若在Thread4中调用objectA.notifyAll()方法,则Thread一、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区别:

  一、sleep()方法会给其余线程运行的机会,而不考虑其余线程的优先级,所以会给较低线程一个运行的机会;yield()方法只会给相同优先级或者更高优先级的线程一个运行的机会。 
  二、当线程执行了sleep(long millis)方法后,将转到阻塞状态,参数millis指定睡眠时间;当线程执行了yield()方法后,将转到就绪状态。 
  三、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()。
 

5、线程中断  

    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
相关文章
相关标签/搜索