并发编程

前言

提到并发编程,很多人会想到多线程;希望让多个线程共同完成一项任务,以提高生产效率。所以要聊并发编程之前,就要明白线程和进程的关系。

进程:在现代操作系统中,每一个独立运行的程序都是一个进程,比如运行中的word,微信等等都是一个独立进程。

线程:在现代操作系统中,线程也叫轻量级进程,每个进程里面可以包含多个线程。CPU资源可以在多个线程之间不断切换,仿佛所有线程在并行执行。每个线程都有自己的计数器,堆栈,和局部变量等属性。这些线程也能够访问共享的内存变量。这将成为日后阻碍我们写出健壮且安全的并发程序的最大障碍。

Java并发编程之线程的应用操作,助你深化学习核心技能

 

使用多线程的原因

正确使用多线程,总是能够给开发人员带来显著的好处,而使用多线程的原因主要有以下几点:

1、更多的处理器核心

随着处理器上的核心数量越来越多,以及超线程技术的广泛运用,现在大多数计算机都比以往更加擅长并行计算,而处理器性能的提升方式,也从更高的主频向更多的核心发展。

2、更快的响应时间

有时我们会编写一些业务逻辑比较复杂的代码,例如,一笔订单的创建,它包括插入订单数据、生成订单快照、发送邮件通知卖家和记录货品销售数量等。用户从单击“订购”按钮开始,就要等待这些操作全部完成才能看到订购成功的结果。但是这么多业务操作,如何能够让其更快地完成呢?

在上面的场景中,可以使用多线程技术,即将数据一致性不强的操作派发给其他线程处理(也可以使用消息队列),如生成订单快照、发送邮件等。这样做的好处是响应用户请求的线程能够尽可能快地处理完成,缩短了响应时间,提升了用户体验。

3、 更好的编程模型

Java为多线程编程提供了一致的编程模型,使开发人员能够更加专注于问题的解决,即为所遇到的问题建立合适的模型,而不是绞尽脑汁地考虑如何将其多线程化。

并发编程需要注意的问题

上下文切换

cpu通过时间分片来执行任务,多个线程在cpu上争抢时间片执行,线程切换需要保存一些状态,再次切换回去需要恢复状态,此为上下文切换成本。

因此并不是线程越多越快,频繁的切换会损失性能

减少上下文切换的方法:

  • 无锁并发编程:例如把一堆数据分为几块,交给不同线程执行,避免用锁
  • 使用CAS:用自旋不用锁可以减少线程竞争切换,但是可能会更加耗cpu
  • 使用最少的线程
  • 使用协程:在一个线程里执行多个任务

死锁

死锁就是线程之间因争夺资源, 处理不当出现的相互等待现象

避免死锁的方法:

  • 避免一个线程同时获取多个锁
  • 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
  • 尝试使用定时锁,lock.tryLock(timeout)
  • 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况

资源限制

程序的执行需要资源,比如数据库连接、带宽,可能会由于资源的限制,多个线程并不是并发,而是串行,不仅无优势,反而带来不必要的上下文切换损耗

常见资源限制

  • 硬件资源限制
  1. 带宽
  2. 磁盘读写速度
  3. cpu处理速度
  • 软件资源限制
  1. 数据库连接数
  2. socket连接数
  • 应对资源限制
  1. 集群化,增加资源
  2. 根据不同的资源限制调整程序的并发度,找到瓶颈,把瓶颈资源搞多一些,或者根据这个瓶颈调整线程数

创建线程的三种方式

继承Thread类

// 继承Thread
class MyThread extends Thread {
    // 重写run方法执行任务
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            // 可以通过this拿到当前线程
            System.out.println(this.getName()+"执行了"+i);
        }
    }
}
 
public class Demo_02_02_1_ThreadCreateWays {
    public static void main(String[] args) {
        // 先new出来,然后启动
        MyThread myThread = new MyThread();
        myThread.start();
        for (int i = 0; i < 10; i++) {
            // 通过Thread的静态方法拿到当前线程
            System.out.println(Thread.currentThread().getName()+"执行了"+i);
        }
    }
}

实现Runnable

// 实现Runnable接口
class MyThreadByRunnable implements Runnable {
 
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            // 不能用this了
            System.out.println(Thread.currentThread().getName() + "执行了" + i);
        }
    }
}
 
public class Demo_02_02_1_ThreadCreateWays {
    public static void main(String[] args) {
        // 实现Runnable接口的方式启动线程
        Thread thread = new Thread(new MyThreadByRunnable());
        thread.start();
        for (int i = 0; i < 10; i++) {
            // 通过Thread的静态方法拿到当前线程
            System.out.println(Thread.currentThread().getName() + "执行了" + i);
        }
    }
}

因为Runnable是函数式接口,用lamba也可以

new Thread(() -> {
    System.out.println("Runnable是函数式接口, java8也可以使用lamba");
}).start();

使用Callable和Future

// 使用Callable
class MyThreadByCallable implements Callable<Integer> {
 
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+"执行了"+i);
            sum+=i;
        }
        return sum;
    }
}
public class Demo_02_02_1_ThreadCreateWays {
    public static void main(String[] args) {
        // 用FutureTask包一层
        FutureTask<Integer> futureTask = new FutureTask<>(new MyThreadByCallable());
        new Thread(futureTask).start();
        try {
            // 调用futureTask的get能拿到返回的值
            System.out.println(futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

这是最复杂的一种方式,他可以有返回值,归纳一下步骤:

  • 搞一个类实现Callable接口,重写call方法,在call执行任务
  • 用FutureTask包装实现Callable接口类的实例
  • 将FutureTask的实例作为Thread构造参数
  • 调用FutureTask实例的get拿到返回值,调这一句会阻塞父线程

Callable也是函数式接口,所以也能用lamba

为啥Thread构造里边能放Runnable,也能放FutureTask? 其实FutureTask继承RunnableFuture,而RunnableFuture继承Runnable和Future,所以FutureTask也是Runnable

Java并发编程之线程的应用操作,助你深化学习核心技能

 

线程间通信

线程开始运行,拥有自己的栈空间,就如同一个脚本一样,按照既定的代码一步一步地执行,直到终止。但是,每个运行中的线程,如果仅仅是孤立地运行,那么没有一点儿价值,或者说价值很少,如果多个线程能够相互配合完成工作,这将会带来巨大的价值。

volatile和synchronized关键字

Java支持多个线程同时访问一个对象或者对象的成员变量,由于每个线程可以拥有这个变量的拷贝(虽然对象以及成员变量分配的内存是在共享内存中的,但是每个执行的线程还是可以拥有一份拷贝,这样做的目的是加速程序的执行,这是现代多核处理器的一个显著特性),所以程序在执行过程中,一个线程看到的变量并不一定是最新的。

关键字volatile可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。

关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性。

通过使用javap工具查看生成的class文件信息来分析synchronized关键字的实现细节,代码如下

public class Synchronized {
 
    public static void main(String[] args) {
        synchronized (Synchronized.class){
            m();
        }
    }
 
    public static synchronized void m(){
 
    }
}

对于同步块的实现使用了monitorenter和monitorexit指令,而同步方法则是依赖方法修饰符上的ACC_SYNCHRONIZED来完成。无论采用哪种方式,其本质是对一个对象的监视器进行获取,而这个获取过程是排他的,也就是同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。

任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获取到监视器(执行该方法)的线程将会被阻塞在同步块和同步方法的入口处,进入BLOCKED状态。

控制线程

join

主线程join一个线程,那么主线程会阻塞直到join进来的线程执行完,主线程继续执行, join如果带超时时间的话,那么如果超时的话主线程也会不再等join进去的线程而继续执行.

join实际就是判断join进来的线程存活状态,如果活着就调用wait(0),如果带超时时间了的话,wait里边的时间会算出来

while (isAlive()) {
    wait(0);
}

API

  • public final void join() throws InterruptedException
  • public final synchronized void join(long millis, int nanos)
  • public final synchronized void join(long millis)

例子

public class Demo_02_06_1_join extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(this.getName() + "  " + i);
        }
    }
 
    public static void main(String[] args) throws InterruptedException {
        Demo_02_06_1_join joinThread = new Demo_02_06_1_join();
        for (int i = 0; i < 100; i++) {
 
            if (i == 10) {
                joinThread.start();
                joinThread.join();
            }
            // 打到9就停了,然后执行joinThread这里边的代码,完事继续从10打
            System.out.println(Thread.currentThread().getName()+"  "+i);
        }
    }
}

sleep

睡觉方法,使得线程暂停一段时间,进入阻塞状态。

API

  • public static native void sleep(long millis) throws InterruptedException
  • public static void sleep(long millis, int nanos) throws InterruptedException

示例

public class Demo_02_06_2_sleep extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            if (i == 5) {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
 
            }
            // 输出到4停止, 5秒后继续
            System.out.println(this.getName() + "  " + i);
        }
    }
 
    public static void main(String[] args) throws InterruptedException {
        Demo_02_06_2_sleep sleepThread = new Demo_02_06_2_sleep();
        sleepThread.start();
    }
}

yield

也是让线程暂停一下,但是是进入就绪状态,让系统重新开始一次新的调度过程,下一次可能运气好被yield的线程又被选中。

Thread.yield()

中断

Java中断机制是一种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理中断。

前面有一些方法声明了InterruptedException, 这意味者他们可以被中断,中断后把异常抛给调用方,让调用方自己处理.

被中断的线程可以自已处理中断,也可以不处理或者抛出去。

public class Demo_02_06_3_interrupt extends Thread {
 
    static class MyCallable implements Callable {
        @Override
        public Integer call() throws InterruptedException {
            for (int i = 0; i < 5000; i++) {
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("3333");
                    throw new InterruptedException("中断我干嘛,关注 微信号 大雄和你一起学编程 呀");
                }
            }
            return 0;
        }
    }
    public static void main(String[] args) throws InterruptedException {
        FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
        Thread thread = new Thread(futureTask);
        thread.start();
        for (int i = 0; i < 100; i++) {
            if (i == 3) {
                thread.interrupt();
            }
        }
        try {
            futureTask.get();
        } catch (ExecutionException e) {
            // 这里会捕获到异常
            e.printStackTrace();
        }
 
    }
}

码字不易,求个关注。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值