java中关于线程的概念
2021/5/6 20:25:20
本文主要是介绍java中关于线程的概念,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
10.线程
10.1.背景知识
- 进程
- 官方点的理解:计算机程序在某个数据集合上的运行活动,进程是操作系统进程资源分配与调度的基本单位。即:正在运行的程序(软件)。
- 每个进程都被操作系统分配了自己独立的内存空间与系统资源,进程之间互不干扰。
- 是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
- 线程与线程调度
- 线程依赖于进程而存在,一个进程有多个子任务,而线程就是进程中的一个子任务,一个执行单元,一个顺序控制流。线程是CPU进行资源分配与调度的进本单位。JVM是多线程的。
- 线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,线程之间共享进程的数据。这个应用程序也可以称之为多线程程序。
- 线程调度:
- 协同式线程调度(Cooperative Thread-Scheduling):所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
- 使用协同式调度的多线程系统,线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,通知系统切换到另外一个线程上去。
- 其最大的好处是实现简单,坏处是线程执行时间不可控
- 抢占式线程调度(Preemptive Thread-Scheduling):优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性)。
- 使用抢占式调度的多线程系统,那么每个线程将由系统来分配执行时间,线程的切换不由线程本身决定。
- 其最大的好处是,线程的执行时间是可控的
- 设置线程的优先级: 我们在java语言中设置的线程优先级,它仅仅只能被看做是一种"建议"(对操作系统的建议),实际上,操作系统本身,有它自己的一套线程优先级 (静态优先级 + 动态优先级),线程优先级并非完全没有用,我们Thread的优先级,它具有统计意义,总的来说,高优先级的线程占用的cpu执行时间多一点,低优先级线程,占用cpu执行时间,短一点
- 获取线程优先级:public final int getPriority()
- 设置线程优先级:public final void setPriority(int newPriority)
- 优先级的范围:1-10,默认是5
- MIN_PRIORITY = 1
- NORM_PRIORITY = 5
- MAX_PRIORITY = 10
- 抢占式调度详解:大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。这些程序是在同时运行,实际上CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。Java语言采用的是抢占式调度。
- 协同式线程调度(Cooperative Thread-Scheduling):所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
- 串行,并行,并发。
- 串行:一个任务接一个任务的执行
- 并发:同一时间段内,多个任务同时执行
- 并行:同一时刻,多个任务同时执行
- 并行是一种理想状态下的并发。
10.2.Thread类
- java.lang.Thread 类,API中该类中定义了有关线程的一些方法
- 构造方法:
- public Thread() :分配一个新的线程对象。
- public Thread(String name) :分配一个指定名字的新的线程对象。
- public Thread(Runnable target) :分配一个带有指定目标新的线程对象。
- public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。
- 常用方法:
-
public String getName() :获取当前线程名称。
-
public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。
-
public void run() :此线程要执行的任务在此处定义代码。
-
public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
-
public static Thread currentThread() :返回对当前正在执行的线程对象的引用
- Thread.sleep():参数是毫秒
- 等效的方法 TimeUnit.SECONDS.sleep(10):休眠10秒,单位更加具体
-
public final void setDaemon(boolean on):将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出
-
public static void yield():暂停当前正在执行的线程对象,并执行其他线程
-
public void interrupt():方法只是改变中断状态而已,它不会中断一个正在运行的线程
-
public final void join(): 等待该线程终止,Join这行代码在哪个线程上运行,就是哪个线程在等待,等待的是使用join方法的这个线程对象执行完。
实际上,join方法使得等待的线程阻塞,阻塞的是join这行代码在执行的那个线程上的线程。
-
//1.获取线程名称public class Demo { public static void main(String[] args) { // 获取main线程名 Thread thread = Thread.currentThread(); System.out.println(thread.getName()); // 创建对象 MyThread t1 = new MyThread(); //Thread(String name) 利用构造函数给线程设置名字 // 分配新的 Thread 对象。 // 给线程设置名字 t1.setName("吴彦祖"); // 通过getName方法得到的默认线程名 Thread-0 String name = t1.getName(); MyThread t2 = new MyThread("彭于晏"); String name1 = t2.getName(); System.out.println(name1); //System.out.println(name1); //System.out.println(name); //启动线程 t1.start(); //t2.start(); }}// 继承Threadclass MyThread extends Thread { public MyThread(String name) { super(name); } public MyThread() { } @Override public void run() { // 在run方法中也能获取到线程名 System.out.println(Thread.currentThread().getName() + ":hello thread"); }}//2.守护线程public class ThreadDaemonDemo { public static void main(String[] args) throws InterruptedException { // 创建对象 ThreadDaemon threadDaemon = new ThreadDaemon(); threadDaemon.setName("PDD"); // 标记成守护线程 threadDaemon.setDaemon(true); // 启动线程 threadDaemon.start(); // 休眠3s Thread.sleep(3000); //System.out.println("123"); }}class ThreadDaemon extends Thread { // 重写run方法 @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(getName() + "-----" + i); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }}//3.线程合并public class ThreadJoinDemo { public static void main(String[] args) throws InterruptedException { System.out.println("main start"); // 创建对象 ThreadJoin threadJoin = new ThreadJoin(); threadJoin.setName("正方形打野"); // 开启线程 threadJoin.start(); threadJoin.join(); System.out.println("main end"); }}class ThreadJoin extends Thread { // 重写run方法 @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(getName() + "------" + i); } }}//4.线程休眠public class ThreadSleepDemo { public static void main(String[] args) { // 创建子类对象 ThreadSleep t = new ThreadSleep(); // 启动线程 t.start(); }}class ThreadSleep extends Thread { @Override public void run() { System.out.println("sleep before"); try { // 当休眠时间到,就会自己醒来。 Thread.sleep(2000); // TimeUnit.MILLISECONDS.sleep() 可以按毫秒,秒,分钟,小时,天 // TimeUnit.SECONDS.sleep(2); // TimeUnit.HOURS.sleep(1); // TimeUnit.DAYS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("sleep after"); }}//5.线程终止public class ThreadStop { public static void main(String[] args) throws InterruptedException { // 创建对象 ThreadInterrupt threadInterrupt = new ThreadInterrupt(); // 启动线程 threadInterrupt.start(); Thread.sleep(3000); // 泼了一盆冷水,叫醒 // 实际上是根据java当中的异常处理机制 //threadInterrupt.interrupt(); threadInterrupt.stop(); }}class ThreadInterrupt extends Thread { // 重写run方法 @Override public void run() { System.out.println("run start"); try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } // stop方法具有固有的不安全性 // 这里就不会再执行了 System.out.println("run end"); }}//6.线程礼让public class ThreadYieldDemo { public static void main(String[] args) { // 创建对象 ThreadYield threadYield = new ThreadYield(); ThreadYield threadYield2 = new ThreadYield(); threadYield.setName("55开"); threadYield2.setName("卢本伟"); // 启动2个线程 threadYield.start(); threadYield2.start(); }}class ThreadYield extends Thread { // 重写run方法 @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(getName()+"------"+i); yield(); // 暂停当前正在执行的线程对象, // 并执行其他线程。 // 虽然放弃了CPU的执行权,但是我们java当中采用的是抢占式的调度方式 // 所以下一次不一定是谁抢到 } }}
10.3.实现多线程
- 创建线程的方式总共有两种,一种是继承Thread类方式,一种是实现Runnable接口方式
10.3.1.方法一
- Java使用 java.lang.Thread 类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。Java中通过继承Thread类来创建并启动多线程的步骤如下:
- 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
- 创建Thread子类的实例,即创建了线程对象
- 调用线程对象的start()方法来启动该线程
public class MyThread extends Thread{ /* * 利用继承中的特点 * 将线程名称传递 进行设置 */ public MyThread(String name){ super(name); } /* * 重写run方法 * 定义线程要执行的代码 */ public void run(){ for (int i = 0; i < 20; i++) { //getName()方法 来自父亲 System.out.println(getName()+i); } }}public class Demo { public static void main(String[] args) { System.out.println("这里是main线程"); MyThread mt = new MyThread("小强"); mt.start();//开启了一个新的线程 for (int i = 0; i < 20; i++) { System.out.println("旺财:"+i); } }}//对于这种方式实现的多线程,如果存在变量不一致的问题,可以采用static关键字来解决。
- 注意事项:
- 一个Thread子类对象代表一个线程
- 只有Thread run()方法中的代码,才会执行在子线程中,保证运行其中的是我们想要在子线程中运行的代码(run方法封装的是被线程执行的任务)
- 一个方法,被哪个线程中的代码调用,被调用的方法,就运行在,调用它的线程中
- 启动线程,必须使用start()方法来启动,才能是Thread中的run方法运行在子线程中。调用run方法执行Thread的run方法代码,这仅仅只是普通的方法调用
- 同一个Thread或Thread子类对象(代表同一个线程),只能被启动一次,如果,我们要启动多个线程,只能创建多个线程对象,并启动这些线程对象
10.3.2.方式二
- 实现 java.lang.Runnable 也是非常常见的一种,只需要重写run方法即可。
- 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动线程。
public class MyRunnable implements Runnable{ @Override public void run() { for (int i = 0; i < 20; i++) { System.out.println(Thread.currentThread().getName()+" "+i); } }}public class Demo { public static void main(String[] args) { //创建自定义类对象 线程任务对象 MyRunnable mr = new MyRunnable(); //创建线程对象 Thread t = new Thread(mr, "小强"); t.start(); for (int i = 0; i < 20; i++) { System.out.println("旺财 " + i); } }}
- Thread类实际上也是实现了Runnable接口的类。
- 在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。
- 实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的。
10.3.4.匿名内部类创建线程
public class NoNameInnerClassThread { public static void main(String[] args) { //new Runnable(){ // public void run(){ // for (int i = 0; i < 20; i++) { // System.out.println("张宇:"+i); // } // } //}; //‐‐‐这个整体 相当于new MyRunnable() Runnable r = new Runnable(){ public void run(){ for (int i = 0; i < 20; i++) { System.out.println("张宇:"+i); } } }; new Thread(r).start(); for (int i = 0; i < 20; i++) { System.out.println("费玉清:"+i); } }}
10.4.Thread和Runnable的区别
- 如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
- 实现Runnable接口比继承Thread类所具有的优势:
- 适合多个相同的程序代码的线程去共享同一个资源。
- 可以避免Java中的单继承的局限性。
- 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
- 线程池只能放入实现Runable或Callable的类的线程,不能直接放入继承Thread的类。
注:在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用Java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实在就是在操作系统中启动了一个进程
10.5.线程安全
10.5.1.线程同步
- 当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题
- 要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了线程同步机制(synchronized)来解决。
- 三种方法:
- 同步代码块。
- 同步方法。
- 锁机制。
10.5.2.同步代码块
- synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
synchronized(同步锁){ //需要同步操作的代码}public class Ticket implements Runnable{ private int ticket = 100; Object lock = new Object(); /* * 执行卖票操作 */ @Override public void run() { //每个窗口卖票的操作 //窗口 永远开启 while(true){ synchronized (lock) { if(ticket>0){//有票 可以卖 //出票操作 //使用sleep模拟一下出票时间 try { Thread.sleep(50); } catch (InterruptedException e) { // TODO Auto‐generated catch block e.printStackTrace(); } //获取当前线程对象的名字 String name = Thread.currentThread().getName(); System.out.println(name+"正在卖:"+ticket‐‐); } } } }}public class Demo { public static void main(String[] args) { //创建线程任务对象 Ticket ticket = new Ticket(); //创建三个窗口对象 Thread t1 = new Thread(ticket, "窗口1"); Thread t2 = new Thread(ticket, "窗口2"); Thread t3 = new Thread(ticket, "窗口3"); //同时卖票 t1.start(); t2.start(); t3.start(); }}
同步锁:
- 锁对象可以是任意的Java对象。
- 对于非static方法,同步锁对象就是this。
- 对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。
10.5.3.同步方法
- 同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
public synchronized void method(){ //可能会产生线程安全问题的代码}public class Ticket implements Runnable{ private int ticket = 100; /* * 执行卖票操作 */ @Override public void run() { //每个窗口卖票的操作 //窗口 永远开启 while(true){ sellTicket(); } } /* * 锁对象:是谁调用这个方法,就是谁 * 隐含锁对象就是 this */ public synchronized void sellTicket(){ if(ticket>0){//有票 可以卖 //出票操作 //使用sleep模拟一下出票时间 try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto‐generated catch block e.printStackTrace(); } //获取当前线程对象的名字 String name = Thread.currentThread().getName(); System.out.println(name+"正在卖:"+ticket‐‐); } }}
10.5.4.Lock锁
- java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
- Lock锁也称同步锁,加锁与释放锁方法化了:
- public void lock() :加同步锁。
- public void unlock() :释放同步锁。
public class Ticket implements Runnable{ private int ticket = 100; Lock lock = new ReentrantLock(); /* * 执行卖票操作 */ @Override public void run() { //每个窗口卖票的操作 //窗口 永远开启 while(true){ lock.lock(); if(ticket>0){//有票 可以卖 //出票操作 //使用sleep模拟一下出票时间 try { Thread.sleep(50); } catch (InterruptedException e) { // TODO Auto‐generated catch block e.printStackTrace(); } //获取当前线程对象的名字 String name = Thread.currentThread().getName(); System.out.println(name+"正在卖:"+ticket--); } lock.unlock(); } }}
10.6.线程状态
- java.lang.Thread.State 这个枚举中给出了六种线程状态
线程状态 | 导致状态发生条件 |
---|---|
New(新建) | 线程刚被创建,但是并未启动。还没调用start方法。 |
Runnable(可运行) | 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。 |
Blocked(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。 |
Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。 |
TimedWaiting(计时等待) | 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。 |
Teminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。 |
10.6.1.Timed Waiting计时等待
- 一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态
- 当我们调用了sleep方法之后,当前执行的线程就进入到“休眠状态”,其实就是所谓的Timed Waiting(计时等待)。
public class MyThread extends Thread { public void run() { for (int i = 0; i < 100; i++) { if ((i) % 10 == 0) { System.out.println("‐‐‐‐‐‐‐" + i); } System.out.print(i); try { Thread.sleep(1000); System.out.print(" 线程睡眠1秒!\n"); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { new MyThread().start(); } }
- 进入 TIMED_WAITING 状态的一种常见情形是调用的 sleep 方法,单独的线程也可以调用,不一定非要有协作关系。
- 为了让其他线程有机会执行,可以将Thread.sleep()的调用放线程run()之内。这样才能保证该线程执行过程中会睡眠
- sleep与锁无关,线程睡眠到期自动苏醒,并返回到Runnable(可运行)状态。
- sleep()中指定的时间是线程不会运行的最短时间。因此,sleep()方法不能保证该线程睡眠到期后就开始立刻执行。
10.6.2.BLOCKED(锁阻塞)
- 一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。线程A与线程B代码中使用同一锁,如果线程A获取到锁,线程A进入到Runnable状态,那么线程B就进入到Blocked锁阻塞状态。
10.6.3.Waiting(无限等待)
- 一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。
public class WaitingTest { public static Object obj = new Object(); public static void main(String[] args) { // 演示waiting new Thread(new Runnable() { @Override public void run() { while (true){ synchronized (obj){ try { System.out.println( Thread.currentThread().getName() +"=== 象,调用wait方法,进入waiting状态,释放锁对象"); obj.wait(); //无限等待 //obj.wait(5000); //计时等待, 5秒 时间到,自动醒来 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println( Thread.currentThread().getName() + "=== 从waiting状态醒来,获取到锁对象,继续执行了"); } } } },"等待线程").start(); new Thread(new Runnable() { @Override public void run() { // while (true){ //每隔3秒 唤醒一次 try { System.out.println( Thread.currentThread().getName() +"‐‐‐‐‐ 等待3秒钟"); Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (obj){ System.out.println( Thread.currentThread().getName() +"‐‐‐‐‐ 获取到锁对象,调用notify方法,释放锁对象"); obj.notify(); } } // } },"唤醒线程").start(); } }
- 通过上述案例我们会发现,一个调用了某个对象的 Object.wait 方法的线程会等待另一个线程调用此对象的Object.notify()方法 或 Object.notifyAll()方法。
- 其实waiting状态并不是一个线程的操作,它体现的是多个线程间的通信,可以理解为多个线程之间的协作关系,多个线程会争取锁,同时相互之间又存在协作关系。就好比在公司里你和你的同事们,你们可能存在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。
- 当多个线程协作时,比如A,B线程,如果A线程在Runnable(可运行)状态中调用了wait()方法那么A线程就进入了Waiting(无限等待)状态,同时失去了同步锁。假如这个时候B线程获取到了同步锁,在运行状态中调用了notify()方法,那么就会将无限等待的A线程唤醒。注意是唤醒,如果获取到锁对象,那么A线程唤醒后就进入Runnable(可运行)状态;如果没有获取锁对象,那么就进入到Blocked(锁阻塞状态)。
注:
我们在翻阅API的时候会发现Timed Waiting(计时等待) 与 Waiting(无限等待) 状态联系还是很紧密的,比如Waiting(无限等待) 状态中wait方法是空参的,而timed waiting(计时等待) 中wait方法是带参的。这种带参的方法,其实是一种倒计时操作,相当于我们生活中的小闹钟,我们设定好时间,到时通知,可是如果提前得到(唤醒)通知,那么设定好时间在通知也就显得多此一举了,那么这种设计方案其实是一举两得。如果没有得到(唤醒)通知,那么线程就处于Timed Waiting状态,直到倒计时完毕自动醒来;如果在倒计时期间得到(唤醒)通知,那么线程从Timed Waiting状态立刻唤醒。
10.7.死锁
- 死锁:线程一占用A资源,等待B资源,线程二占用B资源,等待A资源,这种互锁现象称为死锁
个人理解:死锁如果用通俗易懂的话来解释,其实就像我们生活中,和迎面而来的人相遇,给对面的人让路,对面的人也给我们让路,然而两个人让路的方向是同一边,这样就是死锁。
- 解决方法:
- 更改加锁顺序
- 新加一把锁,把这个操作变成原子操作
public class Demo { public static void main(String[] args) { DieLock dieLock1 = new DieLock(true); DieLock dieLock2 = new DieLock(false); new Thread(dieLock1).start(); new Thread(dieLock2).start(); }}// 描述锁对象class MyLock { public static final Object objA = new Object(); public static final Object objB = new Object(); public static final Object objAB = new Object();}class DieLock implements Runnable { boolean flag; public DieLock(boolean flag) { this.flag = flag; } @Override public void run() { if (flag) { synchronized (MyLock.objA) { System.out.println("if A"); // 发生了线程切换 synchronized (MyLock.objB) { System.out.println("if B"); } } } } else { synchronized (MyLock.objB) { System.out.println("else B"); // 打印了else B synchronized (MyLock.objA) { System.out.println("else A"); } } } //解决方法: //1.新加一把锁 if (flag) { synchronized (MyLock.objAB) { synchronized (MyLock.objA) { System.out.println("if A"); // 发生了线程切换 synchronized (MyLock.objB) { System.out.println("if B"); } } }else { synchronized (MyLock.objAB) { synchronized (MyLock.objB) { System.out.println("else B"); // 打印了else B synchronized (MyLock.objA) { System.out.println("else A"); } } } } //2.更改加锁顺序,就是把原来程序的其中一个分支调换顺序 }}
10.8.线程通信
- 以生产者,消费者,包子铺为例子,说明线程通信
public class Box { // 包子 Food food; // 生产包子 只有生产者线程调用setFood方法 public synchronized void setFood(Food newFood) throws InterruptedException { //先判断蒸笼是否为空 //如果蒸笼为空,没有包子 if (food == null) { //做包子并放入蒸笼, food = newFood; System.out.println(Thread.currentThread().getName() + "做了:" + food); Thread.sleep(3000); // 通知消费者来吃 notify(); //notifyAll(); } else { //如果蒸笼非空 //说明蒸笼里有包子, // 阻止自己,不在生产 try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } //System.out.println("生产者 wait 后面的代码"); } } // 吃包子 只有消费者线程调用eatFood方法 public synchronized void eatFood() throws InterruptedException { // 先判断蒸笼状态 if (food == null) { //如果蒸笼为空,没有包子 //阻止自己 try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } //System.out.println("消费者 wait 后面的代码"); } else { //如果蒸笼非空 有包子 //吃包子 System.out.println(Thread.currentThread().getName() + "吃了:" + food); Thread.sleep(3000); food = null; //通知生产者去再生产包子 notify(); //notifyAll(); } }}// 该类描述包子class Food { String name; int price; public Food(String name, int price) { this.name = name; this.price = price; } @Override public String toString() { return "Food{" + "name='" + name + '\'' + ", price=" + price + '}'; }}public class ConsumerTask implements Runnable { // 蒸笼 Box box; public ConsumerTask(Box box) { this.box = box; } @Override public void run() { while (true) { try { box.eatFood(); } catch (InterruptedException e) { e.printStackTrace(); } } }}public class ProducerTask implements Runnable { Box box; Food[] foods = {new Food("庆丰包子", 8), new Food("上海生煎包", 2), new Food("广式叉烧包", 10)}; Random random; public ProducerTask(Box box) { this.box = box; random = new Random(); } @Override public void run() { while (true) { // 只生产包子 int index = random.nextInt(foods.length); try { box.setFood(foods[index]); } catch (InterruptedException e) { e.printStackTrace(); } } }}public class Demo { public static void main(String[] args) throws InterruptedException { Box box = new Box(); ProducerTask producerTask = new ProducerTask(box); ConsumerTask consumerTask = new ConsumerTask(box); //创建2个线程并启动 // 生产者线程 Thread t1 = new Thread(producerTask); // 消费者线程 Thread t2 = new Thread(consumerTask); Thread t3 = new Thread(producerTask); // 消费者线程 Thread t4 = new Thread(consumerTask); t1.setName("生产者1"); t2.setName("消费者1"); t3.setName("生产者2"); t4.setName("消费者2"); t1.start(); t2.start(); t3.start(); t4.start(); }}
- 解释:
- 对于notify方法,两个生产者,两个消费者,当生产者生产完毕后,通知的却不一定是消费者,notify方法是随机的,如果另一个生产者抢到了执行权,进入if判断语句,发现已经有包子了,那么程序就会执行else语句,进入等待。
- 对于notifyAll方法,当生产者生产完毕后,唤醒所有线程,三个线程需要争夺执行权限,此时如果另一个生产者抢到了执行权,会进入阻塞状态,剩余两个消费者再次争夺,当某个消费者消费完成后,执行notifyAll方法,唤醒其余线程,进入阻塞状态的生产者会立即执行完wait后面的代码,并立即加入本轮争夺,也就是说,此次消费者消费完成后,依旧是有两个生产者争夺执行权。
- 某个线程想要执行,需要两个条件同时满足,第一,获取到锁对象,第二,抢到了cpu执行权,即其他线程调用了notify方法或notifyAll方法,本线程争夺到了cpu执行权。但是需要注意的是,两个条件必须同时满足,假设这样一种情形,某个程序只有两个线程,一个线程正在运行时,调用了notify方法或notifyAll方法,但是自身却没有执行wait方法释放锁对象,那么另一个线程就会进入阻塞状态,直到正在运行的线程释放了锁对象。
10.9.线程池
10.9.1.概念
- 一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
- 优点:
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
10.9.1.线程池的创建
- Java里面线程池的顶级接口是 java.util.concurrent.Executor ,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 java.util.concurrent.ExecutorService 。 java.util.concurrent.Executors 线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象
- 不过在java doc中,并不提倡我们直接使用ThreadPoolExecutor,而是使用Executors类中提供的几个静态方法来创建线程池:
Executors.newCachedThreadPool(); //创建一个缓冲池,缓冲池容量大小为Integer.MAX_VALUEExecutors.newSingleThreadExecutor(); //创建容量为1的缓冲池Executors.newFixedThreadPool(int n); //创建固定容量大小为n的缓冲池
- 一般需要根据任务的类型来配置线程池大小:
- 如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 N(CPU) + 1
- 如果是IO密集型任务,参考值可以设置为2*N(CPU)
- 不过在java doc中,并不提倡我们直接使用ThreadPoolExecutor,而是使用Executors类中提供的几个静态方法来创建线程池:
- 线程池的创建:JDK5提供了Executors来产生线程池,有如下方法
- public ExecutorService newCachedThreadPool()
- 会根据需要创建新线程,也可以自动删除,60s处于空闲状态的线程
- 线程数量可变,立马执行提交的异步任务(异步任务:在子线程中执行的任务)
- public ExecutorService newFixedThreadPool(int nThreads)
- 线程数量固定
- 维护一个无界队列(暂存已提交的来不及执行的任务)
- 按照任务的提交顺序,将任务执行完毕
- public ExecutorService newSingleThreadExecutor()
- 单个线程
- 维护了一个无界队列(暂存已提交的来不及执行的任务)
- 按照任务的提交顺序,将任务执行完毕
- public ExecutorService newCachedThreadPool()
10.9.2.线程池的使用
- 定义了一个使用线程池对象的方法如下:
- public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行
- 使用线程池中线程对象的步骤:
- 创建线程池对象。
- 创建Runnable接口子类对象。(task)
- 提交Runnable接口子类对象。(take task)
- 关闭线程池(一般不做)。
- ExecutorService(接口):Future submit(Callable task)
- 代表有返回值的异步任务
- Future对象代表异步任务的计算结果 可通过get方法获取
- public Future<?> submit(Runnable task):代表没有返回值的异步任务
- 关闭线程池:
- public shutdown():启动一次顺序关闭,执行以前提交的任务,但不接收新任务
- public shutdownNow():立刻关闭
public class ThreadPoolDemo { public static void main(String[] args) { // 创建线程池对象 ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象 // 创建Runnable实例对象 MyRunnable r = new MyRunnable(); //自己创建线程对象的方式 // Thread t = new Thread(r); // t.start(); ‐‐‐> 调用MyRunnable中的run() // 从线程池中获取线程对象,然后调用MyRunnable中的run() service.submit(r); // 再获取个线程对象,调用MyRunnable中的run() service.submit(r); //线程池只有两个线程对象,此任务会进入队列,等待下一轮执行 service.submit(r); // 注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。 // 将使用完的线程又归还到了线程池中 // 关闭线程池,执行完全部任务后关闭 //service.shutdown(); //执行两个任务就会关闭 //service.shutdownNow(); }}class MyRunnable implements Runnable { @Override public void run() { System.out.println("我要一个教练"); try { //这里的sleep函数可以清楚地看到执行顺序 Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("教练来了: " + Thread.currentThread().getName()); System.out.println("教我游泳,教完后,教练回到了游泳池"); }}
10.10.定时器
- 定时器是一个应用十分广泛的线程工具,可用于调度多个定时任务以后台线程的方式来执行。
- 定时器用来管理时间
- Timer触发执行的任务就是定时任务,即TimerTask
- 用来完成调度定时任务的功能
//创建定时器Timer timer = new Timer()//调度定时任务//在指定的时间点,调度定时任务执行schedule(TimerTask task, Date time)//在delay毫秒的延时之后,首次调度task执行,之后每period毫秒执行一次定时任务schedule(TimerTask task, long delay, long period)//在firstTime时间点,首次执行,之后每period毫秒执行一次schedule(TimerTask task, Date firstTime, long period)//在delay毫秒的延时后,首次调度task执行,之后每period毫秒执行一次scheduleAtFixedRate(TimerTask task, long delay, long period) SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");Date firstTime = simpleDateFormat.parse("2021-01-23 07:38:59");
- TimerTask(抽象类):表示待执行的定时任务
- 定义一个定时任务
- 继承TimerTask
- 重写run方法
- 终止定时器
- timer 当中的的cancle方法:定时器中所有任务都会被终止
public class Demo { public static void main(String[] args) throws ParseException { // /在指定的时间点,调度定时任务执行 //schedule(TimerTask task, Date time) 在delay毫秒的延时之后,首次调度task执行,之后每period毫秒执行一次定时任务 //schedule(TimerTask task, long delay, long period) // 创建定时器 Timer timer = new Timer(); // 进行任务调度 String s = "2021-01-23 11:43:40"; // SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //Date firstTime = simpleDateFormat.parse("2021-01-23 07:38:59"); SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date date = simpleDateFormat.parse(s); //timer.schedule(new MyTask(),date); timer.schedule(new MyTask(),3000,5000); timer.cancel(); }}/***如何定义一个定时任务**- 继承TimerTask- 重写run方法* */class MyTask extends TimerTask { @Override public void run() { // 定时任务要做的事情写到run方法里 System.out.println("boom! 爆炸了!"); }}
10.11.Callable
public class Demo { public static void main(String[] args) throws ExecutionException, InterruptedException { // 创建线程池 ExecutorService pool = Executors.newFixedThreadPool(2); // 提交任务 Future future = pool.submit(new MyCallable()); //get() 获取计算结果 //如有必要,等待计算完成,然后获取其结果。 System.out.println("get before"); String s = (String) future.get(); System.out.println(s); }}/*可以理解为多线程的实现方式3 ,但是必须要依赖线程池*/class MyCallable implements Callable { @Override public Object call() throws Exception { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + "------" +i); } Thread.sleep(3000); return "hello"; }}
- Runnable和Callable的区别:
- Callable规定的被重写的方法是call(),Runnable规定的被重写的方法是run()
- Callable的任务执行后可以有返回值,而Runnable的任务没有返回值
- call方法可以抛出异常,run方法不可以
- 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
10.12.volatile
- volatile 是一个类型修饰符。volatile 的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略
- 特性:
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)
- 禁止进行指令重排序。(实现有序性)
- volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。
关于volatile原子性可以理解为把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步
- volatile写:当写一个volatile变量时,JMM会把该线程对应的本地中的共享变量值刷新到主内存。
- volatile读:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
- volatile指令重排:volatile 变量的内存可见性是基于内存屏障(Memory Barrier)实现。总结来说就是JMM内部会有指令重排,并且会有af-if-serial跟happen-before的理念来保证指令的正确性。内存屏障就是基于4个汇编级别的关键字来禁止指令重排的,其中volatile的规则如下:
第一个操作/第二个操作 普通读写 volatile读 volatile写 普通读写 不允许 volatile读 不允许 不允许 不允许 volatile写 不允许 不允许
这篇关于java中关于线程的概念的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-26Mybatis官方生成器资料详解与应用教程
- 2024-11-26Mybatis一级缓存资料详解与实战教程
- 2024-11-26Mybatis一级缓存资料详解:新手快速入门
- 2024-11-26SpringBoot3+JDK17搭建后端资料详尽教程
- 2024-11-26Springboot单体架构搭建资料:新手入门教程
- 2024-11-26Springboot单体架构搭建资料详解与实战教程
- 2024-11-26Springboot框架资料:新手入门教程
- 2024-11-26Springboot企业级开发资料入门教程
- 2024-11-26SpringBoot企业级开发资料详解与实战教程
- 2024-11-26Springboot微服务资料:新手入门全攻略