声明:本博客代码完整,所有实例均可正常运行,代码仓库链接在文章底部,需要的可自取。
Java中重要机制之一就是支持内部多线程:在一个Java程序中允许同时运行多个任务。
1.线程概念
1.1 什么是线程?
我们写一段程序,然后运行这段程序,当我们运行这段程序,实际上就启动了一个线程,也就是说线程是运行代码程序的单位。 而这个代码程序,就是一个任务。
线程本质上讲就是执行任务的对象,如果把线程比作一台打印机,那么任务就是一份需要打印的文档。
线程提供了运行一个任务的机制,对于Java, 可以在一个程序中并发地启动多个线程,这些线程可以同时运行。
1.2 使用多线程的好处
多线程可以使程序执行的更快,让程序的交互性更强,从而使得程序执行效率的提升。
简单来说,如果当我们写一段程序处理一份数据时,如果我们使用多线程编程去解决这个问题,我们能更快的获得结果。
如果使用多线程让程序同时执行不同类型的任务,就能让程序同时服务多个不同的请求,请求和请求之间可以不用排队,很快就能得到响应。
2.Java如何定义任务和线程
本节主要学习如何使用Java语言编写多线程程序。
2.1 任务定义
在Java中定义一个任务就是定义实现Runnable接口的一个类(Runnable中只有一个run方法)
2.1.1 Runnable接口
// 任务接口,Java中定义一个任务类必须要实现这个接口
public interface Runnable {
public abstract void run();
}
可以看到,Runnable只有一个方法的定义,就是run,也就是说每个任务需要实现这个接口,给出run的程序,线程具体执行的代码就是这个run的实现。
例如下面定义一个名为PrintChar的任务类:
public class PrintChar implements Runnable{
private final char charToPrint;
private final int times;
public PrintChar(char c,int t) {
charToPrint=c;
times=t;
}
@Override
public void run() {
for(int i=0;i<times;i++) {
System.out.print(charToPrint);
}
}
}
定义好这个类后,当我们学习了Thread的使用,便可以创建任务对象,然后让线程去执行它。
2.2 线程定义
在Java中,我们只需要新建一个Thread对象,然后将具体任务对象作为参数传入,接着执行start方法即可开启一个线程并执行一个任务。
例如:
public static void main(String[] args) {
//新建任务对象:这里新建了两个任务
Runnable printA=new PrintChar('a',100);
Runnable printB=new PrintChar('b',100);
//新建线程对象:这里新建了两个线程
Thread thread1=new Thread(printA);
Thread thread2=new Thread(printB);
//并行运行thread1,thread2
thread1.start();
thread2.start();
}
这样的话,在程序中,这两个线程是不会相互影响的同时运行的,如果使用单线程(常规编程),那么花费的时间会更长。
3.学习Thread类
通过上面的学习,我们写出了第一个多线程程序,可以看到多线程涉及的类主要有两个,一个是我们自定义的任务类,另一个就是Jdk提供了Thread类,下面我们对其进行学习。
下面我们看下这个类的类图:
3.1 Thread实现了Runnable接口
从上面我们可以看出Thread也实现了Runnable接口,所以,我们是不是也能通过Thread定义自己的任务呢?
答案是可以的:
public class TaskThreadDemo extends Thread {
private final char charToPrint;
private final int times;
public TaskThreadDemo(char charToPrint, int times) {
this.charToPrint = charToPrint;
this.times = times;
}
public void run(){
for(int i=0;i<times;i++) {
System.out.print(charToPrint);
}
}
}
然后使用它:
public static void main(String[] args) {
Thread thread1 = new TaskThreadDemo('a',100);
Thread thread2 = new TaskThreadDemo('b',100);
thread1.start();
thread2.start();
}
但是这种方法并不常用,因为,让任务类去继承Thread并不是一个灵活的做法,在Java中一个类只能继承一个类,但可以实现多个接口,我们应该让任务类去继承更有价值的类。
3.2 Thread中的方法学习
可以通过下面的例子进行学习
public static void main(String[] args) throws InterruptedException {
//创建任务
Runnable printA=new PrintChar('a',5);
Runnable printB=new PrintChar('b',5);
//新建线程
Thread thread1 = new Thread(printA);
Thread thread2 = new Thread(printB);
//理论上优先级值越小, 执行越优先
thread1.setPriority(10);
thread2.setPriority(1);
//并行开始运行thread1,thread2
thread1.start();
thread2.start();
//等待thread1,thread2执行结束
thread1.join();
thread2.join();
System.out.println();
/**
* 利用isAlive对线程状态进行检查
*/
while (true){
//让当前线程进入休眠状态
Thread.sleep(1000);
boolean thread1Alive = thread1.isAlive();
boolean thread2Alive = thread2.isAlive();
if(!thread1Alive&&!thread2Alive){
System.out.println("thread1 和 thread2 运行完毕!");
break;
}
if(thread1Alive) System.out.println("thread1 正在运行");
if(thread2Alive) System.out.println("thread2 正在运行");
}
}
4.线程池
操作系统创建线程的开销是比较大的,我们不能在程序中无止境的创建线程,但是我们可以利用已经存在的线程进行多线程任务执行,这时,我们就可以使用线程池来达到目的了。
我们可以使用线程池来高效执行任务
前面的对单个任务创建一个线程显然对大量任务而言使不够高效的,原因是:为每个任务开始一个新线程可能会限制吞吐量并且造成性能降低。线程池是管理并发执行任务个数的理想方法。
Java提供Executor接口,ExecutorService接口,Executors类来支持线程池。
他们类图如下:
4.1 Executor接口
此接口仅定义了一个方法execute, 这个方法相当于Thread中的start, 它让线程吃执行传入其中的任务。
//线程池用此方法执行传入的任务
void execute(Runnable command);
4.2 ExecutorService接口
这个就是接口是线程池接口,其中定义了线程池包含的方法
public interface ExecutorService extends Executor {
//关闭线程池,已经提交的任务继续执行,不接受新任务
void shutdown();
//尝试停止所有正在执行的任务,停止等待任务的处理,并返回等待执行的任务列表。
List<Runnable> shutdownNow();
//如果线程池已经关闭,则返回true
boolean isShutdown();
//如果执行shutdown后,线程池中所有任务结束,则返回true
boolean isTerminated();
//执行shutdown后,进入阻塞等待状态直到所有任务都完成/超时
boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException;
// 提交一个带有返回值的任务
<T> Future<T> submit(Callable<T> task);
//提交一个任务并返回一个Future对象
//这个Future对象能在在任务完成后使用get得到result对象
<T> Future<T> submit(Runnable task, T result);
//提交一个任务并返回一个Future对象
//这个Future对象能在任务完成后使用get得到null
Future<?> submit(Runnable task);
// 执行提交的集合中所有的任务并返回一个Future列表
// 可以使用Future.isDone来判断任务是否执行完成,如果完成返回true
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException;
//执行提交的集合中所有的任务并返回一个Future列表(任务结束或超时后可获取状态)
//可以使用Future.isDone来判断任务是否执行完成,如果完成返回true
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException;
//执行给顶的任务列表,返回已成功完成的任务结果
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException;
//执行给顶的任务列表,返回已成功完成的任务结果,如果超时则抛出异常
<T> T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
4.3 AbstractExecutorService类
这个类提供了线程池接口的抽象实现,提供了一些可复用的代码。
感兴趣的同学可以学习下, 本节的目的不是解读源码,所以略。
4.4 ThreadPoolExecutor
它是ExecutorService的一个具体实现。
它继承了AbstractExecutorService抽象类,它是线程池的一种具体实现。
我们可以直接使用它来构造自己的线程池。
但是由于这个类的使用比较复杂,所以我们可以先使用Jdk中对这个类的封装。
4.5 Executors类
这个类是线程池的一个工厂类,它提供了一些ThreadPoolExecutor的对象生成方法,
例如:
//创建一个指定线程数的线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
//创建一个不限数量的线程池,当需要线程时,线程池会新建一个
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
就上面两种线程池而言,一般前者更安全,因为后者存在线程超量的问题。
4.6 使用线程池
public class ThreadPoolStudy {
public static void main(String[] args) {
//创建一个线程池,且里面最多容纳1个线程
ExecutorService executor= Executors.newFixedThreadPool(1);
//添加任务到线程池中,实际上,这里的两个任务的执行时串行的
//因为前面定义线程池中线程的数量为1,只有一个线程执行任务
executor.execute(new PrintChar('a',5));
executor.execute(new PrintChar('b',5));
//关闭执行器
executor.shutdown();
}
static class PrintChar implements Runnable{
private final char charToPrint;
private final int times;
public PrintChar(char c,int t) {
charToPrint=c;
times=t;
}
public void run() {
for(int i=0;i<times;i++) {
System.out.print(charToPrint);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
5. 关键字线程同步
线程同步用于协调相互依赖的线程的安全执行,保存程序中数据不出问题。
在计算机中存在这样一个问题:如果一个共享资源被多个线程同时访问,则这个共享资源可能存在会遭到破坏的风险。
因此为了避免这种情况发生,我们应该防止多个线程同时进入程序的某一特定修改共享资源的部分,这一部分称为临界区(critical region)
5.1 同步方法
在Java中,我们可以用关键字synchronized来保证多线程安全执行,以便一次只有一个线程可以访问这个方法,被synchronized修饰的方法我们可以称之为同步方法。
一个同步方法在执行之前需要加锁。锁是一种实现资源排他使用的机制。
对于实例同步方法,要给调用该方法的对象加锁。
对于静态同步方法,要给这个类加锁。
5.1.1 实例演示
// 完整代码地址在文章最后
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(1000);
Account account = new Account();
UnSynAccount unSynAccount = new UnSynAccount();
for(int i=0;i<500;i++)
executorService.execute(account);
for(int i=0;i<500;i++)
executorService.execute(unSynAccount);
executorService.shutdown();
while (true){
if(executorService.isTerminated()){
System.out.println(account.toString());
System.out.println(unSynAccount.toString());
break;
}
}
}
5.2 同步语句
如果同步方法中只有几行代码是临界区,那么仅仅为了这几行临界区而牺牲整个方法的性能,这显然不值得,所以Java中存在同步语句机制来解决这一问题。
同步语句允许设置同步方法中的部分代码,而不必是整个方法,这大大增强了程序的并发能力。
当执行方法中的某一代码块时,同步语句不仅可用于对this对象的加锁,还可用于对任何对象加锁,这个代码块称为同步块(synchronized block)
其一般形式为:
synchronized(expr){
statements;
}
表达式expr求值结果必须是一个对象的引用,如果对象已经被另一个线程锁定,则在解锁之前该线程将被阻塞,当获准对一个对象加锁时,该线程执行同步块中的语句,然后解除锁。
实例演示:
// 完整代码地址在文章最后
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(1000);
Account account = new Account();
UnSynAccount unSynAccount = new UnSynAccount();
SynBlockAccount blockAccount = new SynBlockAccount();
for(int i=0;i<500;i++)
executorService.execute(blockAccount);
for(int i=0;i<500;i++)
executorService.execute(unSynAccount);
executorService.shutdown();
while (true){
if(executorService.isTerminated()){
System.out.println(blockAccount.toString());
System.out.println(unSynAccount.toString());
break;
}
}
}
6.API加锁实现同步(Lock类)
我们可以显式地采用锁对象和状态来同步线程。
Java可以显式地加锁,这给协调线程带来了更多的控制功能,在Java中提供了Lock接口来定义相应的锁机制。
我们可以使用Lock具体的实现来实现加锁操作。
ReentrantLock是Lock的一个具体实现。
6.1 Lock接口
Lock接口中定义如下
//得到一个锁
void lock();
//获得一个锁如果线程没被中断
void lockInterruptibly() throws InterruptedException;
//当锁没有被占用,才获取锁。
boolean tryLock();
//尝试获取锁直到获取或超时
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//释放锁
void unlock();
//返回一个绑定了锁的Condition
Condition newCondition();
6.2 ReentrantLock
ReentrantLock是对Lock的具体实现,我们可以使用它来实现对临界区的保护,使用方式如下:
Lock lock = new ReentrantLock();
lock.lock();
try{
//临界代码块
}catch(Exception e){
}finally{
//确保锁能被正常释放
lock.unlock();
}
这个结构能确保任何时刻只有一个线程进入临界区。 一旦一个线程锁定了锁锁对象,其他任何线程都无法正常执行lock语句。 当其他线程调用lock时,他们会暂停,直到前面的线程释放了这个锁对象。
6.2.1 演示实例
public class LockStudy {
private static Account account = new Account();
public static void main(String[] args) {
//创建线程池
ExecutorService executor= Executors.newCachedThreadPool();
//循环添加任务
for(int i=0;i<1000;i++) {
executor.execute(new AddAPennyTask());
}
// 关闭线程池
executor.shutdown();
//所有任务都执行完
while(true) {
if(executor.isTerminated()){
System.out.println("任务执行完毕!!");
break;
}
}
System.out.println("what is balance? "+account.getBalance());
}
public static class AddAPennyTask implements Runnable{
public void run() {
account.deposit(1);
}
}
private static class Account{
//创建一个互斥锁
private static final Lock lock = new ReentrantLock();
private int balance = 0;
public int getBalance(){
return balance;
}
public void deposit(int amount) {
//获取一个锁
lock.lock();
try{
int newBalance=balance+amount;
Thread.sleep(5);
balance=newBalance;
} catch(InterruptedException ex) {
System.out.println("线程中断!!");
} finally{
//释放锁
lock.unlock();
}
}
}
}
7.本章完整代码地址
Java基础学习/src/main/java/Progress/exa28_1 · 严家豆/Study - 码云 - 开源中国 (gitee.com)