网站首页 > 精选教程 正文
Java 线程:线程的交互
线程交互的基础知识
首先我们从 Object 类中的三个方法来学习。
关于 等待/通知,要记住的关键点是:
- 必须从同步环境内调用 wait()、notify()、notifyAll()方法。线程不能调用对象上的等待或通知方法,除非它拥有那个对象的锁。
- wait()、notify()、notifyAll()都是 Object 的实例方法。与每个对象具有锁一样,每个对象可以有一个线程列表,他们等待来自该信号。线程通过执行对象上的 wait 方法获得这个等待列表。从那时候起,它不再执行任何其他指令,直到调用对象的 notify 方法为止。如果多个线程在同一个对象上等待,则将只选择一个线程(不保证顺序)继续执行。如果没有线程等待,则不采取任何特殊操作。
敲黑板!!!上面这段话是重点。会用 wait、notify 方法的童鞋先理解这段话,不会用 wait、notify 方法的童鞋请看懂下面的例子再结合例子理解。
public static void main(String[] args) {
Thread1 thread1 = new Thread1();
thread1.start();
synchronized (thread1.obj) {
try {
System.out.println("等待 thread1 完成计算。。。");
//线程等待
thread1.obj.wait();
// thread1.sleep(1000);//思考一下,如果把上面这行代码注掉,执行这行代码
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("thread1 对象计算的总和是:" + thread1.total);
}
}
public static class Thread1 extends Thread {
int total;
public final Object obj = new Object();
@Override
public void run() {
synchronized (obj) {
for (int i = 0; i < 101; i++) {
total += i;
}
//(完成计算了)唤醒在此对象监视器上等待的单个线程,在本例中线程 thread1 被唤醒
obj.notify();
System.out.println("计算结束:" + total);
}
}
}
}
以上代码的两个 synchronize 代码块的锁都用 Thread1 的实例对象也是可以的,这里为了方便大家理解必须要用同一个锁,才 new 了一个 Obj 对象。
注意:当在对象上调用 wait 方法时,执行该代码的线程立即放弃它在对象上的锁。然而调用 notify 时,并不意味着这时线程会放弃其锁。如果线程仍然在完成同步代码,则线程在同步代码结束之前不会放弃锁。因此,调用了 notify 并不意味着这时该锁变得可用。
上面的运行结果忘记粘贴出来了,童鞋们自行测试吧~
多个线程在等待一个对象锁时使用 notifyAll()
在多数情况下,最好通知等待某个对象的所有线程。如果这么做,可以在对象使用 notifyAll()让所有在此对象上等待的线程重新活跃。
public class ThreadMutual extends Thread{
int total;
public static void main(String[] args) {
ThreadMutual t = new ThreadMutual();
new Thread1(t).start();
new Thread1(t).start();
new Thread1(t).start();
new Thread1(t).start();
new Thread1(t).start();
new Thread1(t).start();
t.start();
}
@Override
public void run() {
synchronized (this) {
for (int i = 0; i < 11; i++) {
total += i;
}
//(完成计算了)唤醒在此对象监视器上等待的单个线程,在本例中线程A被唤醒
System.out.println("计算结束:" + total);
notifyAll();
}
}
public static class Thread1 extends Thread {
private final ThreadMutual lock;
public Thread1(ThreadMutual lock){
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "得到结果:"+lock.total);
}
}
}
}
计算结束:55
Thread-5得到结果:55
Thread-6得到结果:55
Thread-4得到结果:55
Thread-3得到结果:55
Thread-2得到结果:55
Thread-1得到结果:55
注意:上面的代码如果线程 t 如果第一个 start,则会发生很多意料之外的情况,比如说notifyAll 已经执行了,wait 的代码还没执行。然后, 就造成了某个线程一直处于等待状态。通常,解决上面问题的最佳方式是利用某种循环,该循环检查某个条件表达式,只有当正在等待的事情还没有发生的情况下,它才继续等待。
Java 线程:线程的调度与休眠
Java 线程的调度是 Java 多线程的核心,只有良好的调度,才能充分发挥系统的性能,提高程序的执行效率。
这里要明确一点,不管程序员怎么编写调度,只能最大限度的影响线程执行的次序,而不能做到精准控制。
线程休眠的目的是使线程让出 CPU 的最简单的做法之一,线程休眠时,会将 CPU资源交给其他线程,以便能轮换执行,当休眠一定时间后,线程会苏醒,进入准备状态等待执行。
线程休眠的方法是 Thread.sleep(),是个静态方法,那个线程调用了这个方法,就睡眠这个线程。
public class Test {
public static void main(String[] args) {
Thread t1 = new MyThread1();
Thread t2 = new Thread(new MyRunnable());
t1.start();
t2.start();
}
}
class MyThread1 extends Thread {
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("线程1第" + i + "次执行!");
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class MyRunnable implements Runnable {
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("线程2第" + i + "次执行!");
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果:
线程2第0次执行!
线程1第0次执行!
线程1第1次执行!
线程2第1次执行!
线程1第2次执行!
线程2第2次执行!
Java 线程:线程的调度-优先级
与线程休眠类似,线程的优先级仍然无法保证线程的执行次序。只不过,优先级高的线程获取 CPU 资源的概率较大,低优先级的并非没有机会执行。
线程的优先级用1-10之间的整数表示,数值越大优先级越高,默认为5.
public class Test {
public static void main(String[] args) {
Thread t1 = new MyThread1();
Thread t2 = new Thread(new MyRunnable());
t1.setPriority(10);
t2.setPriority(1);
t2.start();
t1.start();
}
}
class MyThread1 extends Thread {
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("线程1第" + i + "次执行!");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class MyRunnable implements Runnable {
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("线程2第" + i + "次执行!");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果:
线程2第0次执行!
线程1第0次执行!
线程1第1次执行!
线程2第1次执行!
线程1第2次执行!
线程2第2次执行!
线程1第3次执行!
线程2第3次执行!
线程1第4次执行!
线程2第4次执行!
线程1第5次执行!
线程2第5次执行!
线程1第6次执行!
线程2第6次执行!
线程1第7次执行!
线程2第7次执行!
线程1第8次执行!
线程2第8次执行!
线程1第9次执行!
线程2第9次执行!
我们可以看到,每隔50ms 打印一次,优先级高的线程1大概率先执行。
Java 线程:线程的调度-让步
线程的让步含义就是使当前运行着的线程让出 CPU 资源,但是给谁不知道,只是让出,线程回到可执行状态。
线程让步使用的是静态方法 Thread.yield(),用法和 sleep 一样,作用的是当前执行线程。
public class Test {
public static void main(String[] args) {
Thread t1 = new MyThread1();
Thread t2 = new Thread(new MyRunnable());
t2.start();
t1.start();
}
}
class MyThread1 extends Thread {
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("线程1第" + i + "次执行!");
}
}
}
class MyRunnable implements Runnable {
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("线程2第" + i + "次执行!");
Thread.yield();
}
}
}
运行结果:
线程1第0次执行!
线程2第0次执行!
线程1第1次执行!
线程2第1次执行!
线程1第2次执行!
线程1第3次执行!
线程1第4次执行!
线程1第5次执行!
线程1第6次执行!
线程1第7次执行!
线程1第8次执行!
线程1第9次执行!
线程2第2次执行!
线程2第3次执行!
线程2第4次执行!
线程2第5次执行!
线程2第6次执行!
线程2第7次执行!
线程2第8次执行!
线程2第9次执行!
Java 线程:线程的调度-合并
线程的合并的含义就是将几个并行线程的线程合并为一个单线程,应用场景是当一个线程必须等待另一个线程执行完毕才能执行,使用 join 方法。
public class Test {
public static void main(String[] args) {
Thread t1 = new MyThread1();
t1.start();
for (int i = 0; i < 10; i++) {
System.out.println("主线程第" + i + "次执行!");
if (i > 2) try {
//t1线程合并到主线程中,主线程停止执行过程,转而执行t1线程,直到t1执行完毕后继续。
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class MyThread1 extends Thread {
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("线程1第" + i + "次执行!");
}
}
}
运行结果:
主线程第0次执行!
主线程第1次执行!
主线程第2次执行!
主线程第3次执行!
线程1第0次执行!
线程1第1次执行!
线程1第2次执行!
主线程第4次执行!
主线程第5次执行!
主线程第6次执行!
主线程第7次执行!
主线程第8次执行!
主线程第9次执行!
不逼逼了,线程 join 只有第一次有效。这里我也很懵逼,我以为线程1第***这句话的打印次数应该是(10-3)*3 次的。这里我们来回顾一下上篇文章说的线程的基本知识,线程是死亡之后就不能重新启动了对吧。我们再来理解一下 join 的概念当一个线程必须等待另一个线程执行完毕才能执行,我们在主线程中join 线程 t1,所以直到 t1执行完毕,才能再次执行主线程。当 i=4 的时候再次执行 t1.join()时,t1 线程已经是处于死亡状态,所以不会再次执行 run 方法。因此 t1线程里面 run 方法的打印语句只执行了三次。为了验证我们的猜想,我建议去阅读以下源码。
以下是 Java8 Thread#join() 方法的源码。
public final void join() throws InterruptedException {
this.join(0L);
}
public final synchronized void join(long var1) throws InterruptedException {
long var3 = System.currentTimeMillis();
long var5 = 0L;
if(var1 < 0L) {
throw new IllegalArgumentException("timeout value is negative");
} else {
if(var1 == 0L) {
while(this.isAlive()) {
this.wait(0L);
}
} else {
while(this.isAlive()) {
long var7 = var1 - var5;
if(var7 <= 0L) {
break;
}
this.wait(var7);
var5 = System.currentTimeMillis() - var3;
}
}
}
}
public final native boolean isAlive();
我们可以看到 t1调用 join 方法的时候调用了重载的方法,并且传了参数0,然后关键来了while(this.isAlive())条件一直满足的情况下,调用了 this.wait(0),这里的 this 相当于对象 t1。
我们来思考一下,t1.wait()到底是哪个线程需要 wait?给你们三秒钟时间。
3...2...1...
好了,我直接说了,大家记住,t1只是个对象,这里不能当成是 t1线程 wait,主线程里面通过对象 t1作为锁,并调用了 wait 方法,其实是主线程 wait 了。while 的判断条件是线程 t1.isAlive(),注意,这里是判断线程 t1是否存活,如果存活,则主线程一直 wait(0),直到 t1 线程执行结束死亡。这样可以了解了吧,再来思考一下如果在 Android 主线程里面调用 join 方法可能会造成什么问题?
这个问题很简单,我就不说答案了。
Java 线程:线程的调度-守护线程
守护线程与普通线程写法上基本没啥区别,调用线程对象的方法 setDaemon(true),则可以将其设置为守护线程。
守护线程的使用情况较少,但并非无用,举例来说,JVM 的垃圾回收、内存管理等线程都是守护线程。还有就是在做数据库应用的时候,使用数据库连接池,连接池本身也包含着很多后台现场,监控连接个数、超时时间、状态等等。
- setDaemon(boolean on)
将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java虚拟机退出。该方法必须在启动线程前调用。
public class ThreadDaemon {
public static void main(String[] args) {
Thread t1 = new MyCommon();
Thread t2 = new Thread(new MyDaemon());
t2.setDaemon(true); //设置为守护线程
t2.start();
t1.start();
}
}
class MyCommon extends Thread {
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("线程1第" + i + "次执行!"+"——————活着线程数量:"+Thread.currentThread().getThreadGroup().activeCount());
try {
Thread.sleep(7);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class MyDaemon implements Runnable {
public void run() {
for (long i = 0; i < 9999999L; i++) {
System.out.println("后台线程第" + i + "次执行!");
try {
Thread.sleep(7);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
啥也别说了,来看结果吧:
后台线程第0次执行!
线程1第0次执行!——————活着线程数量:4
后台线程第1次执行!
线程1第1次执行!——————活着线程数量:4
后台线程第2次执行!
线程1第2次执行!——————活着线程数量:4
后台线程第3次执行!
线程1第3次执行!——————活着线程数量:4
后台线程第4次执行!
线程1第4次执行!——————活着线程数量:4
后台线程第5次执行!
从上面的结果我们可以看出,前台线程是包装执行完毕的,后台线程还没有执行完毕就退出了。也就是说除了守护线程以外的其他线程执行完之后,守护线程也就结束了。
然后,我们来看看,为什么活着的线程数量会是4,明明只开了两个子线程呀,加上 main 线程也才三个,那再加一个垃圾回收线程吧哈哈哈哈。
这个问题也是我在学习过程中困扰了很久的问题。之前纠结的是,main 线程执行完了,如果还有子线程在运行。那么 main 线程到底是先结束还是等待子线程执行结束之后再结束?main 线程结束是不是代表程序退出?
然后我就 Debug 线程池里面所有的线程,发现里面有一个叫 DestoryJavaVM 的线程,然后我也不知道这是个什么东西,遂问了一下度娘,度娘告诉我~
DestroyJavaVM:main执行完后调用JNI中的jni_DestroyJavaVM()方法唤起DestroyJavaVM线程。 JVM在Jboss服务器启动之后,就会唤起DestroyJavaVM线程,处于等待状态,等待其它线程(java线程和native线程)退出时通知它卸载JVM。线程退出时,都会判断自己当前是否是整个JVM中最后一个非deamon线程,如果是,则通知DestroyJavaVM线程卸载JVM
大概就是酱紫吧,4个线程分别是两个我手动开的子线程,一个DestroyJavaVM ,还有一个大概是垃圾回收线程吧,哈哈哈哈,如果不对,请务必拍砖~
Java 线程:线程的同步-同步方法\同步块
上一篇已经就同步问题做了详细的讲解。
对于多线程来说,不管任何编程语言,生产者消费者模型都是最经典的。这里我们拿一个生产者消费者模型来深入学习吧~
实际上,应该是“生产者-消费者-仓储”模型,离开了仓储,生产者消费者模型就显得没有说服力。
对于此模型,应该明确以下几点:
- 生产者仅仅在仓储未满时候生产,仓满则停止生产
- 消费者仅仅在仓储有产品时候才能消费,仓空则等待
- 当消费者发现仓储没产品可消费时候会通知生产者生产
- 生产者在生产出可消费产品时候,应该通知等待的消费者去消费
此模型将要的知识点,我们上面都学过了,直接撸代码吧~
public class Model {
public static void main(String[] args) {
Godown godown = new Godown(30);
Consumer c1 = new Consumer(50, godown);
Consumer c2 = new Consumer(20, godown);
Consumer c3 = new Consumer(30, godown);
Producer p1 = new Producer(10, godown);
Producer p2 = new Producer(10, godown);
Producer p3 = new Producer(10, godown);
Producer p4 = new Producer(10, godown);
Producer p5 = new Producer(10, godown);
Producer p6 = new Producer(10, godown);
Producer p7 = new Producer(40, godown);
c1.start();
c2.start();
c3.start();
p1.start();
p2.start();
p3.start();
p4.start();
p5.start();
p6.start();
p7.start();
}
}
/**
* 仓库
*/
class Godown {
public static final int max_size = 100;//最大库存量
public int curnum; //当前库存量
Godown() {
}
Godown(int curnum) {
this.curnum = curnum;
}
/**
* 生产指定数量的产品
*
* @param neednum
*/
public synchronized void produce(int neednum) {
//测试是否需要生产
while (neednum + curnum > max_size) {
System.out.println("要生产的产品数量" + neednum + "超过剩余库存量" + (max_size - curnum) + ",暂时不能执行生产任务!");
try {
//当前的生产线程等待
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//满足生产条件,则进行生产,这里简单的更改当前库存量
curnum += neednum;
System.out.println("已经生产了" + neednum + "个产品,现仓储量为" + curnum);
//唤醒在此对象监视器上等待的所有线程
notifyAll();
}
/**
* 消费指定数量的产品
*
* @param neednum
*/
public synchronized void consume(int neednum) {
//测试是否可消费
while (curnum < neednum) {
try {
//当前的生产线程等待
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//满足消费条件,则进行消费,这里简单的更改当前库存量
curnum -= neednum;
System.out.println("已经消费了" + neednum + "个产品,现仓储量为" + curnum);
//唤醒在此对象监视器上等待的所有线程
notifyAll();
}
}
/**
* 生产者
*/
class Producer extends Thread {
private int neednum; //生产产品的数量
private Godown godown; //仓库
Producer(int neednum, Godown godown) {
this.neednum = neednum;
this.godown = godown;
}
public void run() {
//生产指定数量的产品
godown.produce(neednum);
}
}
/**
* 消费者
*/
class Consumer extends Thread {
private int neednum; //生产产品的数量
private Godown godown; //仓库
Consumer(int neednum, Godown godown) {
this.neednum = neednum;
this.godown = godown;
}
public void run() {
//消费指定数量的产品
godown.consume(neednum);
}
}
已经消费了20个产品,现仓储量为10
已经生产了10个产品,现仓储量为20
已经生产了10个产品,现仓储量为30
已经生产了10个产品,现仓储量为40
已经生产了10个产品,现仓储量为50
已经消费了30个产品,现仓储量为20
已经生产了40个产品,现仓储量为60
已经生产了10个产品,现仓储量为70
已经消费了50个产品,现仓储量为20
已经生产了10个产品,现仓储量为30
在本例中,要说明的是当发现不能满足生产者或消费条件的时候,调用对象的 wait 方法,wait 方法的作用是释放当前线程的所获得的锁,并调用对象的 notifyAll()方法,通知(唤醒)该对象上其他等待的线程,使其继续执行。这样,整个生产者、消费者线程得以正确的协作执行。
Java 线程:volatile 关键字
Java 语言包含两种内在同步机制:同步块(方法)和 volatile 变量。这两种机制的提出都是为了实现代码线程的安全性。其中 volatile 变量的同步性较差(但有时它更简单并且开销更低),并且其使用也容易出错。
首先考虑一个问题,为什么变量需要volatile来修饰呢?要搞清楚这个问题,首先应该明白计算机内部都做什么了。比如做了一个i++操作,计算机内部做了三次处理:读取-修改-写入。同样,对于一个long型数据,做了个赋值操作,在32系统下需要经过两步才能完成,先修改低32位,然后修改高32位。
假想一下,当将以上的操作放到一个多线程环境下操作时候,有可能出现的问题,是这些步骤执行了一部分,而另外一个线程就已经引用了变量值,这样就导致了读取脏数据的问题。
通过这个设想,就不难理解volatile关键字了。
猜你喜欢
- 2024-10-29 Java线程:它们的内存效率高吗? java线程内存溢出
- 2024-10-29 一文搞懂Java多线程 java多线程使用方法
- 2024-10-29 JAVA反射机制详解,一学就会 java反射机制实现原理
- 2024-10-29 Java中的锁详解 java中锁的概念
- 2024-10-29 2022新出java100+经典面试题,赶紧保存
- 2024-10-29 「超级详细」Java线程实现原理 java线程实现的三种方式
- 2024-10-29 AI预测手机睡眠模式 手机监测睡眠质量
- 2024-10-29 一文详解 Java 的几把 JVM 级锁 java中的各种锁详细介绍
- 2024-10-29 “全栈2019”Java多线程第十五章:后台线程中的finally
- 2024-10-29 详细介绍一下Java中的wait()方法与sleep()方法的区别与联系?
你 发表评论:
欢迎- 最近发表
- 标签列表
-
- nginx反向代理 (57)
- nginx日志 (56)
- nginx限制ip访问 (62)
- mac安装nginx (55)
- java和mysql (59)
- java中final (62)
- win10安装java (72)
- java启动参数 (64)
- java链表反转 (64)
- 字符串反转java (72)
- java逻辑运算符 (59)
- java 请求url (65)
- java信号量 (57)
- java定义枚举 (59)
- java字符串压缩 (56)
- java中的反射 (59)
- java 三维数组 (55)
- java插入排序 (68)
- java线程的状态 (62)
- java异步调用 (55)
- java中的异常处理 (62)
- java锁机制 (54)
- java静态内部类 (55)
- java怎么添加图片 (60)
- java 权限框架 (55)
本文暂时没有评论,来添加一个吧(●'◡'●)