Java多线程之基础


文章大部分内容摘自gitbook:深入浅出Java多线程


进程和线程概念

批处理操作系统

批处理操作系统,把⼀系列需要操作的指令写下来,形成⼀个清单,⼀次性交给计算机。用户将多个需要执行的程序写在磁带上,然后交由计算机去读取并逐个执行这些程序,并将输出结果写在另⼀个磁带上。

批处理操作系统在⼀定程度上提高了计算机的效率,但是由于批处理操作系统的指令运行方式仍然是串行的,内存中始终只有⼀个程序在运行,后面的程序需要等待前面的程序执行完成后才能开始执行,而前面的程序有时会由于I/O操作、网络等原因阻塞,所以批处理操作效率也不高。

进程的提出

批处理操作系统的瓶颈在于内存中只存在⼀个程序,那么内存中能不能存在多个程序呢? 这是人们亟待解决的问题。于是,科学家们提出了进程的概念。

进程就是应用程序在内存中分配的空间,也就是正在运行的程序,各个进程之间互不干扰。同时进程保存着程序每⼀个时刻运行的状态。此时,CPU采用时间片轮转的方式运行进程:CPU为每个进程分配⼀个时间段,称作它的时间片。如果在时间片结束时进程还在运行,则暂停这个进程的运行,并且CPU分配给另⼀个进程(这个过程叫做上下文切换)。如果进程在时间片结束前阻塞或结束,则CPU立即进行切换,不用等待时间片用完。使用进程+CPU时间片轮转方式的操作系统,在宏观上看起来同⼀时间段执行多个任务,换句话说,进程让操作系统的并发成为了可能。虽然并发从宏观上看有多个任务在执行,但在事实上,对于单核CPU来说,任意具体时刻都只有⼀个任务在占用CPU资源。

线程的提出

虽然进程的出现,使得操作系统的性能大大提升,但是随着时间的推移,⼈们并不满足⼀个进程在⼀段时间只能做⼀件事情,如果⼀个进程有多个子任务时,只能逐个得执行这些子任务,很影响效率。
比如杀毒软件在检测用户电脑时,如果在某一项检测中卡住了,那么后面的检测项也会受到影响。或者说当你使用杀毒软件中的扫描病毒功能时,在扫描病毒结束之前,无法使用杀毒软件中清理垃圾的功能,这显然无法满足人们的要求。

于是⼈们又提出了线程的概念,让⼀个线程执行⼀个子任务,这样一个进程就包含了多个线程,每个线程负责⼀个单独的子任务。

进程与线程的区别

进程让操作系统的并发性成为了可能,而线程让进程的内部并发成为了可能。

使用多线程的方式实现并发有以下几个好处:
1:进程间的通信比较复杂,而线程间的通信比较简单,通常情况下,我们需要使用共享资源,这些资源在线程间的通信比较容易。
2:进程是重量级的,而线程是轻量级的,故多线程方式的系统开销更小。

进程和线程的区别:
进程是⼀个独立的运行环境,而线程是在进程中执行的⼀个任务。他们两个本质的区别是是否单独占有内存地址空间及其它系统资源(比如I/O)

  • 进程单独占有⼀定的内存地址空间,所以进程间存在内存隔离,数据是分开的,数据共享复杂但是同步简单,各个进程之间互不干扰;而线程共享所属进程占有的内存地址空间和资源,数据共享简单,但是同步复杂。
  • 进程单独占有⼀定的内存地址空间,⼀个进程出现问题不会影响其他进程,不影响主程序的稳定性,可靠性高;⼀个线程崩溃可能影响整个程序的稳定性,可靠性较低。
  • 进程单独占有⼀定的内存地址空间,进程的创建和销毁不仅需要保存寄存器和栈信息,还需要资源的分配回收以及页调度,开销较大;线程只需要保存寄存器和栈信息,开销较小。
  • 进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位,即CPU分配时间的单位 。

上下文切换

上下文切换(有时也称做进程切换或任务切换)是指 CPU 从⼀个进程(或线程)切换到另⼀个进程(或线程)。上下文是指某⼀时间点 CPU 寄存器和程序计数器的内容。

CPU通过为每个线程分配CPU时间片来实现多线程机制。CPU通过时间片分配算法来循环执行任务,当前任务执行⼀个时间片后会切换到下⼀个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。

上下文切换通常是计算密集型的,意味着此操作会消耗大量的 CPU 时间,故线程也不是越多越好。


Java多线程入门类和接口

继承Thread类

package 线程;

public class MyThread extends Thread{
	
	private MyThread() {
		super();
	}
	public MyThread(String name) {
		//第二种方法给线程设置名称,通过调用Thread构造方法
		super(name);
	}
	@Override
	public void run() {
		//一般被多线程执行的代码比较耗时
		for (int i = 0; i < 1000; i++) {
			System.out.println("你好," + getName());
		}
	}
	
	public static void main(String[] args) {
		 MyThread my1 = new MyThread();
		 //第一种获取线程名称
		 my1.setName("线程1");
		 MyThread my2 = new MyThread("线程2");
		 
		 
		 //run方法为普通方法调用,无法实现多线程
		 /*my.run();
		 my.run();*/
		 
		 //start方法使该线程执行;java虚拟机调用该线程的run方法
		 my1.start();
		 my2.start();
		 
		 //为主线程设置名称
		 Thread.currentThread().setName("主线程");
		 //返回当前正在执行的线程名称
		 System.out.println(Thread.currentThread().getName());
	}

}

实现Runnable类

package 线程的创建;
/*
 * 此种创建线程使用了静态代理设计模式
 */
class MyThread1 implements Runnable {
	//重写Runnable接口的run方法
	public void run() {
		int i =0;
		while (i++<10) {
			System.out.println(Thread.currentThread().getName() + "的run()方法在运行");
		}
	}
}

public class TestRunnable {	

	public static void main(String[] args) {
		// TODO 自动生成的方法存根

		//创建Runnable接口实现类的实例化
		//(创建真实角色)
		MyThread1 myThread = new MyThread1();
		//使用Thread(Runnable target, String name)构造方法创建线程对象
		//(创建代理角色+真实角色引用)
		Thread thread1 = new Thread(myThread, "thread1");
		//调用线程对象的start()方法启动线程
		thread1.start();
		//创建并启动另一个线程thread2
		Thread thread2 = new Thread(myThread, "thread2");
		thread2.start();
	}

}

Callable、Future与FutureTask

package 线程;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;

public class MyCallable implements Callable<Object> {

	@Override
	public Object call() throws Exception {
		Thread.sleep(1000);
		return "Callable有返回值";
	}
	
	public static void main(String[] args) throws Throwable {
		// 创建线程池工具
		ExecutorService executor = Executors.newCachedThreadPool();
		// 新建任务
		FutureTask<Object> futureTask = new FutureTask<>(new MyCallable());
		// 通过线程池工具提交任务
		executor.submit(futureTask);
		// 输出线程任务返回值
		System.out.println(futureTask.get());
	}

}

Java线程组和线程优先级

线程组

Java中用ThreadGroup来表示线程组,可以使用线程组对线程进行批量控制每个Thread必然存在于一个ThreadGroup中,Thread不能独立于ThreadGroup存在。执行main()方法线程的名字是main,如果在new Thread时没有显式指定,那么默认将父线程(当前执行new Thread的线程)线程组设置为自己的线程组。

package 线程组;

public class ThreadGroup1 {
	
	public static void main(String[] args) {
		Thread thread = new Thread(() -> {
			// 获取当前线程组的名字(使用线程组对线程批量控制)
			System.out.println("thread当前线程组名字:" + 
		Thread.currentThread().getThreadGroup().getName());
			// 获取当前线程名字
			System.out.println("thread线程名字:" + Thread.currentThread().getName());
		});
		// 开启线程
		thread.start();
		//获取主线程的名字
		System.out.println("执行main方法的线程名字:" + Thread.currentThread().getName());
	}
	
}

线程的优先级

Java中线程优先级可以指定,范围是1~10。但是并不是所有的操作系统都支持10级优先级的划分(比如有些操作系统只⽀持3级划分:低,中,高),Java只是给操作系统一个优先级的参考值,线程最终在操作系统的优先级是多少还是由操作系统决定。

package 线程;

/*
 * priority:
 * 	调用方式:线程名称.priority(),括号中填入数字,不可以超过1000(最大值为10,最小为1,默认为5)
 *  位置:放在start之前
 * 	
 */

public class MyPriority extends Thread{

	private MyPriority(String name) {
		super(name);
	}
	@Override
	public void run() {
		for (int i = 0; i < 100; i++) {
			System.out.println(i + ",你好," + getName());
		}
	}
	public static void main(String[] args) {
		// TODO 自动生成的方法存根

		MyPriority my1 = new MyPriority("线程1");
		MyPriority my2 = new MyPriority("线程2");
		
		//设置线程优先级,在线程启动前设置
		//my1.setPriority(1000);1000值过大出现非法异常
		/*
		 * 线程优先级:最大10,最小1,Java默认线程优先级5
		 * 线程执行顺序是由调度程序来决定
		 * 线程执行是随机(概率的大小问题)的,并不是值越大,越先执行
		 * Java程序中对线程所设置的优先级只是给操作系统一个建议,操作系统不一定采纳
		 * 而真正的调度顺序由操作系统的线程调度算法决定
		 */
		my1.setPriority(1); 
		my2.setPriority(10);
		
		//启动线程
		my1.start();
		my2.start();
	}

}

一个线程必然存在于一个线程组中,那么当线程和线程组的优先级不一致的时候将会怎样呢?

package 线程组;

public class ThreadGroupPriority {
	
	public static void main(String[] args) {
		ThreadGroup threadGroup = new ThreadGroup("group1");
		threadGroup.setMaxPriority(6);
		Thread thread = new Thread(threadGroup, "thread1");
		// 如果某个线程的优先级大于线程组的优先级,那么线程的优先级会失效,取而代之的是线程组的优先级
		thread.setPriority(9);
		System.out.println("我是线程组的优先级:" + threadGroup.getMaxPriority());
		System.out.println("我是线程的优先级:" + thread.getPriority());
	}

}

输出如下:
在这里插入图片描述
所以,如果某个线程优先级大于线程所在线程组的最大优先级,那么该线程的优先级将会失效,取而代之的是线程组的最大优先级。

线程组常用方法

1:获取当前线程名字

Thread.currentThread().getThreadGroup().getName();

2:复制线程组(复制一个线程数组到另一个线程组)

ThreadGroup threadGroup = new ThreadGroup("Group1");
// 创建一个新的线程数组
Thread[] threads = new Thread[threadGroup.activeCount()];
// 将线程数组复制到线程组中
threadGroup.enumerate(threads);

3:线程组统一异常处理

ThreadGroup threadGroup2 = new ThreadGroup("Group2") {
	// 继承ThreadGroup并重新定义以下方法
	// 在线程成员抛出unchecked exception
	// 会执行此方法
	public void uncaughtException(Thread t, Throwable e) {
		System.out.println(t.getName() + ": " + e.getMessage());
	}
};
// 这个线程是Group的一员
Thread thread = new Thread(threadGroup2, new Runnable() {
	
	@Override
	public void run() {
		throw new RuntimeException("测试异常");
	}
});

输出结果如下:
在这里插入图片描述


Java线程的状态及主要转化方法

操作系统中的线程状态转换

在这里插入图片描述

Java线程的六个状态

  • NEW
// 处于NEW状态的线程此时尚未启动(未调用start方法)
public static void testStateNew() {
	Thread thread = new Thread(() -> {});
	System.out.println(thread.getState());
	thread.start();
	System.out.println(thread.getState());	
	//在调用一次start()之后,threadStatus的值会改
	// 变(threadStatus !=0),此时再次调⽤start()⽅法会抛出
	// IllegalThreadStateException异常。
	thread.start();
}
  • RUNNABLE
public static void testStateRunnable() {
	Thread thread = new Thread(() -> {});
	// 开启线程
	thread.start();
	System.out.println(thread.getState());
}
  • BLOCKED
public static void main(String[] args) {
	
	/**
	 * 处于BLOCKED状态的线程正等待锁的释放以进入同步区。
	 * 1:该状态没有占用锁
	 * 2:不需要其他线程唤醒
	 * 
	 * 假如今天你下班后准备去食堂吃饭。你来到食堂仅有的⼀个窗口,发现前面
		已经有个人在窗口前了,此时你必须得等前面的⼈从窗口离开才行。
		假设你是线程t2,你前⾯的那个人是线程t1。此时t1占有了锁(食堂唯⼀的
		窗口),t2正在等待锁的释放,所以此时t2就处于BLOCKED状态。
	 */
	
}
  • WAITING
public static void main(String[] args) {
		
	/**
	 * 
	 * 处于等待状态的线程变成RUNNABLE状态需要其他线程唤醒。
	 * 1:该状态占有锁
	 * 2:需要其它线程唤醒
	 * 
	 * 调⽤如下3个⽅法会使线程进⼊等待状态:
		Object.wait():使当前线程处于等待状态直到另⼀个线程唤醒它;(要是其他线程不进行唤醒,那么此线程会一直等待)
		Thread.join():等待线程执⾏完毕,底层调⽤的是Object实例的wait⽅法;
		LockSupport.park():除⾮获得调⽤许可,否则禁⽤当前线程进⾏线程调度。
	 */
	
}
  • TIME_WAITING
public static void main(String[] args) {
		
		
	/**
	 * TimedWaitingThread:线程等待⼀个具体的时间,时间到后会被⾃动唤醒。
	 * 
	 * 以下方法会是线程进入超时等待状态
	 * 	1:Thread.sleep(long millis):使当前线程睡眠指定时间;

	 * 	2:Object.wait(long timeout):线程休眠指定时间,等待期间可以通过
			notify()/notifyAll()唤醒;	
	 *  3:Thread.join(long millis):等待当前线程最多执⾏millis毫秒,如果millis为0,则
			会⼀直执⾏;
	 *  4:LockSupport.parkNanos(long nanos): 除⾮获得调⽤许可,否则禁⽤当前线
			程进⾏线程调度指定时间;
	 *  5:LockSupport.parkUntil(long deadline):同上,也是禁⽌线程进⾏调度指定时
			间;
	 */
	
}
  • TERMINATED
public static void main(String[] args) {
		
	/**
	 * 
	 * 终⽌状态。此时线程已执⾏完毕。
	 * 
	 */
	
}

线程状态的转换

根据上面的六个状态可以得到下面的线程状态转换图:

在这里插入图片描述


Java线程间的通信

锁与同步

在Java中,锁的概念都是基于对象的,所以我们⼜经常称它为对象锁。线程和锁的关系,我们可以⽤婚姻关系来理解。⼀个锁同⼀时间只能被⼀个线程持有。也就是说,⼀个锁如果和⼀个线程“结婚”(持有),那其他线程如果需要得到这个锁,就得等这个线程和这个锁“离婚”(释放)。可以以解释为:线程同步是线程之间按照⼀定的顺序执⾏。为了达到线程同步,我们可以使⽤锁来实现它。

package 线程通信;


/**
 * 锁和同步:
 * 	为了达到线程同步,可以使用锁来实现它
 * @author 15447
 *
 */

public class ObjectLock {
  
	private static Object lock = new Object();
	
	/**
	 * 在循环体外添加了lock对象线程锁
	 */
	static class ThreadA implements Runnable {
		@Override
		public void run() {
			synchronized (lock) {
				for (int i = 0; i < 100; i++) {
					System.out.println("Thread A" + i);
				}
			}
		}	
	}
	
	/**
	 * 在循环体外添加了lock对象线程锁
	 */
	static class ThreadB implements Runnable {
		@Override
		public void run() {
			synchronized (lock) {
				for (int i = 0; i < 100; i++) {
					System.out.println("Thread B" + i);
				}	
			}
		}	
	}
	
	public static void main(String[] args) {
		new Thread(new ThreadA()).start();
		try {
			Thread.sleep(10);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		new Thread(new ThreadB()).start();
	}
	
}

输出结果为:根据线程和锁的关系,同⼀时间只有⼀个线程持有⼀个锁,那么线程B就会等线程A执⾏完成后释放 lock ,线程B才能获得锁 lock 。

等待/通信机制

上⾯⼀种基于“锁”的⽅式,线程需要不断地去尝试获得锁,如果失败了,再继续尝试。这可能会耗费服务器资源。⽽等待/通知机制是另⼀种⽅式。Java多线程的等待/通知机制是基于 Object 类的 wait() ⽅法和 notify() , notifyAll() ⽅法来实现的。

package 线程通信;

/**
 * wait、notify一定要放在同步块中
 * @author 15447
 *
 */

public class WaitAndNotify {
	
	private static Object lock = new Object();
	
	static class ThreadA implements Runnable {
		@Override
		public void run() {
			synchronized (lock) {
				for (int i = 0; i < 5; i++) {
					System.out.println("ThreadA:" + i);
					try {
						// 唤醒另一个线程
						lock.notify();
						// 当前线程进入等待状态
						lock.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
//				lock.notify();
			}
		}
	}
	
	static class ThreadB implements Runnable {
		@Override
		public void run() {
			synchronized (lock) {
				for (int i = 0; i < 5; i++) {
					System.out.println("ThreadB:" + i);
					try {
						lock.notify();
						lock.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
//				lock.notify();
			}
		}
	}
	
	public static void main(String[] args) {
		new Thread(new ThreadA()).start(); 
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		new Thread(new ThreadB()).start(); 
	}

}

输出结果如下:

在这里插入图片描述

信号量

volatile 关键字的⾃⼰实现的信号量通信。让线程A输出0,然后线程B输出1,再然后线程A输出2…以此类推。应该怎样实现呢?

package 线程通信;

/**
 * volitile(实现信号量通信)关键字能够保证内存的可⻅性,如果⽤volitile关键字声明了⼀个变
      量,在⼀个线程⾥⾯改变了这个变量的值,那其它线程是⽴⻢可⻅更改后的值的。
 * @author 15447
 *
 */
public class Signal {

	//  volatile关键字的⾃⼰实现的信号量通信
	private static volatile int signal = 0;
	
	static class ThreadA implements Runnable {
		@Override
		public void run() {
			while (signal < 10) {
				if (signal % 2 == 0) {
					System.out.println("ThreadA:" + signal);
					synchronized (this) {
						signal++;
					}
				}
			}
		}
	}
	
	static class ThreadB implements Runnable {
		@Override
		public void run() {
			while (signal < 10) {
				if (signal % 2 == 1) {
					System.out.println("ThreadB:" + signal);
					synchronized (this) {
						signal++;
					}
				}
			}
		}
	}
	
	public static void main(String[] args) {
		new Thread(new ThreadA()).start(); 
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		new Thread(new ThreadB()).start(); 
	}
	
}

输出结果如下:

在这里插入图片描述
使⽤了⼀个 volatile 变量 signal 来实现了“信号量”的模型。这⾥需要注意的是, volatile 变量需要进⾏原⼦操作。 signal++ 并不是⼀个原⼦操作,所以我们需要使⽤ synchronized 给它“上锁”。

管道

管道是基于“管道流”的通信⽅式。JDK提供了 PipedWriter 、 PipedReader 、 PipedOutputStream 、 PipedInputStream 。其中,前⾯两个是基于字符的,⾯两个是基于字节流的。

package 线程通信;

import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;

/**
 * 线程管道
 * @author 15447
 *
 */

public class Pipe {
	
	static class ReaderThread implements Runnable {
		private PipedReader reader;

		public ReaderThread(PipedReader reader) {
			this.reader = reader;
		}
		
		@Override
		public void run() {
			System.out.println("this is reader");
			int receive = 0;
			try {
				while ((receive = reader.read()) != -1) {
					System.out.print((char)receive);
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	
	static class WriterThread implements Runnable {
		private PipedWriter writer;
	
		public WriterThread(PipedWriter writer) {
			this.writer = writer;
		}
		
		@Override
		public void run() {
			System.out.println("this is writer");
			int receive = 0;
			try {
				writer.write("test");
			} catch (IOException e) {
				e.printStackTrace();
			} finally {
				try {
					writer.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
	
	public static void main(String[] args) {
		
		/**
		 * 1. 线程ReaderThread开始执⾏,
			2. 线程ReaderThread使⽤管道reader.read()进⼊”阻塞“,
			3. 线程WriterThread开始执⾏,
			4. 线程WriterThread⽤writer.write("test")往管道写⼊字符串,
			5. 线程WriterThread使⽤writer.close()结束管道写⼊,并执⾏完毕,
			6. 线程ReaderThread接受到管道输出的字符串并打印,
			7. 线程ReaderThread执⾏完毕。
		 */
		
		PipedReader pipedReader = new PipedReader();
		PipedWriter pipedWriter = new PipedWriter();
		try {
			pipedWriter.connect(pipedReader);
		} catch (IOException e) {
			e.printStackTrace();
		}
		new Thread(new ReaderThread(pipedReader)).start();
		new Thread(new WriterThread(pipedWriter)).start();
	}

}

输出结果如下:
在这里插入图片描述
管道通信的应⽤场景:使⽤管道多半与I/O流相关。当我们⼀个线程需要先另⼀个线程发送⼀个信息(⽐如字符串)或者⽂件等等时,就需要使⽤管道通信了。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lw中

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值