Java多线程编程技术方案原理

一 ,多线程相关的一些概念

 

1,线程和进程:

线程指的是进程中一个单一顺序的控制流,

进程中可以并发多个线程,每条线程并行执行不同的任务,被认为是一个计算资源的集合。进程不能被任务是一个应用,因为有些应用有多个进程组成。

 

2,并行与并发:

并发:单核cpu运行多线程时,在一个时间段内,多个线程交叉运行,给人感觉是同时运行。

并行:多核cpu运行 多线程时,真正的在同一时刻运行。

所以如果是单核多线程并发执行,并不需要处理多线程的同步问题。

 

3,线程同步和线程安全:

线程同步: 多个并行执行的线程在同一时刻不会执行指定的程序段。

线程安全:在多线程并行运行环境下而不会引发数据错误。线程安全问题都是由“全局变量及静态变量”引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

区别:线程同步是解决线程安全的方法之一。

 

4,多线程的优点和缺点:

  1,有写耗时的任务放在后台运行,不用等待。

  2,一些等待的任务,比如用户输入,文件读取和网络收发数据,不用等待完成。

  3,整体效率更高

  缺点:线程切换消耗,线程的同步控制,更多的线程空间。

 

5,线程池技术:

线程池技术并不是实现线程同步,或者实现线程安全的技术。只是解决了大量线程的重建的消耗,池中保持一定数量的线程,即时开始处理任务。它是一种实现高并发的方案。比如说在Reactor多线程模型中使用了线程池技术。

 

 

6,JVM并发编程中的3个概念:

6.1,原子性

可认为时不可切分的操作,可以说一个指令,也可以有序的多个指令。要么完全执行成功,要么失败回退。

注意:一条Java语句,并不等同于一个原子性指令。

6.2,可见性

举例有三个线程并发,A , B , C线程, 访问同一个变量时,A线程修改了这个变量的值,B, C线程能够立即看得到修改的值。

6.3,有序性 

程序执行的顺序按照代码的先后顺序执行。 JVM 内部对程序的有序性做了一些规范,能保证程序运行有序性。

但是存在指令重排的问题。一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,最终会保证结果的一致性。

 

以上3个因素发生变化,都会影响到“线程同步”问题。以下同步解决方案中,都是为了解决这3个概念中的问题。

 

 

二,线程安全的技术方案

 

2.1 volatile变量技术:

作用于被操作的共享变量对象上。解决可见性问题。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。而普通变量不能保证完全的可见性。

2.1.1作用:

  1,保证了可见性   

  2,避免了JVM的指令重排序

缺点是volatile也无法保证对变量的任何操作都是原子性的。不能形成内存栅栏。

 

2.1.2  使用场景:

volatile关键字用于声明简单类型变量,如int、float、boolean等数据类型。如果这些简单数据类型声明为volatile,对它们的操作就会变成原子级别的。
原因是声明为volatile的简单变量,如果当前值由该变量以前的值相关,那么volatile关键字不起作用,也就是说如下的表达式都不是原子操作:

n = n + 1;
n++;

//作为标记状态量

volatile boolean flag = false;

 //线程1
while(!flag){          //默认情况执行
    doSomething();
}
 
//线程2
public void setFlag() {  //线程2执行后,线程1停止
    flag = true;             //对于变量的操作时原子性的,不依赖于前值。
}

实现功能;实现了并发运行,以及对2个线程的切换。

1,   默认,线程1执行, 线程2未运行。   

2,线程2运行, 线程1停止。 



2.2  synchronized技术 (Java的线程同步技术之一)

在并发编程中,解决了原子性问题, 作用于多个线程的共同代码上。某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。

  Synchronized 是一种独占锁。在修饰静态方法时,锁的是类对象,如 Object.class。修饰非静态方法时,锁的是对象,即 this。修饰方法块时,锁的是括号里的对象。 每个对象有一个锁和一个等待队列,锁只能被一个线程持有,其他需要锁的线程需要阻塞等待。锁被释放后,对象会从队列中取出一个并唤醒,唤醒哪个线程是不确定的,不保证公平性。
  synchronized 锁住的是对象而非代码,只要访问的是同一个对象的 synchronized 方法,即使是不同的代码,也会被同步顺序访问。举例, A类内有 2个synchronized方法, 比如 add()  subtract()  且都是非static方法,2个并发线程分别访问这2个方法,即使执行不同的代码段,也依然被同步执行。因为这2个方法对应了同一个对象锁。 如果其中add() 方法为static方法,则可以并发执行,不需要同步。
 
  synchronized方法不能防止非synchronized方法被同时执行,所以,一般在保护变量时,需要在所有访问该变量的方法上加上synchronized。举例,A类内有 1个synchronized方法, 比如 add() , 一个非 synchronized方法 ,比如 subtract() ,2个方法内对同一共享变量进行操作,并不能保证线程安全。所以如果需要保证变量的线程安全性,需要对所有操作该变量的方法,添加synchronized锁。

 

2.2.1  同步方法;

可理解为对“该方法所属的对象加锁” ,一次只能一个线程拿到锁,开始执行。也就是说,在同一个时间片内,并发执行的线程(同时执行到当前方法的线程)只有一个可以运行,其他线程必须等待。所以一般效率比较低。

 

2.2.2  同步代码块:

同步代码块锁定的代码范围更小,多个并发线程更大灵活性更大,毕竟代码块外部的指令依旧可以执行。而且锁定代码块也会更快执行完成。

 

 

2.3  CAS机制:属于乐观锁机制,适合并发冲突比较少的场合。

  线程A 操作共享变量后,假定其他线程并没有操作该变量。则直接放回该变量。 如果内存的值不是预期值(也即是其他线程操作了该变量,虽然概率很低),则不做任何改变,返回 false。

2.3.1  机制说明:

1,操作变量后,和主内存对比,  失败后,一般开启继续重试。

2,重试的过程是,继续循环过程1,继续拉去新的共享变量,操作然后重新对比。 可理解为,总能找都一个时间片,没有存在并发冲突。

 

CAS 是 compare and swap 的简写,即比较并交换。它是指一种操作机制,而不是某个具体的类或方法。在 Java 平台上对这种操作进行了包装。在 Unsafe 类中,调用代码如下

unsafe.compareAndSwapInt(this, valueOffset, expect, update);

作为一种底层机制, CAS 在 Java 的原子类和并发包中有大量使用。

 

CAS 底层是靠调用 CPU 指令集的 cmpxchg 完成的。但是这种方案容易引发一个典型问题:ABA 问题 ,可以通过加版本号解决。

 

 

2.4  等待唤醒机制:或者称为“线程通信模型”

wait/notify 就是线程间的一种协作机制。

 

2.5  Atomic : 原子类

2. 6  Lock锁机制:

 AQS是AbstractQueuedSynchronizer的缩写)

 (后续补充,  还有补充代码实践演示。)

 

 

三, 其他一些相关概念:

 

悲观锁和乐观锁概念:

悲观锁:和独占锁是一个意思,它假设一定会发生冲突,因此获取到锁之后会阻塞其他等待线程。典型方案是synchronized方案

乐观锁:假设不会产生冲突,先去尝试执行某项操作,失败了再进行其他处理(一般都是不断循环重试),适用于同步冲突比较少的场合。典型方案是CAS方案

 

公平锁和非公平锁:

公平锁是指各个线程在加锁前先检查有无排队的线程,按排队顺序去获得锁。

非公平锁是指线程加锁前不考虑排队问题,直接尝试获取锁,获取不到再去队尾排队。

 

可重入锁和不可重入锁:

如果一个线程已经获取到了一个锁,那么它可以访问被这个锁锁住的所有代码块。不可重入锁与之相反。 

 

死锁概念:两个线程相互等待的状态就形成了死锁。

 

posted @ 2022-06-30 12:10  gaussen126  阅读(146)  评论(0编辑  收藏  举报