共计 8305 个字符,预计需要花费 21 分钟才能阅读完成。
一、线程平安的概念
线程平安是多线程编程是的计算机程序代码中的一个概念。在领有共享数据的多条线程并行执行的程序中,线程平安的代码会通过同步机制保障各个线程都能够失常且精确的执行,不会呈现数据净化等意外状况。上述是百度百科给出的一个概念解释。换言之,线程平安就是某个函数在并发环境中调用时,可能解决好多个线程之间的共享变量,是程序可能正确执行结束。也就是说咱们想要确保在多线程拜访的时候,咱们的程序还可能依照咱们的预期的行为去执行,那么就是线程平安了。
二、导致线程不平安的起因
首先,能够来看一段代码,来看看是不是线程平安的,代码如下:
package com.company;
public class TestThread {
private static class XRunnable implements Runnable{
private int count;
public void run(){for(int i= 0; i<5; i++){getCount();
}
}
public void getCount(){
count++;
System.out.println(" "+count);
}
}
public static void main(String[] args) {XRunnable runnable = new XRunnable();
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
Thread t3 = new Thread(runnable);
t1.start();
t2.start();
t3.start();}
}
1234567891011121314151617181920212223242526272829
复制代码
输入的后果为:
2
3
2
5
4
7
6
10
11
12
9
8
13
14
15
123456789101112131415
复制代码
《2020 最新 Java 根底精讲视频教程和学习路线!》
从代码上进行剖析,当启动了三个线程,每个线程应该都是循环 5 次得出 1 到 15 的后果,然而从输入的后果,就能够看到有两个 2 输入,呈现像这种状况表明这个办法基本就不是线程平安的。咱们能够这样了解 :在每个过程的内存空间中都会有一块非凡的公共区域,通常称为 堆(内存),之所以会输入两个 2,是因为过程内的所有线程都能够拜访到该区域,当第一个线程曾经取得 2 这个数了,还没来得及输入,下一个线程在这段时间的空隙取得了 2 这个值,故输入时会输入 2 的值。
三、线程平安问题
要思考线程平安问题,就须要先思考 Java 并发的三大根本个性: 原子性 、 可见性 以及 有序性。
3.1 原子性
原子性是指在一个操作中就是 cpu 不能够在中途暂停而后再调度,即不被中断操作,要不全副执行实现,要不都不执行。就好比转账,从账户 A 向账户 B 转 1000 元,那么必然包含 2 个操作:从账户 A 减去 1000 元,往账户 B 加上 1000 元。2 个操作必须全副实现。
那程序中原子性指的是最小的操作单元,比方自增操作,它自身其实并不是原子性操作,分了 3 步的,包含读取变量的原始值、进行加 1 操作、写入工作内存。所以在多线程中,有可能一个线程还没自增完,可能才执行到第二部,另一个线程就曾经读取了值,导致后果谬误。那如果咱们能保障自增操作是一个原子性的操作,那么就能保障其余线程读取到的肯定是自增后的数据。
3.2 可见性
当多个线程拜访同一个变量时,一个线程批改了这个变量的值,其余线程可能立刻看失去批改的值。
若两个线程在不同的 cpu,那么线程 1 扭转了 i 的值还没刷新到主存,线程 2 又应用了 i,那么这个 i 值必定还是之前的,线程 1 对变量的批改线程没看到这就是可见性问题。
3.3 有序性
程序执行的程序依照代码的先后顺序执行,在多线程编程时就得思考这个问题。
案例:抢票
当多个线程同时共享,同一个全局变量或动态变量(即局部变量不会),做写的操作时,可能会产生数据抵触问题,也就是线程平安问题。然而做读操作是不会产生数据抵触问题。
Consumer 类:
package com.company;
public class Consumer implements Runnable{
private int ticket = 100;
public void run(){while(ticket>0){System.out.println(Thread.currentThread().getName() + "售卖第" + (100-ticket+1) + "张票");
ticket--;
}
}
}
1234567891011121314
复制代码
主类:
package com.company;
public class ThreadSafeProblem {public static void main(String[] args){Consumer abc = new Consumer();
new Thread(abc, "窗口 1").start();
new Thread(abc, "窗口 2").start();}
}
12345678910
复制代码
后果:
从输入后果来看,售票窗口买票呈现了计票的问题,这就是线程平安呈现问题了。
四、如何确保线程平安?
解决办法:应用多线程之间应用 关键字 synchronized、或者应用 锁(lock),或者volatile 关键字。
①synchronized(主动锁,锁的创立和开释都是主动的);
②lock 手动锁(手动指定锁的创立和开释)。
③volatile 关键字
为什么能解决?如果可能会产生数据抵触问题(线程不平安问题),只能让以后一个线程进行执行。代码执行实现后开释锁,而后能力让其余线程进行执行。这样的话就能够解决线程不平安问题。
4.1 synchronized 关键字
4.1.1 同步代码块
synchronized(同一个锁){// 可能会产生线程抵触问题}
123
复制代码
将可能会产生线程平安问题地代码,给包含起来,也称为 同步代码块 。synchronized 应用的锁能够是对象锁也能够是动态资源,如×××.class,只有持有锁的线程能力执行同步代码块中的代码。没持有锁的线程即便获取 cpu 的执行权,也进不去。
锁的开释 是在 synchronized 同步代码执行结束后主动开释。
同步的前提:
1,必须要有两个或者两个以上的线程,如果小于 2 个线程,则没有用,且还会耗费性能(获取锁,开释锁)
2,必须是多个线程应用同一个锁
弊病:多个线程须要判断锁,较为耗费资源、抢锁的资源。
例子:
public class ThreadSafeProblem {public static void main(String[] args) {Consumer abc = new Consumer();
// 留神要应用同一个 abc 变量作为 thread 的参数,// 如果你应用了两个 Consumer 对象,那么就不会共享 ticket 了,就天然不会呈现线程平安问题
new Thread(abc,"窗口 1").start();
new Thread(abc,"窗口 2").start();}
}
class Consumer implements Runnable{
private int ticket = 100;
@Override
public void run() {while (ticket > 0) {synchronized (Consumer.class) {if (ticket > 0) {System.out.println(Thread.currentThread().getName() + "售卖第" + (100-ticket+1) + "张票");
ticket--;
}
}
}
}
}
1234567891011121314151617181920212223
复制代码
4.1.2 同步函数
就是将 synchronized 加在办法上。
分为两种:
第一种是 非动态同步函数,即办法是非动态的,应用的this 对象锁,如下代码所示
第二种是 动态同步函数 ,即办法是用 static 润饰的,应用的锁是 以后类的 class 文件(xxx.class)。
public synchronized void sale () {if (ticket > 0) {System.out.println(Thread.currentThread().getName() + "售卖第" + (100-ticket+1) + "张票");
ticket--;
}
}
123456
复制代码
4.1.3 多线程死锁线程
如下代码所示,
线程 t1,运行后在同步代码块中须要 oj 对象锁,,运行到 sale 办法时须要 this 对象锁
线程 t2,运行后须要调用 sale 办法,须要先获取 this 锁,再获取 oj 对象锁
那这样就会造成,两个线程互相期待对方开释锁。就造成了死锁状况。简略来说就是:
同步中嵌套同步, 导致锁无奈开释。
class ThreadTrain3 implements Runnable {
private static int count = 100;
public boolean flag = true;
private static Object oj = new Object();
@Override
public void run() {if (flag) {while (true) {synchronized (oj) {sale();
}
}
} else {while (true) {sale();
}
}
}
public static synchronized void sale() {
// 前提 多线程进行应用、多个线程只能拿到一把锁。// 保障只能让一个线程 在执行 毛病效率升高
synchronized (oj) {if (count > 0) {
try {Thread.sleep(50);
} catch (Exception e) {// TODO: handle exception}
System.out.println(Thread.currentThread().getName() + ", 发售第" + (100 - count + 1) + "票");
count--;
}
}
}
}
public class ThreadDemo3 {public static void main(String[] args) throws InterruptedException {ThreadTrain3 threadTrain1 = new ThreadTrain3();
Thread t1 = new Thread(threadTrain1, "①号窗口");
Thread t2 = new Thread(threadTrain1, "②号窗口");
t1.start();
Thread.sleep(40);
threadTrain1.flag = false;
t2.start();}
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
复制代码
4.2 Lock
能够视为 synchronized 的增强版,提供了更灵便的性能。该接口提供了限时锁期待、锁中断、锁尝试等性能。synchronized 实现的同步代码块,它的锁是主动加的,且当执行完同步代码块或者抛出异样后,锁的开释也是主动的。
Lock l = ...;
l.lock();
try {// access the resource protected by this lock} finally {l.unlock();
}
1234567
复制代码
然而 Lock 锁 是须要手动去加锁和开释锁,所以 Lock 相比于 synchronized 更加的灵便。且还提供了更多的性能比如说
tryLock()办法 会尝试获取锁,如果锁不可用则返回 false,如果锁是能够应用的,那么就间接获取锁且返回 true,官网代码如下:
Lock lock = ...;
if (lock.tryLock()) {
try {// manipulate protected state} finally {lock.unlock();
}
} else {// perform alternative actions}
12345678910
复制代码
例子:
/*
* 应用 ReentrantLock 类实现同步
* */
class MyReenrantLock implements Runnable{
// 向上转型
private Lock lock = new ReentrantLock();
public void run() {
// 上锁
lock.lock();
for(int i = 0; i < 5; i++) {System.out.println("以后线程名:"+ Thread.currentThread().getName()+",i ="+i);
}
// 开释锁
lock.unlock();}
}
public class MyLock {public static void main(String[] args) {MyReenrantLock myReenrantLock = new MyReenrantLock();
Thread thread1 = new Thread(myReenrantLock);
Thread thread2 = new Thread(myReenrantLock);
Thread thread3 = new Thread(myReenrantLock);
thread1.start();
thread2.start();
thread3.start();}
}
123456789101112131415161718192021222324252627
复制代码
输入后果:
由此咱们能够看出,只有当以后线程打印结束后,其余的线程才可持续打印,线程打印的数据是分组打印,因为以后线程持有锁,但线程之间的打印程序是随机的。
即调用lock.lock() 代码的线程就持有了“对象监视器”,其余线程只有期待锁被开释再次争抢。
4.3 volatile 关键字
先来看一段谬误的代码示例:
class ThreadVolatileDemo extends Thread {
public boolean flag = true;
@Override
public void run() {System.out.println("子线程开始执行");
while (flag) { }
System.out.println("子线程执行完结...");
}
public void setFlag(boolean flag){this.flag=flag;}
}
public class ThreadVolatile {public static void main(String[] args) throws InterruptedException {ThreadVolatileDemo threadVolatileDemo = new ThreadVolatileDemo();
threadVolatileDemo.start();
Thread.sleep(3000);
threadVolatileDemo.setFlag(false);
System.out.println("flag 已被批改为 false!");
}
}
12345678910111213141516171819202122232425
复制代码
输入后果:
尽管 flag 已被批改,然而子线程仍然在执行,这里产生的起因就是Java 内存模型(JMM) 导致的。
因为主线程休眠了 3 秒,所以子线程没有意外的话是肯定会被执行 run 办法的。而当子线程因为调用 start 办法而执行 run 办法时,会将 flag 这个共享变量拷贝一份正本存到线程的本地内存中。此时线程中的 flag 为 true,即便主线程在休眠后批改了 flag 值为 false,子线程也不会晓得,即不会批改本人正本的 flag 值。所以这就导致了该问题的呈现。
留神:在测试时,肯定要让主线程进行 sleep 或其余耗时操作,如果没有这步操作,很有可能在子线程执行 run 办法而拷贝共享变量到线程本地内存之前,主线程就曾经批改了 flag 值。
这里再来介绍一下 Java 内存模型 吧!!!
在 Java 内存模型 规定了所有的变量(这里的变量是指成员变量,动态字段等然而不包含局部变量和办法参数,因为这是线程公有的)都存储在 主内存 中,每条线程还有本人的 工作内存 ,线程的工作内存中拷贝了该线程应用到的主内存中的变量(只是正本,从主内存中拷贝了一份,放到了线程的本地内存中), 线程对变量的所有操作都必须在工作内存中进行,而不能间接读写主内存。 不同的线程之间也无奈间接拜访对方工作内存中的变量,线程间变量的传递均须要本人的工作内存和主存之间进行数据同步进行。
而 JMM 就作用于工作内存和主存之间数据同步过程。他规定了如何做数据同步以及什么时候做数据同步。
1. 首先要将共享变量从主内存拷贝到线程本人的工作内存空间,工作内存中存储着主内存中的变量正本拷贝;
2. 线程对正本变量进行操作,(不能间接操作主内存);
3. 操作实现后通过 JMM 将线程的共享变量正本与主内存进行数据的同步,将数据写入主内存中;
4. 不同的线程间无法访问对方的工作内存,线程间的通信 (传值) 必须通过主内存来实现。
当多个线程同时拜访一个数据的时候,可能本地内存没有及时刷新到主内存,所以就会产生线程平安问题
JMM 是在线程调 run 办法的时候才将共享变量写到本人的线程本地内存中去的,而不是在调用 start 办法的时候。
解决办法:
当呈现这种问题时,就能够应用 Volatile 关键字 进行解决。
Volatile 关键字的作用 是变量在多个线程之间可见。应用 Volatile 关键字 将解决线程之间可见性,强制线程每次读取该值的时候都去“主内存”中取值。
只须要在 flag 属性上加上该关键字即可。
public volatile boolean flag = true;
1
复制代码
子线程每次都不是读取的线程本地内存中的正本变量了,而是间接读取主内存中的属性值。
volatile 尽管 具备可见性 ,然而 不具备原子性。
4.4 synchronized、volatile 和 Lock 之间的区别
synochronizd 和 volatile 关键字区别:
1)volatile 关键字 解决的是变量在多个线程之间的可见性;而 sychronized 关键字 解决的是多个线程之间访问共享资源的同步性。
tip: final 关键字也能实现可见性:被 final 润饰的字段在结构器中一旦初始化实现,并且结构器没有把 “this”的援用传递进来(this 援用逃逸是一件很危险的事件,其它线程有可能通过这个援用拜访到了 ” 初始化一半 ” 的对象),那在其余线程中就能看见 final;
2)volatile只能用于润饰变量,而 synchronized 能够润饰办法,以及代码块。(volatile是线程同步的轻量级实现,所以 volatile 性能比 synchronized 要好,并且随着 JDK 新版本的公布,sychronized 关键字 在执行上失去很大的晋升,在开发中应用 synchronized 关键字 的比率还是比拟大);
3)多线程拜访 volatile 不会产生阻塞,而 sychronized 会呈现阻塞;
4)volatile能保障变量在多个线程之间的可见性,但不能保障原子性;而 sychronized 能够保障原子性,也能够间接保障可见性,因为它会将公有内存和私有内存中的数据做同步。
线程平安 蕴含 原子性 和可见性 两个方面。
对于用 volatile 润饰的变量,JVM 虚拟机只是保障从主内存加载到线程工作内存的值是最新的。
一句话阐明 volatile 的作用:实现变量在多个线程之间的可见性。
synchronized 和 lock 区别:
1)Lock是一个接口,而 synchronized 是 Java 中的关键字,synchronized是内置的语言实现;
2)synchronized在产生异样时,会主动开释线程占有的锁,因而不会导致死锁景象产生;而 Lock 在产生异样时,如果没有被动通过 unLock() 去开释锁,则很可能造成死锁景象,因而应用 Lock 时须要在 finally 块中开释锁;
3)Lock能够让期待锁的线程响应中断,而 synchronized 却不行,应用 synchronized 时,期待的线程会始终期待上来,不可能响应中断;
4)通过 Lock 能够晓得有没有胜利获取锁,而 synchronized 却无奈办到。
5)Lock能够进步多个线程进行读操作的效率(读写锁)。
在性能上来说 ,如果竞争资源不强烈,两者的性能是差不多的,而当竞争资源十分强烈时(即有大量线程同时竞争),此时Lock 的性能要远远优于synchronized。所以说,在具体应用时要依据适当状况抉择。
链接:https://juejin.cn/post/690714…