赞
踩
在系统学完Java的面向对象编程之后,我们需要认真地来学习Java并发编程,我们在学习计算机操作系统的时候也都了解过进程、线程和协程的概念。在这篇文章中荔枝主要会梳理有关线程创建、线程生命周期、同步锁和死锁、线程通信和线程池的知识,并给出相应的精简示例,希望能帮助有需要的小伙伴们哈哈哈~~~
进程
我们知道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<Integer> task = new FutureTask<Integer>((Callable<Integer>)()->{
- ...
- ...
- });
- 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();
- }
- }

现有的所有企业都采用的是多线程并发的方式来开发的,也要求我们能够应对在高并发场景下保证系统服务的高可用的要求,所以多线程和异步编程我们必须牢牢掌握。这几章可能会比较枯燥,难度也会比较大,荔枝也是啃了一段时间嘿嘿嘿,在学这部分之前一定要把面向对象学好,要不然会晕哈哈哈~~~
今朝已然成为过去,明日依然向往未来!我是小荔枝,在技术成长的路上与你相伴,码文不易,麻烦举起小爪爪点个赞吧哈哈哈~~~ 比心心♥~~~
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。