Java中多线程的一些基础概念(不涉及高并发)

可以关注我的公众号:水啾的Java笔记:

目录:

  1. 一些概念:

    1. 并发与并行

    2. 线程与进程

    3. 线程调度

  2. 创建多线程

    1. 第一种方式:创建Thread类的子类

    2. Thread类的构造方法

    3. Thread类的常用方法

    4. 获取线程名称的方式

    5. 设置线程名称的方式:

    6. 第二种方式:实现 Runnable 接口

    7. 实现Runnable接口的方式比继承Thread类的方式的好处

    8. 两种方式都可以用匿名内部类实现简化

    9. 在Java中,每次运行程序至少启动两个线程。一个main线程,一个垃圾回收线程。

  3. 线程同步

    1. 线程安全问题

    2. 同步锁:可以想象为在对象上标记了一个锁

    3. 线程同步的第一种方式:同步代码块

    4. 线程同步的第二种方式:同步方法

    5. 线程同步的第三种方式:锁机制【Lock锁】

  4. 线程状态

    1. 六种线程状态

    2. 等待唤醒机制

  5. 线程池(JDK1.5之后)

    1. 原因与本质

    2. 线程池的顶级接口Excutor

    3. 真正的线程池接口ExcutorService

    4. 创建线程池的方法

    5. 线程池的使用步骤

 

 

 

一些概念:

  1. 并发与并行

    1. 并发:指两个或多个事件在【微观】上同一个时间段内发生【单CPU:宏观上同时发生,微观上分时交替运行】

    2. 并行:指两个或多个事件在同一时刻发生【多CPU,多任务同时执行】

  2. 线程与进程

    1. 进程:指同一个内存中运行的应用程序,每个程序都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行的基本单位;系统运行一个程序即是一个程序从创建到运行再到消亡的过程。

    2. 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为“多线程程序”;
    3. 多线程程序并不能提高程序的运行速度,但能够提高程序运行的效率,提高CPU的使用率。

  3.线程调度当系统中只有一个CPU时,系统会以某种顺序执行多个线程,这就是线程调度。

    1. 分时调度:所有线程轮流使用cpu的使用权,平均分配每个线程占用cpu的时间。

    2. 抢占式调度:优先让优先级别高的线程使用cpu,如果线程的优先级相同,那么会随机选择一个线程(线程随机性),Java使用的是抢占式调度。

  4.一个特殊的线程——主线程:

主线程:执行主方法(main)的线程

 

单线程程序:java程序中只有一个线程

 

执行从main方法开始,从上到下依次执行。

  1. JVM执行main方法,main方法会进入到栈内存;

  2. JVM会找操作系统开辟一条mian方法通向cpu的执行路径;

  3. cpu就可以通过这个路径来执行main方法;

而这个路径有一个名字,叫mian(主)线程。

 

单线程程序有一个弊端,就是上面的语句有异常了抛给了jvm,那么就会中断程序,后面的语句是执行不到的。

 

创建多线程

一、第一种方式:创建Thread类的子类

java.lang.Thread extends Object implements Runnable

步骤:

  1. 创建一个Thread类的子类

  2. 在Thread类的子类中重写Thread类中的run方法,设置线程任务

  3. 创建Thread类的子类对象

  4. 调用Thread类的子类的start方法,开启新的线程,执行run方法

    1. 多次执行同一个线程为非法,会抛出:

      1. IllegalThreadStatedException

 

public void start()

使该线程开始执行;Java 虚拟机调用该线程的 run 方法。

结果是两个线程并发地运行;当前线程(以主线程为例)(从调用返回给 start 方法)和另一个线程(执行其 run 方法)。

多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。

抛出:

IllegalThreadStateException - 如果线程已经启动。

 

代码演示:

步骤1,步骤2:

 1 package Page17_Thread;
 2 //1.创建一个Thread类的子类
 3 public class E01_MyThread extends Thread {
 4     public E01_MyThread(String name) {
 5         super(name);
 6     }
 7  //   2.在Thread类的子类中重写Thread类中的run方法,设置线程任务(开启线程要做什么?)
 8     @Override
 9     public void run() {
10         for (int i = 0; i < 10; i++) {
11             System.out.println(getName()+":正在执行中!+"+i);
12         }
13     }
14 }

步骤3,步骤4:

 1 package Page17_Thread;
 2 public class E01_Test {
 3     public static void main(String[] args) {
 4         //3.创建Thread类的子类对象
 5         E01_MyThread mt=new E01_MyThread("我的线程!");
 6         //4.调用Thread类中的方法start方法,开启新的线程,执行run方法.
 7         mt.start();
 8         for (int i = 0; i < 10; i++) {
 9             System.out.println("main线程"+i);
10         }
11 //        mt.start();
12     }
13 }

 

多次执行同一个线程为非法的代码演示:

 1 //多次启动一个线程是非法的:
 2 public class E01_Test {
 3     public static void main(String[] args) {
 4         E01_MyThread mt=new E01_MyThread("我的线程!");
 5         mt.start();
 6         for (int i = 0; i < 10; i++) {
 7             System.out.println("main线程"+i);
 8         }
 9         mt.start();
10     }
11 }

 

 

 

另外:

java程序属于抢占式调度,哪个线程的优先级高就优先执行;同一个优先级,随机选择一个执行

 1 package Page17_Thread;
 2 
 3 public class E01_MyThread extends Thread {
 4     @Override
 5     public void run() {
 6         for (int i = 0; i < 20; i++) {
 7             System.out.println("run+"+i);
 8         }
 9     }
10 }
11 
12 package Page17_Thread;
13 
14 public class E01_Test {
15     public static void main(String[] args) {
16         E01_MyThread mt=new E01_MyThread();
17         mt.start();
18         for (int i = 0; i < 20; i++) {
19             System.out.println("main+"+i);
20         }
21     }
22 }

 

 

 

多线程随机性打印原理图示:

 

 

 多线程内存图示:

 

 

 

二、Thread类的构造方法

  1. public Thread():分配一个新的线程对象。

  2. public Thread(String name):分配一个指定了名字的新的线程对象。

  3. public Thread(Runnable target):分配一个带有指定目标的新的线程对象。

  4. public Thread(Runnable target, String name):分配一个带有指定目标的新的线程对象,并指定线程的名字。

 

 

三、Thread类的常用方法

  1. public String getName():获取当前线程名称。

  2. public final void setName(String name):设置线程名称,会先调用checkAccess方法。

  3. public void start():导致此线程的执行;Java虚拟机调用此线程的run方法。

  4. public void run():此线程要执行的任务在此定义代码。

  5. public static void sleep(long millis):使当前正在执行的线程以指定的毫秒数暂停。

  6. public static Thread currentThread():返回对当前正在执行的线程对象的引用。

  7. public void checkAccess():判定当前运行的线程是否有权修改该线程。

  8. public long getId():返回该线程的标识符

  9. public int getPriority():返回该线程的优先级

  10. public Thread.state getState():返回该线程的状态。

 

 

sleep演示:

sleep

public static void sleep(long millis) throws InterruptedException

在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。该线程不丢失任何监视器的所属权。

参数:

millis - 以毫秒为单位的休眠时间。

抛出:

InterruptedException - 如果任何线程中断了当前线程。当抛出该异常时,当前线程的中断状态 被清除。

代码:模拟毫表

 1 public static void main(String[] args) {
 2     for (int i = 0; i < 20; i++) {
 3         System.out.println("第"+i+"秒");
 4         try {
 5             Thread.sleep(1000);//这个方法是有异常的。
 6         } catch (InterruptedException e) {
 7             e.printStackTrace();
 8         }
 9     }
10 }

 

四、获取线程名称的方式

  1. 第一种方式:使用Thread类中方法getName()

  2. 第二种方式:先获取正在执行的线程 currentThread(),再使用方法getName()。Thread.currentThread().getName();

  3. 注意:获取主线程的名称不能直接使用 getName() 。

 

第一种方式获取线程名称代码演示:

 1 //第一种方式:1.使用Thread类中的方法getName()返回该线程的名称。
 2 //String getName()返回线程的名称。
 3 package Page17_Thread;
 4 
 5 public class E01_MyThread extends Thread {
 6     @Override
 7     public void run() {
 8         System.out.println(getName());
 9     }
10 }
11 //************************************************
12 package Page17_Thread;
13 public class E01_Test {
14     public static void main(String[] args) {
15         E01_MyThread mt=new E01_MyThread();
16         mt.start();
17         new E01_MyThread().start();
18         new E01_MyThread().start();
19     }
20 }

 

 

 第二种方式获取线程名称代码演示:

 1 //第二种方式:2.currentThread()可以先获取到当前正在执行的线程,使用线程中的方法getName()获取线程的名称
 2 //static Thread currentThread()返回当前正在执行的线程对象的引用。
 3 package Page17_Thread;
 4 public class E01_MyThread extends Thread {
 5     @Override
 6     public void run() {
 7         Thread t = Thread.currentThread();
 8         System.out.println(t);
 9     }
10 }
11 //****************************************************
12 package Page17_Thread;
13 public class E01_Test {
14     public static void main(String[] args) {
15         E01_MyThread mt=new E01_MyThread();
16         mt.start();
17         new E01_MyThread().start();
18         new E01_MyThread().start();
19     }
20 }

 

 

 

1 public class E01_MyThread extends Thread {
2     @Override
3     public void run() {
4         Thread t = Thread.currentThread();
5         System.out.println(t);
6         String name = t.getName();
7         System.out.println(name);
8     }
9 }

 

 

 

 1 //扩展2
 2 //链式编程
 3 public class E01_MyThread extends Thread {
 4     @Override
 5     public void run() {
 6         /*Thread t = Thread.currentThread();
 7         System.out.println(t);
 8         String name = t.getName();
 9         System.out.println(name);*/
10         System.out.println(Thread.currentThread().getName());
11     }
12 }

 

 

 

 1 //扩展3
 2 //获取主线程的名称
 3 public class E01_Test {
 4     public static void main(String[] args) {
 5         E01_MyThread mt=new E01_MyThread();
 6         mt.start();
 7         new E01_MyThread().start();
 8         new E01_MyThread().start();
 9         //获取主线程的名称,不能直接使用getName,因为没有继承Thread类
10         System.out.println(Thread.currentThread().getName());
11     }
12 }

 

 

 

五、设置线程名称的方式:

  1. 第一种方式:setName(String name)

  2. 第二种方式:创建一个带参数的构造方法,参数传递线程的名称👉public Thread(String name),调用父类的带参构造方法,把线程名称传递给父类,让父类 Thread 给子线程起一个名字。

 

setName

public final void setName(String name)

改变线程名称,使之与参数 name 相同。

首先调用线程的 checkAccess 方法,且不带任何参数。这可能抛出 SecurityException。

参数:

name - 该线程的新名称。

抛出:

SecurityException - 如果当前线程不能修改该线程。

另请参见:

getName(), checkAccess()

checkAccess

public final void checkAccess()

判定当前运行的线程是否有权修改该线程。

如果有安全管理器,则调用其 checkAccess 方法,并将该线程作为其参数。

六、第二种方式:实现 Runnable 接口

  1. 创建一个Runnable接口的实现类;

  2. 在实现类中重写Runnable接口中的方法run,设置线程任务;

  3. 创建一个Runnable接口的实现类的对象;

  4. 创建Thread类对象,构造方法中传递Runnable接口中的实现类的对象;

  5. 调用Thread类中的start方法,开启新的线程执行run方法。

 

第四步中使用的Thread类对象的构造方法:

 

 

 

 

代码演示:

 1 //1.创建一个 Runnable 接口的实现类
 2 public class RunnableImpl implements Runnable {
 3 //2.在实现类中重写 Runnable 接口的 run 方法,设置线程任务
 4     @Override
 5     public void run() {
 6         for (int i = 0; i < 5; i++) {
 7             System.out.println(Thread.currentThread().getName()+"-->"+i);
 8         }
 9     }
10 }
11 public static void main(String[] args) {
12 //3.创建一个 Runnable 接口的实现类对象
13     RunnableImpl ri=new RunnableImpl();
14 //4.创建 Thread 类对象,构造方法中传递 Runnable 接口的实现类对象
15     Thread t=new Thread(ri);
16 //5.调用 Thread 类中的start 方法,开启新的线程执行run方法
17     t.start();
18     for (int i = 0; i < 5; i++) {
19         System.out.println(Thread.currentThread().getName()+"-->"+i);
20     }
21 }

 

 

 

七、实现Runnable接口的方式比继承Thread类的方式的好处:

  1. 避免了单继承的局限性

    1. 一个类只能继承一个类,类继承了Thread类就不能再继承其他的类

    2. 类实现了Runnable接口,还可以继承其他的类、实现其他的接口

  2. 增强了程序的扩展性,减低了程序的耦合性(解耦)

    1. 实现Runnable接口的方式,把设置线程任务和开启新线程进行了分类(解耦)

  3. 适合多个相同的程序代码的线程去共享同一个资源

  4. 线程池只能放入实现Runnable或 Callable类的线程,不能直接放入继承Thread的类。

 

 

八、两种方式都可以用匿名内部类实现简化

匿名内部类的作用:简化代码

把子类继承父类,重写父类的方法,创建子类对象,合成一步完成

把实现接口,重写接口中的方法,创建实现类对象,合成一步完成。

匿名内部类的最终产物:子类/实现类对象,而这个类没有对象。

 

格式 :

new 父类/接口(){

  重写父类/接口中的方法

}

 

第一种方式的匿名内部类的代码演示:

 1 public static void main(String[] args) {
 2     new Thread(){
 3         @Override
 4         public void run() {
 5             for (int i = 0; i < 6; i++) {
 6                 System.out.println("你好"+i);
 7             }
 8         }
 9     }.start();
10     for (int i = 0; i < 8; i++) {
11         System.out.println("你不好"+i);
12     }
13 }

第二种方式的匿名内部类的代码演示:

 1 public static void main(String[] args) {
 2     Thread t=new Thread(new Runnable() {
 3         @Override
 4         public void run() {
 5             for (int i = 0; i < 13; i++) {
 6                 System.out.println(Thread.currentThread().getName()+"-->>"+i);
 7             }
 8         }
 9     });
10     t.start();
11     for (int i = 0; i < 9; i++) {
12         System.out.println(Thread.currentThread().getName()+"-->>"+i);
13     }
14 }

九、在Java中,每次运行程序至少启动两个线程。一个main线程,一个垃圾回收线程。每当使用Java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实是在操作系统中启动了一个进程。

线程同步

一、线程安全问题都是由全局变量及静态变量引起的,若每个线程中对全局变量、静态变量只有读操作而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则可能会影响线程安全。

 

买票图示:

 

 

 线程不安全的买票代码:

 1 public class TicketImpl implements Runnable {
 2     private int ticket = 20;
 3     @Override
 4     public void run() {
 5         while (true) {
 6             if (ticket > 0) {
 7                 try {
 8                     //使用睡眠是为提高出错的概率。
 9                     Thread.sleep(100);
10                 } catch (InterruptedException e) {
11                     e.printStackTrace();
12                 }
13                 String name = Thread.currentThread().getName();
14                 System.out.println(name + "正在卖" + ticket--);
15             }
16         }
17     }
18 }
 1 public static void main(String[] args) {
 2         System.out.println("没有同步");
 3         TicketImpl ticket=new TicketImpl();
 4   
 5         Thread t1=new Thread(ticket,"窗口1");
 6         Thread t2=new Thread(ticket,"窗口2");
 7         Thread t3=new Thread(ticket,"窗口3");
 8         t1.start();
 9         t2.start();
10         t3.start();
11     }

运行结果:

不同窗口卖出了同一张票和不存在的票,而且不能正常地停止程序。

 

上述代码线程安全问题产生的原因:

 

 

 

 

二、同步锁:可以想象为在对象上标记了一个锁

  1. 锁对象可以是任意类型,一般用Object;

  2. 多个线程对象,要使用同一把锁;

  3. 在任何时候,最多允许一个线程拥有同步锁,谁拿到同步锁谁就就如代码快,其他的线程只能在外等着(BLOCKED)。

 

三、线程同步的第一种方式:同步代码块

  1. 格式:

    1. synchronized(同步锁对象){需要同步操作的代码}
  2. 执行完同步代码块就会把锁对象归还给同步代码块i。没有执行完毕不会归还锁。

  3. 程序频繁的判断锁、获取锁、释放锁,程序的效率会减低。

 

注意:1.通过代码块中的锁对象,可以使用任意的对象

2.但是必须保证多个线程使用的锁对象是同一个

3.锁对象的作用:

把同步代码块锁住,只让要给线程在代码块中执行。

 

代码演示:

 1 public class Ticket1 implements Runnable {
 2     private int ticket1 = 100;
 3     Object obj=new Object();
 4     @Override
 5     public void run() {
 6         while (true) {
 7             synchronized(obj) {
 8                 if (ticket1 > 0) {
 9                     try {
10                         //提高安全问题出现的概率
11                         Thread.sleep(101);
12                     } catch (InterruptedException e) {
13                         e.printStackTrace();
14                     }
15                     String name = Thread.currentThread().getName();
16                     System.out.println(name + "正在卖" + ticket1--);
17                 }
18             }
19         }
20     }
21 }

 

 

 

四、线程同步的第二种方式:同步方法

  1. 格式:

    1. 修饰符 synchronizec 返回值类型 方法名(参数列表){方法体}
  2. 对于非静态方法,同步锁就是this;对于静态方法,使用当前方法所在类的字节码对象。

代码演示:

 1 //同步方法
 2 public class Ticket2 implements Runnable {
 3     private int ticket2 = 100;
 4     @Override
 5     public void run() {
 6         while (true) {
 7             sellTicket();
 8         }
 9     }
10     
11     private synchronized void sellTicket() {
12         if(ticket2>0){
13             try {
14                 Thread.sleep(100);
15             } catch (InterruptedException e) {
16                 e.printStackTrace();
17             }
18             String name=Thread.currentThread().getName();
19             System.out.println(name+"正在买"+ticket2--);
20         }
21     }
22 }

同步方法也会把方法内部的代码锁住,只让一个线程执行。

同步方法的锁对象是谁?就是实现类对象, new Ticket2() ,也就是this

 

静态同步方法:

静态的同步方法,锁对象是谁?不能是this

this是创建对象之后产生的,静态方法优先于对象

静态 方法的锁对象是本类的class属性--》class文件对象(反射)

 1 public class Ticket2_static implements Runnable {
 2     private static int ticket2 = 100;
 3     @Override
 4     public void run() {
 5         while (true) {
 6             sellTicketStatic();
 7         }
 8     }
 9 
10     private static synchronized void sellTicketStatic() {
11         if(ticket2>0){
12             try {
13                 Thread.sleep(100);
14             } catch (InterruptedException e) {
15                 e.printStackTrace();
16             }
17             String name=Thread.currentThread().getName();
18             System.out.println(name+"正在买"+ticket2--);
19         }
20     }
21 }

五、线程同步的第三种方式:锁机制【Lock锁】

  1. java.util.concurrent.Lock 接口提供了比同步代码块和同步方法更加广泛的锁定操作,更加面向对象。

  2. 实现类:java.util.concurrent.locks.ReentrantLock implements Lock

    1. 在成员位置创建实现类的对象,用多态。

  3. 获取锁:void lock():在可能出现安全问题的代码前调用;

  4. 释放锁:void unlock():在可能出现安全问题的代码后调用

    1. 最好放在 try...catch...finally...  中的finally子句中。

 

 

使用步骤:

1.在成员位置创建一个ReentrantLock对象

2.在可能会出现安全问题的代码前调用Lock接口中的方法 lock 获取锁

3.在可能会出现安全问题的代码后调用Lock接口中的方法 unlock 获取锁

代码演示:

 1 public class Ticket3 implements Runnable {
 2     private int ticket3 = 100;
 3     //1.在成员位置创建一个ReentrantLock对象
 4     Lock lock=new ReentrantLock();
 5     @Override
 6     public void run() {
 7         while (true) {
 8             //2.在可能会出现安全问题的代码前调用Lock接口中的方法 lock 获取锁
 9             lock.lock();
10             if(ticket3>0){
11                 try {
12                     Thread.sleep(100);
13                 } catch (InterruptedException e) {
14                     e.printStackTrace();
15                 }
16                 String name = Thread.currentThread().getName();
17                 System.out.println(name+"正在卖"+ticket3--);
18             }
19             //3.在可能会出现安全问题的代码后调用Lock接口中的方法 unlock 获取锁
20             lock.unlock();
21         }
22     }
23 }

将unlock放在finally里,保证一定会释放锁,无论是否产生异常

 1 public class Ticket3_finally implements Runnable {
 2     private int ticket3 = 100;
 3     //1.在成员位置创建一个ReentrantLock对象
 4     Lock lock=new ReentrantLock();
 5     @Override
 6     public void run() {
 7         while (true) {
 8             //2.在可能会出现安全问题的代码前调用Lock接口中的方法 lock 获取锁
 9             lock.lock();
10             if(ticket3>0){
11                 try {
12                     Thread.sleep(100);
13                     String name = Thread.currentThread().getName();
14                     System.out.println(name+"正在卖"+ticket3--);
15                 } catch (InterruptedException e) {
16                     e.printStackTrace();
17                 }finally{   //放在这里的好处是,无论是否出现异常,最后一定会释放锁。
18                     //3.在可能会出现安全问题的代码后调用Lock接口中的方法 unlock 获取锁
19                     lock.unlock();
20                 }
21             }
22 
23         }
24     }
25 }

以上代码都无法自己正常停止🛑,run方法更改如下后可以正常停止:

 1 public void run() {
 2     for (; ; ) {
 3         //2.在可能会出现安全问题的代码前调用Lock接口中的方法 lock 获取锁
 4         lock.lock();
 5         try {
 6             Thread.sleep(100);
 7             String name = Thread.currentThread().getName();
 8             System.out.println(name + "正在卖" + ticket3--);
 9         } catch (InterruptedException e) {
10             e.printStackTrace();
11         } finally {   //放在这里的好处是,无论是否出现异常,最后一定会释放锁。
12             //3.在可能会出现安全问题的代码后调用Lock接口中的方法 unlock 获取锁
13             lock.unlock();
14         }
15         if (ticket3 <= 1) {
16             break;
17         }
18     }
19 }

线程状态

一、六种线程状态

  1. New(新建):线程刚被创建,但是还未启动,还没有调用start方法。

  2. Runnable(可运行):线程可以在Java虚拟机中运行的状态,可能正在运行自己的代码,也可能没有,这取决于系统处理器。

  3. Blocked(锁阻塞):当一个线程试图获取一个对象锁,而该对象锁被其他线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。

  4. Waitting(无限等待):一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waitting状态。进入该状态后是不能自动唤醒的,必须等待另一个线程调用notify方法或者notifyAll方法才能够唤醒。

    1. 不带超时值的 Object.wait()

    2. 不带超时值的 Thread.join()

    3. Locksupport.park()

  5. Timed Waitting(计时等待):同Waitting状态,有几个方法有超时参数,调用它们将进入 Timed Waitting 状态。这一状态将一直保持到超时期满或者收到唤醒通知。带有超时参数的常用方法有 Thread.sleep 、Object.wait。

    1. 进入Timed Waitting 状态的使用的一种常用情形是调用sleep方法,单独的线程也可以调用,不一定非要有协作关系;

    2. 为了让其他线程有机会执行,可以将Thread.sleep() 的调用放在run方法之内,这样才能保证该线程执行过程中会睡眠;

    3. Thread.sleep() 与锁无关,线程睡眠到期会自动苏醒,并返回到 Runnable状态。

  6. Teminated(被终止):因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

 

在给定的时间点上,一个线程只能处于一种状态。这种状态是虚拟机状态,它们并没有放映所有操作系统的线程状态。

 

六种线程示意图:

 

 

Timed Waiting (计时等待)代码:

计时秒表中用到 Thread.sleep方法就是使线程进入计时等待

 1 public static void main(String[] args) {
 2     for (int i = 0; i < 200; i++) {
 3         System.out.println("第"+i+"秒");
 4         try {
 5             Thread.sleep(1000);//这个方法是有异常的。异常类型InterruptedException中断异常
 6         } catch (InterruptedException e) {
 7             e.printStackTrace();
 8         }
 9     }
10 }

 

 

锁阻塞:API中的介绍:受阻塞并且正在等待监视器锁的某一线程的线程状态。

线程A 与线程B 代码中使用同一锁,如果线程A 获取到锁 ,线程 A 进入到Runnable 状态,那么线程 B 进入到 Blocked锁阻塞状态。

除此,Waiting 以及 Time Waiting 也会在某种状态下进入阻塞状态。

 

 

Waiting (无限等待)public static final Thread.State WAITING

某一等待线程的线程状态。某一线程因为调用下列方法之一而处于等待状态:

  • 不带超时值的 Object.wait

  • 不带超时值的 Thread.join

  • LockSupport.park

处于等待状态的线程正等待另一个线程,以执行特定操作。 例如,已经在某一对象上调用了 Object.wait() 的线程正等待另一个线程,以便在该对象上调用 Object.notify() 或 Object.notifyAll()。已经调用了 Thread.join() 的线程正在等待指定线程终止。

 1 public class E08_WaitingDemo {
 2     public static Object obj=new Object();
 3 
 4     public static void main(String[] args) {
 5         new Thread(new Runnable() {
 6             @Override
 7             public void run() {
 8                 while(true){
 9                     synchronized (obj){
10                         try {
11                             System.out.println(Thread.currentThread().getName()+"==" +
12                                     "获取到锁对象,调用wait方法,进入waiting状态,释放锁对象");
13 //                            obj.wait();
14                             obj.wait(5000);
15                         } catch (InterruptedException e) {
16                             e.printStackTrace();
17                         }
18                         System.out.println(Thread.currentThread().getName()+"从waiting状态" +
19                                 "醒来,获取到锁对象,继续执行了");
20                     }
21                 }
22             }
23         },"等待线程").start();
24 
25         new Thread(new Runnable() {
26             @Override
27             public void run() {
28                 while(true){
29                     try {
30                         System.out.println(Thread.currentThread().getName()+"---等待3秒钟");
31                         Thread.sleep(3000);
32                     } catch (InterruptedException e) {
33                         e.printStackTrace();
34                     }
35                     synchronized (obj){
36                         System.out.println(Thread.currentThread().getName()+"---获取到锁对象" +
37                                 "调用notify方法,释放锁对象");
38                         obj.notify();
39                     }
40                 }
41             }
42         },"唤醒线程").start();
43     }
44 }

 

 

注意:一个调用了某个对象的Object.wait方法的线程会等待另一个线程调用此对象的Object.notify()方法或 Object.notifyAll()方法。

其实waiting状态并不是一个线程的操作,它体现的是多个线程间的通信,可以理解为多个线程之间的协作关系,多个线程会争取锁,同时相互之间又存在协作关系。好比公司的同事,有竞争,更多的是协作。

当多个线程协作时,比如A、B线程,如果A线程在Runnable(可运行)状态中调用了wait()方法,那么A线程就就如了Waiting(无限等待)状态,同时失去了同步锁。假如这个时候B线程获取到了同步锁,在运行状态中调用了notify()方法,那么就会将无限等待状态的A线程唤醒。注意是唤醒,如果获取到锁对象,那么A线程唤醒后就进入Ruannable(可运行)状态;如果没有获取锁对象,那么就进入Blocked(锁阻塞)状态。

 

 

 

二、等待唤醒机制

  1. 线程间通信:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同,需要线程通信来避免不同线程对同一资源的争夺,这就是等待唤醒机制。

  2. wait:线程不再参与调度,不会浪费cpu资源,不会竞争锁。等待别的线程执行一个通知(notify),将这个线程从 wait set 中释放出来,重新进入到调度队列(read queue)中。

  3. notify:

  4. notifyAll:

  5. wait方法与notify方法必须由同一个锁对象调用

  6. wait方法与notify方法必须要在同步代码块或者同步方法中使用。因为必须要有锁对象。

     

     

    等待唤醒 案例代码实现:

    1.创建一个顾客线程(消费者):告知老板要的包子的种类和数量,调用wait方法,放弃cpu的执行权,进入到WAINTING 状态(无限等待)

    2.创建一个老板线程(生产者):花了一定的时间做包子,做好包子之后调用notify方法,唤醒顾客吃包子

    注意:

    顾客和老板线程,必须使用同步代码块包裹起来,保证等待和唤醒只能由一个在执行。

    同步使用的锁对象必须保证是唯一的

    只有锁对象才能调用 wait和notify方法

    Object 类中的方法:

    void wait():在其他线程调用此对象的notify()方法或 notifyAll()方法前,导致当前线程等待

    void notify():唤醒在此对象监视器上等待的单个线程。

    唤醒之后会继续执行wait之后的代码

 1 public static void main(String[] args) {
 2     //创建锁对象,保证唯一的锁对象
 3     Object obj = new Object();
 4     //创建一个顾客线程
 5     new Thread() {
 6         @Override
 7         public void run() {
 8             //一直买包子
 9             while(true){
10                 //保证等待和唤醒的线程只能由有一个执行,需要使用同步技术
11                 synchronized (obj) {
12                     System.out.println("顾客告知老板要买包子");
13                     //调用wait方法,放弃cpu的执行,进入到WAITING状态(无限等待)
14                     try {
15                         obj.wait();
16                     } catch (InterruptedException e) {
17                         e.printStackTrace();
18                     }
19                     //等待notify唤醒之后继续执行的代码
20                     System.out.println("顾客吃包子。");
21                     System.out.println("顾客吃完包子了。");
22                     System.out.println("+++++++++++++++++++++");
23                 }
24             }
25         }
26     }.start();
27     //创建一个老板线程
28     new Thread() {
29         @Override
30         public void run() {
31             //一直做包子
32             while(true){
33                 try {
34                     Thread.sleep(2000); //花了2秒做包子
35                 } catch (InterruptedException e) {
36                     e.printStackTrace();
37                 }
38                 //保证等待和唤醒的线程只能有一个执行,需要使用同步技术
39                 synchronized (obj) {
40                     System.out.println("2秒后老板做好了包子,告知顾客");
41                     //做好包子之后,调用notify方法,唤醒顾客吃包子
42                     obj.notify();
43                 }
44             }
45         }
46     }.start();
47 }

进入到TIMEWAITING(计时等待)有两种方式:

1.使用sleep(long m)方法,在毫秒值结束之后,线程睡醒进入到 Runnable(可运行)/Blocked(锁阻塞) 状态

2.使用wait(long m) 方法,wait方法如果在毫秒值结束之后,还没有被 notify唤醒,就会自动醒来,进入到 Runnable/Blocked 状态

 1 public static void main(String[] args) {
 2     Object obj = new Object();
 3     new Thread() {
 4         @Override
 5         public void run() {
 6             while(true){
 7                 synchronized (obj) {
 8                     System.out.println("顾客告知老板要买包子");
 9                     try {
10                         obj.wait(5000);//wait方法如果在毫秒值结束之后,还没有被 notify唤醒,就会自动醒来,进入到 Runnable/Blocked 状态
11                     } catch (InterruptedException e) {
12                         e.printStackTrace();
13                     }
14                     System.out.println("顾客吃包子。");
15                     System.out.println("顾客吃完包子了。");
16                     System.out.println("+++++++++++++++++++++");
17                 }
18             }
19         }
20     }.start();
21 }

唤醒的方法:

1.void notify() 唤醒在此对象监视器上等待的单个线程

2.void notifyAll() 唤醒在此对象监视器上等待的所有线程

 

 使用notify唤醒

 1 public static void main(String[] args) {
 2     Object obj = new Object();
 3     new Thread() {
 4         @Override
 5         public void run() {
 6             while(true){
 7                 synchronized (obj) {
 8                     System.out.println("顾客1告知老板要买包子");
 9                     try {
10                         obj.wait();
11                     } catch (InterruptedException e) {
12                         e.printStackTrace();
13                     }
14                     System.out.println("包子做好了,顾客1吃包子。");
15                     System.out.println("+++++++++++++++++++++");
16                 }
17             }
18         }
19     }.start();
20     new Thread() {
21         @Override
22         public void run() {
23             while(true){
24                 synchronized (obj) {
25                     System.out.println("顾客2告知老板要买包子");
26                     try {
27                         obj.wait();
28                     } catch (InterruptedException e) {
29                         e.printStackTrace();
30                     }
31                     System.out.println("包子做好了,顾客2吃包子。");
32                     System.out.println("+++++++++++++++++++++");
33                 }
34             }
35         }
36     }.start();
37     new Thread() {
38         @Override
39         public void run() {
40             while(true){
41                 try {
42                     Thread.sleep(2000); //花了2秒做包子
43                 } catch (InterruptedException e) {
44                     e.printStackTrace();
45                 }
46                 synchronized (obj) {
47                     System.out.println("2秒后老板做好了包子,告知顾客");
48                     obj.notify();
49                 }
50             }
51         }
52     }.start();
53 }

结果:只能随机唤醒顾客1线程或顾客2线程

使用notifyAll唤醒

 1 public static void main(String[] args) {
 2     Object obj = new Object();
 3     new Thread() {
 4         @Override
 5         public void run() {
 6             while(true){
 7                 synchronized (obj) {
 8                     System.out.println("顾客1告知老板要买包子");
 9                     try {
10                         obj.wait();
11                     } catch (InterruptedException e) {
12                         e.printStackTrace();
13                     }
14                     System.out.println("包子做好了,顾客1吃包子。");
15                     System.out.println("11111111111111111111");
16                 }
17             }
18         }
19     }.start();
20     new Thread() {
21         @Override
22         public void run() {
23             while(true){
24                 synchronized (obj) {
25                     System.out.println("顾客2告知老板要买包子");
26                     try {
27                         obj.wait();
28                     } catch (InterruptedException e) {
29                         e.printStackTrace();
30                     }
31                     System.out.println("包子做好了,顾客2吃包子。");
32                     System.out.println("222222222222222222222");
33                 }
34             }
35         }
36     }.start();
37     new Thread() {
38         @Override
39         public void run() {
40             while(true){
41                 synchronized (obj) {
42                     System.out.println("顾客3告知老板要买包子");
43                     try {
44                         obj.wait();
45                     } catch (InterruptedException e) {
46                         e.printStackTrace();
47                     }
48                     System.out.println("包子做好了,顾客3吃包子。");
49                     System.out.println("333333333333333333333333");
50                 }
51             }
52         }
53     }.start();
54     new Thread() {
55         @Override
56         public void run() {
57             while(true){
58                 synchronized (obj) {
59                     System.out.println("顾客4告知老板要买包子");
60                     try {
61                         obj.wait();
62                     } catch (InterruptedException e) {
63                         e.printStackTrace();
64                     }
65                     System.out.println("包子做好了,顾客4吃包子。");
66                     System.out.println("444444444444444444444444");
67                 }
68             }
69         }
70     }.start();
71     new Thread() {
72         @Override
73         public void run() {
74             while(true){
75                 try {
76                     Thread.sleep(5000); //花了2秒做包子
77                 } catch (InterruptedException e) {
78                     e.printStackTrace();
79                 }
80                 synchronized (obj) {
81                     System.out.println("5秒后老板做好了包子,告知顾客");
82                     obj.notifyAll();
83                 }
84             }
85         }
86     }.start();
87 }

 

线程间通信

多个线程在处理同一个资源,但是处理的动作(线程任务)却不相同。比如:线程A用来生产包子,线程B用来吃包子,包子可以理解为同一资源,线程A与线程B处理的动作,一个是生成,一个是消费,那么线程A与线程B之间就存在线程通信问题。

多个线程并发执行,在默认情况下是CPU随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望它们有规律的执行,那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。

如何保证线程间通信有效利用资源:

多个线程在处理同一个资源,并且任务不同时,需要线程通信来解帮助解决线程之间对同一个变量的使用或操作。就是多个线程在操作同一份数据时,避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效的利用资源。而这种手段即——等待唤醒机制。

 

等待唤醒机制概述

等待唤醒机制就是在一个线程进进行了规定操作后,就就进入等待状态(wait()),等待其他线程执行完它们的指定代码过后再将其唤醒(notify());在有多个线程进行等待时,如果需要,可以使用notifyAll()来唤醒所有的等待线程。

wait/notify就是线程间的一种协作机制。

等待唤醒中的方法:

1.wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费CPU资源,也不会去竞争锁,这时的线程状态即是 WAITING  。它还要等着别的线程执行一个特别的动作,也即是 “通知(notify)”在这个对象上等待的线程从 wait set 中释放出来,重新进入到调度队列(ready queue )中。

2.notify:则选取所通知的对象的 wait set 上的一个线程释放;例如,餐馆有空位置后,等厚就餐醉酒的顾客最先入座。

3.notifyAll:则释放所通知的对象的 wait set 上的全部线程。

注意:哪怕只通知了要给等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的是在同步内,而此刻它已经不持有锁,所以它需要再次尝试去获取锁(很可能面临其他线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。

如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态;否则,从 wait set 出来后,又进入 entry set ,线程就从 WAITING 状态又变成 BLOCKED 状态。

调用wait和notify 方法需要注意的细节

1.wait方法与notify 方法必须要由同一个对象锁调用。因为:对应的锁对象可以通过 notify 唤醒使用同一个 锁对象调用 的wait方法后的线程。

2.wait方法与notify方法是属于Object 类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object 类的。

3.wait方法与notify方法必须要在同步代码块或者是函数中使用。因为要通过锁对象调用这两个方法。

 

 

 等待唤醒机制代码实现——包子类&包子铺类:

 1 //包子类
 2 public class baoZi {
 3     private String pier;    //包子皮
 4     private String xian;    //包子馅
 5     private boolean flag;   //是否有包子
 6 
 7     public String getPier() {
 8         return pier;
 9     }
10 
11     public void setPier(String pier) {
12         this.pier = pier;
13     }
14 
15     public String getXian() {
16         return xian;
17     }
18 
19     public void setXian(String xian) {
20         this.xian = xian;
21     }
22 
23     public boolean isFlag() {
24         return flag;
25     }
26 
27     public void setFlag(boolean flag) {
28         this.flag = flag;
29     }
30 
31     public baoZi(String pier, String xian, boolean flag) {
32 
33         this.pier = pier;
34         this.xian = xian;
35         this.flag = flag;
36     }
37 
38     public baoZi() {
39 
40     }
41 }    
 1 //包子铺类。锁是资源,所以将包子作为包子铺类和吃货类的锁。
 2 public class baoZiPu extends Thread {
 3     baoZi bz;
 4 
 5     public baoZiPu(String name, baoZi bz) {
 6         super(name);
 7         this.bz = bz;
 8     }
 9 
10     @Override
11     public void run() {
12         int count=0;        //增加包子的种类
13         while(true){
14             synchronized (bz){
15                 if(bz.isFlag()){
16                     try {
17                         bz.wait();
18                     } catch (InterruptedException e) {
19                         e.printStackTrace();
20                     }
21                 }
22                 System.out.println("包子铺开始做包子");
23                 if(count%2==0){
24                     bz.setPier("薄皮");
25                     bz.setXian("三鲜馅");
26                 }else{
27                     bz.setPier("厚皮");
28                     bz.setXian("猪肉馅");
29                 }
30                 count++;
31                 try {
32                     System.out.println("包子铺生成包子需要3秒");
33                     Thread.sleep(3000);
34                 } catch (InterruptedException e) {
35                     e.printStackTrace();
36                 }
37                 System.out.println("已经做好了"+bz.getPier()+bz.getXian()+"的包子");
38                 System.out.println("吃货可以来吃包子了");
39                 System.out.println("==================");
40                 bz.setFlag(true);
41                 bz.notify();
42             }
43         }
44     }
45 }

等待唤醒机制代码实现——吃货类&测试类:

 1 //吃货类
 2 public class chiHuo extends Thread {
 3     baoZi bz;
 4 
 5     public chiHuo(String name,baoZi bz) {
 6         super(name);
 7         this.bz = bz;
 8     }
 9 
10     @Override
11     public void run() {
12         while(true){
13             synchronized (bz){
14                 if(!bz.isFlag()){
15                     try {
16                         bz.wait();
17                     } catch (InterruptedException e) {
18                         e.printStackTrace();
19                     }
20                 }
21                 System.out.println("吃货"+getName()+"正在吃"+bz.getPier()+bz.getXian()+"的包子");
22                 try {
23                     System.out.println("吃货吃包子需要2秒");
24                     Thread.sleep(2000);
25                 } catch (InterruptedException e) {
26                     e.printStackTrace();
27                 }
28                 bz.setFlag(false);
29                 System.out.println("吃货"+getName()+"已经把包子吃完了,包子铺可以开始生产包子了。");
30                 System.out.println("++++++++++++++++++");
31                 bz.notify();
32             }
33         }
34     }
35 }
1 public class text {
2     public static void main(String[] args) {
3         baoZi bz=new baoZi();
4         new chiHuo("张三",bz).start();
5         new baoZiPu("包子铺",bz).start();
6     }
7 }

线程池(JDK1.5之后)

一、原因与本质

  1. 需要线程池的原因:如果并发的线程数量很多,并且每个都是执行一个时间很短的任务就结束了,这样频繁的创建线程就会大大减低系统的效率。

  2. 线程池其实就是一个容纳了多个线程的容器,其中的线程可以反复使用,剩下了频繁创建和销毁线程的操作,无需因反复创建线程而消耗过多资源。

 

 

 

 

 

二、线程池的顶级接口

java.util.concurrent.Excutor——严格意义上它只是一个执行线程的工具

static ExecutorService newFixedThreadPool(in nThreads)  创建一个可重用固定线程数的线程池。

参数:

in nThreads:要创建的线程池中包含的线程数量

返回值:

ExecurtorService接口,返回的是ExecutorService接口的实现类对象,我们可以使用ExecutorService接口接收(面向接口编程)

 

三、真正的线程池接口

java.util.concurrent.ExcutorService

方法:

  1. public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行。Future接口是用来记录线程任务执行完毕后产生的结果。调用start方法。

  2. shutdown():销毁线程池

 

四、创建线程池的方法

在工厂 java.util.concurrent.Excutors 中,提供了一些静态方法用来创建线程池

    public static ExcetorService newFixedThreadPool(int nThreads):创建一个线程数量最大为参数的线程池对象。

 

五、合理利用线程池的三个好处

  1. 减低资源消耗。减少了创建和销毁线程的次数,每个线程都可以被重复使用,可以执行多个任务。

  2. 提高响应速度。当任务到达时,任务不需要等待线程创建就能立即执行。

  3. 提高线程的可管管理性。可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存,而把服务器累趴下:每个线程需要 1MB的内存,线程开的越多,消耗的内存就越大,最后死机。

 

六、线程池的使用步骤

  1. 使用线程池的工厂类 Excutors 里边提供的静态方法 newFixedThreadPool(int nThread)生成一个指定线程数量的线程池对象

  2. 创建一个 Runnable 接口的实现类,重写 run 方法,设置线程任务;

  3. 调用 ExcutorService 中的 submit 方法,传递线程任务(实现类对象),开启线程,执行 run 方法。

  4. 调用 ExcutorService 中的方法 shutdown 销毁线程池【不建议执行此步骤】

 

注意:2和3和合并,传递匿名对象。

 

 

代码演示:

 1 public static void main(String[] args) {
 2     //1.使用线程池的工厂类Executors里边提供的静态方法 newFixedThreadPool(in nThreads) 生成一个指定线程数量的线程池
 3     ExecutorService es = Executors.newFixedThreadPool(2);
 4     //3.调用 ExecutorService 中的方法 submit,传递线程任务(实现类),开启线程,执行 run 方法
 5     es.submit(new RunnableImpl());//线程:pool-1-thread-1执行 .//线程池还在,程序不会关闭
 6     //线程运行完毕后会回到线程池,有任务时再调用。
 7     es.submit(new RunnableImpl());
 8     es.submit(new RunnableImpl());
 9     es.submit(new RunnableImpl());
10     //4.调用 ExecutorService 中的方法 shutdown 销毁线程池(不建议执行)
11     es.shutdown();
12     //下面继续调用线程,会报错,因为线程池已经关闭了
13     es.submit(new RunnableImpl());//ava.util.concurrent.RejectedExecutionException
14 }
15 //2.创建一个类,实现Runnable 接口,重写 run 方法,设置线程任务
16 public class RunnableImpl implements Runnable {
17     @Override
18     public void run() {
19         System.out.println("线程:"+Thread.currentThread().getName()+"执行");
20     }
21 }

 

可以关注我的公众号:水啾的Java笔记:

 

posted @ 2021-08-03 22:02  水啾2  阅读(44)  评论(0)    收藏  举报