(二)线程的应用及挑战

文章简介

上一篇文章咱们了解了进程和线程的发展历史、线程的生命周期、线程的优点和使用场景,这一篇,咱们从Java层面更进一步了解线程的使用java

内容导航

  1. 并发编程的挑战
  2. 线程在Java中的使用

并发编程的挑战

引入多线程的目的在第一篇提到过,就是为了充分利用CPU是的程序运行得更快,固然并非说启动的线程越多越好。在实际使用多线程的时候,会面临很是多的挑战算法

线程安全问题

线程安全问题值的是当多个线程访问同一个对象时,若是不考虑这些运行时环境采用的调度方式或者这些线程将如何交替执行,而且在代码中不须要任何同步操做的状况下,这个类都可以表现出正确的行为,那么这个类就是线程安全的
好比下面的代码是一个单例模式,在代码的注释出,若是多个线程并发访问,则会出现多个实例。致使没法实现单例的效果数据库

public class SingletonDemo {
   private static SingletonDemo singletonDemo=null;
   private SingletonDemo(){}
    public static SingletonDemo getInstance(){
        if(singletonDemo==null){/***线程安全问题***/
           singletonDemo=new SingletonDemo();
        }
        return singletonDemo;
    }
}

一般来讲,咱们把多线程编程中的线程安全问题归类成以下三个,至于每个问题的本质,在后续的文章中咱们会单独讲解编程

  1. 原子性
  2. 可见性
  3. 有序性

上下文切换问题

在单核心CPU架构中,对于多线程的运行是基于CPU时间片切换来实现的伪并行。因为时间片很是短致使用户觉得是多个线程并行执行。而一次上下文切换,实际就是当前线程执行一个时间片以后切换到另一个线程,而且保存当前线程执行的状态这个过程。上下文切换会影响到线程的执行速度,对于系统来讲意味着会消耗大量的CPU时间安全

减小上下文切换的方式网络

  1. 无锁并发编程,在多线程竞争锁时,会致使大量的上下文切换。避免使用锁去解决并发问题能够减小上下文切换
  2. CAS算法,CAS是一种乐观锁机制,不须要加锁
  3. 使用与硬件资源匹配合适的线程数

死锁

在解决线程安全问题的场景中,咱们会比较多的考虑使用锁,由于它使用比较简单。可是锁的使用若是不恰当,则会引起死锁的可能性,一旦产生死锁,就会形成比较严重的问题:产生死锁的线程会一直占用锁资源,致使其余尝试获取锁的线程也发生死锁,形成系统崩溃多线程

如下是死锁的简单案例架构

public class DeadLockDemo {
    //定义锁对象
    private final Object lockA = new Object();
    private final Object lockB = new Object();
    private void deadLock(){
        new Thread(()->{
            synchronized (lockA){
                try {
                    Thread.sleep(4000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lockB){
                    System.out.println("Lock B");
                }
            }
        }).start();
        new Thread(()->{
            synchronized (lockB){
                synchronized (lockA){
                    System.out.println("Lock A");
                }
            }
        }).start();
    }
    public static void main(String[] args) {
        new DeadLockDemo().deadLock();
    }
}

经过jstack分析死锁

1.首先经过jps获取当前运行的进程的pid并发

6628 Jps
17588 RemoteMavenServer
19220 Launcher
19004 DeadLockDemo

2.jstack打印堆栈信息,输入 jstack19004, 会打印以下日志,能够很明显看到死锁的信息提示异步

Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x000000001d461e68 (object 0x000000076b310df8, a java.lang.Object),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x000000001d463258 (object 0x000000076b310e08, a java.lang.Object),
  which is held by "Thread-1"
解决死锁的手段
1.保证多个线程按照相同的顺序获取锁
2.设置获取锁的超时时间,超过设定时间之后自动释放
3.死锁检测

资源限制

资源限制主要指的是硬件资源和软件资源,在开发多线程应用时,程序的执行速度受限于这两个资源。硬件的资源限制无非就是磁盘、CPU、内存、网络;软件资源的限制有不少,好比数据库链接数、计算机可以支持的最大链接数等
资源限制致使的问题最直观的体现就是前面说的上下文切换,也就是CPU资源和线程资源的严重不均衡致使频繁上下文切换,反而会形成程序的运行速度降低

资源限制的主要解决方案,就是缺啥补啥。CPU不够用,能够增长CPU核心数;一台机器的资源有限,则增长多台机器来作集群。

线程在Java中的使用

在Java中实现多线程的方式比较简单,由于Java中提供了很是方便的API来实现多线程。
1.继承Thread类实现多线程
2.实现Runnable接口
3.实现Callable接口经过Future包装器来建立Thread线程,这种是带返回值的线程
4.使用线程池ExecutorService

继承Thread类

继承Thread类,而后重写run方法,在run方法中编写当前线程须要执行的逻辑。最后经过线程实例的start方法来启动一个线程

public class ThreadDemo extends Thread{
    @Override
    public void run() {
        //重写run方法,提供当前线程执行的逻辑
        System.out.println("Hello world");
    }
    public static void main(String[] args) {
        ThreadDemo threadDemo=new ThreadDemo();
        threadDemo.start();
    }
}
Thread类实际上是实现了Runnable接口,所以Thread本身也是一个线程实例,可是咱们不能直接用 newThread().start()去启动一个线程,缘由很简单,Thread类中的run方法是没有实际意义的,只是一个调用经过构造函数传递寄来的另外一个Runnable实现类的run方法,这块的具体演示会在Runnable接口的代码中看到
public
class Thread implements Runnable {
    /* What will be run. */
    private Runnable target;
    ...
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
    ...

实现Runnable接口

若是须要使用线程的类已经继承了其余的类,那么按照Java的单一继承原则,没法再继承Thread类来实现线程,因此能够经过实现Runnable接口来实现多线程

public class RunnableDemo implements Runnable{
    @Override
    public void run() {
        //重写run方法,提供当前线程执行的逻辑
        System.out.println("Hello world");
    }
    public static void main(String[] args) {
        RunnableDemo runnableDemo=new RunnableDemo();
        new Thread(runnableDemo).start();
    }
}
上面的代码中,实现了Runnable接口,重写了run方法;接着为了可以启动RunnableDemo这个线程,必需要实例化一个Thread类,经过构造方法传递一个Runnable接口实现类去启动,Thread的run方法就会调用target.run来运行当前线程,代码在上面.

实现Callable接口

在有些多线程使用的场景中,咱们有时候须要获取异步线程执行完毕之后的反馈结果,也许是主线程须要拿到子线程的执行结果来处理其余业务逻辑,也许是须要知道线程执行的状态。那么Callable接口能够很好的实现这个功能

public class CallableDemo implements Callable<String>{
    @Override
    public String call() throws Exception {
        return "hello world";
    }
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<String> callable=new CallableDemo();
        FutureTask<String> task=new FutureTask<>(callable);
        new Thread(task).start();
        System.out.println(task.get());//获取线程的返回值
    }
}
在上面代码案例中的最后一行 task.get()就是获取线程的返回值,这个过程是阻塞的,当子线程尚未执行完的时候,主线程会一直阻塞直到结果返回

使用线程池

为了减小频繁建立线程和销毁线程带来的性能开销,在实际使用的时候咱们会采用线程池来建立线程,在这里我不打算展开多线程的好处和原理,我会在后续的文章中单独说明。

public class ExecutorServiceDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //建立一个固定线程数的线程池
        ExecutorService pool = Executors.newFixedThreadPool(1);
        Future future=pool.submit(new CallableDemo()); 
        System.out.println(future.get());
    }
}
pool.submit有几个重载方法,能够传递带返回值的线程实例,也能够传递不带返回值的线程实例,源代码以下
/*01*/Future<?> submit(Runnable task);
/*02*/<T> Future<T> submit(Runnable task, T result);
/*03*/<T> Future<T> submit(Callable<T> task);

扫码关注公众号

相关文章
相关标签/搜索