前言
在系统学完Java的面向对象编程之后,我们需要认真地来学习Java并发编程,我们在学习计算机操作系统的时候也都了解过进程、线程和协程的概念。在这篇文章中荔枝主要会梳理有关线程创建、线程生命周期、同步锁和死锁、线程通信和线程池的知识,并给出相应的精简示例,希望能帮助有需要的小伙伴们哈哈哈~~~
文章目录
一、基础概念
进程
我们知道CPU是主机上的中央核心处理器,CPU的核数代表着主机能在一个瞬间同时并行处理的任务数,单核CPU只能在内存中并发处理任务。而在现有的操作系统中,几乎都支持进程这个概念。进程是程序的在内存中的一次执行过程,具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位。
线程
线程在程序中是独立的、并发的执行流,与分隔的进程相比隔离性会更小,线程之间共享内存、文件句柄和其它的进程应有的状态。线程比进程具有更高的性能,这是由于同一进程中的线程具有共性。简单理解,多线程是进程中并行执行的多个子程序。
并发性和并行的区别
并行是指在同一时刻,有多条指令在多个处理器上同时执行;而并发是指在同一时刻只能执行,但是通过多进程快速轮换执行可以达到同时执行的效果。CPU主频就代表着这些进程之间频繁切换的速度。
二、创建线程的三种方式
2.1 通过继承Thread类来启用
Java语言中JVM允许程序运行多个线程并通过java.lang.Thread类来实现。
Thread类的特性
每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体,并通过该Thread对象的start()方法来启动线程。
流程:
- 定义子类继承Thread类;
- 子类中重写Thread类中的run方法;
- 创建Thread子类对象,即创建了线程对象;
- 调用线程对象start方法:启动线程,调用run方法。
具体代码示例
首先构建一个继承Thread类的子类
//继承Thread类的方式实现多线程
public class TestThread extends Thread{
@Override
public void run(){
System.out.println("多线程运行的代码");
}
}
调用线程
public class Test{
public static void main(String[]args){
Thread t = new TestThread();
t.start(); //启动线程
}
}
2.2 实现Runnable接口来实现
流程
- 定义子类,实现Runnable接口。
- 子类中重写Runnable接口中的run方法。
- 通过Thread类含参构造器创建线程对象。
- 将Runnable接口的子类对象作为实际参数传递给Thread类的构造方法中。
- 调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。
实现Runnable接口
public class TestRunnable implements Runnable{
@Override
public void run(){
System.out.println("实现Runnable接口运行多线程");
}
}
实现多线程
public class Test{
public static void main(String[]args){
Thread t = new Thread(new TestRunnable);
//带有线程名称的实例化线程对象。可以通过Thread.currentThread().getName()获取
//Thread t = new Thread(new TestRunnable,"the FirstThread");
t.start(); //启动线程
}
}
与继承Thread类的区别
- 继承Thread:线程代码存放Thread子类run方法中。重写run方法
- 实现Runnable:线程代码存在接口的子类的run方法。实现run方法
实现Runnable接口方法的好处
实现Runnable接口方法通过继承Runnable接口避免了当继承的局限性,同时也使得多个线程可以同时共享一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。
2.3 实现Callable接口
在前面通过实现Runnable接口创建多线程时,Thread类的作用就是把run方法包装成线程的执行体。而从Java5以后,Java提供了一个Callable接口中的call()方法作为线程执行体,同时call()方法可以有返回值,也可以抛出异常。
public class Test{
public static void main(String[]args){
//创建callable对象
ThirdThread tt = new ThirdThread();
//使用FutureTask来包装Callable对象
FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)()->{
...
...
});
new Thread(task,"有返回值的线程").start();
try{
//获取线程返回值
System.out.println("子线程的返回值" + task.get());
}catch (EXception ex){
ex.printStackTrace();
}
}
}
Callable接口实现类和Runnable接口实现类的区别在于是否有参数返回!
三、Thread类的相关方法
常用方法如下:
- void start():启动线程,并执行对象的run(0方法
- run():线程在被调度时执行的操作
- String getName():返回线程的名称
- void setName(String name):设置该线程名称
- static currentThread():返回当前线程
public class Test{
public static void main(String[]args){
TestRun r1 = new TestRun();
Thread t1 = new Thread(r1);
//为线程设置名称
t1.setName("线程t1");
t1.start(); //启动线程
System.out.println(t1.getName()); //若没指定,系统默认给出的线程名称是Thread-0....
}
}
public class TestRun implements Runnable{
@Override
public void run(){
System.out.println("实现Runnable接口运行多线程");
}
}
线程优先级
线程的优先级设置增加了线程的执行顺序靠前的概率,是用一个数组1-10来表示的,默认的优先级是5。涉及的方法有:getPriority()和setPriority()
//获取优先级
t1.getPriority();
//设置优先级
t1.setPriority(10);
线程让步
static void yield()线程让步,即暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程,若队列中没有同优先级的线程,则跳过。
Thread.yield();
线程阻塞
join():当某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到join()方法加入的join线程执行完为止。
try{
//获取线程返回值
t1.join();
}catch (EXception ex){
ex.printStackTrace();
}
线程睡眠
try{
Thread.sleep(1000);//当前线程睡眠1000毫秒
}catch(InterruptedException e)(
e.printStackTrace();
}
线程生命结束
t1.stop();
判断当前线程是否存活
t1.isAlive();
四、生命周期
线程从创建、启动到死亡经历了一个完整的生命周期,在线程的生命周期中一般要经历五种状态:新建——就绪——运行——阻塞——死亡。
- 新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态;
- 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,也就是在执行.start()方法后;
- 运行:当就绪的线程被调度并获得处理器资源时,便进入运行状态,run()方法定义了线程的操作和功能,此时run()方法的代码开始执行;
- 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态;
- 死亡:线程完成了它的全部工作或线程被提前强制性地中止 。
线程可能以如下三种方法结束:
- run或call方法执行完成后
- 线程抛出一个未捕获的Exception或Error
- 直接调用了stop()方法
五、同步锁和死锁
5.1 同步锁
多线程模式的提出势必就会带来线程同步的问题,在保证数据一致性上,我们需要为线程加上同步锁。Java中对于多线程安全的问题提出了同步机制,即在方法声明的时候加入synchronized关键字来修饰或者直接使用synchronized来锁一个demo
5.1.1 synchronized加锁的两种方式
synchronized同步锁关键字修饰
//使用synchronized同步锁关键字修饰需要同步执行的方法体
public synchronized void drawing(int money){
需要同步执行的代码
}
注意:
在普通方法上加同步锁synchronized,锁的是整个对象,不是某一个方法。如果是不同对象的话那么就是不同的锁。静态的方法加synchronized对于所有的对象都是同一个锁!
synchronized锁一段demo
使用这种方法来锁指向this的代码块使用的都是同一个同步锁。如果改成方法对象的话比如Account对象的话就是不同的同步锁。
synchronized(this){ //表示当前的对象的代码块被加了synchronized同步锁
demo...
}
5.1.2 Lock
相比于上面的synchronized相应的锁操作,Lock提供了更为广泛的锁操作。其中包括ReadWriteLock(读写锁)和ReentrantLock(可重入锁),ReadWriteLock提供了ReentrantReadWriteLock的实现类。在Java8中引入了一个新的StampedLock类替代了传统的ReentrantReadWriteLock并给出了三种锁模式:Write、ReadOptimistic和Reading。
ReentrantLock 实现demo
class x{
//定义锁对象
private final ReentrantLock lock = new ReentrantLock();
//...
//定义需要保证线程安全的方法
public void m(){
lock.lock();
try{
//需要保证线程安全的demo
}
finally{
lock.unlock();
}
}
}
5.2 死锁
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
解决方法
- 专门的算法、原则,比如加锁顺序一致
- 尽量减少同步资源的定义,尽量避免锁未释放的场景
六、线程通信
当我们手动开启并在控制台中输出两个线程的运行过程的时候,程序并不能每次都准确的控制两个线程的轮换执行的先后次序,所以Java中也提供了一些机制来保证线程的协调运行。在传统的Java中,基于同步锁synchronized关键字提供了借助于Object类的wait()、notify()和notifyAll()方法来控制线程的阻塞情况,而之后也出现了基于Condition和阻塞队列BlockingQueue来控制线程阻塞的情况。
6.1 传统的线程通信
Object类中提供的wait()、notify()和notifyAll()方法必须由一个同步监视器对象来调用,所以这三种方法必须基于同步锁synchronized关键字。
- wait():该方法会导致当前线程进入等待状态,直到其它的线程调用notify()或notifyAll()方法来唤醒该线程,wait方法有三种形式:不带时间参数(等待唤醒)、带毫秒时间参数(时间到自动唤醒)和带毫微秒的时间参数(时间到自动唤醒)。调用wait方法当前线程会释放对同步监视器的锁定。
- notify():唤醒该同步监视器上等待的单个线程,这种选择是按照优先级最高的来唤醒结束其等待状态。
- notifyAll():唤醒等待的所有线程。
//使用时直接调用方法就行,但必须是在有synchronized修饰的方法内去调用才可
wait();
notify();
notifyAll();
6.2 使用Condition来控制线程通信
对于程序不使用synchronized关键字来保证同步锁,而是采用Lock对象来保证同步,Java中提供了Condition类来保证线程通信。Contidion类中提供了类似于synchronized关键字中的三种方法:await()、signal()和signalAll(),替代了同步监视器的功能。
- await():类似于wait方法,会使得当前线程进入等待状态,直到其它线程调用signal()或signalAll()来唤醒。
- signal():唤醒单个线程。
- signalAll():唤醒多个线程。
//显示定义Lock对象
Lock lock = new ReentrantLock();
//获取Condition
Condition cond = lock.newCondition();
//需要同步的方法中加锁
public void fun(){
//加锁过程
lock.lock();
try{
if(条件) cond.await(); //线程进入等待
else{
//唤醒其他线程
cond.signalAll();
}
}catch(InterruptedException e){
e.printStrackTrace();
}finally{
//锁的释放
lock.unlock();
}
}
6.3 使用阻塞队列来控制线程通信
除了上述两种方法,Java5中还提供了BlockingQueue接口来作为线程同步的工具。它的工作原理是这样滴:当生产者往BlockingQueue接口中放入元素直至接口队列满了,线程阻塞;消费者从BlockingQueue接口队列中取元素直至队列空了,线程阻塞。BlockingQueue接口继承了Queue接口并提供了如下三组方法。
- 在队列尾部添加元素:add(E e)、offer(E e)、put(E e),当队列已满的时候,这三个方法分别会抛出异常、返回false和阻塞线程。
- 在队列头部删除并返回删除元素:remove()、poll()、take()方法。当该队列已空时,这三个方法分别会抛出异常、返回false和阻塞线程。
- 在队列头部取出但不删除元素:element()和peek(),当该队列已空时,分别会抛出异常和返回false
在Java7之后,阻塞队列出现了新增,分别是:ArrayBlockingQueue、LinkedBlockingQueue、priorityBlockingQueue、SynchornizedQueue和DelayQueue这五个类。
七、线程池
系统启动一个新线程的成本是比较高的,尤其是当系统本身已经有大量的并发线程时,会导致系统性能急剧下降,甚至会导致JVM崩溃,因此我们通常采用线程池来维护系统的并发线程。与数据库连接池类似的时,线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象或Callable对象传给线程池,线程池就会启动后一个空闲的线程来执行它们的run()或call()方法,当运行结束后,该线程不会死亡而是返回线程池中进入空闲等待状态。
ExecutorService代表尽快执行线程的线程池,程序只需要将一个Runnable对象或Callable对象传给线程池,就会尽快执行线程任务;ScheduledExecutorService代表可在指定延迟后或周期性地执行线程任务的线程池。
7.1 ExecutorService类使用示例
使用线程池的步骤如下:
- 调用Executors类的静态工厂方法调用创建一个ExecutorService对象,该对象就代表着一个线程池;
- 创建Runnable实现类或Callable实现类的实例,作为线程执行的任务;
- 调用ExecutorService对象的submit()方法来提交Runnable或者Callable对象实例;
- 结束任务时,调用ExecutorService对象的shutdown()方法来关闭线程池;
//开启6个线程的线程池
ExecutorService pool = Executors.newFixedThreadPool(6);
//创建Runnable实现类
Runnable target = ()->{...}
//提交线程任务到线程池
pool.submit();
//关闭线程
pool.shutdown();
用完一个线程池后,应该调用该线程池的shutdown()方法,该方法将启动线程池的关闭序列并不再接受新任务,线程池中的任务依次执行完毕后线程死亡;或者调用线程池的shutdownNow()方法来直接停止所有正在执行的活动任务。
7.2 Java8中的ForkJoinPool
计算机发展到现在其实基本的硬件都支持多核CPU,为了更好地利用硬件设备的资源,Java中提供了一个ForkJoinPool来支持将一个任务拆分成多个小任务并行计算。ForkJoinPool是ExecutorService的实现类,是一个特殊的线程池。
构造器的两种方法
- ForkJoinPool(int num):创建一个包含num个并行线程的ForkJoinPool;
- ForkJoinPool():以Runtime.availableProcessors()方法的返回值作为parallelism参数(上面我写成了num)来创建改线程池
实现通用池的两个静态方法
- ForkJoinPool commonPool():改方法返回一个通用池,通用池的状态不会受到shutdown()等方法的影响,System.exit(0)除外。
- int getCommonPoolParallelism():该方法返回通用池的并行级别
注意:
ForkJoinPool.submit(ForkJoinTask task) ,其中ForkJoinTask代表着一个可以并行和合并的任务,他有两个抽象的子类:RecursiveAction和RecursiveTask,分别代表着有返回值和无返回值的任务。
class PrintTask extends RecursiveAction{
...
@Override
protected void compute(){
......
//分割任务
PrintTask t1 = new PrintTask(start,middle);
PrintTask t2 = new PrintTask(middle,end);
//并行执行子任务
t1.fork();
t2.fork();
}
}
public class Test{
public static void main(String[]args) throws Exception{
//实例化通用池对象
ForkJoinPool pool = new ForkJoinPool();
pool.submit(new PrintTask(0,1000));
//线程等待完成
pool.awaitTermination(2,TimeUnit.SECONDS);
//关闭线程池
pool.shutdown();
}
}
总结
现有的所有企业都采用的是多线程并发的方式来开发的,也要求我们能够应对在高并发场景下保证系统服务的高可用的要求,所以多线程和异步编程我们必须牢牢掌握。这几章可能会比较枯燥,难度也会比较大,荔枝也是啃了一段时间嘿嘿嘿,在学这部分之前一定要把面向对象学好,要不然会晕哈哈哈~~~
今朝已然成为过去,明日依然向往未来!我是小荔枝,在技术成长的路上与你相伴,码文不易,麻烦举起小爪爪点个赞吧哈哈哈~~~ 比心心♥~~~