avatar

Catalog
Java多线程

基础多线程

线程创建与启动

java用多线程非常简单,只需要两步

java
1
2
3
4
//创建线程
Thread thread=new thread();
//启动线程
thread.start();

有的教程讲的特别复杂,但是核心就这两步。

其中start()方法是最核心的,里面有一个 native start0()方法,看到这个名字就知道线程的启动的具体过程已经不受java控制了。

但是线程启动后,要干的事情,java还是能控制的,也就是run() 方法。线程启动后就会去执行run() 方法,做线程应该做的事情。

run() 方法有两种实现步骤:

  • Thread类实现了Runnable 接口,也就是说Thread中有一个默认的run()方法,我们可以新定义一个类继承Thread类,重写其中的run()方法,run()方法中实现我们自己想让线程做的事情。
  • 我们定义一个新的类 实现Runnable 接口,然后把这个类作为参数传给Thread类,因为Thread类的构造方法中有一种这样的构造方法。

对比这两种方法,其实没什么本质区别,都是要对Thread中默认的run()方法进行改写,因为默认的run()方法里面是空的。第一种方法说白了就是在改写run()方法的同时改了Thread类的名字,但是第二种通过构造函数没有改名字。


不管是通过哪种方式,值得注意的是,线程必须要通过start()方法才能启动(因为start()方法里面调用的是native的方法),然后线程会自动执行我们定义的run()方法,如果我们不调用start() 方法,直接调用Thread.run()方法,这和调用一个普通类的方法没什么两样,并没有启动新的线程,仍然还是在原来的线程中执行。

线程停止

通常通过一个外部标志位的方式停止线程

java
1
2
3
4
5
boolean flag=true; //当想要停止线程时,通过一个公开的方法将flag设置为false;
run(){
while(flag){
}
}

线程休眠

java
1
Thread.sleep(1000)  //里面的单位是毫秒,sleep()里面也会调用native方法,将当前进程休眠

线程礼让

线程礼让通过如下代码实习,礼让即当前线程让出cpu,然后大家再一起竞争cpu

java
1
Thread.yeild(); //注意,调用时调用的是Thread类的方法,而不是对象的方法,

线程强制执行

调用后强制执行该线程,阻塞其他线程

java
1
thread.join()//调用时调用的是thread的对象

线程状态

java
1
2
Thread.State//是一个枚举类型,枚举了所有的线程状态
thread.getState()//可以获取线程的状态

线程的优先级

java
1
2
3
4
5
6
7
//线程的优先级用数字表示,范围从1~10
Thread.MIN_PRORITY=1;
Thread.MAX_PRIORITY=10;
Thread.NORM_PRIORITY=5;
//使用以下方法改变或获取优先级
getPriority()
setPriority(int x)

守护线程

jvm不管守护线程有没有结束,只要用户线程结束了,都会退出

java
1
thread.setDaemon(true) //将线程设置为守护线程

线程同步

java
1
2
3
4
5
6
synchronized 对一个方法使用,如果这个方法不是静态方法,那么锁的就是这个类所属的对象,即 this
如果这个类是静态方法,那么锁的就是这里类,即 类.class
synchronized(){

}
可以锁任意对象

线程通信

当一个代码块被加synchronized 加锁以后,线程在这个代码块的地方只能单线程的执行了,但是这时候,此线程可能会需要和其他线程配合,比如需要其他线程给自己一个东西,但是其他线程没准备好,那么此时该线程就需要暂停一下,否则线程就一下子执行完了,那么这个方法就是wait(),执行wait后,线程让出cpu,让其他线程执行,其他线程把东西准备好之后,调用notifyAll(),唤醒所有被暂停的线程,让他们再次去抢占cpu,这就是线程通信。

虚假唤醒

当一个条件满足时,很多线程都被唤醒了,但是只有其中部分是有用的唤醒,其它的唤醒都是无用功 。

比如说买货,如果商品本来没有货物,突然进了一件商品,这是所有的线程都被唤醒了 ,但是只能一个人买,所以其他人都是假唤醒,获取不到对象的锁。所以需要循环判断,当被一个人买走后,其他人又必须等待,所以wait()必须放在while里面,当被唤醒时,再次判断,如果不满足又继续wait。如果用if就会虚假唤醒,逻辑就不对了。

synchronized和ReentrantLock

synchronized一个关键字其实包含了加锁和解锁的步骤,而利用ReentrantLock把这两步分开了,灵活性更高。

比如synchronized在获取锁阶段如果获取不到,就会卡在那里一直去获取锁,这可能导致死锁哦,而ReentrantLock可以利用trylock()方法去尝试获取,如果获取不到,就返回false,这样就不会死锁了。

所以使用ReentrantLock比使用synchronized更安全

Condition

condition必须先拿到锁

java
1
2
3
Lock lock=new ReentactLock();
Codition cod1=lock.newCodition();
Codition cod2=lock.newCodition();

Condition是条件锁,可以设置多个条件队列,从而实现队列级别的精准唤醒。

一个内置锁仅仅能相应一个条件队列。这有个缺陷。就是当一个锁相应多个条件谓词时,多个条件谓词仅仅能公用一个条件队列,这时候唤醒等待线程时有可能出现唤醒丢失的情况。

显式锁和显式条件队列避免了这个问题,一个显示锁能够相应多个条件Condition,一个Condition维护一个条件队列,这样对于多个条件谓词,比方isFull和isEmpty,能够使用两个Condition。对每一个条件谓词单独await,唤醒时能够单独signal。效率更高。

管程

管程,指的是管理共享变量以及对其操作过程,让它们支持并发访问

其实,就是把共享变量以及各个进程之间操作这个变量的方法放在同一个类中,方便管理,而其他类只需要调用管程中的方法就好了。

一个管程定义了一个数据结构和可以并发进程所运行的一组操作,这组操作能同步进程和改变管程中的数据

  1. 数据

  2. 方法

  3. 它的方法可以同步并发进程的操作

说白了,管程就是一个专门为并发编程提出的概念,它表示一个对象自己维护自己的状态,而且可以依据自身状态来同步并发的线程操作。而不是把这样的同步的手段交给调用者来处理。

参考:https://www.cnblogs.com/lcchuguo/p/5382760.html

三个同步工具类

CountDownLatch

是一个计时器闭锁,通过它可以完成过类似于阻塞当前进程的功能,即:一个线程或多个线程一直等待,直到其他线程执行的操作完成。CountDownLatch用一个给定的计数器来初始化,该计数器的操作是原子操作,即同时只能有一个线程去操作该计数器。调用该类await方法的线程会一直处于阻塞状态,直到其他线程调用countDown方法使当前计数器的值变为零,每次调用countDown计数器的值减1。当计数器值减至零时,所有因调用await()方法而处于等待状态的线程就会继续往下执行。

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.example.demo.CountDownLatchDemo;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
* 主线程等待子线程执行完成再执行
*/
public class CountdownLatchTest1 {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(3);
final CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
System.out.println("子线程" + Thread.currentThread().getName() + "开始执行");
Thread.sleep((long) (Math.random() * 10000));
System.out.println("子线程"+Thread.currentThread().getName()+"执行完成");
latch.countDown();//当前线程调用此方法,则计数减一
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
service.execute(runnable);
}

try {
System.out.println("主线程"+Thread.currentThread().getName()+"等待子线程执行完成...");
latch.await();//阻塞当前线程,直到计数器的值为0
System.out.println("主线程"+Thread.currentThread().getName()+"开始执行...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

CyclicBarrier

CyclicBarrier较CountDownLatch而言主要多了两个功能:

  1. 支持重置状态,达到循环利用的目的。这也是Cyclic的由来。CyclicBarrier中有一个内部类Generation,代表当前的同步处于哪一个阶段。当最后一个任务完成,执行任务的线程会通过nextGeneration方法来重置Generation。也可以通过CyclicBarrier的reset方法来重置Generation。
  2. 支持barrierCommand,当最后一个任务运行完成,执行任务的线程会检查CyclicBarrier的barrierCommand是否为null,如果不为null,则运行该任务。
java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class CyclicBarrierDemo {

static class TaskThread extends Thread {

CyclicBarrier barrier;

public TaskThread(CyclicBarrier barrier) {
this.barrier = barrier;
}

@Override
public void run() {
try {
Thread.sleep(1000);
System.out.println(getName() + " 到达栅栏 A");
barrier.await();
System.out.println(getName() + " 冲破栅栏 A");

Thread.sleep(2000);
System.out.println(getName() + " 到达栅栏 B");
barrier.await();
System.out.println(getName() + " 冲破栅栏 B");
} catch (Exception e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) {
int threadNum = 5;
CyclicBarrier barrier = new CyclicBarrier(threadNum, new Runnable() {

@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 完成最后任务");
}
});

for(int i = 0; i < threadNum; i++) {
new TaskThread(barrier).start();
}
}

}

Semaphore

synchronized的语义是互斥锁,就是在同一时刻,只有一个线程能获得执行代码的锁。但是现实生活中,有好多的场景,锁不止一把。

比如说,又到了十一假期,买票是重点,必须圈起来。在购票大厅里,有5个售票窗口,也就是说同一时刻可以服务5个人。要实现这种业务需求,用synchronized显然不合适,因为synchronized只能同时服务一个人。

查看Java并发工具,发现有一个Semaphore类,天生就是处理这种情况的。

先用Semaphore实现一个购票的小例子,来看看如何使用.

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package semaphore;

import java.util.concurrent.Semaphore;

public class Ticket {

public static void main(String[] args) {
Semaphore windows = new Semaphore(5); // 声明5个窗口

for (int i = 0; i < 8; i++) {
new Thread() {
@Override
public void run() {
try {
windows.acquire(); // 占用窗口
System.out.println(Thread.currentThread().getName() + ": 开始买票");
sleep(2000); // 睡2秒,模拟买票流程
System.out.println(Thread.currentThread().getName() + ": 购票成功");
windows.release(); // 释放窗口
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}
}
}

Semaphore和线程池的区别

信号量Semaphore是一个并发工具类,用来控制可同时并发的线程数,其内部维护了一组虚拟许可,通过构造器指定许可的数量,每次线程执行操作时先通过acquire方法获得许可,执行完毕再通过release方法释放许可。如果无可用许可,那么acquire方法将一直阻塞,直到其它线程释放许可。

线程池用来控制实际工作的线程数量,通过线程复用的方式来减小内存开销。线程池可同时工作的线程数量是一定的,超过该数量的线程需进入线程队列等待,直到有可用的工作线程来执行任务。

使用Seamphore,你创建了多少线程,实际就会有多少线程进行执行,只是可同时执行的线程数量会受到限制。但使用线程池,你创建的线程只是作为任务提交给线程池执行,实际工作的线程由线程池创建,并且实际工作的线程数量由线程池自己管理。

简单来说,线程池实际工作的线程是work线程,不是你自己创建的,是由线程池创建的,并由线程池自动控制实际并发的work线程数量。而Seamphore相当于一个信号灯,作用是对线程做限流,Seamphore可以对你自己创建的的线程做限流(也可以对线程池的work线程做限流),Seamphore的限流必须通过手动acquire和release来实现。

并发原理

并发编程中的三个概念

并发编程必须保证原子性、可见性和有序性

原子性

就是说一个操作或者多个操作在执行过程中是不会被打断的

可见性

可见性是指当多个线程访问同一个变量时,如果一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

有序性

即程序的执行顺序按照代码的先后顺序执行。

编译器为了提高运行效率,会对指令进行重排,虽然不会保证程序执行顺序同代码中的顺序一致,但是运行结果是和代码顺序执行的结果是一致的(重排时会考虑数据依赖性)

但是在多线程环境下,指令重排就会出问题。

总线锁和缓存一致性协议

在早期的CPU当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。比如上面例子中 如果一个线程在执行 i = i +1,如果在执行这段代码的过程中,在总线上发出了LOCK#锁的信号,那么只有等待这段代码完全执行完毕之后,其他CPU才能从变量i所在的内存读取变量,然后进行相应的操作。这样就解决了缓存不一致的问题。

但是上面的方式会有一个问题,由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。

所以就出现了缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

volatile

volatle 意为“不稳定的”,限定在变量前就是说这个变量是不稳定的,那么在多线程环境下,线程每次都应该从内存中读取该值,而不是从线程的缓存中读取值。

也就是说,volatile保证了可见性。(锁保证原子性,volatile保证可见性和有序性)

volatile还能禁止指令重排序,也就是保证了有序性。volatile禁止指令重排的意思是

java
1
2
3
4
5
语句1
语句2
volatile 语句3
语句4
语句5

在执行语句3之前,语句1和语句2可以重排,但当执行语句3时,语句1和语句2都必须执行完毕。

只有语句3执行完之后,语句4和语句5才可以参与执行,并且他们之间是可以重排的

详解:https://www.cnblogs.com/dolphin0520/p/3920373.html

CAS

CAS全称是compare and swap,就是比较并替换的意思。一直比较并替换,如果成功就返回,如果失败就一直重试。是借助C来调用CPU底层指令实现的。

ABA问题

CAS存在ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。

循环时间长开销大

自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。

只能保证一个共享变量的原子操作

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

AQS

AQS全称AbstractQueuedSynchronizer。用大白话说,AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。

在这里插入图片描述

java
1
2
3
4
5
6
7
8
9
10
11
12
//返回同步状态的当前值
protected final int getState() {
return state;
}
// 设置同步状态的值
protected final void setState(int newState) {
state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

AQS底层使用了模板方法模式。自定义同步器时需要重写下面几个 AQS 提供的模板方法:

java
1
2
3
4
5
isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。

原子类

原子类就是基于volatile和CAS实现的线程安全的类

梳理

CAS保证原子性,volatile保证可见性和有序性。把这些特性整合在一起,就形成了整个concurrent包得以实现的基石。如果我们仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式:

  1. 首先,声明共享变量为volatile;
  2. 然后,使用CAS的原子条件更新来实现线程之间的同步;
  3. 同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。

AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类来实现的。从整体来看,concurrent包的实现示意图如下:

img

锁的分类

可重入锁与不可重入锁

可重入锁就是如果一个锁被一个线程获取,那么该线程可以再次获得该锁,否则就是不可重入锁。

乐观锁和悲观锁

悲观锁就是使用时先上锁再使用。

乐观锁就是使用时不上锁,直到更新时判断一下这个数据有没有更新过。根据这个思路可以想到一种实现乐观锁的机制就是带版本号的乐观锁。

公平锁和非公平锁

公平锁就是竞争锁的时候不排队

非公平锁就是竞争锁的时候要排队

互斥锁和共享锁

互斥锁就是一个事务对一个数据加了互斥锁之后,其他事务就不能再对该数据加锁,比如写锁就要是互斥锁

共享锁就是一个事务如果对一个数据加了共享锁,其他事务还能对该数据加锁,不过只能加共享锁。

自旋锁

自旋锁就是一直循环等待而不阻塞线程,比如CAS就是这样

轻量级锁、偏向锁、重量级锁

java对synchronized关键字的一种优化的锁分配机制

线程池

池化技术 - 简单点来说,就是提前保存大量的资源,以备不时之需。

对于线程,内存,oracle的连接对象等等,这些都是资源,程序中当你创建一个线程或者在堆上申请一块内存时,都涉及到很多系统调用,也是非常消耗CPU的,如果你的程序需要很多类似的工作线程或者需要频繁的申请释放小块内存,如果没有在这方面进行优化,那很有可能这部分代码将会成为影响你整个程序性能的瓶颈。

池化技术主要有线程池,内存池,连接池,对象池等等。

对象池就是提前创建很多对象,将用过的对象保存起来,等下一次需要这种对象的时候,再拿出来重复使用

java
1
2
3
4
5
ExecutorService service = Executor.newFixedThreadPool(n);
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.shutdown();
Author: realLiuSir
Link: http://yoursite.com/2020/04/26/Java%E5%A4%9A%E7%BA%BF%E7%A8%8B/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶