写点什么

多线程与高并发之锁

用户头像
彭阿三
关注
发布于: 2020 年 09 月 11 日
多线程与高并发之锁

韩愈说过这样一句话:“业精于勤荒于嬉,行成于思毁于随””。天才就是无止境刻苦勤奋的努力。成绩优与良;才思浓与淡,都是由勤奋注定的。


概念

进程

进程指正在运行的程序,进程拥有一个完整的、私有的基本运行资源集合。通常,每个进程都有自己的内存空间。


进程往往被看作是程序或应用的代名词,然而,用户看到的一个单独的应用程序实际上可能是一组相互 协作的进程集合。


为了便于进程之间的通信,大多数操作系统都支持进程间通信(IPC),如 pipes 和 sockets。IPC 不仅支持同一系统上的通信,也支持不同的系统。IPC 通信方式包括管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams 等方式,其中 Socket 和 Streams 支持不同主机上的两个进程 IPC。

线程

线程有时也被称为轻量级的进程。进程和线程都提供了一个执行环境,但创建一个新的线程比创建一个 新的进程需要的资源要少。


线程是在进程中存在的 — 每个进程最少有一个线程。线程共享进程的资源,包括内存和打开的文件。这样提高了效率,但潜在的问题就是线程间的通信。


多线程的执行是 Java 平台的一个基本特征。每个应用都至少有一个线程 – 或几个,如果算上“系统”线程的话,比如内存管理和信号处理等。但是从程序员的角度来看,启动的只有一个线程,叫主线程。


简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程。


并发和并行

  • 并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。

  • 并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。

  • 在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如 hadoop 分布式集群


线程安全

基本概念

  1. 何谓竞态条件:当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。在临界区中使用适当的同步就可以避免竞态条件,如使用 synchronized 或者加锁机制。

  2. 何谓线程安全:允许被多个线程同时执行的代码称作线程安全的代码。线程安全的代码不包含竞态条件。


对象的安全

局部基本类型变量:局部变量存储在线程自己的栈中。也就是说,局部变量永远也不会被多个线程共享。所以,基础类型的 局部变量是线程安全的。下面是基础类型的局部变量的一个例子:


public class ThreadTest {public static voidmain(String[]args){ MyThread share = new MyThread(); for (int i=0;i<10;i++){ new Thread(share,"线程"+i).start(); } }}

class MyThread implementsRunnable{ public void run() { int a =0; ++a; System.out.println(Thread.currentThread().getName()+":"+a); }}


//打印结果线程0:1线程1:1线程2:1线程3:1线程4:1线程5:1线程6:1线程7:1线程8:1线程9:1


复制代码

无论多少个线程对 run()方法中的基本类型 a 执行++a 操作,只是更新当前线程栈的值,不会影响其他线程,也就是不共享数据;


对象的局部引用和基础类型的局部变量不太一样,尽管引用本身没有被共享,但引用所指的对象并没有存储在线程的栈内。所有的对象都存在共享堆中。如果在某个方法中创建的对象不会逃逸出(即该对象不会被其它方法获得,也不会被非局部变量引用 到)该方法,那么它就是线程安全的。实际上,哪怕将这个对象作为参数传给其它方法,只要别的线程获取不到这个对象,那它仍是线程安全的。


对象成员存储在堆上。如果两个线程同时更新同一个对象的同一个成员,那这个代码就不是线程安全的。如果两个线程同时调用同一个实例上的同一个方法并且有更新操作,就会有竞态条件问题。


JAVA 内存模型

线程之间的通信

线程的通信是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种共享内 存和消息传递。Java 的并发采用的是共享内存模型。


Java 内存模型结构

Java 内存模型(简称 JMM),JMM 决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度 来 看,JMM 定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory) 中, 每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。 本地内存是 JMM 的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他 的硬件和编译器优化。



从上图来看,线程 A 与线程 B 之间如要通信的话,必须要经历下面 2 个步骤:

  1. 首先,线程 A 把本地内存 A 中更新过的共享变量刷新到主内存中去。

  2. 然后,线程 B 到主内存中去读取线程 A 之前已更新过的共享变量。


CAS 乐观锁

乐观锁:不加锁,假设没有冲突去完成某项操作,如果因为冲突失败就重试,直到成功为止。其实现方式有一种比较典型的就是 Compare and Swap( CAS )。


从思想上来说,Synchronized 属于悲观锁,悲观地认为程序中的并发情况严重,所以严防死守。CAS 属乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。


CAS 的缺点:


1.CPU 开销较大 在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给 CPU 带来很大的压力。

2.不能保证代码块的原子性 CAS 机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证 3 个变量共同进行原子性的更新,就不得不使用 Synchronized 了。


Synchronized 块


概念

Java 中的同步块用 synchronized 标记。 同步块在 Java 中是同步在某个对象上。 所有同步在一个对象上 的同步块在同时只能被一个线程进入并执行操作。所有其他等待进入该同步块的线程将被阻塞,直到执行该同步块中的线程退出。


Synchronized 块的几种方式
  • 实例方法

  • 静态方法

  • 实例方法中的同步块

  • 静态方法中的同步块

上述同步块都同步在不同对象上。实际需要那种同步块视具体情况而定。


下面是一个同步的实例方法:



//不同实例调用不会阻塞public class MethodSync { public synchronized void test(){ try { System.out.println(Thread.currentThread().getName() + " test 进入了同步方法"); Thread.sleep(5000); System.out.println(Thread.currentThread().getName() + " test 休眠结束"); } catch (InterruptedException e) { e.printStackTrace(); } }}
/** 每个线程都会重新创建一个新的对象所以不会阻塞*/public class MyThread extends Thread { @Override public void run() { MethodSync sync = new MethodSync(); System.out.println(Thread.currentThread().getName() + " test 准备进入"); sync.test(); }}
public class Test { public static void main(String[] args) { new MyThread().start(); new MyThread().start(); }}
//Thread-1 test 准备进入//Thread-0 test 准备进入//Thread-1 test 进入了同步方法//Thread-0 test 进入了同步方法//Thread-0 test 休眠结束//Thread-1 test 休眠结束
复制代码


//同一个实例调用会阻塞public class MethodSync {    public synchronized void test1(){        try {            System.out.println(Thread.currentThread().getName() + " test1 进入了同步方法");            Thread.sleep(5000);            System.out.println(Thread.currentThread().getName() + " test1 休眠结束");        } catch (InterruptedException e) {            e.printStackTrace();        }    }}/*** 每个线程用同一个MethodSync对象调用test1()所以线程阻塞*/public class MyThread extends Thread {    static MethodSync sync = new MethodSync();    @Override    public void run() {        System.out.println(Thread.currentThread().getName() + " test 准备进入");        sync.test1();    }}
public class Test { public static void main(String[] args) { new MyThread().start(); new MyThread().start(); }}

//Thread-0 test1 准备进入//Thread-0 test1 进入了同步方法//Thread-1 test1 准备进入//Thread-0 test1 休眠结束//Thread-1 test1 进入了同步方法//Thread-1 test1 休眠结束
复制代码


synchronized 关键字锁住了调用当前方法的当前实例,如果不同实例不受同步锁 synchronized 关键字影响,如果相同实例调用的当前方法则受关键字 synchronized 约束。


同步代码块传参变量对象 (锁住的是变量对象)
  • 同一个属性对象才会实现同步

// 敲重点   同一个属性对象才会实现同步//  Integer 负128-正127区间的数是放在缓存里的内存地址一致是同一个属性对象//  如果不在这里区间的则会创建不同的对象不受同步锁控制public class MethodSync {    public Integer lockObject;    public MethodSync(Integer lockObject) {        this.lockObject = lockObject;    }        //锁住了实例中的成员变量    public void test2() {        synchronized (lockObject) {            try {                System.out.println(Thread.currentThread().getName() + " test2 进入了同步方法");                Thread.sleep(5000);                System.out.println(Thread.currentThread().getName() + " test2 休眠结束");            } catch (InterruptedException e) {                e.printStackTrace();            }        }    }	}

public class MyThread extends Thread {
@Override public void run() { System.out.println(Thread.currentThread().getName() + " test 准备进入"); MethodSync sync = new MethodSync(127); sync.test2(); }}

public class Test { public static void main(String[] args) { new MyThread().start(); new MyThread().start(); }}
//Thread-0 test2 准备进入//Thread-1 test2 准备进入//Thread-0 test2 进入了同步方法//Thread-0 test2 休眠结束//Thread-1 test2 进入了同步方法//Thread-1 test2 休眠结束
复制代码


同一个实例对象的成员属性肯定是同一个,此处列举的是不同实例的情况,但是 依旧实现了同步,原因如下:

Integer 存在静态缓存,范围是-128 ~ 127,当使用 Integer A = 127 或者 Integer A = Integer.valueOf(127) 这样的形式,都是从此缓存拿。如果使用 Integer A = new Integer(127),每次都是一个新的对象。此例中,两个对象实例的成员变量 lockObject 其实是同一个对象,因此实现了同步。还有字符串常量池也要注意。所以此处关注是,同步代码块传参的对象是否是同一个。这跟第二个方式其实是同一种。


同步代码块传参 class 对象(全局锁)
  •  所有调用该方法的线程都会实现同步

//类对象锁,全局锁public class MethodSync {
//全局锁,类是全局唯一的 public void test3() { synchronized (MethodSync.class) { try { System.out.println(Thread.currentThread().getName() + " test3 进入了同步方法"); Thread.sleep(5000); System.out.println(Thread.currentThread().getName() + " test3 休眠结束"); } catch (InterruptedException e) { e.printStackTrace(); } } }
}
public class MyThread extends Thread {
@Override public void run() { System.out.println(Thread.currentThread().getName() + " test 准备进入"); MethodSync sync = new MethodSync(); sync.test3(); }}
public class Test { public static void main(String[] args) { new MyThread().start(); new MyThread().start(); }}
//Thread-0 test3 准备进入//Thread-1 test3 准备进入//Thread-0 test3 进入了同步方法//Thread-0 test3 休眠结束//Thread-1 test3 进入了同步方法//Thread-1 test3 休眠结束
复制代码


修饰静态方法(全局锁)

JLS 规范里面有明确的定义 static 方法锁的是 Class object

synchronized 修饰静态方法锁的是类对象,全局锁。


public class MethodSync {   

//全局锁,类是全局唯一的 public static synchronized void test4() { synchronized (MethodSync.class) { try { System.out.println(Thread.currentThread().getName() + " test4 进入了同步方法"); Thread.sleep(5000); System.out.println(Thread.currentThread().getName() + " test4 休眠结束"); } catch (InterruptedException e) { e.printStackTrace(); } } }
}
public class MyThread extends Thread {
@Override public void run() { System.out.println(Thread.currentThread().getName() + " test 准备进入"); MethodSync.test4(); }}

public class Test { public static void main(String[] args) { new MyThread().start(); new MyThread().start(); }}//Thread-0 test4 准备进入//Thread-1 test4 准备进入//Thread-0 test4 进入了同步方法//Thread-0 test4 休眠结束//Thread-1 test4 进入了同步方法//Thread-1 test4 休眠结束
复制代码


静态方法的 synchronized,锁住了该方法所在的类对象上,因为一个类只能对应一个类对象,所以同时只有一个线程执行类中的静态同步方法.


Synchronized 的升级过程

*********锁升级只能向上升级,不能向下降级。*********

只有一个线程访问的时候,加锁代码为偏向锁(偏向锁并非锁,只是打了一个标记,标记了线程 ID,每次判断 ID 相同直接执行,大大的提升了性能,如果来了其他线程会把标记删除,升级为 CAS 锁),当多个线程来同时访问的时候会升级为轻量级锁,(默认情况下,自旋的次数为 10 次,用户可以通过-XX:PreBlockSpin 来进行更改。或者线程等待数超过 CPU 数的一半,锁升级),当达到升级条件时,会升级为重量级锁,重量级锁会进行排队,不会消耗 CPU 资源。

关键字 Volatile

Volatile 是轻量级的 synchronized,在多处理器环境下, 可以保证共享变量的可见性。它不会引起线 程上下文的切换和调度,正确的使用 Volatile,比 synchronized 的使用和执行成本更低。

使用场景:一写多读


概念

可见性:是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改一 个 共享变量时,另一个线程马上就能看到。比如:用 volatile 修饰的变量,就会具有可见性。 volatile 修饰的变量不允许线程内部缓存和重排序(指令重排(volatile 不具备原子性即非线程安全)


Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性, volatile 是因为其本身包含“禁止指令重排序”的语义,synchronized 是由“ 一个变量在同一个时 刻只允许一条线程对其进行 lock 操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步 块只能串行执行。


Java 语言提供了一种稍弱的同步机制, 即 volatile 变量, 用来确保将变量的更新操作通知到其他线 程。 当把变量声明为 volatile 类型后, 编译器与运行时 都会注意到这个变量是共享的, 因此不会将该 变量上的操作与其他内存操作一起重排序。 volatile 变量不会被缓存在寄存器或者对其他处理器不可 见的地方,因此在读取 volatile 类型的变量时总会返回最新写入的值。


在访问 volatile 变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此 volatile 变量是 种比 sychronized 关键字更轻量级的同步机制。


当一个变量定义为 volatile 之后,将具备两种特性:

  • 保证此变量对所有的线程的可见性, 这里的 “可见性”, 如本文开头所述, 当一个线程修改 了这个 变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普 通变量做不到这点,普通变量的值在线程间传递均需要通过主内存来完成。

  • 禁止指令重排序优化。有 volatile 修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏 障之前的位置),只有一个 CPU 访问内存时,并不需要内存屏障;(什么是指令重排序:是指 CPU 采 用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理。

线程本地变量

概念

Java 中的 ThreadLocal 类允许我们创建只能被同一个线程读写的变量。因此,如果一段代码含有一个 ThreadLocal 变量的引用, 即使两个线程同时执行这段代码,它们也无法访问到对方的 ThreadLocal 变 量。

如何创建 ThreadLocal 变量


以下代码展示了如何创建一个 ThreadLocal 变量:


private ThreadLocal myThreadLocal = new ThreadLocal();//设置值myThreadLocal.set("A thread local value”);                  //读取值
String threadLocalValue = (String) myThreadLocal.get();
复制代码

ThreadLocal 对象。我们只需要实例化对象一次,并且也不需要知道它是被哪个线程实例化。虽然所有的线程都能访问到这个 ThreadLocal 实例,但是每个线程却只能访问到自己通过调用 ThreadLocal 的 set()方法设置的值。 即使是两个不同的线程在同一个 ThreadLocal 对象上设置了不同的值,他们仍然无法访问到对方的值。


关于 InheritableThreadLocal


InheritableThreadLocal 类是 ThreadLocal 类的子类 。 ThreadLocal 中每个线程拥有它自己的值, 与 ThreadLocal 不同的是, InheritableThreadLocal 允许一个线程以及该线程创建的所有子线程都可以 访问它保存的值。


发布于: 2020 年 09 月 11 日阅读数: 930
用户头像

彭阿三

关注

java工程师 2019.06.28 加入

一个慵懒的程序员。

评论

发布
暂无评论
多线程与高并发之锁