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

简介: 多线程与高并发编程基础

1. 基本概念

1.1. 进程

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

1.2. 线程

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


packagebasic_concepts;
importjava.util.concurrent.TimeUnit;
publicclassWhatIsThread {
privatestaticclassThread1extendsThread {
@Overridepublicvoidrun() {
for (inti=0; i<10; i++) {
try {
TimeUnit.MICROSECONDS.sleep(1);
                } catch (InterruptedExceptione) {
e.printStackTrace();
                }
System.out.println("Thread1");
            }
        }
    }
publicstaticvoidmain(String[] args) {
// 普通方法调用newThread1().run();
// 启动一个线程,并执行它的 run 方法newThread1().start();
for (inti=0; i<10; i++) {
try {
TimeUnit.MICROSECONDS.sleep(1);
            } catch (InterruptedExceptione) {
e.printStackTrace();
            }
System.out.println("main");
        }
    }
}

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

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


packagebasic_concepts;
publicclassHowToCreateThread {
staticclassMyThreadextendsThread {
@Overridepublicvoidrun() {
System.out.println("Hello MyThread!");
        }
    }
staticclassMyRunimplementsRunnable {
@Overridepublicvoidrun() {
System.out.println("Hello MyRun!");
        }
    }
publicstaticvoidmain(String[] args) {
// 通过继承 Thread 类来创建线程newMyThread().start();
// 通过实现 Runnable 接口来创建线程newThread(newMyRun()).start();
// 使用 Runnable 的 lambda 表达式来创建线程 newThread(() -> {
System.out.println("Hello Lambda!");
        }).start();
// 使用线程池来创建线程ExecutorServiceexecutorService=Executors.newCachedThreadPool();
executorService.execute(newMyRun());
executorService.shutdown();
    }
}

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


packagebasic_concepts;
publicclassSleepYieldJoin {
publicstaticvoidmain(String[] args) {
//        testSleep();//        testYield();testJoin();
    }
staticvoidtestSleep() {
newThread(() -> {
for (inti=0; i<100; i++) {
System.out.println("A"+i);
try {
// 使线程休眠500毫秒Thread.sleep(500);
                } catch (InterruptedExceptione) {
e.printStackTrace();
                }
            }
        }).start();
    }
staticvoidtestYield() {
newThread(() -> {
for (inti=0; i<100; i++) {
System.out.println("A"+i);
if (i%10==0) {
Thread.yield();
                }
            }
        }).start();
newThread(() -> {
for (inti=0; i<100; i++) {
System.out.println("B"+i);
if (i%10==0) {
Thread.yield();
                }
            }
        }).start();
    }
staticvoidtestJoin() {
Threadt1=newThread(() -> {
for (inti=0; i<100; i++) {
System.out.println("A"+i);
try {
Thread.sleep(500);
                } catch (InterruptedExceptione) {
e.printStackTrace();
                }
            }
        });
Threadt2=newThread(() -> {
try {
t1.join();
            } catch (InterruptedExceptione) {
e.printStackTrace();
            }
for (inti=0; i<100; i++) {
System.out.println("B"+i);
try {
Thread.sleep(500);
                } catch (InterruptedExceptione) {
e.printStackTrace();
                }
            }
        });
t1.start();
t2.start();
    }
}

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

线程状态


packagebasic_concepts;
publicclassThreadState {
staticclassMyThreadextendsThread {
@Overridepublicvoidrun() {
System.out.println(this.getState());
for (inti=0; i<10; i++) {
try {
Thread.sleep(500);
                } catch (InterruptedExceptione) {
e.printStackTrace();
                }
System.out.println(i);
            }
        }
    }
publicstaticvoidmain(String[] args) {
Threadt=newMyThread();
// 通过 getState 方法获取线程状态System.out.println(t.getState());
t.start();
try {
t.join();
        } catch (InterruptedExceptione) {
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 常见的应用场景:


packagesynchronize;
/*** synchronized关键字* 对某个对象加锁*/publicclassSynchronize {
privatestaticintcount=10;
privateObjectobject=newObject();
publicvoidlockObject() {
/*** 不能用String常量 Integer Long*/synchronized (object) {
count--;
System.out.println(Thread.currentThread().getName() +" count = "+count);
        }
    }
publicvoidlockThis() {
synchronized (this) {
count--;
System.out.println(Thread.currentThread().getName() +" count = "+count);
        }
    }
/*** 等同于在方法的代码执行时要synchronized (this)*/publicsynchronizedvoidsynchronizedMethod() {
count--;
System.out.println(Thread.currentThread().getName() +" count = "+count);
    }
/*** 等同于synchronized(Synchronize.class)*/publicsynchronizedstaticvoidstaticSynchronizedMethod() {
count--;
System.out.println(Thread.currentThread().getName() +" count = "+count);
    }
publicstaticvoidlockClass() {
synchronized (Synchronize.class) {
count--;
System.out.println(Thread.currentThread().getName() +" count = "+count);
        }
    }
staticclassMyRunimplementsRunnable {
@Overridepublicvoidrun() {
Synchronizesynchronize=newSynchronize();
synchronize.lockObject();
        }
    }
publicstaticvoidmain(String[] args) {
for (inti=0; i<10; i++) {
newThread(newMyRun()).start();
        }
    }
}

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


packagesynchronize;
publicclassThreadSafeimplementsRunnable {
privateintcount=100;
@Overridepublicsynchronizedvoidrun() {
count--;
System.out.println(Thread.currentThread().getName() +" count = "+count);
    }
publicstaticvoidmain(String[] args) {
ThreadSafethreadSafe=newThreadSafe();
for (inti=0; i<100; i++) {
newThread(threadSafe, "Thread"+i).start();
        }
    }
}

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


packagesynchronize;
/*** 同时调用同步和非同步方法*/publicclassSynchronizeCallNonSynchronize {
publicsynchronizedvoidsynchronizedMethod() {
System.out.println(Thread.currentThread().getName() +" synchronized method start...");
try {
Thread.sleep(10000);
        } catch (InterruptedExceptione) {
e.printStackTrace();
        }
System.out.println(Thread.currentThread().getName() +" synchronized method end");
    }
publicvoidnonSynchronizedMethod() {
try {
Thread.sleep(5000);
        } catch (InterruptedExceptione) {
e.printStackTrace();
        }
System.out.println(Thread.currentThread().getName() +" non-synchronized method call");
    }
publicstaticvoidmain(String[] args) {
SynchronizeCallNonSynchronizesynchronizeCallNonSynchronize=newSynchronizeCallNonSynchronize();
/*new Thread(() -> synchronizeCallNonSynchronize.synchronizedMethod(), "t1").start();new Thread(() -> synchronizeCallNonSynchronize.nonSynchronizedMethod(), "t2").start();*/newThread(synchronizeCallNonSynchronize::synchronizedMethod, "t1").start();
newThread(synchronizeCallNonSynchronize::nonSynchronizedMethod, "t2").start();
    }
}

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


packagesynchronize;
importjava.util.concurrent.TimeUnit;
/*** 模拟银行账户*/publicclassThreadSafeAccount {
privateStringname;
privatedoublebalance;
publicsynchronizedvoidset(Stringname, doublebalance) {
this.name=name;
try {
Thread.sleep(2000);
        } catch (InterruptedExceptione) {
e.printStackTrace();
        }
this.balance=balance;
    }
/*** 如果对业务写方法加锁,* 而对业务读方法不加锁,* 会产生脏读问题(dirtyRead)* @param name* @return*/publicsynchronizeddoublegetBalance(Stringname) {
returnthis.balance;
    }
publicstaticvoidmain(String[] args) {
ThreadSafeAccountaccount=newThreadSafeAccount();
newThread(() ->account.set("zhangsan", 100.0)).start();
try {
TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedExceptione) {
e.printStackTrace();
        }
System.out.println(account.getBalance("zhangsan"));
try {
TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedExceptione) {
e.printStackTrace();
        }
System.out.println(account.getBalance("zhangsan"));
    }
}

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


packagesynchronize;
importjava.util.concurrent.TimeUnit;
/*** 一个同步方法可以调用另一个同步方法,* 一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁,* 也就是说synchronized获得的锁是可重入的*/publicclassReentrantSynchronized {
synchronizedvoidm() {
System.out.println("m start");
try {
TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedExceptione) {
e.printStackTrace();
        }
System.out.println("m end");
    }
synchronizedvoidm1() {
System.out.println("m1 start");
try {
TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedExceptione) {
e.printStackTrace();
        }
m2();
System.out.println("m1 end");
    }
synchronizedvoidm2() {
try {
TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedExceptione) {
e.printStackTrace();
        }
System.out.println("m2");
    }
staticclassChildextendsReentrantSynchronized {
@Overridesynchronizedvoidm() {
System.out.println("child m start");
super.m();
System.out.println("child m end");
        }
    }
publicstaticvoidmain(String[] args) {
newReentrantSynchronized().m1();
newChild().m();
    }
}

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


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

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

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

锁升级:

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

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

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

目录
相关文章
|
7天前
|
安全 程序员 API
|
4天前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####
|
5天前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
23 4
|
5天前
|
消息中间件 供应链 Java
掌握Java多线程编程的艺术
【10月更文挑战第29天】 在当今软件开发领域,多线程编程已成为提升应用性能和响应速度的关键手段之一。本文旨在深入探讨Java多线程编程的核心技术、常见问题以及最佳实践,通过实际案例分析,帮助读者理解并掌握如何在Java应用中高效地使用多线程。不同于常规的技术总结,本文将结合作者多年的实践经验,以故事化的方式讲述多线程编程的魅力与挑战,旨在为读者提供一种全新的学习视角。
25 3
|
6天前
|
安全 Java 调度
Java中的多线程编程入门
【10月更文挑战第29天】在Java的世界中,多线程就像是一场精心编排的交响乐。每个线程都是乐团中的一个乐手,他们各自演奏着自己的部分,却又和谐地共同完成整场演出。本文将带你走进Java多线程的世界,让你从零基础到能够编写基本的多线程程序。
18 1
|
10天前
|
缓存 Java 调度
Java中的多线程编程:从基础到实践
【10月更文挑战第24天】 本文旨在为读者提供一个关于Java多线程编程的全面指南。我们将从多线程的基本概念开始,逐步深入到Java中实现多线程的方法,包括继承Thread类、实现Runnable接口以及使用Executor框架。此外,我们还将探讨多线程编程中的常见问题和最佳实践,帮助读者在实际项目中更好地应用多线程技术。
17 3
|
12天前
|
监控 安全 Java
Java多线程编程的艺术与实践
【10月更文挑战第22天】 在现代软件开发中,多线程编程是一项不可或缺的技能。本文将深入探讨Java多线程编程的核心概念、常见问题以及最佳实践,帮助开发者掌握这一强大的工具。我们将从基础概念入手,逐步深入到高级主题,包括线程的创建与管理、同步机制、线程池的使用等。通过实际案例分析,本文旨在提供一种系统化的学习方法,使读者能够在实际项目中灵活运用多线程技术。
|
10天前
|
缓存 安全 Java
Java中的多线程编程:从基础到实践
【10月更文挑战第24天】 本文将深入探讨Java中的多线程编程,包括其基本原理、实现方式以及常见问题。我们将从简单的线程创建开始,逐步深入了解线程的生命周期、同步机制、并发工具类等高级主题。通过实际案例和代码示例,帮助读者掌握多线程编程的核心概念和技术,提高程序的性能和可靠性。
10 2
|
11天前
|
Java
Java中的多线程编程:从基础到实践
本文深入探讨Java多线程编程,首先介绍多线程的基本概念和重要性,接着详细讲解如何在Java中创建和管理线程,最后通过实例演示多线程的实际应用。文章旨在帮助读者理解多线程的核心原理,掌握基本的多线程操作,并能够在实际项目中灵活运用多线程技术。
|
12天前
|
Java 数据处理 开发者
Java多线程编程的艺术:从入门到精通####
【10月更文挑战第21天】 本文将深入探讨Java多线程编程的核心概念,通过生动实例和实用技巧,引导读者从基础认知迈向高效并发编程的殿堂。我们将一起揭开线程管理的神秘面纱,掌握同步机制的精髓,并学习如何在实际项目中灵活运用这些知识,以提升应用性能与响应速度。 ####
37 3