一、线程平安的概念

线程平安是多线程编程是的计算机程序代码中的一个概念。在领有共享数据的多条线程并行执行的程序中,线程平安的代码会通过同步机制保障各个线程都能够失常且精确的执行,不会呈现数据净化等意外状况。上述是百度百科给出的一个概念解释。换言之,线程平安就是某个函数在并发环境中调用时,可能解决好多个线程之间的共享变量,是程序可能正确执行结束。也就是说咱们想要确保在多线程拜访的时候,咱们的程序还可能依照咱们的预期的行为去执行,那么就是线程平安了。

二、导致线程不平安的起因

首先,能够来看一段代码,来看看是不是线程平安的,代码如下:

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 15123456789101112131415复制代码

《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...