java学习笔记15--多线程编程基础2

简介:

线程的生命周期

1、线程的生命周期

线程从产生到消亡的过程

一个线程在任何时刻都处于某种线程状态(thread state) 

线程生命周期状态图

诞生状态

线程刚刚被创建

就绪状态

线程的 start 方法已被执行

线程已准备好运行

运行状态

处理机分配给了线程,线程正在运行

阻塞状态(Blocked)

在线程发出输入/输出请求且必须等待其返回

遇到用synchronized标记的方法而未获得其监视器暂时不能进入执行时

休眠状态(Sleeping)

执行sleep方法而进入休眠

死亡状态

线程已完成或退出

2、死锁问题

死锁

线程在运行过程中,其中某个步骤往往需要满足一些条件才能继续进行下去,如果这个条件不能满足,线程将在这个步骤上出现阻塞

线程A可能会陷于对线程B的等待,而线程B同样陷于对线程C的等待,依次类推,整个等待链最后又可能回到线程A。如此一来便陷入一个彼此等待的轮回中,任何线程都动弹不得,此即所谓死锁(deadlock)

对于死锁问题,关键不在于出现问题后调试,而是在于预防

设想一个游戏,规则为3个人站在三角形的三个顶点的位置上,三个边上放着三个球,如图所示。每个人都必须先拿到自己左手边的球,才能再拿到右手边的球,两手都有球之后,才能够把两个球都放下

创建3个线程模拟3个游戏者的行为。

class Balls {
    boolean flag0 = false;    //0号球的标志变量,true表示已被人拿,false表示未被任何人拿
    boolean flag1 = false;    //1号球的标志变量
    boolean flag2 = false;    //2号球的标志变量
}
class Player0 extends Thread {    //0号游戏者的类
    private Balls ball;
    public Player0(Balls b) {
        this.ball = b;
    }
    public void run() {
        while(true) {
            while(ball.flag1 == true){}    //如果1号球已被拿走,则等待
            ball.flag1 = true;    //拿起1号球
            while(ball.flag0 == true){}    //如果0号球已被拿走,则等待
            if(ball.flag1 == true && ball.flag0 == false) {
                ball.flag0 = true;    //拿起0号球
                System.out.println("Player0 has got two balls!");
                ball.flag1 = false;    //放下1号球
                ball.flag0 = false;    //放下0号球
                try {
                    sleep(1);        //放下后休息1ms
                } catch (Exception e) {
                    
                }
            }
        }
    }
}
class Player1 extends Thread {    //1号游戏者的类
    private Balls ball;
    public Player1(Balls b) {
        this.ball = b;
    }
    public void run() {
        while(true) {
            while(ball.flag0 == true){}
            ball.flag0 = true;
            while(ball.flag1 == true){}
            if(ball.flag0 == true && ball.flag1 == false) {
                ball.flag1 = true;
                System.out.println("Player0 has got two balls!");
                ball.flag0 = false;
                ball.flag1 = false;
                try {
                    sleep(1);
                } catch (Exception e) {
                    
                }
            }
        }
    }
}
class Player2 extends Thread {    //2号游戏者的类
    private Balls ball;
    public Player2(Balls b) {
        this.ball = b;
    }
    public void run() {
        while(true) {
            while(ball.flag2 == true){}
            ball.flag2 = true;
            while(ball.flag1 == true){}
            if(ball.flag2 == true && ball.flag1 == false) {
                ball.flag1 = true;
                System.out.println("Player0 has got two balls!");
                ball.flag2 = false;
                ball.flag1 = false;
                try {
                    sleep(1);
                } catch (Exception e) {
                    
                }
            }
        }
    }
}
public class playball {
    public static void main(String[] args) {
        Balls ball = new Balls();    //创建一个球类对象
        Player0 p0 = new Player0(ball);    //创建0号游戏者
        Player1 p1 = new Player1(ball);    //创建1号游戏者
        Player2 p2 = new Player2(ball);    //创建2号游戏者
        p0.start();    //启动0号游戏者
        p1.start();    //启动1号游戏者
        p2.start();    //启动2号游戏者
    }
}

运行结果:

若干次后将陷入死锁,不再有输出信息,即任何人都不能再同时拥有两侧的球

程序说明:

如果刚好3个人都拿到了左手边的球,都等待那右手边的球,则因为谁都不能放手,则这3个线程都将陷入无止尽的等待当中,这就构成了死锁

为了便于观察死锁发生的条件,我们在每个游戏者放下两边的球后增加了sleep语句

为了避免死锁,需要修改游戏规则,使每个人都只能先抢到两侧中号比较小的球,才能拿另一只球,这样就不会再出现死锁现象

3、控制线程的生命

结束线程的生命

用stop方法可以结束线程的生命

但如果一个线程正在操作共享数据段,操作过程没有完成就用stop结束的话,将会导致数据的不完整,因此并不提倡使用此方法

通常,可通过控制run方法中循环条件的方式来结束一个线程

线程不断显示递增整数,按下回车键则停止执行:

class TestThread1 extends Thread {
    private boolean flag = true;
    public void stopme() {    //在此方法中控制循环条件
        flag = false;
    }
    public void run() {
        int i = 0;
        while(flag) {
            System.out.println(i++);  //如果flag为真则一直显示递增整数
        }
    }
}
public class ext8_12 {
    public static void main(String[] args)  throws IOException{
        TestThread1 t = new TestThread1();
        t.start();
        new BufferedReader(new InputStreamReader(System.in)).readLine();    //等待键盘输入
        t.stopme();        //调用stopme方法结束t线程
    }
}

运行效果为按下回车键后则停止显示

线程的优先级

线程调度

在单CPU的系统中,多个线程需要共享CPU,在任何时间点上实际只能有一个线程在运行

控制多个线程在同一个CPU上以某种顺序运行称为线程调度

Java虚拟机支持一种非常简单的、确定的调度算法,叫做固定优先级算法。这个算法基于线程的优先级对其进行调度

线程的优先级

每个Java线程都有一个优先级,其范围都在1和10之间。默认情况下,每个线程的优先级都设置为5

在线程A运行过程中创建的新的线程对象B,初始状态具有和线程A相同的优先级

如果A是个后台线程,则B也是个后台线程

可在线程创建之后的任何时候,通过setPriority(int priority)方法改变其原来的优先级

基于线程优先级的线程调度

具有较高优先级的线程比优先级较低的线程优先执行

对具有相同优先级的线程,Java的处理是随机的

底层操作系统支持的优先级可能要少于10个,这样会造成一些混乱。因此,只能将优先级作为一种很粗略的工具使用。最后的控制可以通过明智地使用yield()函数来完成

我们只能基于效率的考虑来使用线程优先级,而不能依靠线程优先级来保证算法的正确性

假设某线程正在运行,则只有出现以下情况之一,才会使其暂停运行

一个具有更高优先级的线程变为就绪状态(Ready);

由于输入/输出(或其他一些原因)、调用sleep、wait、yield方法使其发生阻塞;

对于支持时间分片的系统,时间片的时间期满

例子: 创建两个具有不同优先级的线程,都从1递增到400000,每增加50000显示一次
class TestThread2 extends Thread {
    private int tick = 1;
    private int num;
    public TestThread2(int i) {
        this.num = i;
    }
    public void run() {
        while(tick < 400000) {
            tick++;
            if((tick % 50000) == 0) {    //每隔50000进行显示
                System.out.println("Thread#" + num +",tick=" + tick);
                yield();    //放弃执行权
            }
        }
    }
}
public class ext8_13 {
    public static void main(String[] args) {
        TestThread2[] runners = new TestThread2[2];
        for(int i = 0; i < 2; i++)    runners[i] = new TestThread2(i);
        runners[0].setPriority(2);
        runners[1].setPriority(3);
        for(int i = 0; i < 2; i++)
            runners[i].start();
    }
}

运行结果如下:

Thread #1, tick = 50000

Thread #1, tick = 100000

Thread #1, tick = 150000

Thread #1, tick = 200000

Thread #1, tick = 250000

Thread #1, tick = 300000

Thread #1, tick = 350000

Thread #1, tick = 400000

Thread #0, tick = 50000

Thread #0, tick = 100000

Thread #0, tick = 150000

Thread #0, tick = 200000

Thread #0, tick = 250000

Thread #0, tick = 300000

Thread #0, tick = 350000

Thread #0, tick = 400000

结果说明:

具有较高优先级的线程1一直运行到结束,具有较低优先级的线程0才开始运行

虽然具有较高优先级的线程1调用了yield方法放弃CPU资源,允许线程0进行争夺,但马上又被线程1抢夺了回去,所以有没有yield方法都没什么区别

如果在yield方法后增加一行sleep语句,让线程1暂时放弃一下在CPU上的运行,哪怕是1毫秒,则线程0也可以有机会被调度。修改后的run方法如下:

    public void run() {
        while(tick < 400000) {
            tick++;
            if((tick % 50000) == 0) {    //每隔50000进行显示
                System.out.println("Thread#" + num +",tick=" + tick);
                yield();    //放弃执行权
                try {
                    sleep(1);
                } catch(Exception e) {
                    
                }
            }
        }
    }

运行结果如下:

Thread #1, tick = 50000

Thread #1, tick = 100000

Thread #1, tick = 150000

Thread #1, tick = 200000

Thread #0, tick = 50000

Thread #1, tick = 250000

Thread #1, tick = 300000

Thread #0, tick = 100000

Thread #1, tick = 350000

Thread #1, tick = 400000

Thread #0, tick = 150000

Thread #0, tick = 200000

Thread #0, tick = 250000

Thread #0, tick = 300000

Thread #0, tick = 350000

Thread #0, tick = 400000

说明

具有较低优先权的线程0在线程1没有执行完毕前也获得了一部分执行,但线程1还是优先完成了执行

Java虚拟机本身并不支持某个线程抢夺另一个正在执行的具有同等优先级线程的执行权

通常,我们在一个线程内部插入yield()语句,这个方法会使正在运行的线程暂时放弃执行,这是具有同样优先级的线程就有机会获得调度开始运行,但较低优先级的线程仍将被忽略不参加调度

目录
相关文章
|
8天前
|
Java 开发者
Java多线程编程中的常见误区与最佳实践####
本文深入剖析了Java多线程编程中开发者常遇到的几个典型误区,如对`start()`与`run()`方法的混淆使用、忽视线程安全问题、错误处理未同步的共享变量等,并针对这些问题提出了具体的解决方案和最佳实践。通过实例代码对比,直观展示了正确与错误的实现方式,旨在帮助读者构建更加健壮、高效的多线程应用程序。 ####
|
7天前
|
安全 Java 开发者
Java 多线程并发控制:深入理解与实战应用
《Java多线程并发控制:深入理解与实战应用》一书详细解析了Java多线程编程的核心概念、并发控制技术及其实战技巧,适合Java开发者深入学习和实践参考。
|
7天前
|
Java 开发者
Java多线程编程的艺术与实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的技术文档,本文以实战为导向,通过生动的实例和详尽的代码解析,引领读者领略多线程编程的魅力,掌握其在提升应用性能、优化资源利用方面的关键作用。无论你是Java初学者还是有一定经验的开发者,本文都将为你打开多线程编程的新视角。 ####
|
6天前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
12天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
37 9
|
9天前
|
安全 Java 开发者
Java多线程编程中的常见问题与解决方案
本文深入探讨了Java多线程编程中常见的问题,包括线程安全问题、死锁、竞态条件等,并提供了相应的解决策略。文章首先介绍了多线程的基础知识,随后详细分析了每个问题的产生原因和典型场景,最后提出了实用的解决方案,旨在帮助开发者提高多线程程序的稳定性和性能。
|
12天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
14天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
15天前
|
Java
java小知识—进程和线程
进程 进程是程序的一次执行过程,是系统运行的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程 线程,与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比
24 1
|
Java
Java多线程编程核心技术(三)多线程通信(下篇)
线程是操作系统中独立的个体,但这些个体如果不经过特殊的处理就不能成为一个整体。线程间的通信就是成为整体的必用方案之一,可以说,使线程间进行通信后,系统之间的交互性会更强大,在大大提高CPU利用率的同时还会使程序员对各线程任务在处理的过程中进行有效的把控与监督。
686 0
下一篇
无影云桌面