在系统学完Java的面向对象编程之后,我们需要认真地来学习Java并发编程,我们在学习计算机操作系统的时候也都了解过进程、线程和协程的概念。在这篇文章中荔枝主要会梳理有关线程创建、线程生命周期、同步锁和死锁、线程通信和线程池的知识,并给出相应的精简示例,希望能帮助有需要的小伙伴们哈哈哈~~~
前言
一、基础概念
二、创建线程的三种方式
2.1 通过继承Thread类来启用
2.2 实现Runnable接口来实现
2.3 实现Callable接口
三、Thread类的相关方法
四、生命周期
五、同步锁和死锁
5.1 同步锁
5.1.1 synchronized加锁的两种方式
5.1.2 Lock
5.2 死锁
六、线程通信
6.1 传统的线程通信
6.2 使用Condition来控制线程通信
6.3 使用阻塞队列来控制线程通信
七、线程池
7.1 ExecutorService类使用示例
7.2 Java8中的ForkJoinPool
总结
进程
我们知道CPU是主机上的中央核心处理器,CPU的核数代表着主机能在一个瞬间同时并行处理的任务数,单核CPU只能在内存中并发处理任务。而在现有的操作系统中,几乎都支持进程这个概念。进程是程序的在内存中的一次执行过程,具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位。
线程
线程在程序中是独立的、并发的执行流,与分隔的进程相比隔离性会更小,线程之间共享内存、文件句柄和其它的进程应有的状态。线程比进程具有更高的性能,这是由于同一进程中的线程具有共性。简单理解,多线程是进程中并行执行的多个子程序。
并发性和并行的区别
并行是指在同一时刻,有多条指令在多个处理器上同时执行;而并发是指在同一时刻只能执行,但是通过多进程快速轮换执行可以达到同时执行的效果。CPU主频就代表着这些进程之间频繁切换的速度。
Java语言中JVM允许程序运行多个线程并通过java.lang.Thread类来实现。
Thread类的特性
每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体,并通过该Thread对象的start()方法来启动线程。
流程:
具体代码示例
首先构建一个继承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(); //启动线程
}
}
流程
实现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类的区别
实现Runnable接口方法的好处
实现Runnable接口方法通过继承Runnable接口避免了当继承的局限性,同时也使得多个线程可以同时共享一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。
在前面通过实现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 task = new FutureTask((Callable)()->{
...
...
});
new Thread(task,"有返回值的线程").start();
try{
//获取线程返回值
System.out.println("子线程的返回值" + task.get());
}catch (EXception ex){
ex.printStackTrace();
}
}
}
Callable接口实现类和Runnable接口实现类的区别在于是否有参数返回!
常用方法如下:
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();
线程从创建、启动到死亡经历了一个完整的生命周期,在线程的生命周期中一般要经历五种状态:新建——就绪——运行——阻塞——死亡。
线程可能以如下三种方法结束:
多线程模式的提出势必就会带来线程同步的问题,在保证数据一致性上,我们需要为线程加上同步锁。Java中对于多线程安全的问题提出了同步机制,即在方法声明的时候加入synchronized关键字来修饰或者直接使用synchronized来锁一个demo
synchronized同步锁关键字修饰
//使用synchronized同步锁关键字修饰需要同步执行的方法体
public synchronized void drawing(int money){
需要同步执行的代码
}
注意:
在普通方法上加同步锁synchronized,锁的是整个对象,不是某一个方法。如果是不同对象的话那么就是不同的锁。静态的方法加synchronized对于所有的对象都是同一个锁!
synchronized锁一段demo
使用这种方法来锁指向this的代码块使用的都是同一个同步锁。如果改成方法对象的话比如Account对象的话就是不同的同步锁。
synchronized(this){ //表示当前的对象的代码块被加了synchronized同步锁
demo...
}
相比于上面的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();
}
}
}
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
解决方法
当我们手动开启并在控制台中输出两个线程的运行过程的时候,程序并不能每次都准确的控制两个线程的轮换执行的先后次序,所以Java中也提供了一些机制来保证线程的协调运行。在传统的Java中,基于同步锁synchronized关键字提供了借助于Object类的wait()、notify()和notifyAll()方法来控制线程的阻塞情况,而之后也出现了基于Condition和阻塞队列BlockingQueue来控制线程阻塞的情况。
Object类中提供的wait()、notify()和notifyAll()方法必须由一个同步监视器对象来调用,所以这三种方法必须基于同步锁synchronized关键字。
//使用时直接调用方法就行,但必须是在有synchronized修饰的方法内去调用才可
wait();
notify();
notifyAll();
对于程序不使用synchronized关键字来保证同步锁,而是采用Lock对象来保证同步,Java中提供了Condition类来保证线程通信。Contidion类中提供了类似于synchronized关键字中的三种方法:await()、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();
}
}
除了上述两种方法,Java5中还提供了BlockingQueue接口来作为线程同步的工具。它的工作原理是这样滴:当生产者往BlockingQueue接口中放入元素直至接口队列满了,线程阻塞;消费者从BlockingQueue接口队列中取元素直至队列空了,线程阻塞。BlockingQueue接口继承了Queue接口并提供了如下三组方法。
在Java7之后,阻塞队列出现了新增,分别是:ArrayBlockingQueue、LinkedBlockingQueue、priorityBlockingQueue、SynchornizedQueue和DelayQueue这五个类。
系统启动一个新线程的成本是比较高的,尤其是当系统本身已经有大量的并发线程时,会导致系统性能急剧下降,甚至会导致JVM崩溃,因此我们通常采用线程池来维护系统的并发线程。与数据库连接池类似的时,线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象或Callable对象传给线程池,线程池就会启动后一个空闲的线程来执行它们的run()或call()方法,当运行结束后,该线程不会死亡而是返回线程池中进入空闲等待状态。
ExecutorService代表尽快执行线程的线程池,程序只需要将一个Runnable对象或Callable对象传给线程池,就会尽快执行线程任务;ScheduledExecutorService代表可在指定延迟后或周期性地执行线程任务的线程池。
使用线程池的步骤如下:
//开启6个线程的线程池
ExecutorService pool = Executors.newFixedThreadPool(6);
//创建Runnable实现类
Runnable target = ()->{...}
//提交线程任务到线程池
pool.submit();
//关闭线程
pool.shutdown();
用完一个线程池后,应该调用该线程池的shutdown()方法,该方法将启动线程池的关闭序列并不再接受新任务,线程池中的任务依次执行完毕后线程死亡;或者调用线程池的shutdownNow()方法来直接停止所有正在执行的活动任务。
计算机发展到现在其实基本的硬件都支持多核CPU,为了更好地利用硬件设备的资源,Java中提供了一个ForkJoinPool来支持将一个任务拆分成多个小任务并行计算。ForkJoinPool是ExecutorService的实现类,是一个特殊的线程池。
构造器的两种方法
实现通用池的两个静态方法
注意:
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();
}
}
现有的所有企业都采用的是多线程并发的方式来开发的,也要求我们能够应对在高并发场景下保证系统服务的高可用的要求,所以多线程和异步编程我们必须牢牢掌握。这几章可能会比较枯燥,难度也会比较大,荔枝也是啃了一段时间嘿嘿嘿,在学这部分之前一定要把面向对象学好,要不然会晕哈哈哈~~~
今朝已然成为过去,明日依然向往未来!我是小荔枝,在技术成长的路上与你相伴,码文不易,麻烦举起小爪爪点个赞吧哈哈哈~~~ 比心心♥~~~