Java基础——Java多线程(什么是线程安全?)

Java
225
0
0
2023-06-06

1 基本概括

2 主要介绍

2.1 线程安全的概念

当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。

2.2 多线程编程的三个概念

2.2.1 原子性

这一点,跟数据库事务的原子性概念差不多,即一个操作(有可能包含有多个子操作)要么全部执行(生效),要么全部都不执行(都不生效)。

2.2.2 可见性

这一点,跟数据库事务的原子性概念差不多,即一个操作(有可能包含有多个子操作)要么全部执行(生效),要么全部都不执行(都不生效)。

2.2.3 有序性

顺序性指的是,程序执行的顺序按照代码的先后顺序执行。

以下面这段代码为例

boolean started = false; // 语句1

long counter = 0L; // 语句2

counter = 1; // 语句3

started = true; // 语句4

从代码顺序上看,上面四条语句应该依次执行,但实际上JVM真正在执行这段代码时,并不保证它们一定完全按照此顺序执行。

处理器为了提高程序整体的执行效率,可能会对代码进行优化,其中的一项优化方式就是调整代码顺序,按照更高效的顺序执行代码。

2.3 解决机制

1、加锁:

a、锁能使其保护的代码以串行的形式来访问,当给一个复合操作加锁后,能使其成为原子操作。一种错误的思想是只要对写数据的方法加锁,其实这是错的,对数据进行操作的所有方法都需加锁,不管是读还是写

b、加锁时需要考虑性能问题,不能总是一味地给整个方法加锁synchronized就了事了,应该将方法中不影响共享状态且执行时间比较长的代码分离出去

c、加锁的含义不仅仅局限于互斥,还包括可见性。为了确保所有线程都能看见最新值,读操作和写操作必须使用同样的锁对象

2、不共享状态:

无状态对象: 无状态对象一定是线程安全的,因为不会影响到其他线程

线程关闭: 仅在单线程环境下使用

3、不可变对象:

可以使用final修饰的对象保证线程安全,由于final修饰的引用型变量(除String外)不可变是指引用不可变,但其指向的对象是可变的,所以此类必须安全发布,也即不能对外提供可以修改final对象的接口

2.4 线程安全的级别

线程安全的级别或者粒度有三种,如下:

(1)线程安全

这种情况下其实没有线程安全问题,比如上面的例子中,每个人都有自己专用的卫生间,所以不会存在竞争问题。

(2)条件安全

条件安全,顾名思义是有条件的,所有人共用几个卫生间,抢到资源的就把门关上,通过门来隔离资源,后面的人就在外面等待直到里面的人出来。

(3)不安全

这种情况下连门都没有,所以并不能很好保证资源安全,所以这种情况***不能让同时让多个人直接使用。

2.5 并发的概念

并发 指单个cpu同时处理多个线程任务,cpu在反复切换任务线程,实际还是串行化的;

并行 指多个cpu同时处理多个线程任务,cpu可以同时处理不同的任务,异步处理;

并发条件

第一,是否有共享变量 第二,是否多线程环境 第三,是否多个线程更新共享变量 一句话:多个线程操作同一个对象;

2.5 并发的优势和风险

2.6 避免并发

2.6.1 线程封闭

什么是线程封闭?

就是把对象封装到一个线程里,只有这一个线程能看到此对象。那么这个对象就算不是线程安全的也不会出现任何安全问题。

实现线程封闭有哪些方法?

ad-hoc 线程封闭

这是完全靠实现者控制的线程封闭,他的线程封闭完全靠实现者实现。

Ad-hoc 线程封闭非常脆弱,应该尽量避免使用。

栈封闭

栈封闭是我们编程当中遇到的最多的线程封闭。

什么是栈封闭呢?

简单的说就是局部变量。

多个线程访问一个方法,此方法中的局部变量都会被拷贝一份到线程栈中。所以局部变量是不被多个线程所共享的,也就不会出现并发问题。所以能用局部变量就别用全局的变量,全局变量容易引起并发问题。

2.6.2 无状态的类

没有任何成员变量的类,就叫无状态的类,这种类一定是线程安全的。

无状态就是一次操作,不能保存数据。无状态对象(Stateless Bean),就是没有实例变量的对象.不能保存数据,是不变类。

2.6.3 让类不可变

让状态不可变,两种方式:

1、 加 final 关键字,对于一个类,所有的成员变量应该是私有的,同样的只要有可能,所有的成员变量应该加上 final 关键字,但是加上 final,要注意如果成员变量又是一个对象时,这个对象所对应的类也要是不可变,才能保证整个类是不可变的。

2、根本就不提供任何可供修改成员变量的地方,同时成员变量也不作为方法的返回值。

2.6.4 volatile

并不能保证类的线程安全性,只能保证类的可见性,最适合一个线程写,多个线程读的情景。

2.6.5 加锁和CAS

我们最常使用的保证线程安全的手段,使用 synchronized 关键字,使用显式锁,使用各种原子变量,修改数据时使用 CAS 机制等等。

2.6.6 安全的发布

类中持有的成员变量,如果是基本类型,发布出去,并没有关系,因为发布出去的其实是这个变量的一个副本。

但是如果类中持有的成员变量是对象的引用,如果这个成员对象不是线程安全的,通过 get 等方法发布出去,会造成这个成员对象本身持有的数据在多线程下不正确的修改,从而造成整个类线程不安全的问题。

2.6.7 ThreadLocal

ThreadLocal 是实现线程封闭的最好方法。

ThreadLocal 内部维护了一个 Map,Map 的 key 是每个线程的名称,而 Map 的值就是我们要封闭的对象。每个线程中的对象都对应着 Map 中一个值,也就是 ThreadLocal 利用 Map 实现了对象的线程封闭。

2.7 实现线程安全的方式

2.7.1 synchronized (自动锁,锁的创建和释放都是自动的)

synchronized(同一个锁){ //可能会发生线程冲突问题 }

锁的释放 是在 synchronized 同步代码执行完毕后自动释放。

同步的前提:

1,必须要有两个或者两个以上的线程 ,如果小于2个线程,则没有用,且还会消耗性能(获取锁,释放锁)

2,必须是多个线程使用同一个锁

弊端 :多个线程需要判断锁,较为消耗资源、抢锁的资源。

2.7.2 lock 手动锁 (手动指定锁的创建和释放)

可以视为synchronized的增强版,提供了更灵活的功能。该接口提供了限时锁等待、锁中断、锁尝试等功能。synchronized实现的同步代码块,它的锁是自动加的,且当执行完同步代码块或者抛出异常后,锁的释放也是自动的。

2.7.3 volatile关键字

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

2)禁止进行指令重排序。

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 可以提高多个线程进行读操作的效率(读写锁)。

3 使用线程安全的简单用例

3.1 同步代码块

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

3.2 Lock锁是需要手动去加锁和释放

 /*
 * 使用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();
    }
} 

3.3 volatile关键字

 public class Singleton3 {
    private static volatile Singleton3 instance = null;
    private Singleton3() {}
    public static Singleton3 getInstance() {
        if (instance == null) {
            synchronized(Singleton3.class) {
                if (instance == null)
                    instance = new Singleton3();
            }
        }
        return instance;
    }
}