1.1 进程和线程

进程Process: 每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1—n个线程。可以把进程简单理解为操作系统中运行的一个程序
线程Thread:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。
多进程是指操作系统能同时运行多个任务(程序)。
多线程是指在同一程序中有多个顺序流在执行。进程是线程的容器,一个进程里可以运行1-N个线程,操作系统以进程为单位分配资源
线程是进程的一个执行分支

下面是两条公路,相当于两个进程。左边是单线程的,右边是多线程的。
12.多线程 - 图1

1.2 实现线程的方式

在JAVA中,创建线程就是创建Thread类(子类)的对象(实例)
java.lang.Thread

  • public class Thread extends Object implements Runnable

    构造方法摘要

    | Constructor and Description | | —- | | Thread()
    分配一个新的 Thread对象。 | | Thread(Runnable target)
    分配一个新的 Thread对象。 |

1.2.1 继承Thread类

  1. class ThreadTest extends Thread{
  2. public void run(){
  3. System.out.println("这个线程开始运行");
  4. }
  5. }
  6. public class Test {
  7. public static void main(String[] args) {
  8. ThreadTest tt = new ThreadTest ();
  9. tt.start(); //启动一个线程
  10. }
  11. }

注意:主函数使用start方法启动新线程,而不是调用run方法,区别如下:
12.多线程 - 图2

1.2.2 实现Runnable接口(优先使用)

  1. class ThreadTest implements Runnable{
  2. public void run(){
  3. System.out.println("这个线程开始运行");
  4. }
  5. }
  6. public class Test {
  7. public static void main(String[] args) {
  8. ThreadTest tt = new ThreadTest ();
  9. Thread t = new Thread(tt);
  10. t.start(); //通过一个线程对象启动它
  11. }
  12. }

这两种方式的区别:
1、继承Thread类:编写简单,可直接操作线程。适用于单继承
2、实现Runnable接口:避免单继承局限性,便于共享资源。
推荐使用实现Runnable接口方式创建线程

思考:为什么不直接调用run()方法?
不管调用什么方法,如何调用,都是只有主线程一条执行路径。

1.2.3 实现Callable接口(了解)

  1. import java.util.concurrent.Callable;
  2. import java.util.concurrent.FutureTask;
  3. //创建Callable接口实现类,并实现call()方法,此方法为线程执行体,且有返回值
  4. class CallableThreadTest implements Callable<Integer>{
  5. @Override
  6. public Integer call() throws Exception {
  7. System.out.println("执行线程");
  8. return 1;
  9. }
  10. }
  11. public class Test {
  12. public static void main(String[] args) {
  13. //使用FutureTask类(实现了Future和Runnable接口)来包装Callable对象
  14. FutureTask<Integer> ft = new FutureTask<>(new CallableThreadTest());
  15. //使用FutureTask对象作为Thread对象的target创建并启动新线程
  16. new Thread(ft).start();
  17. //调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
  18. try {
  19. System.out.println("子线程的返回值:" + ft.get());
  20. } catch (Exception e) {
  21. e.printStackTrace();
  22. }
  23. }
  24. }

Runnable和Callable的区别:

  1. Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
  2. Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
  3. call方法可以抛出异常,run方法不可以。
  4. 运行Callable任务可以拿到一个Future对象。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

1.3 线程生命周期

线程对象的生命周期,也就是线程在五种状态之间的转换。
线程生命周期.webp

  • [NEW]新建状态
    尚未启动的线程处于此状态。新创建了一个线程对象,但未调用start方法
  • [RUNNABLE]就绪状态,即可运行状态,复合状态
    包含ready和running两个状态
    ready:表示该线程处于可被调度调度的状态
    运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
    Thread.yield()方法可以将线程由Running状态变为ready 状态
  • [BLOCKED]阻塞状态
    被阻塞等待监视器锁定的线程处于此状态。
    线程发起I/O操作或者申请由其他线程独占的资源,不占用CPU
    当阻塞I/O执行完毕可或者获得了申请的资源,状态就变为RUNNABLE

阻塞I/O是指,进程会一直阻塞,直到数据拷贝完成;阻塞I/O模式时最普遍使用的I/O模式,如输入、输出、接受连接等

  • [WAITING]
    正在等待另一个线程执行特定动作的线程处于此状态。
    线程执行了object.wait() 或thread.join()方法就会把线程转换为等待状态,执行object.notify()方法或者加入的线程执行完毕,线程将重回RUNNABLE状态

    1. (傻傻的等)
  • [TIMED_WAITING]
    正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。
    与WAITING的区别是不会无限期的等待,而是等待指定时间后不管期望的操作是否执行完毕,线程都将重回RUNNABLE状态

    1. (只等等指定长度的时间)
  • [TERMINATED]终止状态
    已退出的线程处于此状态。

1.4 线程运行的不确定性

注意:并不是调用start()方法后,线程就开始运行。而是线程进入就绪状态,等待调度来运行它。所以一个线程的运行是不确定的。

实例:

  1. class ThreadTest extends Thread{
  2. public void run(){
  3. for(int i=0;i<100;i++){
  4. System.out.println(this.getName() + ":" + i);
  5. }
  6. }
  7. }
  8. public static void main(String[] args) {
  9. ThreadTest tt1 = new ThreadTest ();
  10. ThreadTest tt2 = new ThreadTest ();
  11. tt1.start(); //运行结果并不确定!
  12. tt2.start(); //运行结果并不确定!
  13. System.out.println(tt1.isAlive()); //当执行输出语句时,线程是否存活也不确定
  14. System.out.println(tt2.isAlive()); //当执行输出语句时,线程是否存活也不确定
  15. }

1.5 线程调度常用方法

方法 功能
getName()
setName()
设置或获取当前线程名称
isAlive() 判断线程是否存活(就绪、运行、阻塞是存活状态)
getPriority() 获取线程优先级
setPriority() 设置线程优先级
Thread.sleep() 强迫线程睡眠(单位:毫秒)(不释放同步锁)
join() 等待某线程结束,再恢复当前线程的运行
yield() 让出CPU资源,当前线程进入就绪状态
wait() 当前线程进入等待状态,即进入等待池。(释放同步锁,由notify()唤醒)
notify()
notifyAll()
唤醒等待池中的某个或全部等待线程

1.5.1 线程名字

设置线程setName()
获取线程名字getName()

  1. RunThread01 r1=new RunThread01();
  2. Thread t1=new Thread(r1);
  3. t1.setName("线程01");
  4. t1.start();
  5. public class RunThread01 implements Runnable {
  6. @Override
  7. public void run() {
  8. for(int i=0;i<=100000;i++){
  9. System.out.println(Thread.currentThread().getName());
  10. }
  11. }
  12. }

1.5.2 判断线程是否存活

  1. ThreadTest t1 = new ThreadTest();
  2. ThreadTest t2 = new ThreadTest();
  3. t1.start();
  4. t2.start();
  5. try {
  6. Thread.sleep(1000); //让主线程睡眠1秒,t1线程肯定死亡。
  7. } catch (InterruptedException e) {
  8. e.printStackTrace();
  9. }
  10. System.out.println(t1.isAlive());

1.5.3 线程的优先级

Java线程有优先级,优先级高的线程会获得
较多的CPU运行机会。

Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。
Java线程的优先级用整数表示,取值范围是1~10; 10最高级。
每个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY(5)。

  1. ThreadTest t1 = new ThreadTest();
  2. t1.setPriority(1); //设置成最低级
  3. ThreadTest t2 = new ThreadTest();
  4. t2.setPriority(10); //设置成最高级
  5. t1.start();
  6. t2.start();

1.5.4 线程睡眠

Thread.sleep() 强迫线程睡眠(单位:毫秒)

  1. class ThreadTest extends Thread{
  2. public void run(){
  3. for(int i=0;i<10;i++){
  4. System.out.println(i);
  5. try {
  6. Thread.sleep(1000); //每隔一秒后输出
  7. } catch (InterruptedException e) { //线程睡眠期间如果被打断,将抛出异常。
  8. e.printStackTrace();
  9. }
  10. }
  11. }
  12. }

1.5.5 合并线程

join()的功能是:等待某线程结束,再恢复当前线程的运行
或者说:join()把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。

  1. class ThreadTest extends Thread{
  2. public void run(){
  3. for(int i=0;i<10;i++){
  4. System.out.println(this.getName() + ":" + i);
  5. try {
  6. Thread.sleep(1000);
  7. } catch (InterruptedException e) {
  8. e.printStackTrace();
  9. }
  10. }
  11. }
  12. }
  13. //相当于将tt线程和main线程合并成一个线程。
  14. //那么main线程会等待tt线程结束后再运行。
  15. public static void main(String[] args) {
  16. ThreadTest tt = new ThreadTest ();
  17. tt.start();
  18. try {
  19. tt.join();
  20. } catch (InterruptedException e) {
  21. e.printStackTrace();
  22. }
  23. for(int i=0;i<10;i++){
  24. System.out.println("main:" + i);
  25. }
  26. }

1.5.6 让出CPU资源

yield() 让出CPU资源,当前线程进入就绪状态

  1. class ThreadTest extends Thread{
  2. public void run(){
  3. for(int i=0;i<100;i++){
  4. System.out.println(this.getName() + ":" + i);
  5. if(i%10==0){
  6. Thread.yield();
  7. }
  8. }
  9. }
  10. }
  11. public static void main(String[] args) {
  12. ThreadTest tt1 = new ThreadTest ();
  13. ThreadTest tt2 = new ThreadTest ();
  14. tt1.start();
  15. tt2.start();
  16. }

运行结果:
运行到10时,线程让出CPU资源,进入就绪状态。
注意: yield()是让出资源,但并不放弃。它会进入就绪状态,也就是说:它还会与其他线程
一起抢占资源,所以yield()的线程,仍然有可能再次抢占资源。
在加上线程运行的不确定性,所以会导致上面的结论并不是绝对的,只是出现的
概率要高一些。

1.5.7 线程等待与唤醒(涉及线程同步,在后续生产者消费者部分进行介绍)

wait():当前线程进入等待状态,即进入等待池。(释放同步锁,由notify()唤醒);
notify()/notifyAll():唤醒等待池中的某个或全部等待线程。

通俗的说:
wait()意思是说,我等会儿再用这把锁,CPU也让给你们,我先休息一会儿!
notify()意思是说,我用完了,你们谁用?

也就是说,wait()会让出对象锁,同时,当前线程休眠,等待被唤醒,如果不被唤醒,就一直等在那儿。
notify()并不会让当前线程休眠,但会唤醒休眠的线程。

注意:
wait会让出CPU而notify不会,notify重在于通知使用object的对象“我用完了!”;
wait重在通知其它同用一个object的线程“我暂时不用了”并且让出CPUT。

1.6 线程的中断

线程的中断
1、自动中断:一个线程完成执行后(即run方法执行完毕),不能再次运行 。
2、手动中断:
stop( ) —— 已过时,基本不用。(不安全,就像是突然停电)
interrupt( ) ——此方法只是改变中断状态,不会中断一个正在运行的线程。
比如:如果当前线程是阻塞状态,那么就结束阻塞状态
3、可通过使用一个标志指示 run 方法退出,从而终止线程(推荐使用)

通常,线程中断的使用场景有以下几个:

  • 点击某个桌面应用中的取消按钮时;
  • 某个操作超过了一定的执行时间限制需要中止时;
  • 多个线程做相同的事情,只要一个线程成功其它线程都可以取消时;
  • 一组线程中的一个或多个出现错误导致整组都无法继续时;
  • 当一个应用或服务需要停止时。

interrupt( )方法说明:
interrupt()方法只是改变中断状态,不会中断一个正在运行的线程。这一方法实际完成的是,给受阻塞的线程发出一个中断信号,这样受阻线程检查到中断标识,就得以退出阻塞的状态。

更确切的说,如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,此时调用该线程的interrupt()方法,那么该线程将抛出一个 InterruptedException中断异常(该线程必须事先预备好处理此异常),从而提早地终结被阻塞状态。如果线程没有被阻塞,这时调用 interrupt()将不起作用,直到执行到wait(),sleep(),join()时,才马上会抛出 InterruptedException。

  1. class ThreadTest extends Thread{
  2. @Override
  3. public void run() {
  4. for(int i=0;i<100;i++) {
  5. System.out.println(this.getName()+":"+i);
  6. if(i>10) {
  7. this.interrupt();
  8. }
  9. try {
  10. this.sleep(1000);
  11. } catch (InterruptedException e) {
  12. e.printStackTrace();
  13. }
  14. }
  15. }
  16. }
  17. public class Test05 {
  18. public static void main(String[] args) {
  19. ThreadTest tt1 = new ThreadTest();
  20. tt1.setName("线1");
  21. tt1.start();
  22. ThreadTest tt2 = new ThreadTest();
  23. tt2.setName("线2");
  24. tt2.start();
  25. }
  26. }

上面代码中,一旦i>10,那么就会中断sleep状态。

1.7 线程同步问题

当多个线程同时操作同一个数据时,就会产生线程同步问题。
12.多线程 - 图4
为了确保在任何时间点一个共享的资源只被一个线程使用,使用了“同步”
当一个线程运行到需要同步的语句后,CPU不去执行其他线程中的、可能影响当前线程中的下一句代码的执行结果的代码块,必须等到下一句执行完后才能去执行其他线程中的相关代码块,这就是线程同步。

下面这个例子中,多个线程共同操作同一个账户里的余额,就有可能出现线程同步错误。

  1. //模拟一个账户,其中有余额1000元。取钱时如果不足1000元就不能取。
  2. class Account{
  3. private int balance = 1000;
  4. public void qu(){
  5. if(balance>=1000){
  6. balance -= 1000;
  7. System.out.println("取了1000元");
  8. }
  9. }
  10. }
  11. class ThreadTest extends Thread{
  12. private Account account;
  13. public ThreadTest(Account account){
  14. this.account = account;
  15. }
  16. public void run(){
  17. try {
  18. Thread.sleep(100);
  19. } catch (InterruptedException e) {
  20. e.printStackTrace();
  21. }
  22. account.qu(); //在线程类中调用取钱的方法
  23. }
  24. }
  25. //模拟多个人同时操作同一个账户。结果有可能出现这样一种情况:
  26. //账户中共有余额1000元,但却被取走了2000元。
  27. public static void main(String[] args) {
  28. Account account = new Account();
  29. ThreadTest tt1 = new ThreadTest(account); //给每个线程传入的是同一个账户
  30. ThreadTest tt2 = new ThreadTest(account); //给每个线程传入的是同一个账户
  31. tt1.start();
  32. tt2.start();
  33. }

1.8线程同步解决方案一synchronized

解决办法:加上synchronized关键词,使取钱的方法成为一个同步方法。
一旦一个包含锁定方法(用synchronized修饰)的线程被CPU调用,其他线程就无法调用相同对象的锁定方法。当一个线程在一个锁定方法内部,所有试图调用该方法的同实例的其他线程必须等待
synchronized:同步锁(互斥锁):
在java语言中,引入了同步锁的概念,每个对象都有一个与之关联的内部锁(排他锁),用以保证共享数据的安全性问题。
关键词synchronized用来给某个方法或某段代码加上一个同步锁。
当调用者调用此方法时,必须获得这把锁才可以调用。
当某个调用者获得这把锁之后,其他调用者就无法获得了。
当调用结束后,调用者释放这把锁,此时其他调用者才可以获得。
这个机制保障了某个同步方法同时只能有一个调用者。

1、锁定方法

  1. class Account{
  2. private int balance = 1000;
  3. public synchronized void qu(){ //同步方法
  4. if(balance>=1000){
  5. balance -= 1000;
  6. System.out.println("取了1000元");
  7. }
  8. }
  9. }

也可以这样写:
2、锁定代码块

  1. class Account{
  2. private int balance = 1000;
  3. public void qu(){
  4. synchronized(this){ //同步块
  5. if(balance>=1000){
  6. balance -= 1000;
  7. System.out.println("取了1000元");
  8. }
  9. }
  10. }
  11. }

1.9 死锁问题

同步锁具有互斥作用,即排他性。但是这又造成了另外一个问题:死锁。
为了完成一个功能,需要调用两个资源。但是,当两个线程同时调用这两
个资源时,就会出现这样的现象:两个线程都不放弃抢到的一个资源,而另一个资源却永远也抢不到。
12.多线程 - 图5

实例:

  1. public class TestLock {
  2. public static String objA = "strA";
  3. public static String objB = "strB";
  4. public static void main(String[] args) {
  5. Lock1 l1=new Lock1();
  6. Thread t1=new Thread(l1);
  7. Lock2 l2=new Lock2();
  8. Thread t2=new Thread(l2);
  9. t1.start();
  10. t2.start();
  11. }
  12. }
  13. public class Lock1 implements Runnable{
  14. @Override
  15. public void run() {
  16. try{
  17. System.out.println("Lock1 running");
  18. while(true){
  19. synchronized(TestLock.objA){
  20. System.out.println("Lock1 lock strA");
  21. Thread.sleep(5000);//获取strA后先等一会儿,让Lock2有足够的时间锁住strB
  22. synchronized(TestLock.objB){
  23. System.out.println("Lock1 lock strB");
  24. }
  25. }
  26. }
  27. }catch(Exception e){
  28. e.printStackTrace();
  29. }
  30. }
  31. }
  32. package com.neuedu.thread.lock;
  33. public class Lock2 implements Runnable{
  34. @Override
  35. public void run() {
  36. try{
  37. System.out.println("Lock2 running");
  38. while(true){
  39. synchronized(TestLock.objB){
  40. System.out.println("Lock1 lock strB");
  41. Thread.sleep(5000);//获取strA后先等一会儿,让Lock2有足够的时间锁住strB
  42. synchronized(TestLock.objA){
  43. System.out.println("Lock1 lock strA");
  44. }
  45. }
  46. }
  47. }catch(Exception e){
  48. e.printStackTrace();
  49. }
  50. }
  51. }

总结:
一、产生死锁的必要条件:
虽然线程在运行过程中可能会发生死锁,但产生死锁是必须具备一定条件的。产生死锁必须同时具备下面四个必要条件,只要其中任意一个条件不成立,死锁就不会产生:
(1)互斥条件:线程对所分配到的资源进行排他性使用,即在一段时间内,某资源只能被一个线程占用。如果此时还有其他进程请求该资源,则请求进程只能等待,直至占有该资源的线程释放该资源。
(2)请求和保持条件:线程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他线程占有,此时请求线程被阻塞,但对自己以获得的资源保持不放。
(3)不可抢占条件:线程已获得的资源在未使用完之前不能被抢占,只能在进程使用完时由自己释放。
(4)循环等待条件。在发生死锁时,必然存在一个线程—资源的循环链,即线程集合{P0,P1,P2,P3,…,Pn}中的P0正在等待P1占用的资源,P1正在等待P2占用的资源,… … ,Pn正在等待已被P0占用的资源。

二、处理死锁的方法
(1)预防死锁。该方法是通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或几个来预防产生死锁。
  (2)避免死锁。在资源的动态分配过程中,用某种方法防止系统进入不安全状态,从而可以避免产生死锁。

(3)检测死锁。通过检测机构及时地检测出死锁的发生,然后采取适当的措施,把进程从思索中解脱出来。
  (4)解除死锁。当检测到系统中已发生死锁时,就采取相应的措施,将进程从死锁状态中解脱出来。常用方法是—-撤销一些进程,回收他们的资源,将他们分配给已处于阻塞状态的进程,使其能继续运行。

1.10 线程同步的第二种解决方案 wait() notify()

例:生产者消费者问题

  1. 生产者-消费者(producer-consumer)问题,也称作有界缓冲区(bounded-buffer)问题,两个线程共享一个公共的固定大小的缓冲区。其中一个是生产者,用于将消息放入缓冲区;另外一个是消费者,用于从缓冲区中取出消息。问题出现在当缓冲区已经满了,而此时生产者还想向其中放入一个新的数据项的情形,其解决方法是让生产者此时进行休眠,等待消费者从缓冲区中取走了一个或者多个数据后再去唤醒它。同样地,当缓冲区已经空了,而消费者还想去取消息,此时也可以让消费者进行休眠,等待生产者放入一个或者多个数据时再唤醒它。
  1. package test06;
  2. //仓库(有界缓冲区)
  3. class Storage{
  4. private int count = 0;
  5. public synchronized void set(){
  6. if(count>=5){
  7. try {
  8. this.wait();
  9. } catch (InterruptedException e) {
  10. e.printStackTrace();
  11. }
  12. }
  13. count++;
  14. System.out.println("生产了一个,仓库中有:" + count);
  15. this.notify();
  16. }
  17. public synchronized void get(){
  18. if(count<=0){
  19. try {
  20. this.wait();
  21. } catch (InterruptedException e) {
  22. e.printStackTrace();
  23. }
  24. }
  25. count--;
  26. System.out.println("消费了一个,仓库中有:" + count);
  27. this.notify();
  28. }
  29. }
  30. //生产者
  31. class Producer extends Thread{
  32. private Storage storage;
  33. public Producer(Storage storage){
  34. this.storage = storage;
  35. }
  36. public void run(){
  37. for(int i=0;i<50;i++){
  38. this.storage.set();
  39. }
  40. }
  41. }
  42. //消费者
  43. class Customer extends Thread{
  44. private Storage storage;
  45. public Customer(Storage storage){
  46. this.storage = storage;
  47. }
  48. public void run(){
  49. for(int i=0;i<50;i++){
  50. this.storage.get();
  51. }
  52. }
  53. }
  54. public class Test2 {
  55. public static void main(String[] args) {
  56. Storage storage = new Storage();
  57. Producer producer = new Producer(storage);
  58. Customer customer = new Customer(storage);
  59. customer.start();
  60. producer.start();
  61. }
  62. }

总结线程同步的常用方法有以下两种:
1、synchronized
2、wait与notify

学生扩展学习:
线程同步还有哪些方法?(volatile lock)

1.10 附录:守护线程

Java的线程分为两种:User Thread(用户线程)、DaemonThread(守护线程)。
用个比较通俗的比喻,任何一个守护线程都是整个JVM中所有非守护线程的保姆。
只要当前JVM实例中尚存任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束是,守护线程随着JVM一同结束工作,Daemon作用是为其他线程提供便利服务,守护线程最典型的应用就是GC(垃圾回收器),他就是一个很称职的守护者。
thread.setDaemon(true) 设置线程为守护线程,该设置必须在thread.start()之前设置
thread.isDaemon() 可以判断当前线程是否为守护线程

课后作业

1、利用Thread实现,要求多线程求解某范围素数每个线程负责1000范围:线程1找1-1000;线程 2 找 1001-2000;线程 3 找2001-3000。编程程序将每个线程找到的素数及时打印。 [必做题]

2、利用Runnable实现,要求多线程求解某范围素数每个线程负责1000范围:线程1找1-1000;线程 2 找 1001-2000;线程 3 找2001-3000。编程程序将每个线程找到的素数及时打印。 [必做题]

3、编写一个Java程序(包括一个主程序类,一个线程类。在主程序类中创建2个线程,将其中一个线程的优先级设为10,另一个线程的优先级设为6。让优先级为10的线程打印200次“线程1正在运行”,优先级为6的线程打印200次“线程2正在运行”。 [选做题]

4、编写一个计时器,每隔一秒钟,在控制台打印出最新时间。 [必做题]