Java多线程01-基本概念

Java多线程01-基本概念

什么是进程和线程?

  • 进程(process)

    • 狭义的定义:进程就是一段程序的执行过程。
    • 广义定义:进程是一个具有一定独立功能的程序关于某次数据集合的一次运行活动,它是操作系统分配资源的基本单元。
    • 进程状态:进程有三个状态,就绪,运行和阻塞。就绪状态其实就是获取了除cpu外的所有资源,只要处理器分配资源马上就可以运行。运行态就是获取了处理器分配的资源,程序开始执行,阻塞态,当程序条件不够时,需要等待条件满足时候才能执行,如等待I/O操作的时候,此刻的状态就叫阻塞态。
  • 线程(Thread)

    • 通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程可以利用进程所拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。
  • 多线程(multiThread)

    • 在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理”。多线程是为了同步完成多项任务,不是为了提高运行效率,而是为了提高资源使用效率来提高系统的效率。线程是在同一时间需要完成多项任务的时候实现的。
    • 最简单的比喻多线程就像火车的每一节车厢,而进程则是火车。车厢离开火车是无法跑动的,同理火车也不可能只有一节车厢。多线程的出现就是为了提高效率。

线程安全问题是怎么产生的?(JMM)

  • 多个线程同时去读写同一份数据的时候,就可能产生线程安全问题。
  • 比如,当线程一去修改了主内存中的共享数据,在线程一还没修改成功之前,然后线程二又读取了这个数据,随后线程一拿到修改后的数据,而线程二读到的还是老数据。

线程之间的通信

  • 线程的通信是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种共享内存和消息传递。
  • 在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信,典型的共享内存通信方式就是通过共享对象进行通信。
  • 在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信,在java中典型的消息传递方式就是wait()和notify()。

Java的线程间通信

  • 利用共享对象实现通信
  • 忙等(busy waiting)
  • wait(), notify() and notifyAll()
  • 信号丢失(Missed Signals)
  • 虚假唤醒(Spurious Wakeups)
  • 多个线程等待相同的信号
  • 不要对String对象或者全局对象调用wait方法

线程通信的目的就是让线程间具有互相发送信号通信的能力。
而且,线程通信可以实现,一个线程可以等待来自其他线程的信号。举个例子,一个线程B可能正在等待来自线程A的信号,这个信号告诉线程B数据已经处理好了。

  • 利用共享对象实现通信
    • 一个实现线程通信的简单的方式就是通过在某些共享的对象变量中设置一个信号值。举个例子,线程A在一个synchronize的语句块中设置一个boolean的成员变量hasDataToProcess为true,线程B在一个synchronize语句块中读取hasDataToProcess,如果为true就执行代码,否则就等待。这样就实现了线程A对线程B的通知。看下面的代码实现:
public class MySignal{

  protected boolean hasDataToProcess = false;

  public synchronized boolean hasDataToProcess(){
    return this.hasDataToProcess;
  }

  public synchronized void setHasDataToProcess(boolean hasData){
    this.hasDataToProcess = hasData;  
  }
}

线程A和B都必须拥有同一个MySignal类的对象实例的引用。如果线程拥有的是不同的实例,那么他们就无法获取到对方的信号。

  • 忙等(busy waiting)
    • 线程B执行的条件是,等待线程A发出通知,也就是等到线程A将hasDataToProcess()设置为true,所以线程b一直在等待信号,在一个循环的检测条件中。这时候线程B就处于一个忙等的状态。,因为线程b在等待的过程中是忙碌的,因为线程B在不断的循环检测条件是否成功。
protected MySignal sharedSignal = ...

...

while(!sharedSignal.hasDataToProcess()){
  //do nothing... busy waiting
}
  • wait(), notify() and notifyAll()
    忙等对于cpu的利用不是一个有效率的选择,除非忙等的时间是非常短的。不然,与其让线程处于忙等的状态,不如直接让线程直接sleep,直到它收到信号再重新激活它。
    • Java有一个内置的方法,可以让线程在等待信号的变为inactive状态。所有类的超类 java.lang.Object 定义了三个方法, wait(), notify(), and notifyAll()
    • 一个线程可以对任何一个对象调用wait方法,这样这个线程就会变成wait状态,inactive,等待其他线程在同一个对象上调用notify方法,来唤醒这个线程。值得注意的是,在调用wait和notify方法之前,必须要先获得这个对象的锁。换句话说,线程必须在synchronize的语句块中调用wait或者notify方法。看下面的代码实例:
public class MonitorObject{
}

public class MyWaitNotify{

  MonitorObject myMonitorObject = new MonitorObject();

  public void doWait(){
    synchronized(myMonitorObject){
      try{
        myMonitorObject.wait();
      } catch(InterruptedException e){...}
    }
  }

  public void doNotify(){
    synchronized(myMonitorObject){
      myMonitorObject.notify();
    }
  }
}
  • 等待的线程可以调用dowait方法,notify线程可以调用donotify方法。当一个线程在一个对象上调用notify方法的时候,这个对象的等待线程队列中的一个线程会被唤醒,获得执行的权利。notifyAll方法则是会将给定对象的等待队列中的所有线程都唤醒。

  • 我们可以看到我们调用wait或者notify方法的时候,都是在synchronize语句块中调用的。这是一个必要条件。一个线程如果没有取得相关对象的锁则无法调用wait和notify方法,会抛出IllegalMonitorStateException异常。

  • 一旦一个线程调用wait方法,他就会释放锁,这就允许其他线程去继续调用wait方法或者notify方法,所以这些方法都必须出现在synchronize语句块中。

  • 一个线程如果被唤醒了,不会立即离开wait方法,因为还没获得锁,要等到那个调用notify的线程离开他的synchronize的语句块,也就是等待他释放锁,才可以获得锁,离开wait。换句话说,换句话,线程要离开wait方法,必须重新获得锁相应对象的锁。如果多个线程被notifyall方法唤醒,那么在某一个时刻,只有一个被唤醒的线程可以离开wait方法,因为每个都必须重新获得锁才可以离开wait方法。

  • 信号丢失(Missed Signals)
    如果在调用notify或者notifyAll的时候,线程等待队列中,没有线程在等待,那么这个唤醒的信号并不会被保存。而是会丢失。所以,如果一个线程在另一个线程调用wait方法等待之前,就调用了notify方法,那么这个notify的信号就被丢失了,这就可能导致那个等待的线程将一直不会被唤醒,因为notify的唤醒信号丢失了。

为了避免信号的丢失,我们可以想办法将信号存起来,利用一个变量。如下面这个例子:

public class MyWaitNotify2{

  MonitorObject myMonitorObject = new MonitorObject();
  boolean wasSignalled = false;

  public void doWait(){
    synchronized(myMonitorObject){
      if(!wasSignalled){
        try{
          myMonitorObject.wait();
         } catch(InterruptedException e){...}
      }
      //clear signal and continue running.
      wasSignalled = false;
    }
  }

  public void doNotify(){
    synchronized(myMonitorObject){
      wasSignalled = true;
      myMonitorObject.notify();
    }
  }
}

我们可以看到,上面的方法在调用notidy之前先将wasSignalled设置为true。dowait方法会先检查wasSignalled变量,如果为true,就直接跳过wait方法,因为已经有notify信号发出了。如果为false,则说明还没有信号发出,就进入wait方法,进行等待。所以,我们利用一个boolean变量就可以解决通知过早的问题。

  • 虚假唤醒(Spurious Wakeups)
    • 有时候因为某些原因,线程可能会在没有调用notify或者notifyAll的情况下被唤醒,这也叫做虚假唤醒(Spurious Wakeups)。如果一个线程被虚假唤醒就会产生很多意想不到的问题,所以必须重视这个问题。
    • 我们使用一个自旋锁机制,也就是用while循环替代if循环,循环检查这样就可以避免虚假唤醒的情况。
public class MyWaitNotify3{

  MonitorObject myMonitorObject = new MonitorObject();
  boolean wasSignalled = false;

  public void doWait(){
    synchronized(myMonitorObject){
      while(!wasSignalled){
        try{
          myMonitorObject.wait();
         } catch(InterruptedException e){...}
      }
      //clear signal and continue running.
      wasSignalled = false;
    }
  }

  public void doNotify(){
    synchronized(myMonitorObject){
      wasSignalled = true;
      myMonitorObject.notify();
    }
  }
}

wait方法现在放在了一个while循环里,如果一个线程被唤醒,但是没有获得信号,那么wasSignalled 仍是false,while循环会进行多次判断,重新将线程变为wait。

我们更好的理解,我们举一个具体的例子:
假设有两个类负责加减:

package Thread;

public class Add {
    private String lock;

    public Add(String lock) {
        super();
        this.lock = lock;
    }

    public void add() {
        synchronized (lock) {
            ValueObject.list.add("anything");
            lock.notifyAll();
        }
    }
}
package Thread;

public class Subtract {
    private String lock;
    public Subtract(String lock) {
        super();
        this.lock = lock;
    }

    public void subtract() {
        try {
            synchronized (lock) {
                if(ValueObject.list.size() == 0) {
                    System.out.println("Wait begin ThreadName:" + Thread.currentThread().getName());
                    lock.wait();
                    System.out.println("Wait end ThreadName:" + Thread.currentThread().getName());
                }
                ValueObject.list.remove(0);
                System.out.println("list size : " + ValueObject.list.size());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
package Thread;

import java.util.ArrayList;
import java.util.List;

public class ValueObject {
    public static List<String list = new ArrayList<();
}

我们建立两个线程

package Thread;

public class ThreadAdd extends Thread {

    private Add p;

    public ThreadAdd(Add p) {
        this.p = p;
    }

    @Override
    public void run() {
        p.add();
    }
}
package Thread;

public class ThreadSubtract extends Thread {

    private Subtract p;

    public ThreadSubtract(Subtract p) {
        this.p = p;
    }


    @Override
    public void run() {
        p.subtract();
    }
}

我们测试

package Thread;

public class Run {

    public static void main(String[] args) throws InterruptedException {

        String lock = new String("");
        Add add = new Add(lock);
        Subtract sub = new Subtract(lock);

        ThreadAdd addthread = new ThreadAdd(add);

        ThreadSubtract sub1 = new ThreadSubtract(sub);
        sub1.start();

        ThreadSubtract sub2 = new ThreadSubtract(sub);
        sub2.start();

        Thread.sleep(1000);
        addthread.start();

    }

}


我们发现发生了异常,这是为什么呢?因为notifyAll同时唤醒了两个减的线程,然后第二个减的线程获得了锁,将size减为0,随后第一个减线程获得锁,再去减就抛异常了,因为它没有继续判断是否为0的条件,所以我们需要在获得锁之后依然去判断条件,也就是将if改为while

package Thread;

public class Subtract {
    private String lock;
    public Subtract(String lock) {
        super();
        this.lock = lock;
    }

    public void subtract() {
        try {
            synchronized (lock) {
                while(ValueObject.list.size() == 0) {
                    System.out.println("Wait begin ThreadName:" + Thread.currentThread().getName());
                    lock.wait();
                    System.out.println("Wait end ThreadName:" + Thread.currentThread().getName());
                }
                ValueObject.list.remove(0);
                System.out.println("list size : " + ValueObject.list.size());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}


这样就可以正确运行了。

  • 多个线程等待相同的信号
    如果你有多个线程在等待队列中,然后你又要调用notifyAll方法,那么使用while来替代if,是一个很好的解决虚假唤醒的方法。只有一个线程在一个时刻会被唤醒,然后可以获得锁,离开wait方法,并清楚wasSignalled 的标识,一旦这个线程离开了synchronize的语句块,其他线程可以获得锁并且离开wait方法。但是,由于wasSignalled 被第一个线程清除了,其他等待的线程因为while的存在会继续回到wait的状态,知道下一个信号来了

不要对String对象或者全局对象调用wait方法
如果我们对一个String对象调用wait方法

public class MyWaitNotify{

  String myMonitorObject = "";
  boolean wasSignalled = false;

  public void doWait(){
    synchronized(myMonitorObject){
      while(!wasSignalled){
        try{
          myMonitorObject.wait();
         } catch(InterruptedException e){...}
      }
      //clear signal and continue running.
      wasSignalled = false;
    }
  }
  public void doNotify(){
    synchronized(myMonitorObject){
      wasSignalled = true;
      myMonitorObject.notify();
    }
  }
}

如果我们在一个空emptyString或者其他的常量String对象上调用wait方法会产生问题。JVM/Compiler 在内部将常量的String变成相同的对象。这就意味着,即使我们有两个不同的MyWaitNotify实例,他们确实引用着同一个对象。这就意味着本来不相关的两个实例,最后通信的结果可能发生不可预测的交叉结果。
如下图所示:

  • 需要注意的是,即使四个线程调用wait和notify都是在同一个对象上的,但是信号都是存储在各自的实例中的,也就是wasSignal是存储在各自实例中的,这就会引起很大的问题。一个来自MyWaitNotify 1的信号可能会唤醒MyWaitNotify 2中的等待线程,但是wasSignal确实存在MyWaitNotify 1中的。
  • 如果notify作用在第二个实例上MyWaitNotify 2,那就可能发生线程A和B被唤醒的情况,但是线程A和B会在while循环中检查wasSignal信号,结果发现依然是false,就会继续等待,所以notify并没有起到作用,这就类似虚假唤醒的情况。
  • 这样发生的情况就是,如果我们调用notify方法,然后notify的又不是自己这个实例的线程,结果就没有线程会被唤醒,这就类似于信号丢失的情况。
  • 但如果我们调用的notifyAll方法就不会出现信号丢失的情况,因为wasSignal会被正确的设置,相应的线程会被唤醒,其他对象的线程会因为while循环继续回到wait状态。
  • 那你也许会说,我们直接调用notifyAll不就可以避免String带来的问题么?确实是这样,但是我们如果在全部情况都调用notifyAll的话,就会出现性能的问题,我们完全没有必要在只有一个线程的情况下,调用notifyAll。
  • 所以,我们不要使用全局的对象或者String变量调用wait。

线程之间的同步

  • 同步是指程序用于控制不同线程之间操作发生相对顺序的机制。
  • 在共享内存并发模型里,同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。
  • 在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。

线程安全问题是怎么解决的?

1.同步方法

  • 在方法声明上加上synchronized
public synchronized void method(){
    //可能会产生安全问题的代码
}
  • 同步方法中的锁对象是this
  • 静态同步方法:在方法声明加上static synchronized
  • 静态同步方法中的锁对象是 类名.class

2.同步代码块

  • 在代码块声明上加上synchronized
synchronized(锁对象){
    //可能会产生安全问题的代码
}
  • 同步代码块中的锁对象可以是任意的对象;但多个线程时,要使用同一个锁对象才能保证线程安全。

3.同步锁

  • lock接口提供了与synchronized关键字类似的同步功能,但是需要在使用是手动获取锁和释放锁。
    • 每个对象都有一个锁来控制同步访问,synchronized关键字可以和对象的锁交互,来实现同步方法和同步代码块。

线程有哪几种状态?

  • 存在5种状态:
    1. Running:可以接受新任务,同时也可以处理阻塞队列里面的任务;
    2. Shutdown:不可以接受新任务,但是可以处理阻塞队列里面的任务;
    3. Stop:不可以接受新任务,也不处理阻塞队列里面的任务,同时还中断正在处理的任务;
    4. Tidying:属于过渡阶段,在这个阶段表示所有的任务已经执行结束了,当前线程池中是不存在有效的线程的,并且将要调用terminated方法;
    5. Terminated:终止状态,这个状态是在调用完terminated方法之后所处的状态

多次start一个线程会怎么样?

  • 一个线程对象只能调用一次start方法.从new到等待运行是单行道,所以如果你对一个已经启动的线程对象再调用一次start方法的话,会产生:IllegalThreadStateException异常.
  • 可以被重复调用的是run()方法。
  • Thread类中run()和start()方法的区别如下:
    • run()方法:在本线程内调用该Runnable对象的run()方法,可以重复多次调用;
    • start()方法:启动一个线程,调用该Runnable对象的run()方法,不能多次启动一个线程;

线程中的sleep()和wait()方法有什么区别?

  • sleep()方法让正在实行的线程主动让出CPU(然后CPU就可以去执行其他任务),在sleep指定时间后CPU再回到该线程继续往下执行。sleep方法只让出了CPU,并不会释放同步资源锁。
  • wait()方法是当前线程让自己暂时退让出同步资源锁,wait()使用notify或者notifyAlll或者指定睡眠时间来唤醒当前等待池中的线程。

创建线程有哪些方式?

  1. 继承Thread类
  2. 实现Runable接口(java.lang)
  3. 实现Callable接口(java.util.concurrent)
/**
 * 启动一个线程的3种方式
 */
public class TraditionalThread {

	public static void main(String[] args) {
		// 1. 继承自Thread类(这里使用的是匿名类)
		new Thread(){
			@Override
			public void run() {
				while(true) {
					try {
						Thread.sleep(500);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println("threadName: " + Thread.currentThread().getName());
				}
			};
		}.start();

		// 2. 实现Runnable接口(这里使用的是匿名类)
		new Thread(new Runnable() {
			@Override
			public void run() {
				while(true) {
					try {
						Thread.sleep(500);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println("threadName: " + Thread.currentThread().getName());
				}
			}
		}).start();

		// 3.即实现Runnable接口,也继承Thread类,并重写run方法
		new Thread(new Runnable() {
			@Override
			public void run() {	// 实现Runnable接口
				while(true) {
					try {
						Thread.sleep(500);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println("implements Runnable thread: " + Thread.currentThread().getName());
				}
			}
		}) {	// 继承Thread类
			@Override
			public void run() {
				while(true) {
					try {
						Thread.sleep(500);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println("extends Thread thread: " + Thread.currentThread().getName());
				}
			}
		}.start();
	}
}

总结

  • 实现Runnable接口相比继承Thread类有如下优势:

    • 可以避免由于Java的单继承特性而带来的局限;
    • 增强程序的健壮性,代码能够被多个线程共享,代码与数据是独立的;
    • 适合多个相同程序代码的线程区处理同一资源的情况。
  • 实现Runnable接口和实现Callable接口的区别:

    • Runnable是自从java1.1就有了,而Callable是1.5之后才加上去的
    • Callable规定的方法是call(),Runnable规定的方法是run()
    • Callable的任务执行后可返回值,而Runnable的任务是不能返回值(是void)
    • call方法可以抛出异常,run方法不可以
    • 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
    • 加入线程池运行,Runnable使用ExecutorService的execute方法,Callable使用submit方法。