Java (三)多线程
1 线程概述
1.1 进程
在操作系统中,每个独立执行的程序都可称为一个进程,也就是 “正在运行的程序”。
实际上,进程不是同时运行的,对于一个 CPU 而言,某个时间段只能运行一个程序,也就是只能执行一个进程。操作系统会为每个进程分配一段有限的 CPU 使用时间,CPU 在这段时间内执行某个进程,然后会在下一段时间切换到另一个进程中去执行。
1.2 线程
在一个进程中还可以有多个执行单元同时运行,这些执行单元被称为线程。
操作系统中至少存在一个线程。
多线程成语运行时,每个线程之间都是独立的,他们可以并发执行。和进程一样,也是由CPU轮流执行的。
1.3 进程和线程的区别
每个进程都有独立的代码和数据空间。线程可以看成是轻量级的进程,属于同一进程的线程共享代码和数据空间。
最根本区别:进程是资源分配的单位,线程是调度和执行的单位。
多进程:在操作系统中能同时运行多个任务(程序)。
多线程:在同一应用程序中有多个顺序流同时进行。
2 线程的创建
Java 提供了两种多线程实现方式,一种是继承 java.lang 包下的 Thread 类;另一种是实现 java.lang.Runnable 接口。
2.1 继承 Thread 类创建多线程
JDK 中提供了一个线程类 Thread,通过继承 Thread 类,并重写 Thread 类中的 run() 方法便可实现多线程。
在 Thread 类中,提供了一个 start() 方法用于启动新线程,线程启动后,系统会自动调用 run() 方法。
【例1-1】实现一个简单的多线程
public class Example01 { public static void main(String[] args) { MyThread mt = new MyThread(); mt.start(); while (true) { System.out.println("main()方法正在运行"); } } } class MyThread extends Thread { public void run() { while (true) { System.out.println("Mythread()的run()方法正在运行。"); } } }
运行结果如下:
从图中的运行结果可以看出,两个 while 循环中的打印语句轮流执行。说明该实例实现了多线程。
2.2 实现 Runnable 接口创建多线程
Thread 有个缺陷:Java 中只支持单继承,一个类如果继承了某个父类就不能再继承 Thread 类了。为了克服弊端,Thread 类提供了另一个构造方法 Thread(Runnable target),该方法中,Runnable 是一个接口,它只有一个 run() 方法。当应用时,只需要为该方法提供一个实现了 Runnable 接口的实例对象,这样创建的线程将调用实现了 Runnable 接口中的 run() 方法作为运行代码。
【例2-1】Runnable 接口实现多线程
public class Example02 { public static void main(String[] args) { MyThread2 mt = new MyThread2(); Thread thread = new Thread(mt); thread.start(); while (true) { System.out.println("main()方法正在运行。"); } } } class MyThread2 implements Runnable { @Override public void run() { //当调用start()方法时,线程从此处开始执行 while (true) { System.out.println("MyThread类的run()方法正在运行。"); } } }
运行结果如下:
MyThread 类实现了 Runnable 接口,并重写了 Runnable 接口中的run() 方法,通过 Thread 类的构造方法将 MyThread 类的实例对象作为参数传入。由运行结果可以看出,实现了多线程。
2.3 两种方法对比
实现 Runnable 接口相对于继承 Thread 类来说,有如下好处:
适合多个相同程序代码的线程处理同一个资源的情况。
避免 Java 单继承带来的局限性。
【例2-2】使用 Runnable 实现四个售票窗口同时售票
public class Example03 { public static void main(String[] args) { TicketWindow tw = new TicketWindow(); new Thread(tw, "窗口1").start(); new Thread(tw, "窗口2").start(); new Thread(tw, "窗口3").start(); new Thread(tw, "窗口4").start(); } } class TicketWindow implements Runnable { private int tickets = 100; @Override public void run() { while (true) { if (tickets >= 0) { Thread th =Thread.currentThread(); String th_name = th.getName(); System.out.println(th_name + ":正在发售第 " + tickets-- + " 张票"); } } } }
运行结果如下:
示例2-2中只创建了一个 TicketWindow 对象,然后创建了四个线程,在每个线程上都去调用这个 TicketWindow 对象中的 run() 方法,这样就可以确保四个线程访问的是同一个 tickets 变量,共享100张票。
2.4 后台线程
新创建的线程默认都是前台线程,如果某个线程对象在启动之前调用了 setDaemon(true) 语句,这个线程就变成了一个后台线程。
如果一个进程中只有后台线程运行,这个进程就会结束。
3 线程的生命周期以及状态转换
3.1 生命周期图
(两张图,结合看效果更佳)
3.2 线程状态
1、新建状态(new)
使用new关键字和Thread类或其子类建立一个线程对象后,该对象就处于新建状态。此时对象不能运行,仅仅由Java虚拟机为其分配了内存。
2、就绪状态(Runnable)
线程对象调用了start()方法之后,线程就进入了就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
3、运行状态(Running)
当处于就绪状态的线程获得CPU使用权时,该线程就开始执行run()方法。当使用完系统分配的时间后,系统就会剥夺该线程占用的CPU资源,让其他线程获得执行的机会。
4、阻塞状态(Blocked)
一个正在执行的线程在某些特殊情况下(如执行耗时的输入/输出操作时)会放弃CPU的使用权,进入阻塞状态。
- 线程试图获取对象的同步锁时,如果该锁被其他线程持有,则当前线程会进入阻塞状态。
- 当线程调用一个阻塞式的IO方法时,该线程会进入阻塞状态。
- 当线程调用了某个对象的wait()方法时,会进入阻塞状态。需要使用notify()方法唤醒该线程。
- 当线程调用了sleep()方法时,会进入阻塞状态
- 当一个线程中调用了另一个线程的join()方法时,会进入阻塞状态。需要等另一个线程结束后,该线程才会结束阻塞状态。
5、死亡状态(Terminated)
run()方法执行完,或者抛出一个异常(Exception)或错误(Error),线程就进入死亡状态。(死了也就死了,别想着复活啥的)
3.3 终止线程的典型方式(重要)
终止线程我们一般不使用JDK提供的stop()/destroy()方法(它们本身也被JDK废弃了)。通常的做法是提供一个boolean型的终止变量,当这个变量置为false,则终止线程的运行。
【示例3-1】终止线程
public class TestTermination implements Runnable{ private String name; private boolean live = true;// 标记变量,表示线程是否可中止; private TestTermination(String name) { super(); this.name = name; } public void run() { int i = 0; //当live的值是true时,继续线程体;false则结束循环,继而终止线程体; while (live) { System.out.println(name + (i++)); } } public void terminate() { //终止方法 live = false; } public static void main(String[] args) { TestTermination tt = new TestTermination("线程A:"); Thread t1 = new Thread(tt);// 新生状态 t1.start();// 就绪状态,此时会执行run()方法 for (int i = 0; i < 15; i++) { System.out.println("主线程" + i); } tt.terminate(); System.out.println("tt 终止!"); } }
输出结果如下:
这里发现一个有意思的事情,当 i 的值很小的时候,多运行几次会出现一种情况:for循环在一个CPU分配的时间片内已经运行结束,并执行terminate()方法终止了进程,run()方法都没来得及运行。
4 线程调度
4.1 线程的优先级
在应用程序中,最直接的线程调度方式就是设置线程的优先级。优先级越高,线程获得CPU执行的机会越大。线程的优先级用1~10之间的整数来表示,数字越大优先级越高,默认为5。
Thread类中还提供了三个静态常量表示线程优先级,如下表所示:
Thread类的静态常量 |
功能描述 |
static int MAX_PRIORITY | 表示线程的最高优先级,相当于10 |
static int MIN_PRIORITY | 表示线程的最低优先级,相当于1 |
static int NORM_PRIORITY | 表示线程的缺省优先级,相当于5 |
注意:
- 处于就绪状态的线程,会进入“就绪队列”等待JVM来挑选
- 使用下列方式获得或设置线程对象的优先级。
- int getPriority();
- void setPriority(int newPriority);
- 优先级低知识意味着获取调度的可能性低。并不意味着:优先级高的一定比优先级低的先调用。
关键代码如下:
t1.setPriority(1);
t2.setPriority(10);
4.2 暂停线程
暂停线程执行常用的方法有sleep()和yield()方法,这两个方法的区别是:
1. sleep()方法:线程休眠,可以让正在运行的线程进入阻塞状态,直到休眠时间满了,进入就绪状态。
2. yield()方法:线程让步,可以让正在运行的线程直接进入就绪状态,让出CPU的使用权。
【示例4-1】sleep()方法
public class TestThreadState { public static void main(String[] args) { StateThread thread1 = new StateThread(); thread1.start(); StateThread thread2 = new StateThread(); thread2.start(); } } //使用继承方式实现多线程 class StateThread extends Thread { public void run() { for (int i = 0; i < 100; i++) { System.out.println(this.getName() + ":" + i); try { Thread.sleep(2000);//调用线程的sleep()方法; } catch (InterruptedException e) { e.printStackTrace(); } } } }
这里的输出结果就不展示了,因为实际效果只有运行时可以看到。其效果大致是每隔两秒打印一次语句。
【示例4-2】yield()方法
public class TestThreadState { public static void main(String[] args) { StateThread thread1 = new StateThread(); thread1.start(); StateThread thread2 = new StateThread(); thread2.start(); } } //使用继承方式实现多线程 class StateThread extends Thread { public void run() { for (int i = 0; i < 100; i++) { System.out.println(this.getName() + ":" + i); Thread.yield();//调用线程的yield()方法; } } }
运行结果如图所示:
从运行效果来看,该方法并没有明显的延迟,很快就运行完成。该方法可以引起线程切换。
4.3 线程的联合join()
也叫线程插队,线程A在运行期间,可以调用线程B的join()方法,让线程B和线程A联合。这样,线程A就必须等待线程B执行完毕后,才能继续执行。如下面示例中,“爸爸线程”要抽烟,于是联合了“儿子线程”去买烟,必须等待“儿子线程”买烟完毕,“爸爸线程”才能继续抽烟。
【示例4-3】join()方法
public class TestThreadState { public static void main(String[] args) { System.out.println("爸爸和儿子买烟故事"); Thread father = new Thread(new FatherThread()); father.start(); } } class FatherThread implements Runnable { public void run() { System.out.println("爸爸想抽烟,发现烟抽完了"); System.out.println("爸爸让儿子去买包红塔山大经典"); Thread son = new Thread(new SonThread()); son.start(); System.out.println("爸爸等儿子买烟回来"); try { son.join(); } catch (InterruptedException e) { e.printStackTrace(); System.out.println("爸爸出门去找儿子跑哪去了"); // 结束JVM。如果是0则表示正常结束;如果是非0则表示非正常结束 System.exit(1); } System.out.println("爸爸高兴的接过烟开始抽,并把零钱给了儿子"); } } class SonThread implements Runnable { public void run() { System.out.println("儿子出门去买烟"); System.out.println("儿子买烟需要10分钟"); try { for (int i = 1; i <= 10; i++) { System.out.println("第" + i + "分钟"); Thread.sleep(1000); } } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("儿子买烟回来了"); } }
运行结果如下:
这里忽然就想到了继承里面的初始化方式。这里就额外的提几句。
假设:两个类A和B,B继承A,AB中均有静态方法和静态成员变量,那么初始化过程是这样的:
// 关键代码如下 Runnable r = new MyThread(); Thread t = new Thread(r,"Test"); Thread.currentThread(); t.isAlive(); t.getPriority(); t.setPriority(); t.setName(); t.getName();
4.4 多线程同步
处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。 这时候,我们就需要用到“线程同步”。
线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用。比如说前面提到的售票服务就是多个线程共享一个对象。
由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突的问题。Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问造成的这种问题。Java中使用synchronized关键字来修饰共享资源代码块。
A)synchronized 方法
通过在方法声明中加入 synchronized关键字来声明,语法如下:
public synchronized void accessVal(int newVal);
synchronized 方法控制对“对象的类成员变量”的访问:每个对象对应一把锁,每个 synchronized 方法都必须获得调用该方法的对象的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。
B)synchronized 块
块可以让我们精确地控制到具体的“成员变量”,缩小同步的范围,提高效率。语法格式如下:
synchronized (lock) { //操作共享资源代码块 }
【示例4-4】
public class TestSync { public static void main(String[] args) { Account a1 = new Account(100, "高"); Drawing draw1 = new Drawing(80, a1); Drawing draw2 = new Drawing(80, a1); draw1.start(); // 你取钱 draw2.start(); // 你老婆取钱 } } /* * 简单表示银行账户 */ class Account { int money; String aname; public Account(int money, String aname) { super(); this.money = money; this.aname = aname; } } /** * 模拟提款操作 * * @author Administrator * */ class Drawing extends Thread { int drawingNum; // 取多少钱 Account account; // 要取钱的账户 int expenseTotal; // 总共取的钱数 public Drawing(int drawingNum, Account account) { super(); this.drawingNum = drawingNum; this.account = account; } @Override public void run() { draw(); } void draw() { synchronized (account) { if (account.money - drawingNum < 0) { System.out.println(this.getName() + "取款,余额不足!"); return; } try { Thread.sleep(1000); // 判断完后阻塞。其他线程开始运行。 } catch (InterruptedException e) { e.printStackTrace(); } account.money -= drawingNum; expenseTotal += drawingNum; } System.out.println(this.getName() + "--账户余额:" + account.money); System.out.println(this.getName() + "--总共取了:" + expenseTotal); } }
运行效果如下图所示:
解释:
“synchronized (account)” 意味着线程需要获得account对象的“锁”才有资格运行同步块中的代码。 Account对象的“锁”也称为“互斥锁”,在同一时刻只能被一个线程使用。A线程拥有锁,则可以调用“同步块”中的代码;B线程没有锁,则进入account对象的“锁池队列”等待,直到A线程使用完毕释放了account对象的锁,B线程得到锁才可以开始调用“同步块”中的代码。
4.5 死锁问题
有这样一个场景:小白和小蓝都要“化妆”,“化妆”需要“镜子”和“口红”。在“化妆”过程中,小白拿了“镜子”,小蓝拿了“口红”。
小白说:你先给我口红,让我画完,我再给你。
小蓝说:你先给我镜子,让我画完,我再给你。
两人据理力争,寸步不让,啊~~~ 天长地久,海枯石烂……
两个线程在运行时都在等待对方的锁,这样便造成了程序的停滞,这种现象叫做死锁。
【示例4-5】死锁问题
package com.zzfan.runnable; class Lipstick {//口红类 } class Mirror {//镜子类 } class Makeup extends Thread {//化妆类继承了Thread类 int flag; String girl; static Lipstick lipstick = new Lipstick(); static Mirror mirror = new Mirror(); @Override public void run() { // TODO Auto-generated method stub doMakeup(); } void doMakeup() { if (flag == 0) { synchronized (lipstick) {//需要得到口红的“锁”; System.out.println(girl + "拿着口红!"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (mirror) {//需要得到镜子的“锁”; System.out.println(girl + "拿着镜子!"); } } } else { synchronized (mirror) { System.out.println(girl + "拿着镜子!"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lipstick) { System.out.println(girl + "拿着口红!"); } } } } } public class TestDeadLock { public static void main(String[] args) { Makeup m1 = new Makeup();//小白的化妆线程; m1.girl = "小白"; m1.flag = 0; Makeup m2 = new Makeup();//小蓝的化妆线程; m2.girl = "小蓝"; m2.flag = 1; m1.start(); m2.start(); } }
运行结果如下图所示:
从运行状态可以看出,两个线程都在等对方的资源,都处于停滞状态。
死锁的解决方法
死锁是由于“同步块需要同时持有多个对象锁造成”的,要解决这个问题,思路很简单,就是:同一个代码块,不要同时持有两个对象锁。 如上面的死锁案例,修改成示例10-11所示。
【示例4-6】死锁问题的解决
package com.zzfan.runnable; class Lipstick {//口红类 } class Mirror {//镜子类 } class Makeup extends Thread {//化妆类继承了Thread类 int flag; String girl; static Lipstick lipstick = new Lipstick(); static Mirror mirror = new Mirror(); @Override public void run() { // TODO Auto-generated method stub doMakeup(); } void doMakeup() { if (flag == 0) { synchronized (lipstick) { System.out.println(girl + "拿着口红!"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } synchronized (mirror) { System.out.println(girl + "拿着镜子!"); } } else { synchronized (mirror) { System.out.println(girl + "拿着镜子!"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } synchronized (lipstick) { System.out.println(girl + "拿着口红!"); } } } } public class TestDeadLock { public static void main(String[] args) { Makeup m1 = new Makeup();// 小白的化妆线程; m1.girl = "小白"; m1.flag = 0; Makeup m2 = new Makeup();// 小蓝的化妆线程; m2.girl = "小蓝"; m2.flag = 1; m1.start(); m2.start(); } }
运行结果如下图所示:
程序正常结束。perfect!
5 线程并发
(转载自https://www.sxt.cn/Java_jQuery_in_action/eleven-threadconcurrent-collaboration.html)
多线程环境下,我们经常需要多个线程的并发和协作。这个时候,就需要了解一个重要的多线程并发协作模型“生产者/消费者模式”。
Ø 什么是生产者?
生产者指的是负责生产数据的模块(这里模块可能是:方法、对象、线程、进程)。
Ø 什么是消费者?
消费者指的是负责处理数据的模块(这里模块可能是:方法、对象、线程、进程)。
Ø 什么是缓冲区?
消费者不能直接使用生产者的数据,它们之间有个“缓冲区”。生产者将生产好的数据放入“缓冲区”,消费者从“缓冲区”拿要处理的数据。
缓冲区是实现并发的核心,缓冲区的设置有3个好处:
Ø 实现线程的并发协作
有了缓冲区以后,生产者线程只需要往缓冲区里面放置数据,而不需要管消费者消费的情况;同样,消费者只需要从缓冲区拿数据处理即可,也不需要管生产者生产的情况。 这样,就从逻辑上实现了“生产者线程”和“消费者线程”的分离。
Ø 解耦了生产者和消费者
生产者不需要和消费者直接打交道。
Ø 解决忙闲不均,提高效率
生产者生产数据慢时,缓冲区仍有数据,不影响消费者消费;消费者处理数据慢时,生产者仍然可以继续往缓冲区里面放置数据 。
【示例5-1】生产者与消费者模式
public class TestProduce { public static void main(String[] args) { SyncStack sStack = new SyncStack();// 定义缓冲区对象; Shengchan sc = new Shengchan(sStack);// 定义生产线程; Xiaofei xf = new Xiaofei(sStack);// 定义消费线程; sc.start(); xf.start(); } } class Mantou {// 馒头 int id; Mantou(int id) { this.id = id; } } class SyncStack {// 缓冲区(相当于:馒头筐) int index = 0; Mantou[] ms = new Mantou[10]; public synchronized void push(Mantou m) { while (index == ms.length) {//说明馒头筐满了 try { //wait后,线程会将持有的锁释放,进入阻塞状态; //这样其它需要锁的线程就可以获得锁; this.wait(); //这里的含义是执行此方法的线程暂停,进入阻塞状态, //等消费者消费了馒头后再生产。 } catch (InterruptedException e) { e.printStackTrace(); } } // 唤醒在当前对象等待池中等待的第一个线程。 //notifyAll叫醒所有在当前对象等待池中等待的所有线程。 this.notify(); // 如果不唤醒的话。以后这两个线程都会进入等待线程,没有人唤醒。 ms[index] = m; index++; } public synchronized Mantou pop() { while (index == 0) {//如果馒头筐是空的; try { //如果馒头筐是空的,就暂停此消费线程(因为没什么可消费的嘛)。 this.wait(); //等生产线程生产完再来消费; } catch (InterruptedException e) { e.printStackTrace(); } } this.notify(); index--; return ms[index]; } } class Shengchan extends Thread {// 生产者线程 SyncStack ss = null; public Shengchan(SyncStack ss) { this.ss = ss; } @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("生产馒头:" + i); Mantou m = new Mantou(i); ss.push(m); } } } class Xiaofei extends Thread {// 消费者线程; SyncStack ss = null; public Xiaofei(SyncStack ss) { this.ss = ss; } @Override public void run() { for (int i = 0; i < 10; i++) { Mantou m = ss.pop(); System.out.println("消费馒头:" + i); } } }
(这个代码没跑过,直接copy过来的,不过应该没问题)
运行结果如下图所示:
线程并发协作总结:
线程并发协作(也叫线程通信),通常用于生产者/消费者模式,情景如下:
1. 生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。
2. 对于生产者,没有生产产品之前,消费者要进入等待状态。而生产了产品之后,又需要马上通知消费者消费。
3. 对于消费者,在消费之后,要通知生产者已经消费结束,需要继续生产新产品以供消费。
4. 在生产者消费者问题中,仅有synchronized是不够的。
· synchronized可阻止并发更新同一个共享资源,实现了同步;
· synchronized不能用来实现不同线程之间的消息传递(通信)。
5. 那线程是通过哪些方法来进行消息传递(通信)的呢?见如下总结:
6. 以上方法均是java.lang.Object类的方法;
都只能在同步方法或者同步代码块中使用,否则会抛出异常。
老鸟建议
在实际开发中,尤其是“架构设计”中,会大量使用这个模式。 对于初学者了解即可,如果晋升到中高级开发人员,这就是必须掌握的内容。
(鉴于这位老鸟的建议,作为初学者的我就大致看了看然后直接转载过来了,没自己写……)
总结
这里面的很多例子,都是用的参考资料里面的例子,基本能看得懂,然后稍稍改了改。
主要是想做个笔记,以后看的时候不用各种翻资料,有这一篇笔记就OK啦。
参考资料
《Java基础入门》
https://www.sxt.cn/Java_jQuery_in_action/eleven-basicconcept.html
https://www.w3cschool.cn/java/java-multithreading.html
(待续)