关于后端:关于线程安全问题这一篇应该是全网讲的最明白的了

37次阅读

共计 8543 个字符,预计需要花费 22 分钟才能阅读完成。

线程平安与不平安

线程平安:当多线程拜访时,采纳了加锁的机制;即当一个线程拜访该类的某一个数据时,会对这个数据进行爱护,其余线程不能对其拜访,直到该线程读取完结之后,其余线程才能够应用。防止出现数据不统一或者数据被净化的状况。
线程不平安:多个线程同时操作某个数据,呈现数据不统一或者被净化的状况。

代码示例:

package thread_5_10;

public class Demo26 {
    static  int  a = 0;
    public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {for (int i = 0; i < 100_0000; i++) {a++;}
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {for (int i = 0; i < 100_0000; i++) {a--;}
            }
        });

        // 开启线程
        t1.start();
        t2.start();

        // 期待线程实现
        //t1.join();
        //t2.join();
        while(t1.isAlive() || t2.isAlive()){ }
        System.out.println(a);
    }
}

运行后果:

493612

后果剖析:



集体整顿了一些材料,有须要的敌人能够间接点击支付。

[Java 基础知识大全](https://jq.qq.com/?_wv=1027&k…
)

[22 本 Java 架构师外围书籍](https://jq.qq.com/?_wv=1027&k…
)

[从 0 到 1Java 学习路线和材料](https://jq.qq.com/?_wv=1027&k…
)

[1000+ 道 2021 年最新面试题](https://jq.qq.com/?_wv=1027&k…

线程不平安的因素:

CPU 是抢占式执行的(抢占资源)
多个线程操作的是同一个变量
可见性
非原子性
编译期优化(指令重排)

volatile

volatile 是指令关键字,作用是确保本指令不会因编译期优化而省略,且每次要求间接读值。能够解决内存不可见和指令重排序的问题,然而不能解决原子性问题

解决线程不平安

有两种加锁形式:

synchronized(jvm 层的解决方案)
Lock 手动锁

synchronized

操作锁的流程

尝试获取锁 a
应用锁(这一步骤是具体的业务代码)
开释锁
synchronized 是 JVM 层面锁的解决方案,它帮咱们实现了加锁和开释锁的过程

代码示例

package thread_5_10;

public class Demo31 {
    // 循环的最大次数
    private final static int maxSize = 100_0000;

    // 定义全局变量
    private static int number = 0;

    public static void main(String[] args) throws InterruptedException {

        // 申明锁对象
        Object obj = new Object();

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {for (int i = 0; i < maxSize; i++) {
                    // 实现加锁
                    synchronized (obj){number++;}

                }
            }
        });

        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {for (int i = 0; i < maxSize; i++) {synchronized (obj){number--;}

                }
            }
        });

        t2.start();

        // 期待两个线程执行实现
        t1.join();
        t2.join();

        System.out.println(number);
    }
}

运行后果:

0

解析:

留神

synchronized 实现分为:

操作系统层面,它是依附互斥锁 mutex
针对 JVM,monitor 实现
针对 Java 语言来说,是将锁信息寄存在对象头中

三种应用场景

应用 synchronized 润饰代码块,(能够对任意对象加锁)
应用 synchronized 润饰静态方法(对以后类进行加锁)
应用 synchronized 润饰一般办法(对以后类实例进行加锁)
润饰静态方法:

package thread_5_10;

public class Demo32 {

    private static int number = 0;
    private static final int maxSize = 100_0000;

    public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {increment();
            }
        });
        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {decrement();
            }
        });
        t2.start();

        t1.join();
        t2.join();
        System.out.println("最终后果为:"+number);
    }

    public synchronized static void increment(){for (int i = 0; i < maxSize; i++) {number++;}
    }

    public synchronized static void decrement(){for (int i = 0; i < maxSize; i++) {number--;}
    }
}

润饰实例办法:

package thread_5_10;

public class Demo33 {

    private static int number = 0;
    private static final int maxSize = 100_0000;

    public static void main(String[] args) throws InterruptedException {Demo33 demo = new Demo33();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {demo.increment();
            }
        });
        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {demo.decrement();
            }
        });
        t2.start();

        t1.join();
        t2.join();
        System.out.println("最终后果:"+number);
    }

    public synchronized  void increment(){for (int i = 0; i < maxSize; i++) {number++;}
    }

    public synchronized  void decrement(){for (int i = 0; i < maxSize; i++) {number--;}
    }
}

Lock 手动锁

代码示例:

package thread_5_10;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Demo34 {

    private static int number = 0;
    private static final int maxSize = 100_0000;

    public static void main(String[] args) throws InterruptedException {

        // 创立 lock 实例
        Lock lock = new ReentrantLock();

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {for (int i = 0; i < maxSize; i++) {lock.lock();
                    try{number++;}finally {lock.unlock();
                    }
                }
            }
        });
        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {for (int i = 0; i < maxSize; i++) {lock.lock();
                    try{number--;}finally {lock.unlock();
                    }
                }
            }
        });
        t2.start();

        t1.join();
        t2.join();
        System.out.println("最终后果为 -->"+number);
    }
}

运行后果:

最终后果为 -->   0

注意事项:

lock()肯定要放在 try 里面

如果放在 try 外面,如果 try 外面出现异常,还没有加锁胜利就执行 finally 外面的开释锁的代码,就会出现异常
如果放在 try 外面,如果没有锁的状况下开释锁,这个时候产生的异样就会把业务代码外面的异样给吞噬掉,减少代码调试的难度

偏心锁与非偏心锁

偏心锁:当一个线程开释锁之后,须要被动唤醒“须要失去锁”的队列来失去锁
非偏心锁:当一个线程开释锁之后,另一个线程刚好执行到获取锁的代码就能够间接获取锁
java 语言中,所有锁的默认实现形式都是非偏心锁

1.synchronized 是非偏心锁
2.reentrantLock 默认是非偏心锁,但也能够显示地申明为偏心锁

显示申明偏心锁格局:

ReentrantLock 源码:

示例一:

package thread_5_10;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Demo36 {public static void main(String[] args) throws InterruptedException {Lock lock = new ReentrantLock(true);

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {for (int i = 0; i < 100; i++) {lock.lock();
                    try{System.out.println("线程 1");
                    }finally {lock.unlock();
                    }
                }
            }
        });



        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {for (int i = 0; i < 100; i++) {lock.lock();
                    try{System.out.println("线程 2");
                    }finally {lock.unlock();
                    }
                }
            }
        });

        Thread.sleep(1000);
        t1.start();
        t2.start();}
}

运行后果:

示例二:

package test;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class test08 {public static void main(String[] args) throws InterruptedException {Lock lock = new ReentrantLock(true);

        Runnable r = new Runnable() {
            @Override
            public void run() {for(char ch: "ABCD".toCharArray()){lock.lock();
                    try{System.out.print(ch);
                    }finally {lock.unlock();
                    }
                }
            }
        };

        Thread.sleep(100);
        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);
        t1.start();
        t2.start();}
}

运行后果:

AABBCCDD

两种锁区别

synchronized 和 lock 的区别

关键字不同
synchronized 主动进行加锁和开释锁,而 Lock 须要手动加锁和开释锁
synchronized 是 JVM 层面上的实现,而 Lock 是 Java 层面锁的实现
润饰范畴不同,synchronized 能够润饰代码块,静态方法,实例办法,而 Lock 只能润饰代码块
synchronized 锁的模式是非偏心锁,而 lock 锁的模式是偏心锁和非偏心锁
Lock 的灵活性更高

死锁

死锁定义

在两个或两个以上的线程运行中,因为资源抢占而造成线程始终期待的问题

当线程 1 领有资源并 1 且试图获取资源 2 和线程 2 领有了资源 2,并且试图获取资源 1 的时候,就发了死锁

死锁示例

package thread_5_11;

public class Demo36 {public static void main(String[] args) {
        // 申明加锁的资源
        Object lock1 = new Object();
        Object lock2 = new Object();

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                // 获取线程名称
                String threadName = Thread.currentThread().getName();

                //1. 获取资源 1
                synchronized (lock1){System.out.println(threadName+"获取到了 lock1");
                    try {

                        //2. 期待 1ms,让线程 t1 和线程 t2 都获取到相应的资源
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {e.printStackTrace();
                    }
                    System.out.println(threadName+"waiting lock2");

                    //3. 获取资源 2
                    synchronized (lock2){System.out.println(threadName+"获取到了 lock2");
                    }
                }
            }
        },"t1");
        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {String threadName = Thread.currentThread().getName();
                synchronized (lock2){System.out.println(threadName+"获取到了 lock2");
                    try {Thread.sleep(1000);
                    } catch (InterruptedException e) {e.printStackTrace();
                    }
                    System.out.println(threadName+"waiting lock1");
                    synchronized (lock1){System.out.println(threadName+"获取到了 lock1");
                    }
                }
            }
        },"t2");

        t2.start();}
}

运行后果:

通过工具来查看死锁:
(1)jdk–>bin–>jconsole.exe


(2)jdk–>bin–>jvisualvm.exe

(3)jdk–>bin–>jmc.exe

死锁的 4 个必要条件

1. 互斥条件:当资源被一个线程领有之后,就不能被其余的线程领有了
2. 占有且期待:当一个线程领有了一个资源之后又试图申请另一个资源
3. 不可抢占:当一个资源被一个线程被领有之后,如果不是这个线程被动开释此资源的状况下,其余线程不能领有此资源
4. 循环期待:两个或两个以上的线程在领有了资源之后,试图获取对方资源的时候造成了一个环路

线程通信

所谓的线程通信就是在一个线程中的操作能够影响另一个线程,wait(休眠线程),notify(唤醒一个线程),notifyall(唤醒所有线程)

wait 办法

注意事项:
1.wait 办法在执行之前必须先加锁。也就是 wait 办法必须配合 synchronized 配合应用
2.wait 和 notify 在配合 synchronized 应用时,肯定要应用同一把锁

运行后果:

wait 之前
主线程唤醒 t1
wait 之后

多线程

package thread_5_13;

public class demo40 {public static void main(String[] args) throws InterruptedException {Object lock = new Object();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                // 调用 wait 办法之前必须先加锁
                synchronized (lock){
                    try {System.out.println("t1 wait 之前");
                        lock.wait();
                        System.out.println("t1 wait 之后");

                    } catch (InterruptedException e) {e.printStackTrace();
                    }
                }

            }
        },"t1");


        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                // 调用 wait 办法之前必须先加锁
                synchronized (lock){
                    try {System.out.println("t2 wait 之前");
                        lock.wait();
                        System.out.println("t2 wait 之后");

                    } catch (InterruptedException e) {e.printStackTrace();
                    }
                }

            }
        },"t2");

        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                // 调用 wait 办法之前必须先加锁
                synchronized (lock){
                    try {System.out.println("t3 wait 之前");
                        lock.wait();
                        System.out.println("t3 wait 之后");

                    } catch (InterruptedException e) {e.printStackTrace();
                    }
                }

            }
        },"t3");

        t1.start();
        t2.start();
        t3.start();
        Thread.sleep(1000);
        System.out.println("主线程调用唤醒操作");

        // 在主线程中唤醒
        synchronized (lock){lock.notify();
        }
    }
}

运行后果:

t1 wait 之前
t2 wait 之前
t3 wait 之前
主线程调用唤醒操作
t1 wait 之后

注意事项:

将 lock.notify()批改为 lock.notifyAll(),则三个线程都能被唤醒
wait 在不传递任何参数的状况下会进入 waiting 状态(参数为 0 也是 waiting 状态);当 wait 外面有一个大于 0 的整数时,它就会进入 timed_waiting 状态
对于 wait 和 sleep 开释锁的代码:

wait 在期待的时候能够开释锁,sleep 在期待的时候不会开释锁

wait 办法与 sleep 办法比照

相同点:
(1)wait 和 sleep 都能够使线程休眠
(2)wait 和 sleep 在执行的过程中都能够接管到终止线程执行的告诉

不同点:
(1)wait 必须 synchronized 一起应用,而 sleep 不必
(2)wait 会开释锁,sleep 不会开释锁
(3)wait 是 Object 的办法,而 sleep 是 Thread 的办法
(4)默认状况下,wait 不传递参数或者参数为 0 的状况下,它会进入 waiting 状态,而 sleep 会进入 timed_waiting 状态
(5)应用 wait 能够被动唤醒线程,而应用 sleep 不能被动唤醒线程

面试题

1. 问:sleep(0)和 wait(0)有什么区别
答:(1)sleep(0)示意过 0 毫秒后继续执行,而 wait(0)会始终期待
(2)sleep(0)示意从新触发一次 CPU 竞争

2. 为什么 wait 会开释锁,而 sleep 不会开释锁
答:sleep 必须要传递一个最大等待时间的,也就是说 sleep 是可控的(对于工夫层面来讲),而 wait 是能够不传递工夫,从设计层面来讲,如果让 wait 这个没有超时等待时间的机制下开释锁的话,那么线程可能会始终阻塞,而 sleep 不会存在这个问题

3. 为什么 wait 是 Object 的办法,而 sleep 是 Thread 的办法
答:wait 须要操作锁,而锁是对象级别(所有的锁都在对象头当中),它不是线程级别,一个线程能够有多把锁,为了灵便起见,所有把 wait 放在 Object 当中

4. 解决 wait/notify 随机唤醒的问题
答:能够应用 LockSupport 中的 park,unpark 办法,留神:locksupport 尽管不会报 interrupted 的异样,然而能够监听到线程终止的指令

最初

都看到这里了,记得点个赞哦!

正文完
 0