JAVA多线程

进程和线程

进程
是指运行中的应用程序,
一个应用程序可以同时启动多个进程。
进程是系统中独立存在的实体,拥有自己独立的资源,拥有自己私有的地址空间。
进程是系统进行资源分配和调度的一个独立单位。

线程
是程序执行流的最小单元。
线程是进程中的一个实体,是被系统独立调度和分派的基本单位
线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源

线程和进程的主要区别在于:
每个进程都需要操作系统为其分配独立的内存地址空间,而同一进程中的所有线程在同一块地址空间中工作,这些线程可以共享同一块内存和系统资源

多线程

概念

定义:
在单个程序中同时运行多个线程完成不同的工作,称为多线程。

实现方法:
继承Thread类和实现Runnable接口
但是由于:
Java语言中只能继承一个类,但可以实现多个接口
一般场景下,我们应尽量选择实现Runnable接口
而且Runnable接口可以实现资源共享。

几个小要点:
每个Java程序最少有一个执行线程。当运行程序的时候, JVM运行负责调用main()方法的执行线程。
无论在任何时候,都可以用Thread.currentThread().getName()来获取当前线程的名字。

实现

继承Thread类

具体步骤:

  • 定义一个继承Thread类的子类,并重写该类的run()方法;
  • 创建Thread子类的实例,即创建了线程对象;
  • 调用该线程对象的start()方法启动线程。

start()方法:用于启动线程,
线程进入就绪状态,该线程获得CPU,立即启动线程,线程一旦启动,执行该对象的run()方法。

注意:

  • 启动线程,调用start方法,不要直接调用run方法
  • 不要覆盖start方案(因为start调用底层代码)
  • 不要重复调用start方法

测试代码:

class Mythread extends Thread {
	public void run() {
		for (int i = 1; i <= 10; i++) {
			System.out.println(Thread.currentThread().getName() + "  " + i);
		}
	}
}

public class Main {

	public static void main(String[] args) {
		for (int i = 1; i <= 10; i++) {
			System.out.println(Thread.currentThread().getName() + "  " + i);
		}
		Scanner sc = new Scanner(System.in);
		Mythread my = new Mythread();// 新建状态
		my.start();// 就绪状态(cpu不一定会运行这个进程)
	}
}

结果

main 1
....
main 10
Thread-0 1
.....
Thread-0 10

可以得出系统正在多线程执行:
因为主函数在输出的Thread-0 x的时候还在进行,(主函数一旦停止,整个程序就停止了。)

利用Runnable接口

基本过程:

  • 定义Runnable接口的实现类,并重写该接口的run()方法;
  • 创建Runnable实现类的实例,并以此实例作为Thread的target对象,即该Thread对象才是真正的线程对象。
    测试代码:
class MyRunnable implements Runnable {
	public void run() {
		for (int i = 1; i <= 10; i++) {
			System.out.println(Thread.currentThread().getName() + "  " + i);
		}
	}
}

public class Main {

	public static void main(String[] args) {
		for (int i = 1; i <= 10; i++) {
			System.out.println(Thread.currentThread().getName() + "  " + i);
		}
		// 创建一个MyRunnable对象
		MyRunnable my = new MyRunnable();

		// 封装出一个线程对象
		Thread thread = new Thread(my);

		// 启动
		thread.start();
	}

}

实现共享数据

方法:
将需要共享的变量设为类变量即可。
测试代码:


class MyRunnable1 implements Runnable {
	public void run() {
		for (int i = 1; i <= 10; i++) {
			System.out.println(Thread.currentThread().getName() + "  " + "编号为" + i + "的票已经卖了。");
		}
	}
}
//实现资源共享
class MyRunnable2 implements Runnable {
	private int num = 10;// 实现资源共享设为私有的,安全

	public void run() {
		while (true) {
			if (num <= 0)//停止条件
				break;
			System.out.println("在" + Thread.currentThread().getName() + "  " + " 编号为" + num-- + "的票已经卖了。");
			//num--一定设置在此语句里面,不然可能出现同一编号的票,被两个窗口卖。

			try {
				Thread.sleep(1);// 执行一次就休眠1毫秒,尽量让每个窗口都能卖票。
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}

		}
	}
}

public class Main {
	public static void Test1() throws InterruptedException {
		System.out.println("三个窗口同时各卖10张票");
		MyRunnable1 myRunnable1 = new MyRunnable1();
		Thread myTread1 = new Thread(myRunnable1, "窗口1");// 创建线程时同时重命名线程名称为:窗口1
		MyRunnable1 myRunnable2 = new MyRunnable1();
		Thread myTread2 = new Thread(myRunnable2, "窗口2");// 创建线程时同时重命名线程名称为:窗口2
		MyRunnable1 myRunnable3 = new MyRunnable1();
		Thread myTread3 = new Thread(myRunnable3, "窗口3");// 创建线程时同时重命名线程名称为:窗口3
		myTread1.start();// 启动线程
		myTread1.join();// 让该方法的所有线程插队,保证该方法执行完毕之后,再执行方法2
		myTread2.start();// 启动线程
		myTread2.join();// 让该方法的所有线程插队,保证该方法执行完毕之后,再执行方法2
		myTread3.start();// 启动线程
		myTread3.join();// 让该方法的所有线程插队,保证该方法执行完毕之后,再执行方法2
	}

	public static void Test2() throws InterruptedException {
		System.out.println("三个窗口共同卖10张票");
		MyRunnable2 myRunnable = new MyRunnable2();
		Thread myTread1 = new Thread(myRunnable, "窗口1");// 创建线程时同时重命名线程名称为:窗口1
		Thread myTread2 = new Thread(myRunnable, "窗口2");// 创建线程时同时重命名线程名称为:窗口2
		Thread myTread3 = new Thread(myRunnable, "窗口3");// 创建线程时同时重命名线程名称为:窗口3
		myTread1.start();// 启动线程
		myTread2.start();// 启动线程
		myTread3.start();// 启动线程
	}

	public static void main(String[] args) throws InterruptedException {
		Test1();
		System.out.println("=========================");
		Test2();
	}
}

守护/后台线程

在Java中有两类线程:前台/用户线程 (User Thread)、后台/守护线程 (Daemon Thread)。

概念

后台线程是指: 在程序运行的时候在后台提供一种通用服务的线程。
一般来说这种线程不是程序必须的部分。(比如垃圾回收线程)

前台线程和后台线程关系:
前台线程执行,后台线程执行;
前台线程不执行,后台线程立即停止

注意:
当所有的前台线程结束时,程序也就终止了。(系统会自动:会杀死进程中的所有后台线程。)

实现方法

在start()之前,使用thread.setDaemon(true)进行设置

线程的调度

线程的生命周期

image
线程共包括以下5种状态。

  1. 新建状态(New): 线程对象被创建后,就进入了新建状态。此时它和其他Java对象一样,由Java虚拟机分配了内存,并初始化其成员变量值。

  2. 就绪状态(Runnable): 也被称为“可执行状态”。处于就绪状态的线程,随时可能被CPU调度执行,取决于JVM中线程调度器的调度。

线程对象被调用了该对象的start()方法,该线程处于就绪状态。

  1. 运行状态(Running): 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。

  2. 阻塞状态(Blocked): 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。

阻塞的情况分三种:
(1) 等待阻塞 -- 通过调用线程的wait()方法,让线程等待某工作的完成。
(2) 同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
(3) 其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

  1. 死亡状态(Dead): 该线程结束生命周期。

线程执行完了、因异常退出了run()方法或者直接调用该线程的stop()方法会处于此状态

Thread类调度线程

线程的优先级
虚拟机按照特定机制为线程分配CPU使用权
分时调度(CPU时间片)、抢占式调度(线程优先级)

优先级分为1~10,默认为5;但是需要注意优先级别高的不一定先执行,只是先执行的可能性大。

setPriority(int newPriority)//设置线程优先级
getPriority() //返回指定线程的优先级
启动线程

void start()//进入就绪状态

线程让步

void yield();//提示线程调度器当前线程愿意放弃当前CPU的使用,自愿转换为就绪状态

如果当前资源不紧张,调度器可以忽略这个提示。
不阻塞该线程,

线程休眠

void sleep(long mills)//进入休眠等待状态

线程插队

void join() // join线程A,会使得线程B进入等待,直到线程A结束
void join(long mills) //join线程A,会使得线程B进入等待,直到到达给定的时间
可以用来实现:让线程/进程A运行结束再执行线程/进程B。

测试代码:


public class My2 {
	public static void main(String[] args) {
		MyThreadd myThread = new MyThreadd();
		myThread.start();
		Thread.yield();//Main线程让步了
		for (int i = 0; i < 10; i++) {
			System.out.println(Thread.currentThread().getName() + "--" + i);
		}
}
}
class MyThreadd extends Thread {
	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.println(Thread.currentThread().getName() + "==" + i);
		}
	}
}
/*
如果Main中	Thread.yield();存在
*结果1:Thread-0==0
****
Thread-0==9
main--0
****
main--9
*/

/*
如果Main中	Thread.yield();存在
*结果2:
main--0
*****
main--9
Thread-0==0
*****
Thread-0==9

*/

测试join

	for (int i = 0; i < 10; i++) {
	System.out.println(Thread.currentThread().getName() + "--" + i);
	if(i==2)
	{
		myThread.join(1);
	}
}
/*结果:main--0
main--1
main--2
Thread-0==0
******
Thread-0==9
main--3
******
main--9

*/

多线程同步

概述

为什么需要多线程同步?
多线程并发执行能够提高执行效率,但会引发安全问题。
比如: 因为线程之间会共享资源,对于“买卖票”的场景,同一时间“买”和“卖”两个线程同时工作
可能会得到:已经没有货物了,但是还能“卖”东西。显然是不对的。

常见线程安全问题:
多窗口卖票问题
同步代码块
同步方法
同步锁

多线程同步作用:
限制某个资源在同一时刻只能被一个线程访问。

实现方法

实现想法:

  1. 锁住应用,使应用最多只能允许一个线程运行,保证线程运行唯一性。
  2. 通过让线程等待和运行的命令,让程序有序进行。
    实现锁住应用: 使用:synchronized关键字修饰。

synchronized

作用:
多线程环境下, 线程1 在执行 synchronized 代码块时,线程2 再执行到这段代码块时就会被阻塞。只有当线程1 执行完此同步方法后,才会释放锁对象,线程2 才有可能获取此同步锁。
原理:
由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
注意:

  1. synchronized是和if、else、for、while一样的关键字
  2. 无法中断一个正在等候获得锁的线程
  3. 无法通过轮询得到锁
    4.同时最多只有一个线程能够获得同步锁

实现运行和等待命令: 使用wait和notify方法

wait和notify

wait() //使当前线程等待使其进入到等待阻塞状态,同时wait()也会让当前线程释放它所持有的锁。 直到其他线程调用该同步锁对象的notify()或notifyAll()方法来唤醒此线程。
wait(long timeout)//让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法唤醒此线程,或者 超过指定的时间量”。
notify() //唤醒在此同步锁对象上等待的单个线程,如果有多个线程都在此同步锁对象上等待,则会任意选择其中某个线程进行唤醒操作,只有当前线程放弃对同步锁对象的锁定,才可能执行被唤醒的线程。
notifyAll()//唤醒所有线程。

所以实现方法: 就是

  1. 使用:synchronized关键字修饰方法,获取该对象的同步锁。
  2. 使用wait和notify方法有序调配线程进行。

同步途径

我们只需掌握第一个“同步方法”即可。

同步方法(常用)

解释:
用synchronized关键字修饰方法。 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。

注意:
如果想让同步线程多次执行,需要再设置继承Thread类的线程控制

class Value {
	int value = 0;// 生产对象
	boolean flag = false;// 判断生产消费状态,生产完毕(待消费)为TRUE,消费完毕(待生产)为FALSE
	
	// 消费者线程
	public synchronized void GetValue() throws InterruptedException {

		if (flag == false) {
			wait();// 等待
		}
		System.out.println("消费者已经购买第" + (value) + "件产品");
		flag = false;
		notify();// 唤醒生产者者线程
	}

	// 生产者线程
	public synchronized void SetValue(int value) throws InterruptedException {

		if (flag == true) {
			wait();// 等待
		}
		this.value = value;
		System.out.println("生产者已经生产第" + (value) + "件产品");
		flag = true;
		notify();// 唤醒消费者线程

	}
}
//建立生产者线程
class SetThread extends Thread {
	Value value;

	public SetThread(Value value) {//带参数构造方法
		super();
		this.value = value;
	}

	public void run() {
		for (int i = 1; i <= 20; i++) {//执行20次
			try {
				value.SetValue(i);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
}
//建立消费者线程
class GetThread extends Thread {
	Value value;

	public GetThread(Value value) {//带参数构造方法
		super();
		this.value = value;
	}

	public void run() {
		for (int i = 1; i <= 20; i++) {//执行20次
			try {
				value.GetValue();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
}

public class Main {

	public static void main(String[] args) throws InterruptedException {
		Value value = new Value();
		SetThread setThread = new SetThread(value);
		GetThread getThread = new GetThread(value);
		setThread.start();//启动生产者线程
		getThread.start();//启动消费者线程
	}

}


同步代码块

用synchronized关键字修饰语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步
原因:
同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

public class TicketRunnable implements Runnable {
	int count = 10000;
	Object lock = new Object();
	@Override
	public void run() {
		while (count > 0) {
			synchronized (lock) {
			      System.out.println(Thread.currentThread().getName() + "卖出第" + (10001 - count) + "张票");
			     count--;
			}
		}
	}
}

同步锁

使用ReentrantLock类
ReenreantLock类的常用方法有:
ReentrantLock() // 创建一个ReentrantLock实例
lock() //获得锁
unlock() //释放锁
优势:
让某个线程在持续获取同步锁失败后返回,不再继续等待

public class TicketRunnable implements Runnable {
	int count = 100;
	private final Lock lock=new ReentrantLock();
	
	@Override
	public void run() {		
		while (count > 0) {
			lock.lock();
			System.out.println(Thread.currentThread().getName() + "卖出第" + (101 - count) + "张票");
			count--;
			lock.unlock();		
		}
	}
}
posted @ 2021-12-13 09:50  kingwzun  阅读(97)  评论(0编辑  收藏  举报