一、并发编程入门

并发编程入门

@author lisiwen

@createTime 2020/07/22

1.上下文切换

​ 单核处理器也是支持多线程执行代码,因为cpu通过给每个线程分配cpu时间片来实现这个机制,时间片是cpu分配给各个线程的时间,因为时间片特别短,所以cpu通过不停切换线程执行,让我们感觉很多线程是同时执行的,时间片一般是十几毫秒(ms)。

​ CPU通过时间片分配算法循环执行任务,当前任务执行一个时间片后会却换到写一个任务,但是,在切换之前会保存上一个任务的状态,以便下一次切换回这个任务时,可以再再加载这个任务的状态。所以任务从保存到再加载的这个过程就是一次上下文的切换。

​ 如下图所示,线程分配到到时间片以执行完整个线程的所有任务。

在这里插入图片描述

1.1多线程一定快吗

下边做了个简单的例子来测试

package thread;

/**
 * 串行、并行性能测试
 *
 * @author: lisiwen
 * @date: 2020/7/22 8:53
 **/
public class ConcurrencyTest {
    private static final long count = 10000L;

    public static void main(String[] args) throws InterruptedException {
        concurrency();
        serial();
    }

    private static void concurrency() throws InterruptedException {
        long start = System.currentTimeMillis();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                int a = 0;
                for (int i = 0; i < count; i++) {
                    a += 5;
                }
            }
        });
        t1.start();
        int b = 0;
        for (int i = 0; i < count; i++) {
            b--;
        }
        // 等待线程执行结束计算执行时间
        t1.join();
        long time = System.currentTimeMillis() - start;
        System.out.println("concurrency:" + time + "ms,b=" + b);
    }

    private static void serial() {
        long start = System.currentTimeMillis();
        int a = 0;
        for (int i = 0; i < count; i++) {
            a += 5;
        }
        int b = 0;
        for (int i = 0; i < count; i++) {
            b--;
        }
        long time = System.currentTimeMillis() - start;
        System.out.println("serial:" + time + "ms,b=" + b + ",a=" + a);
    }
}

运行结果:

循环次数串行执行耗时/ms并发执行耗时/ms并发和串行比较结果
1亿232120并发节省约一半时间
1000万2315并发节省约一半时间
10万44并发和串行差不多时间
1万02并发比串行更耗时

​ 可以看到数据量在1000万级别以上时时间才有明显差距,甚至在1万数据量时并发更耗时,是因为创建线程和销毁线程还有切换上下文消耗了更多的时间,使得并发耗时远大于串行耗时。

1.2 死锁

​ 锁是一个非常有用的工具,运用场景非常多,因为他使用起来非常简单,而且易于理解。但同时它也会带来一些困扰,那就是可能会引起死锁,一旦产生死锁,就会造成系统功能不可用。我们先来看一段代码,这段代码会引起死锁,使线程t1和t2互相等待对方释放锁。

package thread;

/**
 * 死锁demo
 *
 * @author: lisiwen
 * @date: 2020/7/22 8:37
 **/
public class DeadLockDemo {

    private static String A="A";
    private static String B="B";


    public static void main(String[] args) {
        new DeadLockDemo().deadLock();
    }

    private void deadLock(){
        Thread t1=new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (A){
                    try{
                        Thread.sleep(2000);

                    }catch (InterruptedException ex){
                        ex.printStackTrace();
                    }
                    synchronized (B){
                        System.out.println(1);
                    }
                }
            }
        });
        Thread t2=new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (B){
                    synchronized (A){
                        System.out.println(2);
                    }
                }
            }
        });
        t1.start();
        t2.start();

    }
}

​ 这段代码是演示死锁的场景,在现实中可能不会写出这样的代码,但是,在一些复杂的业务场景中,你可能会遇到这种情况,比如t1拿到锁之后,因为一些异常情况没有释放锁(死循环)。又或者是t1拿到一个数据库锁,释放锁的时候抛出了异常,没有释放掉。

​ 一旦出现死锁,业务是可感知的,因为不能继续提供服务了,那么只能通过dump线程查看到底是哪个线程吃咸了问题,一下线程信息告诉我们DeadLockDemo类的31行和41行引起的死锁

"Thread-1@488" prio=5 tid=0xf nid=NA waiting for monitor entry
  java.lang.Thread.State: BLOCKED
	 blocks Thread-0@486
	 waiting for Thread-0@486 to release lock on <0x1ee> (a java.lang.String)
	  at thread.DeadLockDemo$2.run(DeadLockDemo.java:41)
	  - locked <0x1ed> (a java.lang.String)
	  at java.lang.Thread.run(Thread.java:748)
	  
"Thread-0@486" prio=5 tid=0xe nid=NA waiting for monitor entry
  java.lang.Thread.State: BLOCKED
	 blocks Thread-1@488
	 waiting for Thread-1@488 to release lock on <0x1ed> (a java.lang.String)
	  at thread.DeadLockDemo$1.run(DeadLockDemo.java:31)
	  - locked <0x1ee> (a java.lang.String)
	  at java.lang.Thread.run(Thread.java:748)

避免死锁的几个常见的方法

  • 避免一个线程同时获得多个锁
  • 避免一个线程在所内同时占用多个资源,尽量保证每个锁值占用一个资源
  • 尝试使用定时锁,使用Lock.tryLock(timeout)来替代使用内部锁机制
  • 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会痴线解锁失败的情况

1.3 资源限制的挑战

  1. 什么是资源限制

    ​ 资源限制是指,在并发编程中,程序速度受限于计算机的硬件资源或者是软件资源,例如硬盘的读写速度为500M/S 一个线程是读取速度为200M/S,同时开启是个线程读取速度也不会突破上限超过500M/S,所以说在并发编程是要考虑到资源限制没不要浪费一些其他不必要的开销。

  2. 资源限制引发的问题

    ​ 在并发变成中,将代码执行速度加快的原则是将代码中串行执行的部分变成并发执行,但是将部分串行代码改成并发执行之后,因受限于资源,仍在串行执行,这时候因改成并发带来的上下文切换和资源调度,会导致速度更慢。

  3. 如何解决资源限制问题

    ​ 对于硬件资源的限制,可以考虑使用集群并行执行程序,既然单机的资源有限,那么久让程序在多机上运行。比如使用OPDS、Hadoop或者自己搭建服务集群,不同的机器处理不用的数据。可以通过“数据ID%机器数”,计算得到一个机器编号,然后由对应的编号机器处理这笔数据。

    ​ 对于软件资源限制,可以考虑使用资源池将资源复用。比如使用连接池将数据库和socket连接复用,或者在调用对方webservice接口获取数据时没,只建立一个连接

  4. 如何在资源限制情况下进行并发编程

    ​ 方法就是根据不同的资源限制调整程序的并发度,比如文件下载程序依赖于两个资源–宽带和硬盘读写速度。

    ​ 有数据库操作时,设计数据库连接数,如果sql语句执行非常快,而线程的数量比数据库的连接数大很多,则某些线程会被阻塞,等待数据库连接。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值