线程安全详解

文章目录

    • 1.线程安全与不安全
    • 2.volatile
    • 3.解决线程不安全
      • 3.1synchronized
        • 3.1.1操作锁的流程
        • 3.1.2代码示例
        • 3.1.3注意
        • 3.1.4三种使用场景
      • 3.2Lock手动锁
      • 3.3公平锁与非公平锁
      • 3.4两种锁区别
    • 4.死锁
      • 4.1死锁定义
      • 4.2死锁示例
      • 4.3死锁的4个必要条件
    • 5.线程通讯
      • 5.1wait方法
      • 5.2wait方法与sleep方法对比

1.线程安全与不安全

线程安全:当多线程访问时,采用了加锁的机制;即当一个线程访问该类的某一个数据时,会对这个数据进行保护,其他线程不能对其访问,直到该线程读取结束之后,其他线程才可以使用。防止出现数据不一致或者数据被污染的情况。
线程不安全:多个线程同时操作某个数据,出现数据不一致或者被污染的情况。

代码示例:

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

结果分析:
线程安全详解_第1张图片
线程安全详解_第2张图片

线程不安全的因素:

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

2.volatile

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

3.解决线程不安全

有两种加锁方式:

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

3.1synchronized

3.1.1操作锁的流程
  • 尝试获取锁a
  • 使用锁(这一步骤是具体的业务代码)
  • 释放锁

synchronized是JVM层面锁的解决方案,它帮我们实现了加锁和释放锁的过程

3.1.2代码示例
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

解析:
线程安全详解_第3张图片

3.1.3注意

synchronized实现分为:

  • 操作系统层面,它是依靠互斥锁mutex
  • 针对JVM,monitor实现
  • 针对Java语言来说,是将锁信息存放在对象头中
3.1.4三种使用场景
  • 使用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--;
        }
    }
}

3.2Lock手动锁

线程安全详解_第4张图片

代码示例:

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里面,如果没有锁的情况下释放锁,这个时候产生的异常就会把业务代码里面的异常给吞噬掉,增加代码调试的难度

3.3公平锁与非公平锁

  • 公平锁:当一个线程释放锁之后,需要主动唤醒“需要得到锁”的队列来得到锁
  • 非公平锁:当一个线程释放锁之后,另一个线程刚好执行到获取锁的代码就可以直接获取锁

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();

    }
}

运行结果:
线程安全详解_第5张图片
示例二:

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

3.4两种锁区别

synchronized和lock的区别

  • 关键字不同
  • synchronized自动进行加锁和释放锁,而Lock需要手动加锁和释放锁
  • synchronized是JVM层面上的实现,而Lock是Java层面锁的实现
  • 修饰范围不同,synchronized可以修饰代码块,静态方法,实例方法,而Lock只能修饰代码块
  • synchronized锁的模式是非公平锁,而lock锁的模式是公平锁和非公平锁
  • Lock的灵活性更高

4.死锁

4.1死锁定义

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

线程安全详解_第6张图片
当线程1拥有资源并1且试图获取资源2和线程2拥有了资源2,并且试图获取资源1的时候,就发了死锁

4.2死锁示例

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();
    }
}

运行结果:
线程安全详解_第7张图片

通过工具来查看死锁:
(1)jdk–>bin–>jconsole.exe
线程安全详解_第8张图片
线程安全详解_第9张图片
(2)jdk–>bin–>jvisualvm.exe
线程安全详解_第10张图片
线程安全详解_第11张图片
(3)jdk–>bin–>jmc.exe
线程安全详解_第12张图片

4.3死锁的4个必要条件

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

5.线程通讯

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

5.1wait方法

注意事项:
1.wait方法在执行之前必须先加锁。也就是wait方法必须配合synchronized配合使用
2.wait和notify在配合synchronized使用时,一定要使用同一把锁
线程安全详解_第13张图片

运行结果:

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释放锁的代码:

线程安全详解_第14张图片

线程安全详解_第15张图片

wait在等待的时候可以释放锁,sleep在等待的时候不会释放锁

5.2wait方法与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的异常,但是可以监听到线程终止的指令

你可能感兴趣的:(多线程,多线程,并发编程,java)