初识多线程

一文彻底搞懂多线程、高并发原理和知识点

📁Java并发体系

彻底理解Java中的各种锁-图灵课堂

进程和线程:

(1)什么是进程

1)例如记事本酷狗音乐等软件,程序启动运行后,操作系统会给其分配系统资源和内存空间;进程是系统层面的资源调度和分配单位;

2)进程就是在内存中运行的应用程序,进程都有自己独立的内存空间,该进程在执行过程中可以独享该块内存空间,所以一个进程出问题,不会影响其他进程;

3)大部分简单的应用程序只会对应一个进程;某些复杂的如杀毒软件、数据库软件等是由多个进程组成的;

4)早期的操作系统是单任务的,同时只能运行一个程序;现在的操作系统大多支持多程序同时执行,这就是进程在起作用。

(2)什么是线程

1) 线程是进程的一个执行单元,其负责当前进程中某部分程序的执行。一个进程中至少有一个线程,一个进程也可以运行多个线程,多个线程间可共享数据;

2.1)多个线程是共享某进程的内存等资源的,一个线程没有独立的运行空间,一个线程出问题,会影响其他线程的正常运行;所以,多进程的程序比多线程的程序更健壮;但是,因为多进程切换时,资源损耗较大,所以多进程比多线程效率低。

2.2)与进程不同的是同类的多个线程共享进程的 方法区 资源,但每个线程有自己的 程序计数器虚拟机栈本地方法栈 ,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

(3)进程和线程的区别

(摘自:https://blog.csdn.net/ThinkWon/article/details/102021274

线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight Process),它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有若干个线程,至少包含一个线程。

根本区别 :进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位

资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

内存分配 :同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的

影响关系 :一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行。

(4)线程提升效率的原因

【多线程能提高运行效率的前提】是线程本身不会一直占用CPU且不占CPU的空闲时间大于线程切换时间,否则在线程时间运行效率变低,在线程需100%占用CPU时,CPU利用率也不会得到提升。即线程是一种压榨CPU的一种方式,使CPU尽量一刻不得闲,从而提升效率。如果,CPU已经达到了极限,再使用线程是起不到作用的。


Thread类创建线程:

(1)Thread类的构造方法:

●Thread(Runnable target),正是通过Runnable接口创建线程时需要用到的构造方法;

(2)Thread类常用方法

●不同功能的线程的区别就在于run()方法中的代码不同;

(3)Thread类也是实现了Runnable接口的

(4)Thread类创建线程示例程序

Java规定一个类必须继承Thread类(或实现Runnable接口)才能成为一个线程类,Java就是这样设计和规定的。

简单示例:其中通过 MyThread myThread = new MyThread(); 实例化线程类得到一个线程,主方法又是一个线程;

(1)创建一个类继承Thread类,得到线程类;(2)线程类添加run()方法,在run方法中添加线程相关代码;(3)实例化线程类对象,调用start()方法启动线程;

// 一个类需要继承Thread类,才能成为一个线程类
class MyThread extends Thread{
 
	// 所有和线程相关的代码都写在run()方法内
	public void run(){
		System.out.println("线程正在执行");
	}
}
 
public class ThreadTest {
	public static void main(String[] aegs){
		MyThread myThread = new MyThread();
		// 一个线程不能多次调用start()方法,即不能多次启动。其不会编译报错,但会产生运行时错误
		myThread.start();
		System.out.println("主方法的线程");
	}
}

……………………………………………………

简单示例:同上;

(1)创建一个类继承Thread类,得到线程类;

(2)线程类添加run()方法;

(3)线程类添加有参构造方法,通过调用Thread类的有参构造,得线程类的有参构造,该有参构造方法可以指定线程的name;

(4)调用线程类的有参构造,MyThread mythread1 = new MyThread(“线程1”);创建线程;线程类每实例化一次就得到一个实际的线程;

(5)在线程类中可以直接调用getName()方法得到当前线程的name;

class MyThread extends Thread{
 
	public MyThread(){}
	// 定义一个构造方法,通过调用父类Thread类的构造方法,给线程指定一个name
	public MyThread(String name){
		super(name);
	}
	// 所有和线程相关的代码都写在run()方法内
	public void run(){
		for(int i = 1;i<=10;i++){
			// 因为当前类是直接继承Thread类的,所以可以直接使用getName()方法,而无需通过实例化对象等方式调用
			System.out.println(getName()+"执行第"+i+"次;");
		}
	}
}
 
public class ThreadTest {
 
	public static void main(String[] args) {
		// 一个类继承了Thread类,并实现run方法后,该类便成了一个线程类,该类的每实例化一个对象,就是新创建了一个实际的线程。
		MyThread mythread1 = new MyThread("线程1");
		MyThread mythread2 = new MyThread("线程2");
		mythread1.start();
		mythread2.start();
	}
}

Runnable接口创建线程

这种方式更常用,因为Java不支持多继承。

(1)Runnable接口说明

(2)示例程序

本例是最容易理解的形式:

可以发现(

1)Printrunnable类实现Runnable接口,并实现run()方法,成为线程类;

(2)通过Thread.currentThread().getName()获取线程name,因为Runnable接口中没有getName()方法,getName()方法实在Thread类的,所以通过Thread类去获取线程名;

(3)实例化线程类对象,通过Thread的有参构造:Thread(Runnable target),得到最终的线程对象;

注:通过(3)可以发现,只有Thread类的对象,或者Thread子类的实例对象,才是最终的可以启动的进程对象;一个实现了Runnable接口的类的实例对象,可以理解为是具备了线程潜质的对象,但其并不是一个可以启动的线程对象,其还是要通过Thread thread = new Thread(Runnable target)来得到最终的线程对象。

// PrintRunnable类实现Runnable接口
class PrintRunnable implements Runnable{
 
	//int i = 1;    // 多个线程处理同一个资源的情况
	@Override
	public void run() {
		int i = 1;
		while(i <= 10){
			// 此时,不能直接使用getName()获取线程name了,因为getName()方法是Thread类里面的;
			System.out.println(Thread.currentThread().getName()+"正在运行"+(i++));
		}
	}
}
 
public class Test {
	public static void main(String[] args) {
 
		PrintRunnable pr1 = new PrintRunnable();
		// Thread类的构造方法中有这种形式:Thread(Runnable target)
		Thread t1 = new Thread(pr1);
		t1.start();
		// PrintRunnable pr2 = new PrintRunnable();
		PrintRunnable pr2 = new PrintRunnable();
		// Thread类的构造方法中有这种形式:Thread(Runnable target)
		Thread t2 = new Thread(pr2);
		t2.start();
	}
}

输出结果:

Thread-1正在运行1
Thread-0正在运行1
Thread-0正在运行2
Thread-1正在运行2
Thread-0正在运行3
Thread-1正在运行3
Thread-0正在运行4
Thread-1正在运行4
Thread-1正在运行5
Thread-1正在运行6
Thread-1正在运行7
Thread-1正在运行8
Thread-1正在运行9
Thread-1正在运行10
Thread-0正在运行5
Thread-0正在运行6
Thread-0正在运行7
Thread-0正在运行8
Thread-0正在运行9
Thread-0正在运行10

……………………………………………………

对上示例改造如下:线程类只实例化一次,通过多次调用Thread thread = new Thread(Runnable target)得到多个线程

// PrintRunnable类实现Runnable接口
class PrintRunnable implements Runnable{
 
	//int i = 1;    // 多个线程处理同一个资源的情况
	@Override
	public void run() {
		int i = 1;
		while(i <= 10){
			// 此时,不能直接使用getName()获取线程name了,因为getName()方法是Thread类里面的;
			System.out.println(Thread.currentThread().getName()+"正在运行"+(i++));
		}
	}
}
 
public class Test {
	public static void main(String[] args) {
 
		PrintRunnable pr1 = new PrintRunnable();
		// Thread类的构造方法中有这种形式:Thread(Runnable target)
		Thread t1 = new Thread(pr1);
		t1.start();
		// PrintRunnable pr2 = new PrintRunnable();
		// Thread类的构造方法中有这种形式:Thread(Runnable target)
		Thread t2 = new Thread(pr1);
		t2.start();
	}
}

输出结果:由结果也可发现,同一个线程类的实例化对象,如果多次调用Thread thread = new Thread(Runnable target),也会得到不同的线程

Thread-1正在运行1
Thread-0正在运行1
Thread-0正在运行2
Thread-0正在运行3
Thread-0正在运行4
Thread-0正在运行5
Thread-1正在运行2
Thread-1正在运行3
Thread-1正在运行4
Thread-1正在运行5
Thread-1正在运行6
Thread-1正在运行7
Thread-1正在运行8
Thread-1正在运行9
Thread-1正在运行10
Thread-0正在运行6
Thread-0正在运行7
Thread-0正在运行8
Thread-0正在运行9
Thread-0正在运行10

……………………………………………………

对上示例改造如下:把循环变量i转移到循环外

// PrintRunnable类实现Runnable接口
class PrintRunnable implements Runnable{
 
	int i = 1;    // 多个线程处理同一个资源的情况
	@Override
	public void run() {
		//int i = 1;
		while(i <= 10){
			// 此时,不能直接使用getName()获取线程name了,因为getName()方法是Thread类里面的;
			System.out.println(Thread.currentThread().getName()+"正在运行"+(i++));
		}
	}
}
 
public class Test {
	public static void main(String[] args) {
 
		PrintRunnable pr1 = new PrintRunnable();
		// Thread类的构造方法中有这种形式:Thread(Runnable target)
		Thread t1 = new Thread(pr1);
		t1.start();
		// PrintRunnable pr2 = new PrintRunnable();
		// Thread类的构造方法中有这种形式:Thread(Runnable target)
		Thread t2 = new Thread(pr1);
		t2.start();
	}
}

输出结果:多个线程处理同一个资源的情况,这是多个线程处理同一个资源的情况。之所以是多个线程处理同一个资源,是因为上面程序中,两个Thread t1 =new Thread(pr1)和Thread t2 = new Thread(pr1)使用了同一个Runnable类型的对象

Thread-0正在运行1
Thread-1正在运行1
Thread-0正在运行2
Thread-1正在运行3
Thread-0正在运行4
Thread-0正在运行6
Thread-1正在运行5
Thread-0正在运行7
Thread-0正在运行9
Thread-0正在运行10
Thread-1正在运行8

1.线程的状态

●新建(New):当创建一个Thread类或Thread子类对象时,线程就进入了新建状态;

●可运行(Runnable):当创建好的线程对象去调用start()方法后,线程就进入了可运行状态;具体线程运行与否要看CPU有没有空,即这个线程获取了CPU的使用权,这个处于可运行状态的线程才会实际执行;有时,也称可运行状态为“就绪状态”。

●正在运行(Running):一个处于可运行状态的线程,一旦获取了CPU的使用权,就可进入正在运行状态

●阻塞(Blocked):当线程遇到干扰时,会进入阻塞状态,进程处于暂停状态;

●终止(Dead)


2.线程的生命周期

线程的生命周期是线程从创建到运行一直到运行结束的过程;也是线程的五个状态的转换过程;可以通过调用Thread类的一些方法,来影响线程的状态。

●I/O请求需要耗费一定的时间,此时就可以让线程进入阻塞状态(暂时暂停),等待I/O请求完成,再继续进行执行;

●stop()方法现在不推荐用了,JDK中已标注为版本过期;

1.sleep()方法

●sleep()方法使线程由正在运行状态转换到阻塞状态;线程暂时放弃CPU使用权

●sleep()方法是Thread类中的方法;

●public static void sleep(long millis):静态方法,可直接用Thread类名字去调用;millis:毫秒,1000ms == 1s;

●sleep()方法:应用场景可能有(1)需要计时的情况,每隔一定时间显示一下时间等;(2)对于一些需要定期刷新数据而不是一直刷新数据的情况

●需要注意一个线程到底能不能执行,或者说一个线程到底能不能“暂停”,不仅和sleep()方法有关,还与CPU是否可用有关;当一个线程sleep()方法执行完毕后,线程只是进入了可运行状态,该线程必须获得CPU的使用权才能真正开始执行。(所以,sleep()方法应用在严格的时钟计算时,可能会存在误差);

……………………………………………………

示例程序:

注:sleep()方法需要捕获异常;

sleep()方法的具体应用场景和应用技巧需要在实际开发中慢慢积累

class MyThread implements Runnable{
 
	@Override
	public void run() {
		// TODO Auto-generated method stub
		for(int i = 1;i<=15;i++){
			System.out.println(Thread.currentThread().getName()+"执行第"+i+"次。");
			try {
				// 为什么要捕获异常:在线程执行sleep()方法时候,可能会因为某些原因线程突然被打断,要处理一下这种异常
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
}

public class SleepDemo {
 
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		MyThread mt = new MyThread();
		Thread thread = new Thread(mt);
		thread.start();
		//Thread thread1 = new Thread(mt);
		//thread1.start();
	}
}

2.join方法

●join()方法使线程由正在运行状态转换到阻塞状态;线程暂时放弃CPU使用权

●join()方法也是Thread类的方法;

●public final void join():是个final方法,字面理解join是加入、抢先进入的意思;

●一个线程调用了join()方法,那么这个线程获得了优先执行权,其他线程需要等待该线程执行完毕后才能执行;join()方法是一种抢占资源的方式;

……………………………………………………

示例程序:join(),无参形式,调用join()方法线程执行结束后,其他线程才会执行。

注:join()方法也需要捕获异常;

join()方法的具体应用场景和应用技巧需要在实际开发中慢慢积累;

class MyThread1 extends Thread{
	public void run(){
		for(int i = 1;i<=10;i++){
			System.out.println(getName()+"正在执行"+i+"次。");
		}
	}
}
 
public class JoinDemo {
 
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		MyThread1 mt = new MyThread1();
		mt.start();
		try {
			// 为什么要捕获异常:在线程执行join()方法时候,可能会因为某些原因线程突然被打断,要处理一下这种异常
			mt.join();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		for(int i = 1;i<=10;i++){
			System.out.println("主线程运行第"+i+"次。");
		}
		System.out.println("主线程运行结束。");
	}
}

输出结果:可以发现,因为mt线程调用了join()方法,所以mt线程获得了优先执行权;

Thread-0正在执行1次。
Thread-0正在执行2次。
Thread-0正在执行3次。
Thread-0正在执行4次。
Thread-0正在执行5次。
Thread-0正在执行6次。
Thread-0正在执行7次。
Thread-0正在执行8次。
Thread-0正在执行9次。
Thread-0正在执行10次。
主线程运行第1次。
主线程运行第2次。
主线程运行第3次。
主线程运行第4次。
主线程运行第5次。
主线程运行第6次。
主线程运行第7次。
主线程运行第8次。
主线程运行第9次。
主线程运行第10次。
主线程运行结束。

若是,mt线程没有调用join方法:

class MyThread1 extends Thread{
	public void run(){
		for(int i = 1;i<=10;i++){
			System.out.println(getName()+"正在执行"+i+"次。");
		}
	}
}
 
public class JoinDemo {
 
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		MyThread1 mt = new MyThread1();
		mt.start();
//		try {
//			// 为什么要捕获异常:在线程执行join()方法时候,可能会因为某些原因线程突然被打断,要处理一下这种异常
//			mt.join();
//		} catch (InterruptedException e) {
//			// TODO Auto-generated catch block
//			e.printStackTrace();
//		}
		for(int i = 1;i<=10;i++){
			System.out.println("主线程运行第"+i+"次。");
		}
		System.out.println("主线程运行结束。");
	}
}

输出结果:mt线程没有调用join()方法时,mt线程和主线程随机获取CPU使用权。

主线程运行第1次。
Thread-0正在执行1次。
主线程运行第2次。
Thread-0正在执行2次。
主线程运行第3次。
Thread-0正在执行3次。
Thread-0正在执行4次。
主线程运行第4次。
主线程运行第5次。
主线程运行第6次。
主线程运行第7次。
主线程运行第8次。
主线程运行第9次。
主线程运行第10次。
主线程运行结束。
Thread-0正在执行5次。
Thread-0正在执行6次。
Thread-0正在执行7次。
Thread-0正在执行8次。
Thread-0正在执行9次。
Thread-0正在执行10次。

……………………………………………………

示例程序:join(long millis):调用join(long millis)方法的线程,其优先执行权只能维持一定时间,一定时间过后丧失优先权,和其他线程一样随机获取执行权

class MyThread1 extends Thread{
	public void run(){
		for(int i = 1;i<=300;i++){
			System.out.println(getName()+"正在执行"+i+"次。");
		}
	}
}
 
public class JoinDemo {
 
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		MyThread1 mt = new MyThread1();
		mt.start();
		try {
			// 为什么要捕获异常:在线程执行join()方法时候,可能会因为某些原因线程突然被打断,要处理一下这种异常
			mt.join(2);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		for(int i = 1;i<=10;i++){
			System.out.println("主线程运行第"+i+"次。");
		}
		System.out.println("主线程运行结束。");
	}
}

输出结果:输出结果截取部分,由执行结果可以发现join(long millis)方法作用。

Thread-0正在执行66次。
Thread-0正在执行67次。
Thread-0正在执行68次。
Thread-0正在执行69次。
主线程运行第1次。
Thread-0正在执行70次。
Thread-0正在执行71次。
主线程运行第2次。
Thread-0正在执行72次。
主线程运行第3次。
Thread-0正在执行73次。
主线程运行第4次。
Thread-0正在执行74次。
主线程运行第5次。
Thread-0正在执行75次。
主线程运行第6次。
Thread-0正在执行76次。
主线程运行第7次。
Thread-0正在执行77次。
主线程运行第8次。
Thread-0正在执行78次。
Thread-0正在执行79次。
Thread-0正在执行80次。
Thread-0正在执行81次。
Thread-0正在执行82次。
Thread-0正在执行83次。
Thread-0正在执行84次。
Thread-0正在执行85次。
Thread-0正在执行86次。

3.线程的优先级

●Java为线程提供了10个优先级,优先级可以使用整数1-10表示,超过这个范围会抛出异常;数字越大,表示优先级越高

●main()方法的线程(主线程)默认优先级是5;

●除了用数字表示优先级,还可以用优先级常量来表示优先级:

MAX_PRIORITY:线程的最高优先级10;

MIN_PRIORITY:线程的最低优先级1;

NORM_PRIORITY:线程的默认优先级5;

●Thread类提供了获取线程优先级的方法、和设置线程优先级的方法:

●线程的优先级只是提供了一个优先级别,优先级高的线程不一定非得先执行,是否先执行与CPU状态,系统状态等很多因素有关;即其执行还是有一定的随机性的;

……………………………………………………

示例程序:主要就是setPriority()方法、和getPriority()方法;

class MyThread2 extends Thread{
	private String name;
	public MyThread2(){}
	public MyThread2(String name){
		// 注:给线程自定义名字,需要调用Thread的有参构造才行,即需要super(name)才行;
		// 这儿的name只是一个方便观察的变量,其实并不是线程的name.
		this.name = name;
	}
	public void run(){
		for(int i = 1;i<=10;i++){
			System.out.println("线程"+name+"正在执行"+i);
		}
	}
}
 
public class PriorityDemo {
 
	public static void main(String[] args) {
		// 获取主线程的优先级
		int mainPriority = Thread.currentThread().getPriority();
		//System.out.println("主线程的优先级为:"+mainPriority);
		MyThread2 mt1 = new MyThread2("线程1");
		MyThread2 mt2 = new MyThread2("线程2");
		int mt1Priority = mt1.getPriority();
		//System.out.println("mt1线程的优先级为:"+mt1Priority);
		// 设置mt1线程的优先级
		//mt1.setPriority(10);
		//或者这样设置mt1的优先级
		mt1.setPriority(Thread.MAX_PRIORITY);
		mt2.setPriority(Thread.MIN_PRIORITY);
		mt1.start();
		mt2.start();
	}
}

输出结果:可以发现,并不是高优先级的线程先执行完后低优先级的线程才执行,其任然存在一定的随机性;

换句话说,优先级高的程序并不一定会比优先级低的线程先运行;

线程线程1正在执行1
线程线程2正在执行1
线程线程1正在执行2
线程线程2正在执行2
线程线程1正在执行3
线程线程2正在执行3
线程线程1正在执行4
线程线程1正在执行5
线程线程1正在执行6
线程线程1正在执行7
线程线程1正在执行8
线程线程1正在执行9
线程线程1正在执行10
线程线程2正在执行4
线程线程2正在执行5
线程线程2正在执行6
线程线程2正在执行7
线程线程2正在执行8
线程线程2正在执行9
线程线程2正在执行10

线程同步

由前面几篇文章可知: 线程什么时候运行是不确定的 ,即:

●各个线程是通过竞争CPU的时间而获得运行机会的;

●各线程什么时候获得CPU时间,占用多久,是不可预测的;

●一个正在运行的线程在什么地方被暂停是不确定的;

●可能一个线程中一个方法(或一个代码块)刚执行到一半,这个线程就丧失了CPU的使用权,从而导致很多问题;

上面几条都透露着几个字:不确定。这个问题,在实际开发中也会带来不可预测的结果,问题;

……………………………………………………

如下面问题:银行账户的存款和取款。

(1)首先,存款和取款各是不同的线程在执行

(2)存款操作和取款操作都是分很多步骤执行;

(3)不同的线程之间切换是随机的,即可能取款线程执行一半就停了,转去执行存款线程,而存款线程执行一般也可能暂停,转去执行取款线程;这种现象就会导致,最终的账户金额存在错误计算

如下面代码:看个大概即可,程序很简单,主要是传达一种问题而已。

(1)Bank类,主要包括,存款方法和取款方法;(这儿用slee()方法,防止输出结果输出太快,看不清;而且实际中,一个方法的执行也需要一定的时间的);

public class Bank {
	private String account;// 账号
	private int balance;// 账户余额
 
	public Bank(String account, int balance) {
		this.account = account;
		this.balance = balance;
	}
 
	public String getAccount() {
		return account;
	}
 
	public void setAccount(String account) {
		this.account = account;
	}
 
	public int getBalance() {
		return balance;
	}
 
	public void setBalance(int balance) {
		this.balance = balance;
	}
 
	@Override
	public String toString() {
		return "Bank [账号:" + account + ", 余额:" + balance + "]";
	}
 
	// 存款
	public void saveAccount() {
 
		// 获取当前的账号余额
		int balance = getBalance();
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		// 修改余额,存100元
		balance += 100;
		// 修改账户余额
		setBalance(balance);
		// 输出存款后的账户余额
		System.out.println("存款后的账户余额为:" + balance);
	}
 
	public void drawAccount() {
			// 在不同的位置处添加sleep方法
 
			// 获得当前的帐户余额
			int balance = getBalance();
			// 修改余额,取200
			balance = balance - 200;
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			// 修改帐户余额
			setBalance(balance);
			System.out.println("取款后的帐户余额:" + balance);
 
	}
}

(2)取款类,做成了一个线程类(因为取款操作是一个单独的线程)

//取款
public class DrawAccount implements Runnable{
	Bank bank;
	public DrawAccount(Bank bank){
		this.bank=bank;
	}
	@Override
	public void run() {
		bank.drawAccount();
	}
 
}

(3)存款类,也做成了一个线程类

//存款
public class SaveAccount implements Runnable{
	Bank bank;
	public SaveAccount(Bank bank){
		this.bank=bank;
	}
	public void run(){
		bank.saveAccount();
	}
}

(4)测试类

public class Test {
 
	public static void main(String[] args) {
		// 创建帐户,给定余额为1000
		Bank bank=new Bank("1001",1000);
		//创建线程对象
		SaveAccount sa=new SaveAccount(bank);
		DrawAccount da=new DrawAccount(bank);
		Thread save=new Thread(sa);
		Thread draw=new Thread(da);
		save.start();
		draw.start();
		try {
 
			draw.join();
			save.join();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println(bank);
	}
 
}

解决办法:

●为了保证在存款或取款的时候,不允许其他线程对账户余额进行操作;需要将Bank对象进行锁定,即某个线程在操作Bank对象时,其他线程都不允许进行操作

●使用关键字 synchronized(同步关键字)实现;该关键字,可以确保共享对象,在同一个时刻只能被一个线程访问,这种机制成为线程同步,或线程互斥。

● synchroniced具体的使用技巧,需要在以后的实际开发中逐步加深理解,逐步总结其应用场景,惯用应用技巧等。

synchroniced:

如上面Bank类改造如下:分别在存款方法和取款方法处,使用了synchroniced关键字。

如: saveAccount()方法添加synchronized后表示:这个方法中的所有代码在执行完毕之前,其他线程是不能把他打断的

public class Bank {
	private String account;// 账号
	private int balance;// 账户余额
 
	public Bank(String account, int balance) {
		this.account = account;
		this.balance = balance;
	}
 
	public String getAccount() {
		return account;
	}
 
	public void setAccount(String account) {
		this.account = account;
	}
 
	public int getBalance() {
		return balance;
	}
 
	public void setBalance(int balance) {
		this.balance = balance;
	}
 
	@Override
	public String toString() {
		return "Bank [账号:" + account + ", 余额:" + balance + "]";
	}
 
	// 存款
	// saveAccount()方法添加synchronized后表示:这个方法中的所有代码在执行完毕之前,其他线程是不能把他打断的。
	public synchronized void saveAccount() {
 
		// 获取当前的账号余额
		int balance = getBalance();
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		// 修改余额,存100元
		balance += 100;
		// 修改账户余额
		setBalance(balance);
		// 输出存款后的账户余额
		System.out.println("存款后的账户余额为:" + balance);
	}
 
	public void drawAccount() {
		// 语句块添加synchronized后表示:这个方法中的所有代码在执行完毕之前,其他线程是不能把他打断的。
		// 因为当前语句块是针对Bank类的,即是针对Bank类型的对象的,所以直接this就行了
		synchronized (this) {
			// 在不同的位置处添加sleep方法
 
			// 获得当前的帐户余额
			int balance = getBalance();
			// 修改余额,取200
			balance = balance - 200;
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			// 修改帐户余额
			setBalance(balance);
			System.out.println("取款后的帐户余额:" + balance);
		}
 
	}
}

线程间通信

可以设想这样一种情况:取款时,账务余额不够了;顺理成章的解决方案是:等待存入足够的钱以后再进行取款操作。(这种业务场景很多,不要仅仅局限于银行存取款喽)

这儿涉及线程间通信。

如下示例:

业务情况:

Queue类:该类相当于一个容器,其有一个变量n;

Producer类:充当生产者线程,该类负责对变量n的值进行加1运算;

Consumer类:充当消费者线程,该类负责对变量n的值进行减1运算;

或者认为Producer类负责数据的生产,Consumer类负责数据的取用;

业务要求:生产一个消费一个。

……………………………………………………

1.首先,是没有添加线程间通信的情况:

Queue类:

public class Queue {
	private int n;
 
	public synchronized int getN() {
		System.out.println("消费:"+n);
		return n;
	}
 
	public synchronized void setN(int n) {
		System.out.println("生产:"+n);
		this.n = n;
	}
}

Producer类,生产类:

// Queue类中包含  创造数据和获取数据两个方法(或称两个业务),若想让两个业务各自相对独立,需要涉及两个独立的线程类,分别对应两个业务的逻辑方法
// 这儿,演示意味浓重,在实际开发中,具体哪些逻辑代码需要设置成一个线程,需要慢慢累积经验
public class Producer implements Runnable{
 
	Queue queue;   // 定义一个Queue类型的变量,再实例化本线程类的时候,该变量会指向线程类实际指向的Queue对象
	public Producer(){}
	public Producer(Queue queue){
		this.queue = queue;
	}
	@Override
	public void run() {
		int i = 0;
		while(true){
			queue.setN(i++);
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
}

Consumer类,消费类:

public class Consumer implements Runnable{
	Queue queue;  // 定义一个Queue类型的变量,再实例化本线程类的时候,该变量会指向线程类实际指向的Queue对象
	public Consumer(){}
	public Consumer(Queue queue){
		this.queue = queue;
	}
 
	@Override
	public void run() {
 
		while(true){
			queue.getN();
 
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
}

Test类,测试类:

public class Test {
 
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Queue queue = new Queue();
		// (1)这个例子,主要说明,在实际业务中,两个进程可以交叉运行,交叉运行的控制机制不能单靠sleep()等控制执行时间的方式;
		// 如果两个线程是互相配合的情况,还需要两个线程互相交流一些数据,只有如此,才能在符合业务逻辑需求的情况下,正确地执行程序
		// (2)下面两个线程,因为线程方法处添加了synchronized同步关键字,这可以保证,在一个方法执行完毕前该线程不会被中途打断;
		// 但,如果当方法(本次)执行完毕,调用的sleep()时间过去后,下一次到底是执行生产方法还是消费方法,是随机的,并没有参考n的值到底是多少;(这个sleep()仅仅是
		// 为了演示方便,拖长方法的执行时间,否则输出结果滑太快,看不清楚输出)
		// (3)也可以发现,n的值是生产类初始化和控制的,消费类只是去获取n的值而已;按照逻辑是,生产类每生产一个,消费类紧接着就消费一个;但因为,生产类和消费
		// 类之间到底该执行谁,没有一个可以完成上述业务需求的逻辑控制,所以会出现逻辑问题。
		new Thread(new Producer(queue)).start();
		new Thread(new Consumer(queue)).start();
	}
}

执行结果(只截取了前面几行):通过结果可以发现,第三行结果应该是生产1,即生产0,消费0之后,按照业务逻辑来说应该执行生产1,消费1…;但显然,第二行执行完后,生产和消费两个线程(在没有逻辑控制的)下一步该执行哪个是完全随机的,这种执行流程是不符合业务要求的。因此,有必要使得两个线程间能够彼此通信,从而使的线程间的执行顺序不再充满随机性,而是按照一定的逻辑执行;由此也是,线程间有点相互参照和依赖的味道。

生产:0
消费:0
消费:0
生产:1
消费:1
生产:2
生产:3
消费:3
消费:3
生产:4
消费:4
生产:5
消费:5
生产:6
生产:7
消费:7
消费:7
生产:8

2.其次,线程间通信:

●wait()方法:中断方法的执行,使线程等待;如果只有wait()方法,可能会出现:生产线程等待消费线程执行,消费线程等待生产线程执行,这样互相等待,谁都不会执行,就会处于死锁的状态

●notify()方法:唤醒处于等待的某一个线程,使其结束等待;

●notifyAll()方法:唤醒所有处于等待的线程,使它们结束等待;这两个方法和wait()方法对应使用;

sleep() 和 wait() 的这 5 个区别,你知道几个?

(1)只使用wait()方法,而没有使用notify()和notifyAll()的情况

Queue类:该类添加了flag变量,并在方法中添加了wait()方法和控制逻辑,但没有使用notify()和notifyAll();

public class Queue {
	private int n;
 
	/**
	 * (不提倡再变量上用这种注释方式)
	 * flag:
	 * (1)flag初始值为false,flase代表Queue里面没有数据,此时不能消费数据,只能生产数据;
	 * (2)flag为true代表Queue里有数据,此时不能再生产数据了,只能消费数据,等到数据被消费了,flag编程false后才能去生产数据
	 */
	boolean flag = false;
 
	public synchronized int getN() {
		// 当flag为false的时候,没有数据,就无法消费,该方法不能执行,需要等待生产方法的执行;
		if(!flag){
			try {
				wait();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		System.out.println("消费:"+n);
		// 消费完毕后,需要把falg设置称false,表示没有数据了
		flag = false;
		return n;
	}
 
	public synchronized void setN(int n) {
		// 当flag为true的时候,有数据了,不能再生产了,需要等待消费方法的执行;数据被消费后,flag编程false后生产方法才能执行
		if(flag){
			try {
				wait();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		System.out.println("生产:"+n);
		this.n = n;
		// 数据生产完毕后,需要把falg设置称true,表示已经有数据了
		flag = true;
	}
}

执行结果:下面是所有的输出结果,发现,其实现了生产一个消费一个的逻辑;但当生产7后,并没有继续出现消费7,这就是出现了死锁的情况。

生产:0
消费:0
生产:1
消费:1
生产:2
消费:2
生产:3
消费:3
生产:4
消费:4
生产:5
消费:5
生产:6
消费:6
生产:7

解决办法:添加notifyAll();时刻不忘唤醒线程。( 注:至于为什么会出现死锁,暂时还不明白

public class Queue {
	private int n;
 
	/**
	 * (不提倡再变量上用这种注释方式)
	 * flag:
	 * (1)flag初始值为false,flase代表Queue里面没有数据,此时不能消费数据,只能生产数据;
	 * (2)flag为true代表Queue里有数据,此时不能再生产数据了,只能消费数据,等到数据被消费了,flag编程false后才能去生产数据
	 */
	boolean flag = false;
 
	public synchronized int getN() {
		// 当flag为false的时候,没有数据,就无法消费,该方法不能执行,需要等待生产方法的执行;
		if(!flag){
			try {
				wait();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		System.out.println("消费:"+n);
		// 消费完毕后,需要把falg设置称false,表示没有数据了
		flag = false;
		// 唤醒线程
		notifyAll();
		return n;
	}
 
	public synchronized void setN(int n) {
		// 当flag为true的时候,有数据了,不能再生产了,需要等待消费方法的执行;数据被消费后,flag编程false后生产方法才能执行
		if(flag){
			try {
				wait();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		System.out.println("生产:"+n);
		this.n = n;
		// 数据生产完毕后,需要把falg设置称true,表示已经有数据了
		flag = true;
		// 唤醒线程
		notifyAll();
	}
}

执行结果:只截取了部分,发现,程序的死锁情况解决了。

生产:0
消费:0
生产:1
消费:1
生产:2
消费:2
生产:3
消费:3
生产:4
消费:4
生产:5
消费:5
生产:6
消费:6
生产:7
消费:7
生产:8
消费:8
生产:9
消费:9
生产:10
消费:10
生产:11
消费:11
生产:12
消费:12
生产:13
消费:13

注: **(1) 具体线 程间通信,还需要通过实际业务来加深;这些可以应用哪些场景,应用时有哪些技巧等等都需要慢慢总结;

(2)线程间通信的场景也是千变万化,具体为了实现线程间通信,如何涉及逻辑代码,如何保证安全和效率的情况下的编写技巧和编写习惯做法,都需要慢慢积累;

Callable:创建线程的第三种方式

Callable接口是属于Executor框架(并发编程的一个框架)中的功能类,Callable接口和Runnable接口的功能相似,但功能比Runnable接口更强大。

(1)Callable接口可以在任务结束时提供一个返回值,Runnable接口没有这个功能;

(2)Callable接口的call()方法可以抛出异常,Runnable接口的run()方法不能抛出异常

(3) Callable接口的深化,可等到接触并发编程的时候再了解


Callable接口的示例程序如下:

创建Callable接口的实现类ThreadCallable,并实现call()方法;

/**
 * 创建Callable接口的实现类,并实现call()方法,该call()方法作将为线程的执行体,有返回值
 * @author Administrator
 *
 */
public class ThreadCallable implements Callable<String>{
 
	@Override
	public String call() throws Exception {
		// call()方法返回值的类型可以根据需求更改
		System.out.println("sdfdsfdsfds");
 
		String str = "创建线程的第三种方式";
		return str;
	}
}

实际调用:

public class CallableTest {
 
	public static void main(String[] args) {
 
		// (1)创建Callable接口实现类的实例对象;
		Callable<String> call = new ThreadCallable();
		// (2)使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值;
		FutureTask<String> ft = new FutureTask<>(call);
		// (3)使用FutureTask对象,作为Thread对象的target创建线程。(FutureTask类实现了Runnable接口)
		Thread t = new Thread(ft);
		// (4)启动线程
		t.start();
		// (5)调用FutureTask对象的get()方法获得子线程结束后的返回值
		try{
			System.out.println(ft.get());
		}catch(ExecutionException e){
			e.printStackTrace();
		}catch(InterruptedException e){
			e.printStackTrace();
		}
	}
}

输出结果:

sdfdsfdsfds
创建线程的第三种方式