当前位置:首页 > Java技术 > 『Java多线程』基础之基础

『Java多线程』基础之基础

2022年09月16日 15:29:58Java技术6

Java基础篇–多线程


线程概念

程序(Program)

程序是指令、数据及其组织形式的描述,也就是存储在磁盘或者其他存储设备中含有指令和数据的文件,是一段静态代码

进程(Process)

进程是受操作系统管理的基本单元,是程序的一次动态执行过程,是系统进行资源分配和调度的基本单位,它对应了从代码加载、执行到执行完毕的整个过程,即进程的创建、运行到消亡的过程。从某种程度上来说,进程是正在运行程序的实例,是一个动态概念

多任务(Multi Task)

在一个系统中可以同时运行多个程序,即有多个独立运行的任务,每个任务对应一个进程。多任务系统(如Windows系统)可以最大限度地利用系统资源

线程(Thread)

​线程是由进程创建的比进程更小的执行单位,它有开始、中间和结束部分,有时被称为轻量级进程(Lightweight Process, LWP)。一个进程可以有多个线程,线程是在进程中独立运行的子任务。与进程不同,线程不能作为具体可执行的命令存在。也就是说,最终用户不能直接执行线程,线程只能运行在进程中

同时执行一个以上的线程,称为多线程

线程的状态与生命周期

线程的状态以枚举类型的形式定义在java.lang.Thread中,其代码为

public enum State{
     
    NEW,             //新建状态
    RUNNABLE,        //就绪状态
    BLOCKED,         //阻塞状态
    WAITING,         //等待转态
    TIMED_WATING,    //定时等待状态
    TERMINATED;      //终止状态
}

『Java多线程』基础之基础 _ JavaClub全栈架构师技术笔记

实现多线程的方式

Java实现多线程的方式有两种:继承Thread类或实现Runnable接口。Thread类和Runnable接口都定义在java.lang包中

Runnable接口中只定义了一个方法run(),实现Runnable接口的类必须实现这个方法,将线程要执行的具体的具体操作代码写入其中,因此也将run()方法称为线程体(Thread Body)。run()方法是线程执行的起点,线程启动后,由系统自动调度执行此方法,就像main()方法是应用程序的执行起点,由系统自动调用执行一样

Thread类实现类Runnable接口,Thread类包含多个构造方法及对线程进行控制的各种方法。无论使用哪一种实现线程的方法,都要用到Thread类,这是因为Runnable接口中只有run()方法,缺少控制线程的相关方法,如启动线程的start()等

Thread类的成员变量
成员变量 说明
static int MAX_PRIORITY 线程的最高优先级
static int NORM_PRIORITY 线程的默认优先级
static int MIN_PRIORITY 线程的最低优先级
Thread类常用的构造方法和成员方法
方法原型 说明
public Thread() 创建一个空线程对象
public Thread(String name) 创建一个名为name的线程对象
public Thread(Runnable task) 为指定任务创建一个线程对象,参数task对象的run()方法将被该线程对象调用,作为其执行代码
public Thread(Runnable task, String name) 功能同上,指定了线程名name
public void start() 启动线程,使该线程由新建状态变为就绪状态
public void run() 线程应执行的代码放在该方法中
public static Thread currentThread() 返回当前正在执行的线程对象的引用
创建线程的方式
  1. 继承Thread类创建线程
class ThreadTest extends Thread{
     
	public void run() {
     
		//...
		System.out.println("继承Thead类创建线程");
	}
}
ThreadTest tt = new ThreadTest();
tt.start();

实现Runnable接口创建线程

class RunnableTest implements Runnable{
     
	public void run() {
     
		//...
		System.out.println("实现Runnable接口创建线程");
	}
}
Thread t = new Thread(new RunnableTest());
t.start();
  1. 两种方法比较
    • 继承Thread类创建线程的方式,程序更简单直接,但由于java只支持单继承,因而存在局限性
    • 实现Runnable接口创建线程的方式,程序稍复杂,但可以避免多重继承的问题。此外,这种方式使得代码和数据资源相对独立,更适合多个线程处理同一数据资源的情况。这是因为在实现Runnable接口创建线程时,需要用到Thread类的构造方法,只要通过构造方法传递相同的Runnable实例(线程体),就可以实现资源共享

    采用Runnable接口的方式创建售票线程

    class BookTickets implements Runnable{
           
    	private int tickets = 50;
    	public void run() {
           
    		while(tickets > 0) {
           
    			System.out.println(Thread.currentThread().getName() + "sells ticket "
    		+ tickets--);
    		}
    	}
    }
    public static void main(String[] args){
           
    	BookTickets bt =  new BookTickets();
    	new Thread(bt).start();
    	new Thread(bt).start();
    	new Thread(bt).start();
    }
    

线程控制的基本方法

线程的优先级

Java给每个线程指定一个优先级,优先级从低到高共分10级,以整数1 ~ 10表示,1级最低,10级最低,默认优先级为5级。Thread类有3个有关线程优先级的静态常量:MIN_PRIORITY(最低优先级1),NORM_PRIORITY(默认优先级5),MAX_PRIORITY(最高优先级10)。可以通过**getPriority()方法来获得线程的优先级,通过setPriority()**方法来设定线程的优先级。优先级高的线程理论上可以获得比优先级低的线程更多的执行机会

对于一个新建的线程,系统会遵循如下原则为其指定优先级

  1. 新建线程将继承创建它的父线程的优先级,父线程指的是创建该线程语句所在的线程
  2. 一般情况下,主线程(main)具有默认优先级
Thread t1 = new Thread(bt);
Thread t2 = new Thread(bt);
Thread t3 = new Thread(bt);
t1.setPriority(3);
t2.setPriority(Thread.NORM_PRIORITY + 5);
t3.setPriority(Thread.MIN_PRIORITY);

注意:理论上优先级高的线程享有更多的执行机会。但是由于线程的优先级不同于线程调度的优先级(多个线程在一个处理器上运行时,处理器以某种顺序运行多个线程,称为线程的调度),每个线程都有自己的线程调度机制,优先级高的线程比优先级低的线程得到执行机会的概率相对高一些,但不是绝对的,有时候优先级低的线程可能会先执行

Java的setPriority()方法只是应用于局部的优先级,这是一个保护方式,用户不会希望一些重要的线程被其他随机的用户线程通过设定的优先级所抢占。所以在编程时要注意,不要有事物逻辑依赖于线程优先级,否则可能产生意外的结果,即不要依赖线程的优先级来设计对调度敏感的算法

线程的基本控制

Thread类中线程控制的常用方法

方法原型 说明
public final void join() throws InterruptedException 暂停当前运行的线程,等待调用该方法的线程结束后再继续执行本线程
public static void sleep(long mills) throws InterruptedException 使线程休眠指定时间,mills以毫秒为单位
public static Thread currentThread() 返回当前正在运行的线程对象
public final int getPriority() 返回线程的优先级
public final String getName() 返回线程的名称
public final int setPriority(int newPriority) 设置线程优先级(范围1 ~ 10)
  • sleep()休眠 Thread.sleep() , while(true)刷新界面

  • join()

    public final void join() throws InterruptedException
    public final void join(long mills) throws InterruptedException
    public final void join(long mills, int nanos) throws InterruptedException
    
线程的终止

线程除正常运行结束外,还可以通过其他方法使其停止运行。使用stop()方法可以强行终止线程,但该方法容易造成数据信息的不一致。如账户转移(数据丢失)。更好的方法是使用退出标志来终止线程,设置一个boolean类型的标志变量

class Runner implements Runnable{
     
	private boolean flag = true;    //设置标志变量
	public void run() {
     
		while(flag) {
     
			System.out.println("当前系统时间为--" + new Date().toString());
			try {
     
				Thread.sleep(600);
			} catch (InterruptedException e) {
     
				e.printStackTrace();
			}
		}
	}
	public void stopRunning() {
     
		flag = false;       //通过为flag变量赋值来终止线程
	}
}
public static void main(String[] args){
     
	Runner r = new Runner();
	Thread t = new Thread(r);
	t.start();
	try {
     
		Thread.sleep(3000);
	} catch (InterruptedException e) {
     
		e.printStackTrace();
	}
	r.stopRunning();
}

线程的同步机制

同步的概念
  • 并发:在同一时间段内有多个线程处于“就绪”状态,不过在任一个时间节点上只有一个线程在处理器上运行
  • 临界资源:多个线程共享的资源或数据称为临界资源或同步资源
  • 临界区:访问临界资源的代码段称为临界区,也称为临界代码区
  • 对象锁:对临界资源对象进行加锁,对象锁是独占排他的
  • 原子操作:不可分割的一段代码,不会被调度机制所打断的操作。原子操作可以是一个操作步骤,也可以是多个操作步骤,其顺序不可以打乱,也不可以只执行部分操作。
  • 同步:同步就是协同步调,统一配合共同完成任务。一个线程在执行某个功能调用时,在没有得到返回结果之前,这个线程会一直等待下去,直到收到返回结果才会继续执行下去
  • 异步:线程不需要等方法执行返回,就继续执行下面的操作语句。当有消息返回时,系统会通知线程进行处理,这样可以提高执行的效率。现实世界本质上是异步的,每个对象同时存在活动,互相通知对方感兴趣的消息,各自处理各自的消息
同步的实现方法

在Java语言中,对临界资源操作的并发控制采用加锁技术。用关键字synchronized为临界资源加锁来保证线程对其操作的完整性。这个锁使得各个线程对临界资源是互斥操作的,称为互斥锁

临界大代码区可以是一个方法或是一个语句块,用synchronized关键字标识,表示必须互斥使用。这两种方法分别称为同步方法和同步语句块。换句话说,synchronized关键字可应用在方法级别(粗粒度锁)或者是代码块级别(细粒度锁)

  1. 同步方法

    public synchronized void aMethod(){
           
          ...
    }
    

当线程执行到一个对象的任意一个同步方法时,这个对象的所有同步方法都被锁定了。当一个对象被某个线程锁定后,其他线程可以访问该对象的非同步方法.此外,调用sleep()方法不会释放锁,即使当前线程调用sleep()方法让出了CPU资源,其他线程也无法访问临界资源

  1. 同步语句块

    synchronized(对象 obj (this)){
           
        //临界代码段
    }
    

同步语句块的形式虽然与同步方法不同,但是原理和效果是一致的。同步语句块是通过锁定指定对象而不仅仅是this对象,来对语句块中包含的代码进行同步;而同步方法锁定的对象是同步方法所属的主体对象自身,是对这个方法里的代码进行同步

注意:使用synchronized进行同步,保证了线程的安全性,但却降低了运行效率。因此,从提高并发度的角度来说,synchronized的粒度越细越好

死锁

多线程在使用互斥机制实现同步的同时,存在”死锁“可能。如果程序中的多个线程互相等待对方持有的 资源,而在得到对方资源之前都不会释放自己的资源,从而导致所有线程都无法继续执行的情况就是死锁(deadlock)。死锁经常发生在多个线程共享资源的时候

程序中必须同时满足以下四个条件才会引发死锁:

  1. 互斥(Mutual exclusion):线程所使用的资源中至少有一个是不能共享的,它在同一时刻只能由一个线程使用
  2. 持有与等待(Hold and Wait):至少有一个线程已经持有了资源,并且正在等待获取其他的线程所持有的资源
  3. 非抢占式(No pre-emption):如果一个一个线程已经持有了某个资源,那么在这个线程释放这个资源之前,别的线程不能把它抢夺过去使用
  4. 循环等待(Circular wait):等待资源形成环

线程之间的通信

线程通信的主要方法
方法原型 说明
public final void wait() throws InterruptedException 使调用wait()方法的线程变为阻塞状态,主动释放对象的互斥锁,并进入该互斥锁的等待队列,直到其他线程调用notify()或notifyAll()方法
public final void wait(long mills) throws InterruptedException 超时等待mills毫秒,如果没有唤醒通知,就超时返回
public final void wait(long mills, int nanos) throws InterruptedException 超时等待mills毫秒,nanos纳秒,若无通知,超时返回
public void notify() 唤醒一个等待该对象互斥锁的线程
public void notifyAll() 唤醒正在等待该对象互斥锁的所有线程
  1. wait()方法

    ​使用wait()方法时,必须先获得对象锁,如果此时临界资源状态不满足特定条件,那么调用该对象的wait()方法。需要注意的是,wait()方法必须在一个同步方法或同步语句中被调用,并且该同步方法或同步语句块锁住了临界资源对象,否则,会抛出IllegalMonitorStateException异常。

    wait()方法和sleep()方法都能使线程进入阻塞状态,但是wait()在放弃CPU资源的同时交出了临界资源的控制权,而sleep()方法却一直占用着临界资源。从wait()方法返回的前提是重新获得了调用对象的锁。通常使用循环模式来调用wait()方法

  2. notify()方法

    ​与wait()方法不同,notify()方法必须执行完其所在的sychronized同步方法或同步语句块才释放对象锁。如果notify()调用的次数小于等待 中线程的数量,会出现部分线程无法被唤醒的情况

  3. notifyAll()方法

    唤醒同步队列中的所有线程,被唤醒的线程需要重新获得对象锁,并等待系统调度

生产者/消费者问题
class Bread{
     
	int id;
	Bread(int id){
     
		this.id = id;
	}
}

class GoodsStack{
     
	int index = 0;
	Bread[] arrbd = new Bread[3];
	public synchronized void push(Bread bd) {
        //生产面包的方法
		while(index == arrbd.length) {
               //判断数组是否已满
			try {
     
				this.wait();          //满时等待,不再生产面包,并释放对象锁
			} catch (InterruptedException e) {
     
				e.printStackTrace();
			}
		}
		arrbd[index] = bd;       //将bd存入数组
		index++;
		System.out.println("生产者" + Thread.currentThread().getName() + 
				"新生产了面包。" + "当前面包数量为: " + this.index);
		this.notify();          //唤醒等待线程
	}
	public synchronized Bread pop() {
           //消费面包的方法
		while(index == 0) {
                     //判断数组是否为空
			try {
     
				this.wait();               //空时等待,不再消费面包
			} catch (InterruptedException e) {
     
				// TODO 自动生成的 catch 块
				e.printStackTrace();
			}
		}
		index--;
		System.out.println("消费者" + Thread.currentThread().getName() + 
				"消费了面包。" + "当前面包数为:" + this.index);
		this.notify();                //唤醒等待线程
		return arrbd[index];          //取出元素
	}
}

class Producer implements Runnable{
     
	GoodsStack gs = null;
	Producer(){
     }
	Producer(GoodsStack gs){
     
		this.gs = gs;
	}
	public void run() {
     
		for(int i = 0; i < 6; i++) {
     
			Bread bd = new Bread(i);      //创建Bread对象
			gs.push(bd);    //将Bread对象bd放入临界资源GoodsStack对象gs中
			try {
     
				//随机休眠一段时间
				Thread.sleep((int)(Math.random() * 200));
			} catch (InterruptedException e) {
     
				e.printStackTrace();
			}
		}
	}
	
}

class Consumer implements Runnable{
     
	GoodsStack gs = null;
	Consumer(){
     }
	Consumer(GoodsStack gs){
     
		this.gs = gs;
	}
	public void run() {
     
		for(int i = 0; i < 12; i++) {
     
			Bread bd = gs.pop();  //从临界资源GoodsStrack对象gs中取出元素
			try {
     
				Thread.sleep((int)(Math.random() * 1000));  //随机休眠一段时间
			} catch (InterruptedException e) {
     
				e.printStackTrace();
			} 
		}
	}
}

public class Main {
     
	public static void main(String[] args){
     
		// TODO 自动生成的方法存根
		GoodsStack gs = new GoodsStack();
		Producer p = new Producer(gs);
		Consumer c = new Consumer(gs);
		new Thread(p).start();     //启动生产者线程
		new Thread(c).start();     //启动消费者线程1
		new Thread(c).start();     //启动消费者线程2
	}
}

定时器类Timer的应用

Timer类的使用

​Timer类是一个实用工具类,用于实现在某个时间或某一段时间后安排执行某项任务。该任务可能被安排执行一次,或者定期重复执行。使用Timer类可以极大地简化程序。在Java中,Timer类对象用于安排在后台线程中执行任务,即在主线程之外起一个单独的线程执行指定的计划任务

Timer类需要和TimerTask类配合使用,可以将Timer类看成一个定时器,TimerTash类表示可以被Timer调度执行的任务

Timer类使用对象的wait()和notify()方法来调度任务

  1. 创建定时器对象

    Timer timer = new Timer()

  2. 创建任务对象

    …extends TimeTask{ …run() }

  3. 安排任务

    通过调用Timer对象的schedule()方法来安排任务

    方法原型 说明
    schedule(TimerTask task, long delay) 安排一个任务,等待delay毫秒后执行
    schedule(TimerTask task, long delay, long period) 安排一个任务,等待delay毫秒后执行,每隔period毫秒再执行(反复执行同一个任务)
    schedule(TimerTask task, Date firstTime, long period) 安排一个任务,等待firstTime时间开始执行,每隔period毫秒再执行(反复执行同一个任务)
  4. 终止定时器

    任务完成后,可以调用Timer类的cancel()方法来终止定时器线程

    如:timer.cancel()

Timer类调度示例

使用Timer类和TimerTask类编写时钟程序

import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.*;
import java.util.Timer;
import javax.swing.*;
class WatchGUI{
          //时钟界面类
	JFrame f  = null;     
	JLabel jl = null;     //用以显示时间
	public WatchGUI(String s){
     
		f = new JFrame(s);
		jl = new JLabel("", JLabel.CENTER);
		f.addWindowListener(new WindowAdapter() {
       //窗口关闭事件处理
			public void windowClosing(WindowEvent e) {
     
				System.exit(0);
			}
		});
	}
}
class WatchTask extends TimerTask{
         //任务类
	String s;
	WatchGUI wg;
	public WatchTask(WatchGUI wg) {
     
		this.wg = wg;
	}
	public void run() {
     
		//显示系统时间
		Calendar c = Calendar.getInstance();
		s = new String(c.get(Calendar.HOUR_OF_DAY) + ":" + c.get(Calendar.MINUTE)
		                  + ":" + c.get(Calendar.SECOND));
		wg.jl.setText(s);
		wg.f.add(wg.jl);
		wg.f.setSize(200, 120);
		wg.f.setVisible(true);
	}
}
public class Main {
     
	public static void main(String[] args){
     
		Timer timer = new Timer();             //创建定时器对象
		WatchGUI wg = new WatchGUI("Watch");   //创建时钟界面对象
		WatchTask wtask = new WatchTask(wg);   //创建任务对象
		timer.schedule(wtask, 0, 1000);        //安排任务
	}
}

备注:参考《java语言程序设计实用教程》

作者:白鳯
来源链接:https://blog.csdn.net/weixin_44368437/article/details/109607956

版权声明:
1、JavaClub(https://www.javaclub.cn)以学习交流为目的,由作者投稿、网友推荐和小编整理收藏优秀的IT技术及相关内容,包括但不限于文字、图片、音频、视频、软件、程序等,其均来自互联网,本站不享有版权,版权归原作者所有。

2、本站提供的内容仅用于个人学习、研究或欣赏,以及其他非商业性或非盈利性用途,但同时应遵守著作权法及其他相关法律的规定,不得侵犯相关权利人及本网站的合法权利。
3、本网站内容原作者如不愿意在本网站刊登内容,请及时通知本站(javaclubcn@163.com),我们将第一时间核实后及时予以删除。


本文链接:https://www.javaclub.cn/java/41996.html

分享给朋友:

“『Java多线程』基础之基础” 的相关文章

Java日志框架那些事儿

Java日志框架那些事儿

在项目开发过程中,我们可以通过 debug 查找问题。而在线上环境我们查找问题只能通过打印日志的方式查找问题。因此对于一个项目而言,日志记录是一个非常重要的问题。因此,如何选择一个合适的日志记录框架也非常重要。在Java开发中,常用的日志记录框架有JDKLog、Log4J、LogBack、SLF4J...

JDBC连接时所犯错误1.字符集设置不合适2.连接MySQL8.0社区版时时区不一致3..包名不能以Java.命名4.驱动被弃用

Microsoft JDBC Driver 的主页为:https://msdn.microsoft.com/en-us/data/aa937724.aspx 下载所需驱动 今天连接时报了四次错,记录下来 1.java.sql.SQLException:...

冒泡排序的原理,思路,以及算法分析(Java实现)

冒泡排序的原理,思路,以及算法分析(Java实现)

冒泡排序 如果遇到相等的值不进行交换,那这种排序方式是稳定的排序方式。 1.原理:比较两个相邻的元素,将值大的元素交换到右边 2.思路:依次比较相邻的两个数,将比较小的数放在前面,比较大的数放在后面。 (1)第一次比较:首先比较第...

枚举法 之Java实现凑硬币

问题? 如何利用1元五元十元凑硬币 Scanner in=new Scanner(System.in); int amout ; amout=in.nextInt(); for(int one =0;one<=amout;one+...

java中将英尺换算为身高

java中将英尺换算为身高

直接上代码 如图所示便是身高的换算,你学到了吗?、 int foot; double inch; Scanner in=new Scanner(System.in); foot=in.nextInt(); inch=in.nextDouble...

Java获取明天的时间(当前时间加一天)

import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Calendar;     public class&nbs...

全网最细笔记java与kotlin的一些异同

本文主要介绍java与kotlin的一些异同 后面可能还会继续比较kotlin和dart 期待吗? 打印日志 Java System.out.print("Amit Shekhar"); S...

图解 Java IO : 二、FilenameFilter源码

图解 Java IO : 二、FilenameFilter源码

Writer      :BYSocket(泥沙砖瓦浆木匠) 微         博:BYSocket 豆  &...

Java 容器 & 泛型:三、HashSet,TreeSet 和 LinkedHashSet比较

Java 容器 & 泛型:三、HashSet,TreeSet 和 LinkedHashSet比较

Writer:BYSocket(泥沙砖瓦浆木匠) 微博:BYSocket 豆瓣:BYSocket 上一篇总结了下ArrayList 、LinkedList和Vector比较,今天泥瓦匠总结下Hash 、LinkedList和Vector比较。其实大家都是...

java 实现图片压缩

转载https://www.cnblogs.com/strongmore/p/14158639.html 添加依赖 <dependency> <groupId>net.coobird</groupId> <artifa...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。