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

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

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)
  • 锁只能升级不能降级

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

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

目录
相关文章
|
1天前
|
安全 算法 Java
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第13天】 在Java开发中,并发编程是一个复杂且重要的领域。它不仅关系到程序的线程安全性,也直接影响到系统的性能表现。本文将探讨Java并发编程的核心概念,包括线程同步机制、锁优化技术以及如何平衡线程安全和性能。通过分析具体案例,我们将提供实用的编程技巧和最佳实践,帮助开发者在确保线程安全的同时,提升应用性能。
10 1
|
2天前
|
数据采集
多线程在编程中的重要性有什么?并以LabVIEW为例进行说明
多线程在编程中的重要性有什么?并以LabVIEW为例进行说明
11 4
|
2天前
|
安全 Java 调度
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第12天】 在现代软件开发中,多线程编程是提升应用程序性能和响应能力的关键手段之一。特别是在Java语言中,由于其内置的跨平台线程支持,开发者可以轻松地创建和管理线程。然而,随之而来的并发问题也不容小觑。本文将探讨Java并发编程的核心概念,包括线程安全策略、锁机制以及性能优化技巧。通过实例分析与性能比较,我们旨在为读者提供一套既确保线程安全又兼顾性能的编程指导。
|
3天前
|
安全 Java
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第11天】在Java并发编程中,线程安全和性能优化是两个重要的主题。本文将深入探讨这两个方面,包括线程安全的基本概念,如何实现线程安全,以及如何在保证线程安全的同时进行性能优化。我们将通过实例和代码片段来说明这些概念和技术。
4 0
|
3天前
|
Java 调度
Java并发编程:深入理解线程池
【5月更文挑战第11天】本文将深入探讨Java中的线程池,包括其基本概念、工作原理以及如何使用。我们将通过实例来解释线程池的优点,如提高性能和资源利用率,以及如何避免常见的并发问题。我们还将讨论Java中线程池的实现,包括Executor框架和ThreadPoolExecutor类,并展示如何创建和管理线程池。最后,我们将讨论线程池的一些高级特性,如任务调度、线程优先级和异常处理。
|
7天前
|
安全 Java 开发者
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第7天】在Java中,多线程编程是提高应用程序性能和响应能力的关键。本文将深入探讨Java并发编程的核心概念,包括线程安全、同步机制以及性能优化策略。我们将通过实例分析,了解如何避免常见的并发问题,如死锁、竞态条件和资源争用,并学习如何使用Java提供的并发工具来构建高效、可靠的多线程应用。
|
7天前
|
缓存 Java
Java并发编程:深入理解线程池
【5月更文挑战第7天】本文将深入探讨Java并发编程中的重要概念——线程池。我们将了解线程池的基本概念,以及如何使用Java的Executor框架来创建和管理线程池。此外,我们还将讨论线程池的优点和缺点,以及如何选择合适的线程池大小。最后,我们将通过一个示例来演示如何使用线程池来提高程序的性能。
|
13天前
|
存储 安全 Java
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第1天】本文将深入探讨Java并发编程的核心概念,包括线程安全和性能优化。我们将详细分析线程安全问题的根源,以及如何通过合理的设计和编码实践来避免常见的并发问题。同时,我们还将探讨如何在保证线程安全的前提下,提高程序的并发性能,包括使用高效的同步机制、减少锁的竞争以及利用现代硬件的并行能力等技术手段。
|
14天前
|
缓存 Java 调度
Java并发编程:深入理解线程池
【4月更文挑战第30天】 在Java并发编程中,线程池是一种重要的工具,它可以帮助我们有效地管理线程,提高系统性能。本文将深入探讨Java线程池的工作原理,如何使用它,以及如何根据实际需求选择合适的线程池策略。
|
14天前
|
Java
Java并发编程:深入理解线程池
【4月更文挑战第30天】 本文将深入探讨Java中的线程池,解析其原理、使用场景以及如何合理地利用线程池提高程序性能。我们将从线程池的基本概念出发,介绍其内部工作机制,然后通过实例演示如何创建和使用线程池。最后,我们将讨论线程池的优缺点以及在实际应用中需要注意的问题。

热门文章

最新文章