多线程与高并发编程之基础篇-阿里云开发者社区

开发者社区> 煜月> 正文

多线程与高并发编程之基础篇

简介: 多线程与高并发编程基础
+关注继续查看

1. 基本概念

1.1. 进程

进程即运行中的程序,比如当你双击QQ.exe这个程序时,操作系统就会启动一个进程。一个程序可以启动多个进程。(比如你可以运行多个QQ.exe程序,相当于启动了多个进程)

1.2. 线程

线程是进程中最小的执行单元。下面我们用一个程序来了解什么是线程:


package basic_concepts;
import java.util.concurrent.TimeUnit;
public class WhatIsThread {
    private static class Thread1 extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                try {
                    TimeUnit.MICROSECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread1");
            }
        }
    }
    public static void main(String[] args) {
        // 普通方法调用
        new Thread1().run();
        // 启动一个线程,并执行它的 run 方法
        new Thread1().start();
        for (int i = 0; i < 10; i++) {
            try {
                TimeUnit.MICROSECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("main");
        }
    }
}

在上面的程序中,线程的 run 方法相当于普通方法的调用,它会以程序流的顺序执行,而 start 方法相当于启动了一个线程,这个线程会和 main线程交替执行,因此我们可以在控制台上看到 “Thread1”和“main”交替进行打印,这也说明了不同的线程是进程中不同的执行路径,Thread1 线程和 main 线程代表了上述程序中两条不同的执行路径。

在 Java 中创建线程有三种方式,一种是继承 Thread 类,一种是实现 Runnable 接口,还有一种是使用线程池来创建线程:


package basic_concepts;
public class HowToCreateThread {
    static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("Hello MyThread!");
        }
    }
    static class MyRun implements Runnable {
        @Override
        public void run() {
            System.out.println("Hello MyRun!");
        }
    }
    public static void main(String[] args) {
        // 通过继承 Thread 类来创建线程
        new MyThread().start();
        // 通过实现 Runnable 接口来创建线程
        new Thread(new MyRun()).start();
        // 使用 Runnable 的 lambda 表达式来创建线程 
        new Thread(() -> {
            System.out.println("Hello Lambda!");
        }).start();
        // 使用线程池来创建线程
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(new MyRun());
        executorService.shutdown();
    }
}

Thread 对象的常用方法有 sleep 、yield 和 join :


package basic_concepts;
public class SleepYieldJoin {
    public static void main(String[] args) {
//        testSleep();
//        testYield();
        testJoin();
    }
    static void testSleep() {
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                System.out.println("A" + i);
                try {
                    // 使线程休眠500毫秒
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
    static void testYield() {
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                System.out.println("A" + i);
                if (i % 10 == 0) {
                    Thread.yield();
                }
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                System.out.println("B" + i);
                if (i % 10 == 0) {
                    Thread.yield();
                }
            }
        }).start();
    }
    static void testJoin() {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                System.out.println("A" + i);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread t2 = new Thread(() -> {
            try {
                t1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for (int i = 0; i < 100; i++) {
                System.out.println("B" + i);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        t2.start();
    }
}

其中 sleep 会使当前线程休眠,进入阻塞状态,如果线程在睡眠状态被中断,将会抛出 InterruptedException 中断异常,yield 会使当前线程暂停并允许其他线程执行,当一个线程执行了yield()方法之后,就会进入就绪状态,CPU 此时就会从就绪状态线程队列中选择与该线程优先级相同或者更高优先级的线程去执行(因此执行了yield()方法之后的线程仍有可能继续执行,如果此时没有和它优先级相同或者比它优先级更高的线程时),join 方法让一个线程等待另外一个线程完成才继续执行,比如在线程 A 执行体中调用 B 线程的join()方法,则 A 线程将会被阻塞,直到 B 线程执行完为止,A 才能得以继续执行。

线程状态


package basic_concepts;
public class ThreadState {
    static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println(this.getState());
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(i);
            }
        }
    }
    public static void main(String[] args) {
        Thread t = new MyThread();
        // 通过 getState 方法获取线程状态
        System.out.println(t.getState());
        t.start();
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(t.getState());
    }
}

线程状态迁移图.jpg

线程的上下文切换

用户态 - 内核态

  • int 0x80 - 128
  • sysenter cpu支持
  • 保存用户态现场
    • 寄存器压栈
    • 进行syscall
    • 内核态返回 eax
    • 恢复用户态现场
    • 用户程序继续执行

1.3. 纤程/协程

CPU - Ring0 - 1 2 - Ring3

  • Ring0 -> 内核态 Ring3 -> 用户态
  • 内核调用/系统调用 线程的操作
  • 用户态启动线程
    • 进入到内核态 - 保存用户态的线程
    • 用户态不经过内核态的线程 - 纤程 golang的go程

2. synchronized关键字

synchronized关键字用于对某个对象加锁,加锁的对象对资源是独占的。它用于解决多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。下面的代码展示了 synchronized 常见的应用场景:


package synchronize;
/**
 * synchronized关键字
 * 对某个对象加锁
 */
public class Synchronize {
    private static int count = 10;
    private Object object = new Object();
    public void lockObject() {
        /**
         * 不能用String常量 Integer Long
         */
        synchronized (object) {
            count --;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
        }
    }
    public void lockThis() {
        synchronized (this) {
            count --;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
        }
    }
    /**
     * 等同于在方法的代码执行时要synchronized (this)
     */
    public synchronized void synchronizedMethod() {
        count --;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
    }
    /**
     * 等同于synchronized(Synchronize.class)
     */
    public synchronized static void staticSynchronizedMethod() {
        count --;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
    }
    public static void lockClass() {
        synchronized (Synchronize.class) {
            count --;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
        }
    }
    static class MyRun implements Runnable {
        @Override
        public void run() {
            Synchronize synchronize = new Synchronize();
            synchronize.lockObject();
        }
    }
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(new MyRun()).start();
        }
    }
}

synchronized 关键字既能保证可见性,又能保证可见性,因此对于下面这个程序,它无论运行多少次,结果都会是 0 :


package synchronize;
public class ThreadSafe implements Runnable {
    private int count = 100;
    @Override
    public synchronized void run() {
        count --;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
    }
    public static void main(String[] args) {
        ThreadSafe threadSafe = new ThreadSafe();
        for (int i = 0; i < 100; i++) {
            new Thread(threadSafe, "Thread" + i).start();
        }
    }
}

同步方法和非同步方法是可以同时调用的,在下面的程序中,非同步方法会穿插在同步方法中执行:


package synchronize;
/**
 * 同时调用同步和非同步方法
 */
public class SynchronizeCallNonSynchronize {
    public synchronized void synchronizedMethod() {
        System.out.println(Thread.currentThread().getName() + " synchronized method start...");
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " synchronized method end");
    }
    public void nonSynchronizedMethod() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " non-synchronized method call");
    }
    public static void main(String[] args) {
        SynchronizeCallNonSynchronize synchronizeCallNonSynchronize = new SynchronizeCallNonSynchronize();
        /*new Thread(() -> synchronizeCallNonSynchronize.synchronizedMethod(), "t1").start();
        new Thread(() -> synchronizeCallNonSynchronize.nonSynchronizedMethod(), "t2").start();*/
        new Thread(synchronizeCallNonSynchronize::synchronizedMethod, "t1").start();
        new Thread(synchronizeCallNonSynchronize::nonSynchronizedMethod, "t2").start();
    }
}

让我们来看一下 synchronized 另一个应用场景,有这样一个面试题:需要你模拟银行账户的存款取款操作,下面的程序模拟了这一过程,需要注意的是,对 getBalance 方法也需要使用 synchronized 关键字来修饰,否则会产生脏读问题:


package synchronize;
import java.util.concurrent.TimeUnit;
/**
 * 模拟银行账户
 */
public class ThreadSafeAccount {
    private String name;
    private double balance;
    public synchronized void set(String name, double balance) {
        this.name = name;
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.balance = balance;
    }
    /**
     * 如果对业务写方法加锁,
     * 而对业务读方法不加锁,
     * 会产生脏读问题(dirtyRead)
     * @param name
     * @return
     */
    public synchronized double getBalance(String name) {
        return this.balance;
    }
    public static void main(String[] args) {
        ThreadSafeAccount account = new ThreadSafeAccount();
        new Thread(() -> account.set("zhangsan", 100.0)).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(account.getBalance("zhangsan"));
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(account.getBalance("zhangsan"));
    }
}

synchronized 是可重入的锁,它允许同一线程重复获得锁。下面的程序验证了 synchronized 是可重入的,如果 synchronized 不可重入,那么下面的程序将会产生死锁,但是事实证明,下面的程序能正常执行完,因此 synchronized 是可重入的:


package synchronize;
import java.util.concurrent.TimeUnit;
/**
 * 一个同步方法可以调用另一个同步方法,
 * 一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁,
 * 也就是说synchronized获得的锁是可重入的
 */
public class ReentrantSynchronized {
    synchronized void m() {
        System.out.println("m start");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("m end");
    }
    synchronized void m1() {
        System.out.println("m1 start");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        m2();
        System.out.println("m1 end");
    }
    synchronized void m2() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("m2");
    }
    static class Child extends ReentrantSynchronized {
        @Override
        synchronized void m() {
            System.out.println("child m start");
            super.m();
            System.out.println("child m end");
        }
    }
    public static void main(String[] args) {
        new ReentrantSynchronized().m1();
        new Child().m();
    }
}

要特别注意的是,在同步业务逻辑中,要非常小心的处理异常,因为一旦产生异常,默认情况下锁会被释放。在下面的程序中,当 count = 5 时,程序产生的异常会导致线程 t1 的锁被释放了,从而让 t2 线程得到锁执行:


package synchronize;
import java.util.concurrent.TimeUnit;
/**
 * 程序在执行过程中,如果出现异常,默认情况锁会被释放
 * 所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。
 * 比如,在一个web app处理过程中,多个servlet线程共同访问同一个资源,这时如果异常处理不合适,
 * 在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据。
 * 因此要非常小心的处理同步业务逻辑中的异常。
 */
public class SynchronizeCatchException {
    int count = 0;
    synchronized void m() {
        System.out.println(Thread.currentThread().getName() + " start");
        while (true) {
            count ++;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (count == 5) {
                // 此处抛出异常,锁将被释放,要想不释放锁,可以在这里进行catch,然后让循环继续
                int i = 1 / 0;
                System.out.println(i);
            }
        }
    }
    public static void main(String[] args) {
        SynchronizeCatchException synchronizeCatchException = new SynchronizeCatchException();
        new Thread(synchronizeCatchException::m, "t1").start();
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(synchronizeCatchException::m, "t2").start();
    }
}

HotSpot虚拟机在对象头(64位)上拿出两位(mark word,这两位记录了锁的类型)来记录对象是否被锁定。

JDK早期的时候,synchronized的实现是重量级的(OS级,找操作系统申请锁),这样造成早期的时候synchronized的效率非常低。后来jdk针对synchronized做了一些改进,引入了锁升级的概念。

锁升级:

  • 只有一个线程访问的时候,先在对象头上的markword中记录这个线程的ID(偏向锁)
  • 如果有线程争用,升级为自旋锁
  • 如果线程自旋多次(默认10次)以后还是无法获取到锁,则会升级为重量级锁(OS)
  • 锁只能升级不能降级

执行时间短(加锁代码),线程数少,用自旋。

执行时间长,线程数多,用系统锁。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
Java多线程高并发学习笔记(一)——Thread&Runnable
进程与线程 首先来看百度百科关于进程的介绍: 进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。它可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体。它不只是程序的代码,还包括当前的活动,通过程序计数器的值和处理寄存器的内容来表示。
945 0
Java基础-23总结多线程,线程实现Runnable接口,线程名字获取和设置,线程控制,线程安全,同步线程
你需要的是什么,直接评论留言。 获取更多资源加微信公众号“Java帮帮” (是公众号,不是微信好友哦) 还有“Java帮帮”今日头条号,技术文章与新闻,每日更新,欢迎阅读 学习交流请加Java帮帮交流QQ群553841695 分享是一种美德,分享更快乐! 1:多线程(理解) (1)多线程:一个应用程序有多条执行路径 进程:正
2757 0
基于MongoDB的高并发高可用政府云平台架构实践
3月12日下午在阿里巴巴西溪园区,举行了MongoDB杭州用户交流会。微软MSDN特邀讲师徐雷分享《基于MongoDB的政府云平台高并发高可用HA架构实践 》,从自身实践出发,讲述了政府云平台分层、技术栈选型、物理架构、API架构及DB数据库架构的设计思路和方法。
8854 0
Java 并发/多线程教程(一)
         本系列译自jakob jenkov的Java并发多线程教程,个人觉得很有收获。由于个人水平有限,不对之处还望矫正!         在早期,计算机只有一个CPU,同一时刻只能执行一个程序,后来有了多任务的说法,多任务是指计算机在同一时刻可以执行多个程序,但这并不是真正意义上的同一时刻,单个CPU 被多个程序共用,操作系统会在运行的运行的程序间相互切换。
863 0
03.Java基础(线程池和Callable<T>)
在以往的工作中,创建线程通常我们关注的都是Thread或者Runnable为主,其实忽略了另外一个比较有用的创建线程的方式,就是Callable接口,下边是Callable配合线程池实现异步任务 import java.
746 0
02.Java基础(线程池)
执行异步任务的时候,如果只是new Thread,存在一下弊端 a. 每次new Thread新建对象性能差。 b. 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。
498 0
Java 并发/多线程教程(六)-并发VS并行
       本系列译自jakob jenkov的Java并发多线程教程,个人觉得很有收获。由于个人水平有限,不对之处还望矫正!         在多线程线程编程中,我们经常提及并发和并行,但是并发和并行究竟是什么意思,他们所要表达是同一回事,还是不是同一加事。
1241 0
01.Java基础(多线程回顾,对比Linux多线程)
两个线程间通信,实现交替打印 public class Thread1 { public static void main(String[] args) { final Printer printer = new Printe...
599 0
+关注
13
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
文娱运维技术
立即下载
《SaaS模式云原生数据仓库应用场景实践》
立即下载
《看见新力量:二》电子书
立即下载