21天学会Java之(Java SE第十二篇):多线程、Lambda表达式

504 阅读31分钟

多线程是Java语言的重要特性,大量应用于网络编程和服务器端程序的开发。最常见的UI界面的底层原理、操作系统底层原理都大量使用了多线程技术。本篇中仅初步讲解多线程的普通应用,并无深入剖析。由于JUC包的内容过多,过于深奥,本人水平有限,本文中也不扩展叙写,希望在对于并发编程有更深一步的理解之后填上这个坑。

多线程的基本概念

对于线程的理解,我们需要先理解程序、进程以及线程的概念。

程序是一个静态的概念,一般对应于操作系统中的一个可执行文件,例如,打开用于敲代码的idea的可执行文件。打开idea可执行文件,将会加载该程序到内存中并开始执行它,于是就产生了“进程”,而我们打开了多个可执行文件,这就产生了多个进程。

对于多任务,多进程大多数人应该就特别熟悉,我们打开电脑上的任务管理器/活动监视器,我们就能看到一大堆进程,这是操作系统的一种能力,看起来可以在同一时刻运行多个程序。例如,我们在敲代码的时候能同时用音乐软件听歌。而如今,人们往往都有多CPU多计算机,但是并发执行的进程数目并不受限于CPU数目。操作系统会为每个进程分配CPU的时间片,给人并行处理的感觉。

多线程程序在更低一层扩展了多任务多概念:单个程序看起来在同时完成多个任务。每个任务在一个线程中执行,线程是控制线程的简称。如果一个程序可以同时运行多个线程,则称这个程序是多线程的程序。

而多线程和多进程的本质区别在于每个进程都拥有自己的一套变量,而线程则共享数据。而这样就会涉及线程安全的问题,下文会介绍这个问题。不过对于共享变量使线程之间的通信比进程之间的通信更有效、更容易。此外,在操作系统中,与进程相比较,线程更“轻量级”,创建、撤销一个线程比启动新进程的开销要小得多,所以线程又被称为轻量级进程。

Java中如何实现多线程

Java中使用多线程非常的简单。下文将会介绍如何创建和使用线程。

通过继承Thread类实现多线程

继承Thread类实现多线程的步骤如下:

  1. 在Java中负责实现线程功能的类是java.lang.Thread类。
  2. 可以通过创建Thread的实例来创建新的线程。
  3. 每个线程都是通过某个特定的Thread对象所对应的方法run()来完成其操作的,方法run()称为线程体。
  4. 通过调用Thread类的start()方法来启动一个线程。

可以参考以下代码理解:

/**
 * 创建线程的方式一:
 * 1.创建:继承Thread并且重写run方法
 * 2.启动:创建子类对象并且运行start方法
 * @author Eddie
 *
 */
public class StartThread extends Thread {
	//程序入口点
	@Override
	public void run() {
		for (int i = 0; i < 20; i++) {
			System.out.println("一边听歌......");
		}	
	}
	
	public static void main(String[] args) {
		//创建子类对象
		StartThread st = new StartThread();
		//启动线程
		st.start();  //不保证立即运行,靠cpu调用
//		st.run();  //仅调用普通的run方法
		for (int i = 0; i < 20; i++) {
			System.out.println("一边敲代码......");
		}
	}
}

这种方法的缺点是:如果类已经继承一个类,则无法继承Thread类(Java只能继承一个父类)。

通过Runnable接口实现多线程

在实际开发中,更多的是通过Runnable接口实现的多线程。这种方式完美解决了继承Thread类的缺点,在实现Runnable接口的同时还可以继承某个类。所以实现Runnable接口的方式要通用一些。

可以参考以下代码理解:

/**
 * 创建线程的方式二:
 * 1.创建:实现Runnable并且重写run方法
 * 2.启动:创建实现类对象和Thread对象并且运行start方法
 * 推荐:避免单继承的局限性,优先使用接口
 * 方便共享资源
 * @author Eddie
 *
 */
public class StartRunnable implements Runnable {
	//线程入口点
	@Override
	public void run() {
		for (int i = 0; i < 20; i++) {
			System.out.println("一边听歌......");
		}
	}
	
	public static void main(String[] args) {
//		//创建子类对象
//		StartRunnable sr = new StartRunnable();
//		Thread t=new Thread(sr);
//		//启动线程
//		t.start();  //不保证立即运行,靠cpu调用
//		st.run();  //仅调用普通的run方法
		
		new Thread(new StartRunnable()).start();  //同样可以使用匿名对象的方式来使用子类
		
		for (int i = 0; i < 20; i++) {
			System.out.println("一边敲代码......");
		}
	}
}

线程状态和生命周期

线程状态

一个成对象在它的生命周期内,需要经历5个状态,如下图所示:

线程生命周期图

  1. 新生状态

    用new关键字建立一个线程对象后,该线程对象处于新生状态。处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态。

  2. 就绪状态

    处于就绪状态的线程已经具备了运行条件,但是还没有被分配到CPU,处于“线程就绪队列”,等待系统为其分配CPU。就绪状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会进入执行状态。一旦获得哦CPU,线程就进入运行状态并自动调用其run方法。下列4种原因会导致线程进入就绪状态:

    • 新建线程:调用start()方法,进入就绪状态。
    • 阻塞线程:阻塞解除,进入就绪状态。
    • 运行线程:调用yield()方法,直接进入就绪状态。
    • 运行线程:JVM将CPU资源从本线程切换到其他线程。
  3. 运行状态

    在运行状态的线程执行其run方法中的代码,直到因调用其他方法而终止,或等待某资源产生阻塞或完成任务死亡。如果在给定的时间片内没有执行结束,线程就会被系统换下来并回到就绪状态,也可能由于某些“导致阻塞的事件”而进入阻塞状态。

  4. 阻塞状态

    阻塞是指暂停一个线程的执行以等待某个条件发生(如其资源就绪)。有4种原因会导致阻塞:

    • 执行sleep(int millsecond)方法,使当前线程休眠,进入阻塞状态。当指定的时间到了之后,线程进入就绪状态。
    • 执行wait()方法,使当前线程进入阻塞状态。当使用notify()方法唤醒这个线程后,它进入就绪状态。
    • 当线程运行时,某个操作进入阻塞状态,例如执行I/O流操作(read()/write()方法本身就是阻塞的方法)。只有当引起该操作阻塞的原因消失后,线程才进入就绪状态。
    • join()线程联合:当某个线程等待另一个线程执行结束并能继续执行时,使用join()方法。
  5. 死亡状态

    死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有两个:一个是正常运行的线程完成了它run()方法内的全部工作;另外一个是线程被强制终止,如通过执行~~stop()destroy()~~方法来终止一个线程(stop()/destroy()方法已经被JDK废弃,不推荐使用)。当一个线程进入死亡状态以后,就不能回到其他状态了。

终止线程的常用方式

上文中提到stop()/destroy()方法已经被JDK废弃,不推荐使用。当我们需要终止线程的时候通常的做法是提供一个boolean类型的终止变量,当这个变量置为false时,终止线程的运行。可以参考以下代码:

/**
 * 终止线程
 * 1.线程正常执行完毕/2.外部干涉,加入标识(这边所要使用的方法)
 * @author Eddie
 *
 */
public class TerminateThread implements Runnable{
	//加入标识 标记线程体是否可以运行
	private boolean flag=true;
	private String name;
	
	public TerminateThread() {
	}
	public TerminateThread(String name) {
		super();
		this.name = name;
	}
	public String getName() {
		return name;
	}
	@Override
	public void run() {
		int i=0;
		//关联标识
		while (flag) {
			System.out.println(name+"运行:"+(i++)+"次。");
		}
	}
	
	//对外提供改变标识的方法。
	public void stop() {
		this.flag=false;
	}
	
	public static void main(String[] args) {
		TerminateThread tt = new TerminateThread("线程");  //新生状态
		new Thread(tt).start();  //就绪状态
		
		for (int i = 0; i < 99; i++) {
			System.out.println("主线程运行了:"+i+"次。");
			if (i==66) {
				System.out.println(tt.getName()+"STOP!");
				tt.stop();
			}
		}
	}
}

暂停线程执行的常用方法

暂停线程的常用方法有sleep()和yield(),这两个方法的区别如下:

  • sleep()方法可以让正在运行的线程进入阻塞状态,直到休眠时间满了,进入就绪状态。
  • yield()方法可以让正在运行的线程直接进入就绪状态,让出CPU的使用权。

sleep()方法使用的示范代码:

public class BlockedSleep {
	public static void main(String[] args) {
		StateThread t1 = new StateThread();
		StateThread t2 = new StateThread();
		t1.start();
		t2.start();
	}
}
//这里为了简洁实用继承的方式实现多线程
class StateThread extends Thread {
	@Override
	public void run() {
		for (int i = 0; i < 50; i++) {
			System.out.println(this.getName() + ":" + i);
			try {
				Thread.sleep(1000);  //调用线程的sleep()方法
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

yield()方法使用的示范代码:

public class BlockedYield {
	public static void main(String[] args) {
		StateThread t1 = new StateThread();
		StateThread t2 = new StateThread();
		t1.start();
		t2.start();
	}
}
//这里为了简洁实用继承的方式实现多线程
class StateThread extends Thread {
	@Override
	public void run() {
		for (int i = 0; i < 99; i++) {
			System.out.println(this.getName() + ":" + i);
			Thread.yield();  //调用线程的yield()方法
		}
	}
}

以上代码可以自己copy进IDE运行看下运行结果,sleep()方法中我们可以感觉到每条结果输出之前的延迟,这是因为Thread.sleep(1000)语句在起作用。而在yield()方法中,代码可以引起线程的切换,但运行没有明显延迟。

联合(合并)线程的使用方法

线程A运行期间,可以调用线程B的join()方法,让线程B和线程A联合。这样,线程A就必须等待线程B执行完毕,才能继续执行。用以下一个例子来说明一下join()方法的使用:

/**
 * join:合并线程,插队线程
 * @author Eddie
 *
 */
public class BlockedJoin02 {
	public static void main(String[] args) {
		new Thread(new father()).start();
	}
}

class father implements Runnable{
	@Override
	public void run() {
		System.out.println("爸爸想抽烟了。");
		System.out.println("拿钱叫儿子去买烟。");
		Thread sonThread=new Thread(new son()); 
		sonThread.start();
		try {
			sonThread.join();  //调用join()方法
			System.out.println("拿到了烟,把零钱给儿子。");
		} catch (InterruptedException e) {
			e.printStackTrace();
			System.out.println("儿子走丢了,出门找儿子。");
		}
	}
}

class son implements Runnable{
	@Override
	public void run() {
		System.out.println("儿子拿了钱,出门买烟。!");
		System.out.println("路过了游戏厅。");
		for (int i = 0; i <= 10; i++) {
			System.out.println("在游戏厅里呆了"+i+"秒。");
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		System.out.println("走出游戏厅去便利店买烟");
		System.out.println("回家把烟给爸爸。");
	}
}

Lambda表达式

Lambda表达式是一个可传递的代码块,可以在以后执行一次或多次。本文仅简单介绍一下如何使用Lambda表达式,以及Lambda在多线程中的使用,更详细的内容可以翻阅相关的书籍。

Lambda表达式的推导

public class LambdaTest01 {
	//非静态内部类
	class Like2 implements ILike{
		@Override
		public void lambda() {
			System.out.println("I like lambda2");
		}
	}
	//静态内部类
	static class Like3 implements ILike{
		@Override
		public void lambda() {
			System.out.println("I like lambda3");
		}
	}
	
	public static void main(String[] args) {
		//外部类
		ILike like=new Like1();
		like.lambda();
		//非静态内部类
		like =new LambdaTest01().new Like2();
		like.lambda();
		//静态内部类
		like =new Like3();
		like.lambda();
		
		//局部内部类
		class Like4 implements ILike{
			@Override
			public void lambda() {
				System.out.println("I like lambda4");
			}
		}
		like=new Like4();
		like.lambda();
		
		//匿名内部类
		like=new ILike() {
			@Override
			public void lambda() {
				System.out.println("I like lambda5");
			}
		};
		like.lambda();
		
		//Lambda表达式
		like=()-> {
				System.out.println("I like lambda5");
			};
		like.lambda();
		
//		Lambda推导必须存在类型
//		()-> {
//			System.out.println("I like lambda5");
//		}.lambda();
		
	}
}
//接口中只能有一个要实现的方法
interface ILike{
	void lambda();
}
//外部类
class Like1 implements ILike{
	@Override
	public void lambda() {
		System.out.println("I like lambda1");
	}
}

Lambda表达式参数的简化过程

public class LambdaTest02 {
	public static void main(String[] args) {
		ILove love=(String a)-> {
				System.out.println("I like lambda-->"+a);
			};
		love.lambda("普通Lambda表达式");
		
		//可以去掉参数类型
		love=(a)-> {
			System.out.println("I like lambda-->"+a);
		};
		love.lambda("去掉参数类型");
		
		//只有一个参数括号可以省略
		love=a-> {
			System.out.println("I like lambda-->"+a);
		};
		love.lambda("省略参数括号");
		
		//只有一行代码可以省略花括号
		love=a->System.out.println("I like lambda-->"+a);
		love.lambda("省略花括号");
		
	}
}

interface ILove{
	void lambda(String a);
}
//外部类
//class Love1 implements ILove{
//	@Override
//	public void lambda(String a) {
//		System.out.println("I like lambda-->"+a);
//	}
//}

Lambda表达式返回值的简化过程

public class LambdaTest03 {
	public static void main(String[] args) {
		//普通的Lambda表达式
		IInsterest insterest=(int a1, int b1)-> {
			System.out.println("I like lambda-->"+(a1+b1));
			return a1+b1;
			};
		insterest.lambda(1, 1);
		
		//去掉参数类型(去掉的话需要全部去掉,仅去掉一个不可行)
		insterest=(a1, b1)-> {
			System.out.println("I like lambda-->"+(a1+b1));
			return a1+b1;
			};
		insterest.lambda(2, 2);
		
		/*
		 * 有两个参数不可省略参数的括号
		 * 有两行代码不可省略花括号
		 */
		
		//如果只有一行代码,并且有返回值可以省略return;
		insterest=(a1, b1)->a1+b1;
		//返回了一个int数值
		System.out.println(insterest.lambda(6, 6));
		
		insterest=(a1, b1)->100;
		//返回了一个int数值
		System.out.println(insterest.lambda(100, 100));
	}
}
interface IInsterest{
	int lambda(int a,int b);
}
//class Insterest implements IInsterest{
//	@Override
//	public int lambda(int a1, int b1) {
//		System.out.println("I like lambda-->"+(a1+b1));
//		return a1+b1;
//	}
//}

Lambda表达式简化线程(用一次)的使用

public class LambdaThread01 {
	//静态内部类
	static class Test1 implements Runnable{
		@Override
		public void run() {
			for (int i = 0; i < 2; i++) {
				System.out.println("一边听歌1");
			}
		}
	}
	//非静态内部类
	class Test2 implements Runnable{
		@Override
		public void run() {
			for (int i = 0; i < 2; i++) {
				System.out.println("一边听歌2");
			}
		}
	}
	
	public static void main(String[] args) {
		//静态内部类
		new Thread(new Test1()).start();
		//非静态内部类
		new Thread(new LambdaThread01().new Test2()).start();
		
		//局部内部类
		class Test3 implements Runnable{
			@Override
			public void run() {
				for (int i = 0; i < 2; i++) {
				System.out.println("一边听歌3");
				}
			}
		}
		
		new Thread(new Test3()).start();
		
		//匿名内部类
		new Thread(new Runnable() {
			@Override
			public void run() {
				for (int i = 0; i < 2; i++) {
					System.out.println("一边听歌4");
					}
			}
		}).start();
		
		//jdk8简化   Lambda表达式
		//因为Thread里只能传入一个实现Runable接口的实现类并且Runable仅需要实现一个run()方法
		new Thread(()-> {
				for (int i = 0; i < 2; i++) {
					System.out.println("一边听歌5");
				}
			}
		).start();
		
		for (int i = 0; i < 5; i++) {
			System.out.println("66666666666");
		}
	}
}

使用Lambda表达式简化多线程

/**
 * 使用Lambda表达式简化多线程
 * Lambda表达式避免匿名内部类定义过多
 * 其实质属于函数式编程的概念
 * @author Eddie
 *
 */
public class LambdaThread02 {
	public static void main(String[] args) {
		new Thread(()->{
			for (int i = 0; i < 10; i++) {
				System.out.println("一边听歌...");
			}
		}).start();
		
		new Thread(()->System.out.println("正在学习Lambda表达式")).start();
		
		for (int i = 0; i < 10; i++) {
			System.out.println("一边写代码...");
		}
	}
}

线程的常用方法

线程也是对象,系统为线程定义了很多方法、优先级、名字等,以便对多线程进行有效地管理。

线程常用的方法

线程的常用方法如下表所示:

方法 功能
getState() 获得线程当前的状态
isAlive() 判断线程是否还“活着”,即线程是否还未终止
getPriority() 获得线程的优先级数值
setPriority() 设置线程的优先级数值
setName() 给线程设置一个名字
getName() 获得线程的名字
currentThread() 取得当前正在运行的线程对象,也就是取得自己本身
setDaemon(boolean on) 将线程设置成守护线程

使用getState()方法观察线程状态

public class AllState {
	public static void main(String[] args) {
		Thread t=new Thread(()->{
			for (int i = 0; i <5; i++) {
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println("模拟线程");
			}
		});
		//观察状态
		State state=t.getState();
		System.out.println(state);  //NEW
		
		t.start();
		state=t.getState();
		System.out.println(state);  //RUNNABLE
		
//		while (state!=State.TERMINATED) {
//			try {
//				Thread.sleep(200);
//			} catch (InterruptedException e) {
//				e.printStackTrace();
//			}
//			state=t.getState();
//			System.out.println(state);  //TIMED_WAITING
//		}
		
		while (true) {
			//活动的线程数
			int threadNum=Thread.activeCount();
			if (threadNum==1) {
				break;
			}
			try {
				Thread.sleep(200);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			state=t.getState();
			System.out.println(state);  //TIMED_WAITING
		}
		state=t.getState();
		System.out.println(state);    //TERMINATED
	}
}

线程的优先级

/**
 * 线程的优先级1-10
 * 1.NORM_PRIORITY 5 默认
 * 2.MIN_PRIORITY 1
 * 3.MAX_PRIORITY 10
 * 概率,不代表绝对的先后顺序
 * @author Eddie
 *
 */
public class PriorityTest {
	public static void main(String[] args) {
		
		MyPriority mp=new MyPriority();
		Thread t1=new Thread(mp,"百度");
		Thread t2=new Thread(mp,"阿里");
		Thread t3=new Thread(mp,"腾讯");
		Thread t4=new Thread(mp,"头条");
		Thread t5=new Thread(mp,"美团");
		Thread t6=new Thread(mp,"滴滴");
		
		//设置优先级需要在线程启动前
		t1.setPriority(Thread.MAX_PRIORITY);
		t2.setPriority(Thread.MAX_PRIORITY);
		t3.setPriority(Thread.MAX_PRIORITY);
		t4.setPriority(Thread.MIN_PRIORITY);
		t5.setPriority(Thread.MIN_PRIORITY);
		t6.setPriority(Thread.MIN_PRIORITY);
		t1.start();
		t2.start();
		t3.start();
		t4.start();
		t5.start();
		t6.start();
		System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());
	}
}

class MyPriority implements Runnable{

	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());
		Thread.yield();
	}
}

其他方法的示例

/**
 * 其他方法
 * isAlive:线程是否还或者
 * Thread.currentThread():当前线程
 * setName.getName:设置和获取代理线程的名称
 * @author Eddie
 *
 */
public class InfoTest {
	public static void main(String[] args) {
		System.out.println(Thread.currentThread().isAlive());
		MyInfo myInfo=new MyInfo("战斗机");
		Thread t=new Thread(myInfo);
		t.setName("公鸡");
		t.start();
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(t.isAlive());
	}
}

class MyInfo implements Runnable{
	private String name;
	
	public MyInfo(String name) {
		super();
		this.name = name;
	}

	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName()+"-->"+name);
	}
}

守护线程

/**
 * 守护线程:是为用户线程服务的;JVM停止不用等待守护线程执行完毕
 * 线程默认用户线程 JVM等待用户线程执行完毕才会停止
 * @author Eddie
 *
 */
public class DaemonTest {
	public static void main(String[] args) {
		God god=new God();
		You you=new You();
		Thread t=new Thread(god);
		t.setDaemon(true);  //将用户线程设置为守护线程
		t.start();
		new Thread(you).start();
		
	}
}

class You implements Runnable{
	@Override
	public void run() {
		for (int i = 1; i <=365*100; i++) {
			System.out.println("Happy Life"+i+"days.");
		}
		System.out.println("die...");
	}
}

class God implements Runnable{
	@Override
	public void run() {
		while (true) {
			System.out.println("Bless you...");
		}
	}
}

线程同步

在处理多线程问题时,如果多个线程同时访问同一个对象,并且某些线程还想修改这个对象时,就需要用到“线程同步”机制。加入线程同步后,我们称为这是线程安全的;线程安全在并发时保证数据的准确性、效率尽可能高。

线程同步的概念

线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程继续使用。

用一个取款机的例子来看下未使用线程同步的情况下会发生的情况:

public class UnsafeTest02 {
	public static void main(String[] args) {
		Account account=new Account(100, "百万账户");
		ATM atm01=new ATM(account, 80);
		ATM atm02=new ATM(account, 70);
		new Thread(atm01,"自己").start();
		new Thread(atm02,"老婆").start();
	}
}

//账户
class Account{
	private int total_assets;  //账户总资产
	private String account_name;  //账户名字
	public Account(int total_assets, String account_name) {
		super();
		this.total_assets = total_assets;
		this.account_name = account_name;
	}
	public int getTotal_assets() {
		return total_assets;
	}
	public void setTotal_assets(int total_assets) {
		this.total_assets = total_assets;
	}
	public String getAccount_name() {
		return account_name;
	}
	public void setAccount_name(String account_name) {
		this.account_name = account_name;
	}
}
//模拟取款
class ATM implements Runnable{
	private Account account;  //取款账户
	private int withdrawMoney;  //取款金额
	private int pocketMoney;  //口袋的钱
	public ATM(Account account, int withdrawMoney) {
		super();
		this.account = account;
		this.withdrawMoney = withdrawMoney;
	}

	@Override
	public void run() {
		if (account.getTotal_assets()<withdrawMoney) {
			return;
		}
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		account.setTotal_assets(account.getTotal_assets()-withdrawMoney);
		pocketMoney+=withdrawMoney;
		System.out.println(Thread.currentThread().getName()+":"+pocketMoney);
		System.out.println(account.getAccount_name()+":"+account.getTotal_assets());
	}
}

由于没有使用线程同步机制,即使我们在线程中判断了剩余余额,但是同样会使两个人都取款成功,这就叫做线程不安全。

实现线程同步

由于同一进程的多个线程共享同一块存储空间,这在带来方便的同时,也带来了访问冲突问题。Java语言提供了专门机制来解决这种冲突,有效避免了同一个数据对象被多个线程同时访问造成的问题。这套机制就是使用synchronized关键字,它包括两种用法:synchronized方法和synchronized块。

  1. synchronized方法

    通过在方法声明中加入synchronized关键字来声明此方法,语法格式如下:

    public synchronized void accessVal(int newVal);
    

    synchronized方法控制对“对象的类成员变量”的访问:每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则所属线程阻塞。方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。

  2. synchronized块

    synchronized方法的缺陷是,若将一个大的方法声明为synchronized将会大大影响程序的工作效率。

    为此,Java提供了更好的解决办法,就是使用synchronized块。synchronized块可以让人们精确地控制具体的“成员变量”,缩小同步的范围,提高效率。且synchronized块可以指定锁的对象,synchronized方法则只能锁本对象。

    通过synchronized关键字可声明synchronized块,语法格式如下:

    synchronized(synObject){
        //允许访问控制的代码
    }
    

将以上取款机的例子加入线程同步:

public class SynBlock01 {
	public static void main(String[] args) {
		Account account=new Account(200, "百万账户");
		SynATM my = new SynATM(account,80);
		SynATM wife = new SynATM(account,90);
		new Thread(my,"自己").start();
		new Thread(wife,"妻子").start();
	}
}
//账户
class Account{
	private int total_assets;  //账户总资产
	private String account_name;  //账户名字
	public Account(int total_assets, String account_name) {
		super();
		this.total_assets = total_assets;
		this.account_name = account_name;
	}
	public int getTotal_assets() {
		return total_assets;
	}
	public void setTotal_assets(int total_assets) {
		this.total_assets = total_assets;
	}
	public String getAccount_name() {
		return account_name;
	}
	public void setAccount_name(String account_name) {
		this.account_name = account_name;
	}
}
class SynATM implements Runnable{
	private Account account;
	private int drawingMoney;
	private int money;
	public SynATM(Account account, int drawingMoney) {
		super();
		this.account = account;
		this.drawingMoney = drawingMoney;
	}

	@Override
	public void run() {
		//提高性能,判断账户是否有钱或者取的钱是否超过账户余额,满足条件直接返回,不需要运行同步块
		if (account.getTotal_assets()<=0 || account.getTotal_assets()<drawingMoney) {
			return;
		}
		//同步块:目标锁定account
		synchronized (account) {
			account.setTotal_assets(account.getTotal_assets()-drawingMoney);
			money+=drawingMoney;
			System.out.println(Thread.currentThread().getName()+"钱包余额:"+money);
			System.out.println(account.getAccount_name()+"余额:"+account.getTotal_assets());
		}
	}
}

synchronized (account)意味着线程需要获得account对象的“锁”才有资格运行同步块中的代码。Account对象的“锁”也称为“互斥锁”,在同一时刻只能被一个线程使用。A线程拥有锁,则可以调用“同步块”中的代码;B线程没有锁,则进入account对象的“锁池队列”等待,直到A线程使用完毕释放了account对象的锁,B线程得到锁才可以调用“同步块”中的代码。

synchronized方法、synchronized块和线程不安全的例子

以下是买票的例子:

public class SynBlock03 {
	public static void main(String[] args) {
		Syn12306 web12306 = new Syn12306();
		
		new Thread(web12306,"黄牛").start();
		new Thread(web12306,"yellow牛").start();
		new Thread(web12306,"ticket_scalper").start();
	}
}

class Syn12306 implements Runnable{
	//票数
	private int ticketNums=10;
	private boolean flag=true;
	
	@Override
	public void run() {
		while (flag) {
			test5();
			try {
				Thread.sleep(20);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	//线程安全,范围太大-->性能效率低下:同步方法,锁定的是SynWeb对象
	public synchronized void test1() {
		if (ticketNums<=0) {
			flag=false;
			return;
		}
		//模拟网络延迟
		try {
			Thread.sleep(200);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
	}
	//线程安全,范围太大-->性能效率低下:同步块,锁定this对象,即SynWeb对象
	public void test2() {
		synchronized(this) {
			if (ticketNums<=0) {
				flag=false;
				return;
			}
			//模拟网络延迟
			try {
				Thread.sleep(200);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
		}
	}
	//线程不安全:同步块,锁定ticketNums对象的属性在变
	public void test3() {
		synchronized((Integer)ticketNums) {
			if (ticketNums<=0) {
				flag=false;
				return;
			}
			//模拟网络延迟
			try {
				Thread.sleep(200);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
		}
	}
	//线程不安全:同步块
	public void test4() {
		//仅锁定下面一部分,线程不安全
		synchronized(this) {
			if (ticketNums<=0) {
				flag=false;
				return;
			}
		}
		//模拟网络延迟
		try {
			Thread.sleep(200);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
	}
	//线程安全:尽可能锁定合理的范围(不是指代码 指数据的完整性)
	//double checking 
	public void test5() {
		if (ticketNums<=0) {  //考虑的是没有票的情况
			flag=false;
			return;
		}
		//仅锁定下面一部分,线程不安全
		synchronized(this) {
			if (ticketNums<=0) {  //考虑的是最后一张票的情况
				flag=false;
				return;
			}
			//模拟网络延迟
			try {
				Thread.sleep(200);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
		}
	}
}

死锁及解决方案

死锁的概念

“死锁”指的是多个线程各自占有一些共享资源,并且互相等待得到其他线程占有的资源才能继续,从而导致两个或者多个线程都在等待对方释放资源,停止执行的情形。

因此,某一个同步块需要同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。用以下一个例子来描述下死锁的形成:

public class DeadLock {
	public static void main(String[] args) {
		new Thread(new MarkUp("大丫", true)).start();
		new Thread(new MarkUp("二丫", false)).start();
	}
}
//镜子
class Mirror{
}
//口红
class Lipstick{
}
//化妆
class MarkUp implements Runnable{
	//不管几个对象只有一份
	static Mirror mirror=new Mirror();
	static Lipstick lipstick=new Lipstick();
	private String girl;
	private boolean flag;
	
	public MarkUp(String girl, boolean flag) {
		this.girl = girl;
		this.flag = flag;
	}

	@Override
	public void run() {
		markup();
	}
	//相互持有对方的对象锁
	private void markup() {
		if (flag) {
			synchronized (mirror) {  //先将镜子锁上
				System.out.println(this.girl+"照镜子。");
				//1秒后,涂口红
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				synchronized (lipstick) {  //然后将口红锁上
					System.out.println(this.girl+"涂口红。");
				}
			}
		}else {
			synchronized (lipstick) {  //先将口红锁上
				System.out.println(this.girl+"涂口红。");
				//2秒后,照镜子
				try {
					Thread.sleep(2000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				synchronized (mirror) {  //然后将镜子锁上
					System.out.println(this.girl+"照镜子。");
				}
			}
		}
	}
}

执行后,两个线程都在等对方的资源,都处于停滞状态。

死锁的解决方法

死锁是由于“同步块需要同时持有多个对象锁”造成的。要解决这个问题,就是同一个代码块不要同时持有两个对象锁。如上面的死锁例子,可以修改如下:

public class DeadLock {
	public static void main(String[] args) {
		new Thread(new MarkUp("大丫", true)).start();
		new Thread(new MarkUp("二丫", false)).start();
	}
}
//镜子
class Mirror{
}
//口红
class Lipstick{
}
//化妆
class MarkUp implements Runnable{
	//不管几个对象只有一份
	static Mirror mirror=new Mirror();
	static Lipstick lipstick=new Lipstick();
	private String girl;
	private boolean flag;
	
	public MarkUp(String girl, boolean flag) {
		this.girl = girl;
		this.flag = flag;
	}

	@Override
	public void run() {
		markup();
	}
	//相互持有对方的对象锁
	private void markup() {
		if (flag) {
			synchronized (mirror) {  //先将镜子锁上
				System.out.println(this.girl+"照镜子。");
				//1秒后,涂口红
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				/*
				synchronized (lipstick) {  //然后将口红锁上
					System.out.println(this.girl+"涂口红。");
				}*/
			}
			synchronized (lipstick) {  //然后将口红锁上
				System.out.println(this.girl+"涂口红。");
			}
		}else {
			synchronized (lipstick) {  //先将口红锁上
				System.out.println(this.girl+"涂口红。");
				//2秒后,照镜子
				try {
					Thread.sleep(2000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				/*
				synchronized (mirror) {  //然后将镜子锁上
					System.out.println(this.girl+"照镜子。");
				}*/
			}
			synchronized (mirror) {  //然后将镜子锁上
				System.out.println(this.girl+"照镜子。");
			}
		}
	}
}

题外内容(与线程同步有相关性)

以下内容与线程同步有相关性,仅写了几个例子来描述。

CAS:比较并交换

public class CAS {
	//库存
	private static AtomicInteger stock=new AtomicInteger(5);
	public static void main(String[] args) {
		for (int i = 0; i < 6; i++) {
			new Thread(new Customer()).start();
		}
	}
	
	public static class Customer implements Runnable{
		@Override
		public void run() {
			synchronized (stock) {
				//模拟延迟
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				Integer left=stock.get();
				if (left<1) {
					System.out.println(Thread.currentThread().getName()+"没抢到,没有库存了");
					return;
				}
				System.out.println(Thread.currentThread().getName()+"抢到了,第"+left+"件商品,剩余"+left+"件商品。");
				stock.set(left-1);
			}
		}
	}
}

指令重排

public class HappenBefore {
	private static int a=0;  //变量1
	private static boolean flag=false;  //变量2
	public static void main(String[] args) throws InterruptedException {
		for (int i = 0; i < 100; i++) {
			a=0;
			flag=false;
			//线程1 读取数据
			Thread t1=new Thread(()->{
				a=1;
				flag=true;
			});
			//线程2 更改数据
			Thread t2=new Thread(()->{
				if (flag) {
					a*=1;
				}
				//指令重排
				if (a==0) {
					System.out.println("Happen before,a->"+a);
				}
			});
			
			t1.start();
			t2.start();
			
			t1.join();
			t2.join();
		}
	}
}

可重入锁:锁可以延续使用

public class LockTest {
	public void test() {
		//第一次获得锁
		synchronized (this) {
			while (true) {
				//第二次获得同样的锁
				synchronized (this) {
					System.out.println("ReentrantLock");
				}
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}
	public static void main(String[] args) {
		new LockTest().test();
	}
}

不可重入锁:锁不可以延续使用

public class LockTest01 {
	Lock lock=new Lock();
	
	public void a() {
		lock.lock();
		doSomething();
		lock.unLock();
	}
	//不可重入
	public void doSomething() {
		lock.lock();
		//............
		lock.unLock();
	}
	
	public static void main(String[] args) {
		new LockTest01().a();
		new LockTest01().doSomething();
	}
}

class Lock{
	//是否占用
	private boolean isLocked=false;
	//使用锁
	public synchronized void lock() {
		while (isLocked) {
			try {
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		isLocked=true;
	}
	//释放锁
	public synchronized void unLock() {
		isLocked=false;
		notify();
	}
}

volatile关键字

volatile用于保证数据的同步,也就是可见性(不保证原子性),可以参考以下例子:

public class ValatileTest {
	private volatile static int num=0;
	public static void main(String[] args) {
		new Thread(()->{
			while (num==0) {  //此处不要编写代码
				
			}
		}).start();
		
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		num=1;
	}
}

线程并发协作(生产者-消费者模式)

生产者-消费者模式的基本概念

多线程环境下,经常需要多个线程能够并发和协作。这是,就需要了解一个重要的多线程并发协作模型“生产者-消费者模式”;

  • 什么是生产者。生产者指的是负责生产数据的模块(这里的模块指的可能是方法、对象、线程、进程等)。
  • 什么是消费者。消费者指的是负责处理数据的模块(这里的模块指的可能是方法、对象、线程、进程等)。
  • 什么是缓冲区。消费者不能直接使用生产者的数据,它们之间有个“缓冲区”。生产者将生产好数据放入“缓冲区”,消费者从“缓冲区”拿出要处理的数据。

缓冲区是实现并发操作的核心。缓冲区设置有如下3个好处:

  • 实现线程的并发协作:有了缓冲区以后,生产者线程只需要往缓冲区里面放置数据,而不需要管消费者消费的情况;同样,消费者只需要从缓冲区拿出数据处理即可,不需要考虑生产者生产的情况。这样,就从逻辑上实现了“生产者线程”和“消费者线程”的分离。
  • 解耦了生产者和消费者。生产者不需要和消费者直接打交道。
  • 解决忙闲不均,提高效率。生产者生产数据慢时,但在缓冲区仍有数据,不影响消费者消费;消费者处理数据慢时,生产者仍然可以继续往缓冲区里面放置数据。

而生产者-消费者模式主要有两种实现方法:管程法以及信号灯法。

线程并发协作(线程通信)的使用情景

  1. 生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。
  2. 对于生产者,没有生产产品之前,消费者要进入等待状态。而生产了产品之后,又需要马上通知消费者消费。
  3. 对于消费者,在消费之后,要通知生产者已经消费结束,需要继续生产新产品以供消费。
  4. 在生产者-消费者问题中,仅适用synchronized是不够的。synchronized可以阻止并发更新同一个共享资源,虽然实现了同步,但它不能用来实现不同线程之间的消息传递(通信),这就需要用到线程通信的方法了。

线程通信的常用方法

方法名 作用
final void wait() 表示线程一直等待,直到得到其他线程通知
void wait(long timeout) 线程等待指定毫秒参数的时间
final void wait(long timeout,int nanos) 线程等待指定毫秒、微秒的时间
final void notify() 唤醒一个处于等待状态的线程
final void notifyAll() 换新同一个对象上所有调用wait()方法的线程,优先级别高的线程优先运行
  • 注意事项: 以上方法均是java.lang.Object类的方法,只能在同步方法或者同步块中使用,否则会抛出异常。

在实际开发中,尤其是“架构设计”中会大量使用“生产者-消费者”模式。初学者仅需了解作用即可,如果想深入理解架构这一部分内容是相当重要的。

生产者消费者实现方法

以下是生产者-消费者模式的实现方法的实例,可结合概念以及注释理解。

管程法

public class CoTest01 {
	public static void main(String[] args) {
		SynContainer container=new SynContainer();
		new Thread(new Producer(container)).start();
		new Thread(new Consumer(container)).start();
	}
}
//生产者
class Producer implements Runnable{
	private SynContainer container;
	public Producer(SynContainer container) {
		this.container = container;
	}
	@Override
	public void run() {
		//生产
		for (int i = 0; i < 100; i++) {
			System.out.println("生产第"+(i+1)+"个面包");
			container.push(new Bread(i));
		}
	}
}
//消费者
class Consumer implements Runnable{
	private SynContainer container;
	public Consumer(SynContainer container) {
		this.container = container;
	}
	@Override
	public void run() {
		//消费
		for (int i = 0; i < 100; i++) {
			System.out.println("买了"+(container.get().getId()+1)+"个面包");
		}
	}
}
//缓冲区
class SynContainer{
	Bread[] breads=new Bread[10];
	private int count =0;
	//存储 生产
	public synchronized void push(Bread bread) {
		//缓冲区(库存)满了停止消费
		if (count==breads.length) {
			try {
				this.wait();  //线程阻塞 停止生产,消费者通知生产解除阻塞
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		//容器未满可以生产
		breads[count]=bread;
		count++;
		//this.notify();
		this.notifyAll();  //生产了商品可以通知生产者恢复消费了
	}
	//获取 消费
	public synchronized Bread get() {
		//缓冲区为空(没有面包)就需要停止消费
		if (count==0) {
			try {
				this.wait();  //线程阻塞 停止消费,生产者通知消费解除阻塞
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		//没有数据只能等待
		count--;
		//this.notify();
		this.notifyAll();  //消费了商品可以通知生产者恢复生产了
		return breads[count];
	}
}
//面包
class Bread{
	private int id;

	public int getId() {
		return id;
	}

	public Bread(int i) {
		super();
		this.id = i;
	}
}

信号灯法

public class CoTest02 {
	public static void main(String[] args) {
		Tv tv=new Tv();
		new Thread(new Actor(tv)).start();
		new Thread(new Audience(tv)).start();
	}
}
//生产者 演员
class Actor implements Runnable{
	private Tv tv;
	public Actor(Tv tv) {
		this.tv = tv;
	}

	@Override
	public void run() {
		for (int i = 0; i < 20; i++) {
			if (i%2==0) {
				this.tv.play("牛逼");
			} else {
				this.tv.play("666");
			}
		}
	}
}

//消费者 观众
class Audience implements Runnable{
	private Tv tv;
	public Audience(Tv tv) {
		this.tv = tv;
	}
	@Override
	public void run() {
		for (int i = 0; i < 20; i++) {
			this.tv.watch();
		}
	}
}
//同一个资源 电视
class Tv{
	private String voice;
	//信号灯:true表示演员表演,观众等待;false表示观众等待,演员表演
	private boolean flag=true;
	
	public synchronized void play(String voice){
		//演员等待
		if (!flag) {
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		System.out.println("演员说了:"+voice);
		this.voice=voice;
		this.notifyAll();  //唤醒
		this.flag=!this.flag;  //切换标志
	}
	
	public synchronized void watch() {
		//观众等待
		if (flag) {
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		System.out.println("观众听到了:"+this.voice);
		this.notifyAll();  //唤醒
		this.flag=!this.flag;  //切换标志
	}
}

任务定时调度

任务定时调度在项目开发中经常用到。在实际开发中可以使用quanz任务框架来开发,也可以使用Timer和Timertask类实现同样的功能。

通过Timer和TimerTask类可以实现定时启动某个线程,通过线程执行某个任务的功能。

Timer和Timertask类

  1. java.util.Timer

    在这种方式中,Timer类的作用类似于闹钟的功能,也就是定时或者每隔一定时间触发一次线程。其实,Timer是JDK中提供的一个定时器工具。使用的时候会在主线程之外起一个单独的线程执行指定的计划任务,可以指定执行一次或者反复执行多次,起到类似闹钟的作用。

  2. java.util.TimerTask

    TimerTask类是一个抽象类,该类实现了Runnable接口,所以该类具备多线程能力。在这种实现方式中,通过继承TimerTask使用该类获得多线程的能力,将需要多线程执行的代码书写在run方法内部,然后通过Timer类启动线程的执行。

可以参考以下例子理解:

public class TimerTest {
	public static void main(String[] args) {
		Timer timer=new Timer();
		//执行安排
		//timer.schedule(new MyTimer(), 3000);  //3000毫秒后执行1次
		//timer.schedule(new MyTimer(), 3000,1000);  //3000毫秒后执行,然后每隔1000毫秒执行一次
		Calendar calendar=new GregorianCalendar(2020,05,06,20,45,00);  //传入一个时间(注意月份0-11)
		//timer.schedule(new MyTimer(), calendar.getTime());  //按预定的时间执行一次
		timer.schedule(new MyTimer(), calendar.getTime(), 1000);  //按预定的时间执行,然后每隔1000毫秒执行一次
	}
}
//任务类
class MyTimer extends TimerTask{
	@Override
	public void run() {
		for (int i = 0; i < 5; i++) {
			System.out.println("放空大脑。。。");
		}
		System.out.println("----------END-------------");
	}
}

在实际使用中,一个Timer可以启动任意多个TimerTask实现的线程,但是多个线程之间会存在阻塞。所以如果多个线程之间需要完全独立的话,最好还是一个Timer启动一个TimerTask。

Quartz的简单例子

使用Quartz框架我们可以到Quartz官网下载开源文件,本文仅描述一个简单的例子,如果想深入了解可以查看文件中的API文档以及源码。

首先我们需要一个创建一个任务的对象:

import java.util.Date;

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

public class HelloJob implements Job {

    public HelloJob() {
    }

    public void execute(JobExecutionContext context)
        throws JobExecutionException {
    	System.out.println("------start-------");
        System.out.println("Hello World! - " + new Date());
        System.out.println("------end-------");
    }
}

以下是一个简单使用例子:

import static org.quartz.DateBuilder.evenSecondDateAfterNow;
import static org.quartz.JobBuilder.newJob;
import static org.quartz.TriggerBuilder.newTrigger;
import static org.quartz.SimpleScheduleBuilder.simpleSchedule;

import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerFactory;
import org.quartz.Trigger;
import org.quartz.impl.StdSchedulerFactory;

import java.util.Date;

/**
 * Quartz学习入门
 * @author WHZ
 *
 */
public class SimpleExample {

  public void run() throws Exception {
    //1、创建Scheduler工厂
    SchedulerFactory sf = new StdSchedulerFactory();
    //2、从工厂中获取调度器
    Scheduler sched = sf.getScheduler();


    //3、创建JobDetail(任务)
    JobDetail job = newJob(HelloJob.class).withIdentity("job1", "group1").build();

    //时间
    //Date runTime = evenMinuteDate(new Date());  //下一分钟
    Date runTime = evenSecondDateAfterNow();  //下一秒
    //4、触发器(触发条件)
    //Trigger trigger = newTrigger().withIdentity("trigger1", "group1").startAt(runTime).build();
    Trigger trigger = newTrigger().withIdentity("trigger1", "group1").startAt(runTime).  //按设定的时间开始运行
    		withSchedule(simpleSchedule().withIntervalInSeconds(5).withRepeatCount(3)).build();  //间隔5秒,重复3次
    
    //5、注册任务和触发条件
    sched.scheduleJob(job, trigger);
    
    //6、启动
    sched.start();

    
    try {
      //5秒后停止(该线程总共运行的时间)
      Thread.sleep(30L * 1000L);
    } catch (Exception e) {
    }
    //7、停止
    sched.shutdown(true);
  }

  public static void main(String[] args) throws Exception {

    SimpleExample example = new SimpleExample();
    example.run();

  }
}

实际开发中,可以使用该开源框架更加方便实现任务的定时调度,实际上该框架底层原理就是Timer和TimerTask类的内容,想要深入了解可以尝试阅读QUARTZ框架的源码。

结语

本篇到此完结,多线程的内容在Java中是极其深奥的一部分。碍于本人水平有限,本文中没有描述JUC包的内容,可以参考相关的API文档以及书籍来学习。而对于更加复杂的系统级程序设计,建议参考更高级的参考文献。希望看到这里的读者能点个赞给个关注,祝各位早日年薪百万!