本文主要讲解下线程的生命周期以及与生命周期有关的5种常见状态,深刻理解线程的产生,运行,阻塞以及销毁。还有Java中与线程生命周期有关的常见方法以及区别。

文章目录

  • 并发编程三要素
  • 上下文切换
  • 线程的生命周期
  • 线程的5种基本状态
  • 线程的调度算法
  • 线程的调度策略
  • 线程调度器和时间片
  • 多线程的实现主要有以下四种实现方式
    • 继承Thread类实现多线程
    • 实现Runnable接口方式实现多线程
    • 使用Callable和Future创建线程
    • 使用Executor框架创建线程池
  • 两种常见的方式来关闭线程的运行
    • 设置开关
    • 调用interrupt方法
  • 线程有关的常用方法
    • run()和start()的区别
    • sleep()方法
    • join()方法
    • yield()方法
    • sleep()和yield()的区别
    • wait()方法
    • wait()和sleep()区别
    • notify() 方法
    • notify()和notifyAll()的区别
  • 为什么线程通信的方法wait(), notify()和notifyAll()被定义在Object类里
  • 两个线程间共享数据
  • 多线程之间的通讯和协作
  • 同步方法和同步块的比较
  • 线程同步和线程互斥的概念
  • 线程同步的实现方式
  • 在监视器(Monitor)内部实现线程同步
  • 线程安全的概念
  • 在Java程序中怎么保证多线程的运行安全
  • 本文小结

并发编程三要素

并发编程三要素(线程的安全性问题体现在):

原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。

可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。(synchronized,volatile)

有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)

出现线程安全问题的原因:

  • 线程切换带来的原子性问题
  • 缓存导致的可见性问题
  • 编译优化带来的有序性问题

解决办法:

  • JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题
  • synchronized、volatile、LOCK,可以解决可见性问题
  • Happens-Before 规则可以解决有序性问题

上下文切换

多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态,让给其他线程使用,这个过程就属于一次上下文切换。

概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。

上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。


线程的生命周期


线程的5种基本状态

  1. 新建(new):新创建了一个线程对象。
  2. 可运行(runnable):线程对象创建后,当调用线程对象的 start()方法,该线程处于就绪状态,等待被线程调度选中,获取cpu的使用权。
  3. 运行(running):可运行状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
  4. 阻塞(block):处于运行状态中的线程由于某种原因,暂时放弃对 CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被 CPU 调用以进入到运行状态。
  5. 死亡(dead):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

阻塞的情况分三种:

  • 等待阻塞:运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waitting queue)中,使本线程进入到等待阻塞状态。
  • 同步阻塞:线程在获取 synchronized同步锁失败(因为锁被其它线程所占用)之后,则JVM会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态;
  • 其他阻塞: 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。

线程的调度算法

计算机通常只有一个 CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU 的使用权才能执行指令。所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得 CPU 的使用权,分别执行各自的任务。在运行池中,会有多个处于就绪状态的线程在等待 CPU,JAVA 虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配 CPU 的使用权

有两种调度模型:分时调度模型和抢占式调度模型

分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的 CPU 的时间片这个也比较好理解。

Java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU。


线程的调度策略

线程调度器选择优先级最高的线程运行,但是,如果发生以下情况,就会终止线程的运行:

(1)线程体中调用了 yield 方法让出了对 cpu 的占用权利

(2)线程体中调用了 sleep 方法使线程进入睡眠状态

(3)线程由于 IO 操作受到阻塞

(4)另外一个更高优先级线程出现

(5)在支持时间片的系统中,该线程的时间片用完


线程调度器和时间片

线程调度器是一个操作系统服务,它负责为 Runnable 状态的线程分配 CPU 时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。

时间分片是指将可用的 CPU 时间分配给可用的 Runnable 线程的过程。分配 CPU 时间可以基于线程优先级或者线程等待的时间。

线程调度并不受到 Java 虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让你的程序依赖于线程的优先级)。


多线程的实现主要有以下四种实现方式

继承Thread类实现多线程

继承Thread类的方法尽管被我列为一种多线程实现方式,但Thread本质上也是实现了Runnable接口的一个实例,它代表一个线程的实例,并且,启动线程的唯一方法就是通过Thread类的start()实例方法。start()方法是一个native方法,它将启动一个新线程,并执行run()方法。这种方式实现多线程很简单,通过自己的类直接extend Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法。例如

package cn.wideth.util;class MyThread extends Thread {@Overridepublic void run() {System.out.println("MyThread.run()");}
}public class Main {public static void main(String[] args){MyThread t1 = new MyThread();t1.start();}
}

实现Runnable接口方式实现多线程

如果自己的类已经extends另一个类,就无法直接extends Thread,此时,必须实现一个Runnable接口

package cn.wideth.util;class MyThread implements Runnable {@Overridepublic void run() {System.out.println("MyThread.run()");}}public class Main {public static void main(String[] args){MyThread myThread = new MyThread();Thread thread = new Thread(myThread);thread.start();}}

使用Callable和Future创建线程

步骤

  1. 创建实现Callable接口的类myCallable
  2. 以myCallable为参数创建FutureTask对象
  3. 将FutureTask作为参数创建Thread对象
  4. 调用线程对象的start()方法

代码实现

package cn.wideth.util;import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;class MyCallable implements Callable<Integer> {@Overridepublic Integer call() {System.out.println(Thread.currentThread().getName() + " call()方法执行中...");return 1;}}public class CallableTest {public static void main(String[] args) {FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyCallable());Thread thread = new Thread(futureTask);thread.start();try {Thread.sleep(1000);System.out.println("返回结果 " + futureTask.get());} catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " main()方法执行完成");}
}

运行结果


使用Executor框架创建线程池


两种常见的方式来关闭线程的运行

设置开关

package cn.wideth.util;class WorkThread extends Thread {// 线程内部设置开关 volatile 多线程可见private volatile boolean flag = true;@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + " working , flag=" + flag);// 通过接收flag来决定 终止或运行while (flag) {//            System.out.println("运行中...");}}public void shutdownThread() {this.flag = false;System.out.println(Thread.currentThread().getName() + " set flag=" + flag);}}public class StopThread {public static void main(String[] args) {WorkThread workThread = new WorkThread();workThread.start();// main线程继续执行业务逻辑  假设运行了3秒try {System.out.println(Thread.currentThread().getName() + " 运行中");Thread.sleep(3000);// 假设触发某个条件,需要退出WorkThread线程workThread.shutdownThread();} catch (InterruptedException e) {e.printStackTrace();}}
}

运行结果


调用interrupt方法

package cn.wideth.util;class WorkThread extends Thread {public WorkThread(String name) {super(name);}@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + " working ");// 死循环while (true) {// 判断 该线程是否被打断if (Thread.interrupted()) {System.out.println(Thread.currentThread().getName() + " received  interrupt signal...");// break (break的话  还会执行 assume some logic is here的代码)// 或者// return (return 的话,如果有后面还有代码的话就不会执行后续的代码了)break;}// assume some logic is here}}
}public class StopThread {public static void main(String[] args) {WorkThread workThread = new WorkThread("workThread");workThread.start();try {// 模拟主线程的业务System.out.println(Thread.currentThread().getName() + " working...");Thread.sleep(3000);// 假设触发了某种条件,需要中断workThread线程的执行 调用interruptworkThread.interrupt();System.out.println("workThread interrupt...");} catch (InterruptedException e) {e.printStackTrace();}}}

运行结果


线程有关的常用方法

run()和start()的区别

每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,run()方法称为线程体。通过调用Thread类的start()方法来启动一个线程。

start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。

start()方法来启动一个线程,真正实现了多线程运行。调用start()方法无需等待run方法体代码执行完毕,可以直接继续执行其他的代码; 此时线程是处于就绪状态,并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, run()方法运行结束, 此线程终止。然后CPU再调度其它线程。

run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接调用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。

package cn.wideth.util;class MyThread extends Thread {public MyThread(String name) {super(name);}@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "线程正在运行...");}
}public class Main {public static void main(String[] args) {System.out.println(Thread.currentThread().getName() + "线程正在运行...");MyThread t1 = new MyThread("A");t1.start();MyThread t2 = new MyThread("B");t2.start();}
}

运行结果:

为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法

这是另一个非常经典的 java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!

new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。

而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

package cn.wideth.util;class MyThread extends Thread {public MyThread(String name) {super(name);}@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "线程正在运行...");}
}public class Main {public static void main(String[] args) {System.out.println(Thread.currentThread().getName() + "线程正在运行...");MyThread t1 = new MyThread("A");t1.run();MyThread t2 = new MyThread("B");t2.run();}
}

运行结果:

总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。


sleep()方法

sleep是Thread类的一个方法,在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。


join()方法

使用方式:join是Thread类的一个方法,启动线程后直接调用,即join()的作用是:“等待该线程终止”,这里需要理解的就是该线程是指的主线程等待子线程的终止。也就是在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行。

Thread t = new AThread();
t.start();
t.join();

为什么要用join()方法:在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。

package cn.wideth.util;class Thread1 extends Thread{private String name;public Thread1(String name) {super(name);this.name=name;}@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + " 线程运行开始!");for (int i = 0; i < 5; i++) {System.out.println("子线程"+name + "运行 : " + i);try {sleep((int) Math.random() * 10);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName() + " 线程运行结束!");}
}public class Main {public static void main(String[] args) {System.out.println(Thread.currentThread().getName()+"主线程运行开始!");Thread1 t1=new Thread1("A");Thread1 t2=new Thread1("B");t1.start();t2.start();System.out.println(Thread.currentThread().getName()+ "主线程运行结束!");}
}

运行结果:


加join以后的代码

package cn.wideth.util;class Thread1 extends Thread{private String name;public Thread1(String name) {super(name);this.name=name;}@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + " 线程运行开始!");for (int i = 0; i < 5; i++) {System.out.println("子线程"+name + "运行 : " + i);try {sleep((int) Math.random() * 10);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName() + " 线程运行结束!");}
}public class Main {public static void main(String[] args) {System.out.println(Thread.currentThread().getName()+"主线程运行开始!");Thread1 mTh1=new Thread1("A");Thread1 mTh2=new Thread1("B");mTh1.start();mTh2.start();try {mTh1.join();} catch (InterruptedException e) {e.printStackTrace();}try {mTh2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+ "主线程运行结束!");}}

运行结果:


yield()方法

Thread.yield()方法作用是:暂停当前正在执行的线程对象,并执行其他线程。yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。

结论在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。

package cn.wideth.util;class ThreadYield extends Thread{public ThreadYield(String name) {super(name);}@Overridepublic void run() {for (int i = 1; i <= 20; i++) {System.out.println("" + this.getName() + "-----" + i);// 当i为10时,该线程就会把CPU时间让掉,让其他或者自己的线程执行(也就是谁先抢到谁执行)if (i ==10) {this.yield();}}}
}public class Main {public static void main(String[] args) {ThreadYield yt1 = new ThreadYield("A");ThreadYield yt2 = new ThreadYield("B");yt1.start();yt2.start();}}

sleep()和yield()的区别

sleep()和yield()的区别: sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。

sleep 方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程。

另外,sleep 方法允许较低优先级的线程获得运行机会,但 yield() 方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程些时获得 CPU 占有权。在一个运行系统中,如果较高优先级的线程没有调用 sleep 方法,又没有受到 I\O 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。

为什么Thread类的sleep()和yield()方法是静态的

Thread 类的 sleep()和 yield()方法将在当前正在执行的线程上运行。所以在其他处于等待状态的线程上调用这些方法是没有意义的。这就是为什么这些方法是静态的。它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以在其他非运行线程调用这些方法。


wait()方法

Obj.wait()与Obj.notify()必须要与synchronized(Obj)一起使用,也就是wait,与notify是针对已经获取了Obj锁进行操作,从语法角度来说就是Obj.wait(),Obj.notify必须在synchronized(Obj){…}语句块内。从功能上来说wait就是说线程在获取对象锁后,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的notify()唤醒该线程,才能继续获取对象锁,并继续执行。相应的notify()就是对对象锁的唤醒操作。但有一点需要注意的是notify()调用后,并不是马上就释放对象锁的,而是在相应的synchronized(){}语句块执行结束,自动释放锁后,JVM会在wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。这样就提供了在线程间同步、唤醒的操作。Thread.sleep()与Object.wait()二者都可以暂停当前线程,释放CPU控制权,主要的区别在于Object.wait()在释放CPU同时,释放了对象锁的控制。

单单在概念上理解清楚了还不够,需要在实际的例子中进行测试才能更好的理解。对Object.wait(),Object.notify()的应用最经典的例子,应该是三线程打印ABC的问题了吧,这是一道比较经典的面试题,题目要求如下:

建立三个线程,A线程打印10次A,B线程打印10次B,C线程打印10次C,要求线程同时运行,交替打印10次ABC。这个问题用Object的wait(),notify()就可以很方便的解决。代码如下:

package cn.wideth.util;class MyThread implements Runnable {private String name;private Object prev;private Object self;public  MyThread(String name, Object prev, Object self) {this.name = name;this.prev = prev;this.self = self;}@Overridepublic void run() {int count = 10;while (count > 0) {synchronized (prev) {synchronized (self) {System.out.print(name);count--;self.notify();}try {prev.wait();} catch (InterruptedException e) {e.printStackTrace();}}}}}public class Main {public static void main(String[] args) throws InterruptedException {Object a = new Object();Object b = new Object();Object c = new Object();MyThread t1 = new MyThread("A", c, a);MyThread t2 = new MyThread("B", a, b);MyThread t3 = new MyThread("C", b, c);new Thread(t1).start();Thread.sleep(100);  //确保按顺序A、B、C执行new Thread(t2).start();Thread.sleep(100);new Thread(t3).start();Thread.sleep(100);}}

运行结果

结果分析:

先来解释一下其整体思路,从大的方向上来讲,该问题为三线程间的同步唤醒操作,主要的目的就是ThreadA->ThreadB->ThreadC->ThreadA循环执行三个线程。为了控制线程执行的顺序,那么就必须要确定唤醒、等待的顺序,所以每一个线程必须同时持有两个对象锁,才能继续执行。一个对象锁是prev,就是前一个线程所持有的对象锁。还有一个就是自身对象锁。主要的思想就是,为了控制执行的顺序,必须要先持有prev锁,也就前一个线程要释放自身对象锁,再去申请自身对象锁,两者兼备时打印,之后首先调用self.notify()释放自身对象锁,唤醒下一个等待线程,再调用prev.wait()释放prev对象锁,终止当前线程,等待循环结束后再次被唤醒。运行上述代码,可以发现三个线程循环打印ABC,共10次。程序运行的主要过程就是A线程最先运行,持有C,A对象锁,后释放A,C锁,唤醒B。线程B等待A锁,再申请B锁,后打印B,再释放B,A锁,唤醒C,线程C等待B锁,再申请C锁,后打印C,再释放C,B锁,唤醒A。看起来似乎没什么问题,但如果你仔细想一下,就会发现有问题,就是初始条件,三个线程按照A,B,C的顺序来启动,按照前面的思考,A唤醒B,B唤醒C,C再唤醒A。但是这种假设依赖于JVM中线程调度、执行的顺序。


wait()和sleep()区别

共同点:

  1. 他们都是在多线程的环境下,都可以在程序的调用处阻塞指定的毫秒数,并返回。
  2. wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态 ,从而使线程立刻抛出InterruptedException。

如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B正在wait/sleep /join,则线程B会立刻抛出InterruptedException,在catch() {} 中直接return即可安全地结束线程。 需要注意的是,InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛出的。对某一线程调用 interrupt()时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。但是,一旦该线程进入到 wait()/sleep()/join()后,就会立刻抛出InterruptedException 。

不同点:

  1. Thread类的方法:sleep(),yield()等
    Object的方法:wait()和notify()等
  2. 每个对象都有一个锁来控制同步访问。Synchronized关键字可以和对象的锁交互,来实现线程的同步。
    sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
  3. wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
  4. sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常

所以sleep()和wait()方法的最大区别是:

sleep()睡眠时,保持对象锁,仍然占有该锁;而wait()睡眠时,释放对象锁。但是wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException(但不建议使用该方法)。

sleep()方法

sleep()使当前线程进入停滞状态(阻塞当前线程),让出CUP的使用、目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会;

sleep()是Thread类的Static(静态)的方法;因此他不能改变对象的机锁,所以当在一个Synchronized块中调用Sleep()方法是,线程虽然休眠了,但是对象的机锁并木有被释放,其他线程无法访问这个对象(即使睡着也持有对象锁)。

在sleep()休眠时间期满后,该线程不一定会立即执行,这是因为其它线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级。

wait()方法

wait()方法是Object类里的方法;当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时失去(释放)了对象的锁(暂时失去锁,wait(long timeout)超时时间到后还需要返还对象锁);其他线程可以访问;

wait()使用notify或者notifyAlll或者指定睡眠时间来唤醒当前等待池中的线程。

wiat()必须放在synchronized block中,否则会在program runtime时扔出”java.lang.IllegalMonitorStateException“异常。


notify() 方法

首先 ,wait()、notify() 方法是针对对象的,调用任意对象的 wait()方法都将导致线程阻塞,阻塞的同时也将释放该对象的锁,相应地,调用任意对象的 notify()方法则将随机解除该对象阻塞的线程,但它需要重新获取该对象的锁,直到获取成功才能往下执行

其次,wait、notify 方法必须在 synchronized 块或方法中被调用,并且要保证同步块或方法的锁对象与调用 wait、notify 方法的对象是同一个,如此一来在调用 wait 之前当前线程就已经成功获取某对象的锁,执行 wait 阻塞后当前线程就将之前获取的对象锁释放。


notify()和notifyAll()的区别

如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。

notifyAll() 会唤醒所有的线程,notify() 只会唤醒一个线程。

notifyAll() 调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而 notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。


为什么线程通信的方法wait(), notify()和notifyAll()被定义在Object类里

Java中,任何对象都可以作为锁,并且 wait(),notify()等方法用于等待对象的锁或者唤醒线程,在 Java 的线程中并没有可供任何对象使用的锁,所以任意对象调用方法一定定义在Object类中。

wait(), notify()和 notifyAll()这些方法在同步代码块中调用

有的人会说,既然是线程放弃对象锁,那也可以把wait()定义在Thread类里面啊,新定义的线程继承于Thread类,也不需要重新定义wait()方法的实现。然而,这样做有一个非常大的问题,一个线程完全可以持有很多锁,你一个线程放弃锁的时候,到底要放弃哪个锁?当然了,这种设计并不是不能实现,只是管理起来更加复杂。

综上所述,wait()、notify()和notifyAll()方法要定义在Object类中。


两个线程间共享数据

在两个线程间共享变量即可实现共享。

一般来说,共享变量要求变量本身是线程安全的,然后在线程内使用的时候,如果有对共享变量的复合操作,那么也得保证复合操作的线程安全性。


多线程之间的通讯和协作

可以通过中断共享变量的方式实现线程间的通讯和协作

Java中线程通信协作的最常见的两种方式:

一.syncrhoized加锁的线程的Object类的wait()/notify()/notifyAll()

二.ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll()

线程间直接的数据交换:

三.通过管道进行线程间通信:1)字节流;2)字符流


同步方法和同步块的比较

同步块是更好的选择,因为它不会锁住整个对象(当然你也可以让它锁住整个对象)。同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致他们停止执行并需要等待获得这个对象上的锁。

同步块更要符合开放调用的原则,只在需要锁住的代码块锁住相应的对象,这样从侧面来说也可以避免死锁。

请知道一条原则:同步的范围越小越好。


线程同步和线程互斥的概念

当一个线程对共享的数据进行操作时,应使之成为一个”原子操作“,即在没有完成相关操作之前,不允许其他线程打断它,否则,就会破坏数据的完整性,必然会得到错误的处理结果,这就是线程的同步。

在多线程应用中,考虑不同线程之间的数据同步和防止死锁。当两个或多个线程之间同时等待对方释放资源的时候就会形成线程之间的死锁。为了防止死锁的发生,需要通过同步来实现线程安全。

线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。


线程同步的实现方式

线程间的同步方法大体可分为两类:用户模式和内核模式。顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。

用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。内核模式下的方法有:事件,信号量,互斥量。

实现线程同步的方法

  • 同步代码方法:sychronized 关键字修饰的方法
  • 同步代码块:sychronized 关键字修饰的代码块
  • 使用特殊变量域volatile实现线程同步:volatile关键字为域变量的访问提供了一种免锁机制
  • 使用重入锁实现线程同步:reentrantlock类是可冲入、互斥、实现了lock接口的锁他与sychronized方法具有相同的基本行为和语义

在监视器(Monitor)内部实现线程同步

在 java 虚拟机中,每个对象( Object 和 class )通过某种逻辑关联监视器,每个监视器和一个对象引用相关联,为了实现监视器的互斥功能,每个对象都关联着一把锁。

一旦方法或者代码块被 synchronized 修饰,那么这个部分就放入了监视器的监视区域,确保一次只能有一个线程执行该部分的代码,线程在获取锁之前不允许执行该部分的代码

另外 java 还提供了显式监视器( Lock )和隐式监视器( synchronized )两种锁方案


线程安全的概念

线程安全是编程中的术语,指某个方法在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。

Servlet 不是线程安全的,servlet 是单实例多线程的,当多个线程同时访问同一个方法,是不能保证共享变量的线程安全性的。

Struts2 的 action 是多实例多线程的,是线程安全的,每个请求过来都会 new 一个新的 action 分配给这个请求,请求完成后销毁。

SpringMVC 的 Controller 是线程安全的吗?不是的,和 Servlet 类似的处理流程。

Struts2 好处是不用考虑线程安全问题;Servlet 和 SpringMVC 需要考虑线程安全问题,但是性能可以提升不用处理太多的 gc,可以使用 ThreadLocal 来处理多线程的问题。


在Java程序中怎么保证多线程的运行安全

  • 方法一:使用安全类,比如 java.util.concurrent 下的类,使用原子类AtomicInteger
  • 方法二:使用自动锁synchronized。
  • 方法三:使用手动锁 Lock。

本文小结

本文围绕线程的生命周期,介绍了线程生命周期5个阶段主要的方法以及遇到的常见问题。

线程的状态和基本操作相关推荐

  1. Java多线程基础学习,Thread解读、java线程的状态、同步和异步、两阶段终止模式

    理论概述 单线程和多线程 为什么要使用多线程呢?多线程有什么好处呢? 如果在程序中,需要读写一个文件,该文件很大,那我们执行到该io操作时,cpu就会等待该io操作执行完才会继续运行下面的代码,进程调 ...

  2. 线程的状态、调度、同步

    线程的状态 java中的线程共五个状态:新建.就绪.运行.阻塞.死亡: 新建状态(New):处于系统创建线程,但未启动此线程,系统未为其分配资源. 就绪状态(Runnable):线程调用start( ...

  3. java线程的状态及状态间的切换

    在 Java 5 以后,线程状态被明确定义在其公共内部枚举类型 java.lang.Thread.State 中. 分别是: 1.        NEW(初始化状态) 2.        RUNNAB ...

  4. JAVA线程六种状态_Java:线程的六种状态及转化

    多线程概述及创建方式 Java:线程的六种状态及转化 关于线程的生命周期,网上书上说法不一,难以统一,本篇做一个总结: java.lang.Thread.State枚举类中定义了六种线程的状态,可以调 ...

  5. 多线程的实现方式_Java中线程的状态及多线程的实现方式

    线程的状态 线程状态图: 说明: 线程共包括以下5种状态.1. 新建状态(New) : 线程对象被创建后,就进入了新建状态.例如,Thread thread = new Thread().2. 就绪状 ...

  6. 线程的状态:分离(detached)和joinable(可结合的)

    线程分离 在任意一个时间点上,线程是可结合(joinable)或者是可分离的(detached).一个可结合线程是可以被其他线程收回资源和杀关闭.在被回收之前,他的存储器资源(栈等)是不释放的.而对于 ...

  7. java的知识点29——join:合并线程 插队线程、线程的状态

    join:合并线程 插队线程  实例: 爸爸和孩子买烟的故事 /*** join:合并线程 插队线程* 爸爸和孩子买烟的故事* @author Administrator**/ public clas ...

  8. java线程主要状态及转换_Java线程状态转换及控制

    线程的状态(系统层面) 一个线程被创建后就进入了线程的生命周期.在线程的生命周期中,共包括新建(New).就绪(Runnable).运行(Running).阻塞(Blocked)和死亡(Dead)这五 ...

  9. 并发基础篇(四): java中线程的状态深入浅出

    一.线程的五种状态   线程的生命周期可以大致分为5种,但这种说法是比较旧的一种说法,有点过时了,或者更确切的来说,这是操作系统的说法,而不是java的说法.但对下面所说的六种状态的理解有所帮助,所以 ...

最新文章

  1. 他自学成才,坐拥38w粉丝,技术类第一大号!
  2. 鸿海拟将相关物流企业在中国大陆上市
  3. 新年新气象,祝所有朋友心想事成
  4. 人力资源管理4个过程及相关重点
  5. hdu 2159 FATE 二维背包
  6. iOS7应用开发6:UINavigation, UITabbar控制器的多态性
  7. 20191222每日一句
  8. 《Android游戏编程入门经典》——4.6节问与答
  9. 樱花动漫中的视频下载分析
  10. 【Python】Tkinter教程
  11. 巧妙帮你保存个人隐私 WinRAR加密全攻略
  12. 如何使用思维导图?思维导图绘制方法介绍
  13. ALOHA协议和CSMA协议
  14. 【cs230】吴恩达Deep Learning-3/3
  15. 微信开发工具无法支持vue文件
  16. 程序员也需要知道的经济学通识
  17. 服务器硬件规格常用查看命令——CPU相关命令
  18. 固态硬盘故障表现及数据恢复方案
  19. 机器学习 - 线性模型
  20. 学生成绩管理系统设计

热门文章

  1. 做项目时的几个感慨(持续更新...)
  2. CRM系统主要业务流程思维导图
  3. Cocos2d-x 3.2:定时器的使用和原理探究(2)
  4. ViBe(Visual Background extractor)背景建模或前景检测
  5. inux下只显示文件
  6. RFID能否让实体零售业度过“寒冬”?
  7. 从应用启动看Activity的创建过程
  8. CGI(通用网关接口)
  9. hdu 3746 kmp求循环节 下标从1开始
  10. trafficserver records.config参数说明